QB64.com

QB64 is a modern extended BASIC programming language that retains QBasic/QuickBASIC 4.5 compatibility and compiles native binaries for Windows, Linux, and macOS.

Animated GIF File Decoder

GIF files can be one frame or animated images made up of many frames that are displayed at a set frame rate. The following program allows you to view either kind of image or use them in a program. _LOADIMAGE can only return one frame of an animated image.


'#######################################################################################
'# Animated GIF decoder v1.0                                                           #
'# By Zom-B                                                                            #
'#######################################################################################

DEFINT A-Z
'$DYNAMIC

DIM SHARED Dbg: Dbg = 0
DIM SHARED powerOf2&(11)
FOR a = 0 TO 11: powerOf2&(a) = 2 ^ a: NEXT a

TYPE GIFDATA
  file AS INTEGER
  sigver AS STRING * 6
  width AS _UNSIGNED INTEGER
  height AS _UNSIGNED INTEGER
  bpp AS _UNSIGNED _BYTE
  sortFlag AS _BYTE ' Unused
  colorRes AS _UNSIGNED _BYTE
  colorTableFlag AS _BYTE
  bgColor AS _UNSIGNED _BYTE
  aspect AS SINGLE ' Unused
  numColors AS _UNSIGNED INTEGER
  palette AS STRING * 768
END TYPE

TYPE FRAMEDATA
  addr AS LONG
  left AS _UNSIGNED INTEGER
  top AS _UNSIGNED INTEGER
  width AS _UNSIGNED INTEGER
  height AS _UNSIGNED INTEGER
  localColorTableFlag AS _BYTE
  interlacedFlag AS _BYTE
  sortFlag AS _BYTE ' Unused
  palBPP AS _UNSIGNED _BYTE
  minimumCodeSize AS _UNSIGNED _BYTE
  transparentFlag AS _BYTE 'GIF89a-specific (animation) values
  userInput AS _BYTE ' Unused
  disposalMethod AS _UNSIGNED _BYTE
  delay AS SINGLE
  transColor AS _UNSIGNED _BYTE
END TYPE

SCREEN _NEWIMAGE(640, 480, 32)

' Open gif file. This reads the headers and palette but not the image data.
' The array will be redimentioned to fit the exact number of frames in the file.

DIM gifData AS GIFDATA, frameData(0 TO 0) AS FRAMEDATA

filename$ = "mygif.gif"  '<<<<<<<<<<<< Enter a file name here!!!

IF LEN(filename$) = 0 THEN END
openGif filename$, gifData, frameData()

' Loop away.
frame = 0
DO
  ' Request a frame. If it has been requested before, it is re-used,
  ' otherwise it is read and decoded from the file.
  _PUTIMAGE (0, 0), getGifFrame&(gifData, frameData(), frame)
  _DELAY frameData(frame).delay
  frame = (frame + 1) MOD (UBOUND(framedata) + 1)
LOOP UNTIL LEN(INKEY$)

'Close the file and free the allocated frames.
codeGif gifData, frameData()
END

'########################################################################################

SUB openGif (filename$, gifData AS GIFDATA, frameData() AS FRAMEDATA) STATIC
file = FREEFILE: gifData.file = file
OPEN "B", gifData.file, filename$

GET file, , gifData.sigver
GET file, , gifData.width
GET file, , gifData.height
GET file, , byte~%%
gifData.bpp = (byte~%% AND 7) + 1
gifData.sortFlag = (byte~%% AND 8) > 0
gifData.colorRes = (byte~%% \ 16 AND 7) + 1
gifData.colorTableFlag = (byte~%% AND 128) > 0
gifData.numColors = 2 ^ gifData.bpp
GET file, , gifData.bgColor
GET file, , byte~%%
IF byte~%% = 0 THEN gifData.aspect = 0 ELSE gifData.aspect = (byte~%% + 15) / 64

IF gifData.sigver <> "GIF87a" AND gifData.sigver <> "GIF89a" THEN _DEST 0: PRINT "Invalid version": END
IF NOT gifData.colorTableFlag THEN _DEST 0: PRINT "No Color Table": END

palette$ = SPACE$(3 * gifData.numColors)
GET file, , palette$
gifData.palette = palette$
IF Dbg AND 1 THEN 
  PRINT "sigver         ="; gifData.sigver
  PRINT "width          ="; gifData.width
  PRINT "height         ="; gifData.height
  PRINT "bpp            ="; gifData.bpp
  PRINT "sortFlag       ="; gifData.sortFlag
  PRINT "colorRes       ="; gifData.colorRes
  PRINT "colorTableFlag ="; gifData.colorTableFlag
  PRINT "bgColor        ="; gifData.bgColor
  PRINT "aspect         ="; gifData.aspect
  PRINT "numColors      ="; gifData.numColors
  FOR i = 0 TO gifData.numColors - 1
    PRINT USING "pal(###) = "; i;
    PRINT HEX$(_RGB32(ASC(gifData.palette, i * 3 + 1), ASC(gifData.palette, i * 3 + 2), ASC(gifData.palette, i * 3 + 3)))
  NEXT
END IF
DO
  GET file, , byte~%%
  IF Dbg AND 2 THEN PRINT "Chunk: "; HEX$(byte~%%)
  SELECT CASE byte~%%
    CASE &H2C ' Image Descriptor
      IF frame > UBOUND(frameData) THEN
        REDIM _PRESERVE frameData(0 TO frame * 2 - 1) AS FRAMEDATA
      END IF

      GET file, , frameData(frame).left
      GET file, , frameData(frame).top
      GET file, , frameData(frame).width
      GET file, , frameData(frame).height
      GET file, , byte~%%
      frameData(frame).localColorTableFlag = (byte~%% AND 128) > 0
      frameData(frame).interlacedFlag = (byte~%% AND 64) > 0
      frameData(frame).sortFlag = (byte~%% AND 32) > 0
      frameData(frame).palBPP = (byte~%% AND 7) + 1
      frameData(frame).addr = LOC(file) + 1

      IF frameData(frame).localColorTableFlag THEN
        SEEK file, LOC(file) + 3 * 2 ^ frameData(frame).palBPP + 1
      END IF
      GET file, , frameData(frame).minimumCodeSize
      IF Dbg AND 2 THEN 
        PRINT "addr                ="; HEX$(frameData(frame).addr - 1)
        PRINT "left                ="; frameData(frame).left
        PRINT "top                 ="; frameData(frame).top
        PRINT "width               ="; frameData(frame).width
        PRINT "height              ="; frameData(frame).height
        PRINT "localColorTableFlag ="; frameData(frame).localColorTableFlag
        PRINT "interlacedFlag      ="; frameData(frame).interlacedFlag
        PRINT "sortFlag            ="; frameData(frame).sortFlag
        PRINT "palBPP              ="; frameData(frame).palBPP
        PRINT "minimumCodeSize     ="; frameData(frame).minimumCodeSize
      END IF
      IF localColors THEN _DEST 0: PRINT "Local color table": END
      IF frameData(frame).disposalMethod > 2 THEN PRINT "Unsupported disposalMethod: "; frameData(frame).disposalMethod: END
      skipBlocks file

      frame = frame + 1
    CASE &H3B ' Trailer
      EXIT DO
    CASE &H21 ' Extension Introducer
      GET file, , byte~%% ' Extension Label
      IF Dbg AND 2 THEN PRINT "Extension Introducer: "; HEX$(byte~%%)
      SELECT CASE byte~%%
        CASE &HFF, &HFE ' Application Extension, Comment Extension
          skipBlocks file
        CASE &HF9
          IF frame > UBOUND(frameData) THEN
            REDIM _PRESERVE frameData(0 TO frame * 2 - 1) AS FRAMEDATA
          END IF

          GET 1, , byte~%% ' Block Size (always 4)
          GET 1, , byte~%%
          frameData(frame).transparentFlag = (byte~%% AND 1) > 0
          frameData(frame).userInput = (byte~%% AND 2) > 0
          frameData(frame).disposalMethod = byte~%% \ 4 AND 7
          GET 1, , delay~%
          IF delay~% = 0 THEN frameData(frame).delay = 0.1 ELSE frameData(frame).delay = delay~% / 100
          GET 1, , frameData(frame).transColor
          IF Dbg AND 2 THEN 
            PRINT "frame           ="; frame
            PRINT "transparentFlag ="; frameData(frame).transparentFlag
            PRINT "userInput       ="; frameData(frame).userInput
            PRINT "disposalMethod  ="; frameData(frame).disposalMethod
            PRINT "delay           ="; frameData(frame).delay
            PRINT "transColor      ="; frameData(frame).transColor
          END IF
          skipBlocks file
        CASE ELSE
          PRINT "Unsupported extension Label: "; HEX$(byte~%%): END
      END SELECT
    CASE ELSE
      PRINT "Unsupported chunk: "; HEX$(byte~%%): END
  END SELECT
LOOP

REDIM _PRESERVE frameData(0 TO frame - 1) AS FRAMEDATA
END FUNCTION

SUB skipBlocks (file)
DO
  GET file, , byte~%% ' Block Size
  IF Dbg AND 2 THEN PRINT "block size ="; byte~%%
  SEEK file, LOC(file) + byte~%% + 1
LOOP WHILE byte~%%
END SUB

FUNCTION getGifFrame& (gifData AS GIFDATA, frameData() AS FRAMEDATA, frame)
IF frameData(frame).addr > 0 THEN
  IF Dbg AND 4 THEN
    PRINT "addr                ="; HEX$(frameData(frame).addr - 1)
    PRINT "left                ="; frameData(frame).left
    PRINT "top                 ="; frameData(frame).top
    PRINT "width               ="; frameData(frame).width
    PRINT "height              ="; frameData(frame).height
    PRINT "localColorTableFlag ="; frameData(frame).localColorTableFlag
    PRINT "interlacedFlag      ="; frameData(frame).interlacedFlag
    PRINT "sortFlag            ="; frameData(frame).sortFlag
    PRINT "palBPP              ="; frameData(frame).palBPP
    PRINT "minimumCodeSize     ="; frameData(frame).minimumCodeSize
    PRINT "transparentFlag     ="; frameData(frame).transparentFlag
    PRINT "userInput           ="; frameData(frame).userInput
    PRINT "disposalMethod      ="; frameData(frame).disposalMethod
    PRINT "delay               ="; frameData(frame).delay
    PRINT "transColor          ="; frameData(frame).transColor
  END IF
  w = frameData(frame).width
  h = frameData(frame).height
  img& = _NEWIMAGE(w, h, 256)
  frame& = _NEWIMAGE(gifData.width, gifData.height, 256)

  _DEST img&
  decodeFrame gifData, frameData(frame)

  _DEST frame&
  IF frameData(frame).localColorTableFlag THEN
    _COPYPALETTE img&
  ELSE
    FOR i = 0 TO gifData.numColors - 1
      _PALETTECOLOR i, _RGB32(ASC(gifData.palette, i * 3 + 1), ASC(gifData.palette, i * 3 + 2), ASC(gifData.palette, i * 3 + 3))
    NEXT
  END IF

  IF frame THEN
    SELECT CASE frameData(frame - 1).disposalMethod
      CASE 0, 1
        _PUTIMAGE , frameData(frame - 1).addr
      CASE 2
        CLS , gifData.bgColor
        _CLEARCOLOR gifData.bgColor
    END SELECT
  ELSE
    CLS , gifData.bgColor
  END IF

  IF frameData(frame).transparentFlag THEN
    _CLEARCOLOR frameData(frame).transColor, img&
  END IF
  _PUTIMAGE (frameData(frame).left, frameData(frame).top), img&
  _FREEIMAGE img&

  frameData(frame).addr = frame&
  _DEST 0
END IF

getGifFrame& = frameData(frame).addr
END FUNCTION


'############################################################################################

SUB decodeFrame (gifdata AS GIFDATA, framedata AS FRAMEDATA)
DIM byte AS _UNSIGNED _BYTE
DIM prefix(4095), suffix(4095), colorStack(4095)

startCodeSize = gifdata.bpp + 1
clearCode = 2 ^ gifdata.bpp
endCode = clearCode + 1
minCode = endCode + 1
startMaxCode = clearCode * 2 - 1
nvc = minCode
codeSize = startCodeSize
maxCode = startMaxCode

IF framedata.interlacedFlag THEN interlacedPass = 0: interlacedStep = 8
bitPointer = 0
blockSize = 0
blockPointer = 0
x = 0
y = 0

file = gifdata.file
SEEK file, framedata.addr

IF framedata.localColorTableFlag THEN
  palette$ = SPACE$(3 * 2 ^ framedata.palBPP)
  GET 1, , palette$

  FOR i = 0 TO gifdata.numColors - 1
    c& = _RGB32(ASC(palette$, i * 3 + 1), ASC(palette$, i * 3 + 2), ASC(palette$, i * 3 + 3))
    _PALETTECOLOR i, c&
  NEXT
END IF

GET file, , byte ' minimumCodeSize

DO
  GOSUB GetCode
  stackPointer = 0
  IF code = clearCode THEN 'Reset & Draw next color direct
    nvc = minCode '           \
    codeSize = startCodeSize ' Preset default codes
    maxCode = startMaxCode '  /

    GOSUB GetCode
    currentCode = code

    lastColor = code
    colorStack(stackPointer) = lastColor
    stackPointer = 1
  ELSEIF code <> endCode THEN 'Draw direct color or colors from suffix
    currentCode = code
    IF currentCode = nvc THEN 'Take last color too
      currentCode = oldCode
      colorStack(stackPointer) = lastColor
      stackPointer = stackPointer + 1
    END IF

    WHILE currentCode >= minCode 'Extract colors from suffix
      colorStack(stackPointer) = suffix(currentCode)
      stackPointer = stackPointer + 1
      currentCode = prefix(currentCode) 'Next color from suffix is described in
    WEND '                                 the prefix, else prefix is the last col.

    lastColor = currentCode '              Last color is equal to the
    colorStack(stackPointer) = lastColor ' last known code (direct, or from
    stackPointer = stackPointer + 1 '      Prefix)
    suffix(nvc) = lastColor 'Automatically, update suffix
    prefix(nvc) = oldCode 'Code from the session before (for extracting from suffix)
    nvc = nvc + 1

    IF nvc > maxCode AND codeSize < 12 THEN
      codeSize = codeSize + 1
      maxCode = maxCode * 2 + 1
    END IF
  END IF

  FOR i = stackPointer - 1 TO 0 STEP -1
    PSET (x, y), colorStack(i)
    x = x + 1
    IF x = framedata.width THEN
      x = 0
      IF framedata.interlacedFlag THEN
        y = y + interlacedStep
        IF y >= framedata.height THEN
          SELECT CASE interlacedPass
            CASE 0: interlacedPass = 1: y = 4
            CASE 1: interlacedPass = 2: y = 2
            CASE 2: interlacedPass = 3: y = 1
          END SELECT
          interlacedStep = 2 * y
        END IF
      ELSE
        y = y + 1
      END IF
    END IF
  NEXT

  oldCode = code
LOOP UNTIL code = endCode

GET file, , byte
EXIT SUB

GetCode:
IF bitPointer = 0 THEN GOSUB ReadByteFromBlock: bitPointer = 8
WorkCode& = LastChar \ powerOf2&(8 - bitPointer)
WHILE codeSize > bitPointer
  GOSUB ReadByteFromBlock

  WorkCode& = WorkCode& OR LastChar * powerOf2&(bitPointer)
  bitPointer = bitPointer + 8
WEND
bitPointer = bitPointer - codeSize
code = WorkCode& AND maxCode
RETURN

ReadByteFromBlock:
IF blockPointer = blockSize THEN
  GET file, , byte: blockSize = byte
  a$ = SPACE$(blockSize): GET file, , a$
  blockPointer = 0
END IF
blockPointer = blockPointer + 1
LastChar = ASC(MID$(a$, blockPointer, 1))
RETURN
END SUB


SUB codeGif (gifData AS GIFDATA, frameData() AS FRAMEDATA)
FOR i = 0 TO UBOUND(FRAMEDATA)
  IF frameData(i).addr < 0 THEN _FREEIMAGE frameData(i).addr
NEXT

CLOSE gifData.file
END SUB


NOTE: This has been reported to only work using 256-color images, and you need to keep the code loading into a 32-bit image destination as the source?

See Also