diff --git a/src/pixie/fileformats/gif.nim b/src/pixie/fileformats/gif.nim new file mode 100644 index 0000000..60a62be --- /dev/null +++ b/src/pixie/fileformats/gif.nim @@ -0,0 +1,159 @@ +import chroma, flatty/binny, pixie/common, pixie/images, math, bitty + +const gifSignatures* = @["GIF87a", "GIF89a"] + +# See: https://en.wikipedia.org/wiki/GIF + +proc decodeGIF*(data: string): Image = + ## Decodes GIF data into an Image. + + let version = data[0 .. 5] + if version notin gifSignatures: + raise newException(PixieError, "Invalid GIF file signature.") + + let + # Read information about the image. + width = data.readInt16(6).int + height = data.readInt16(8).int + flags = data.readUint8(10).int + hasColorTable = (flags and 0x80) != 0 + originalDepth = ((flags and 0x70) shr 4) + 1 + colorTableSorted = (flags and 0x8) != 0 + colorTableSize = 2 ^ ((flags and 0x7) + 1) + bgColorIndex = data.readUint8(11) + pixelAspectRatio = data.readUint8(11) + + result = newImage(width, height) + + # Read the main color table. + var colors: seq[ColorRGBA] + var i = 0xD + if hasColorTable: + for c in 0 ..< colorTableSize: + let + r = data.readUint8(i + 0) + g = data.readUint8(i + 1) + b = data.readUint8(i + 2) + colors.add(rgba(r, g, b, 255)) + i += 3 + + # Read the image blocks. + while true: + let blockType = data.readUint8(i) + i += 1 + case blockType: + of 0x2c: # IMAGE + let + left = data.readUint16(i + 0) + top = data.readUint16(i + 2) + width = data.readUint16(i + 4) + height = data.readUint16(i + 6) + flags = data.readUint8(i + 8) + + hasColorTable = (flags and 0x80) != 0 + interlace = (flags and 0x40) != 0 + colorTableSorted = (flags and 0x8) != 0 + colorTableSize = 2 ^ ((flags and 0x7) + 1) + + i += 9 + + # Make sure we support the GIF features. + if left != 0 and top != 0 and + width.int != result.width and height.int != result.height: + raise newException(PixieError, "Image block offsets not supported.") + + if hasColorTable: + raise newException(PixieError, "Color table per block not supported.") + + if interlace: + raise newException(PixieError, "Interlacing not supported.") + + # Read the lzw data chunks. + let lzwMinBitSize = data.readUint8(i) + i += 1 + var lzwData = "" + while true: + let lzwEncodedLen = data.readUint8(i) + i += 1 + if lzwEncodedLen == 0: + # Stop reading when chunk len is 0. + break + lzwData.add data[i ..< i + lzwEncodedLen.int] + i += lzwEncodedLen.int + + let + clearMark = 1 shl lzwMinBitSize + endMark = clearMark + 1 + + # Turn full lzw data into bit stream. + var + bs = newBitStream(lzwData) + bitSize = lzwMinBitSize + 1 + currentCodeTableMax = (1 shl (bitSize)) - 1 + codeLast: int = -1 + codeTable: seq[seq[int]] + colorIndexes: seq[int] + + # Main decode loop. + while codeLast != endMark: + + var + # Read variable bits out of the table. + codeId = bs.read(bitSize.int) + # Some time we need to carry over table information. + carryOver: seq[int] + + if codeId == clearMark: + # Clear and re-init the tables. + bitSize = lzwMinBitSize + 1 + currentCodeTableMax = (1 shl (bitSize)) - 1 + codeLast = -1 + codeTable.setLen(0) + for x in 0 ..< endMark + 1: + codeTable.add(@[x]) + + elif codeId == endMark: + # Exit we are done. + break + + elif codeId < codeTable.len and codeTable[codeId].len > 0: + # Its in the current table, use it. + let current = codeTable[codeId] + colorIndexes.add(current) + carryOver = @[current[0]] + + elif codeLast != -1 and codeLast != clearMark and codeLast != endMark: + # Its in the current table use it. + var previous = codeTable[codeLast] + carryOver = @[previous[0]] + colorIndexes.add(previous & carryOver) + + if codeTable.len == currentCodeTableMax and bitSize < 12: + # We need to expand the codeTable max and the bit size. + inc bitSize + currentCodeTableMax = (1 shl (bitSize)) - 1 + + if codeLast != -1 and codeLast != clearMark and codeLast != endMark: + # We had some left over and need to expand table. + codeTable.add(codeTable[codeLast] & carryOver) + + codeLast = codeId + + # Convert color indexes into real colors. + for j, idx in colorIndexes: + result.data[j] = colors[idx] + + of 0x21: + # Skip over all extensions (mostly animation information) + let extentionType = data.readUint8(i) + inc i + let byteLen = data.readUint8(i) + inc i + i += byteLen.int + doAssert data.readUint8(i) == 0 + inc i + of 0x3b: + # Exit block byte - we are done. + return + else: + raise newException(PixieError, "Invalid GIF block type.") diff --git a/tests/images/gif/3x5.gif b/tests/images/gif/3x5.gif new file mode 100644 index 0000000..759169a Binary files /dev/null and b/tests/images/gif/3x5.gif differ diff --git a/tests/images/gif/3x5.png b/tests/images/gif/3x5.png new file mode 100644 index 0000000..5de239e Binary files /dev/null and b/tests/images/gif/3x5.png differ diff --git a/tests/images/gif/audrey.gif b/tests/images/gif/audrey.gif new file mode 100644 index 0000000..d6b3dac Binary files /dev/null and b/tests/images/gif/audrey.gif differ diff --git a/tests/images/gif/audrey.png b/tests/images/gif/audrey.png new file mode 100644 index 0000000..a2aace5 Binary files /dev/null and b/tests/images/gif/audrey.png differ diff --git a/tests/images/gif/sunflower.gif b/tests/images/gif/sunflower.gif new file mode 100644 index 0000000..c895d7e Binary files /dev/null and b/tests/images/gif/sunflower.gif differ diff --git a/tests/images/gif/sunflower.png b/tests/images/gif/sunflower.png new file mode 100644 index 0000000..e9b8c9b Binary files /dev/null and b/tests/images/gif/sunflower.png differ diff --git a/tests/test_gif.nim b/tests/test_gif.nim new file mode 100644 index 0000000..852607b --- /dev/null +++ b/tests/test_gif.nim @@ -0,0 +1,41 @@ +import pixie/fileformats/gif, pixie/fileformats/png, print, parseutils, flatty/hexprint, flatty/binlisting + +# let binary = decodeBinListing """ +# 0: 47 49 46 38 39 61 +# 6: 03 00 +# 8: 05 00 +# A: F7 +# B: 00 +# C: 00 +# D: 00 00 00 +# 10: 80 00 00 +# 85: 00 00 00 +# 30A: FF FF FF +# 30D: 21 F9 +# 30F: 04 +# 310: 01 +# 311: 00 00 +# 313: 10 16 +# 314: 00 +# 315: 2C +# 316: 00 00 00 00 +# 31A: 03 00 05 00 +# 31E: 00 +# 31F: 08 +# 320: 0B +# 321: 00 51 FC 1B 28 70 A0 C1 83 01 01 +# 32C: 00 +# 32D: 3B +# """ + +# echo hexPrint(binary) +# writeFile("tests/images/gif/3x5.gif", binary) + +var img = decodeGIF(readFile("tests/images/gif/3x5.gif")) +writeFile("tests/images/gif/3x5.png", img.encodePng()) + +var img2 = decodeGIF(readFile("tests/images/gif/audrey.gif")) +writeFile("tests/images/gif/audrey.png", img2.encodePng()) + +var img3 = decodeGIF(readFile("tests/images/gif/sunflower.gif")) +writeFile("tests/images/gif/sunflower.png", img3.encodePng())