Skip to content

Instantly share code, notes, and snippets.

@ArvidSilverlock
Last active January 8, 2025 07:42
Show Gist options
  • Select an option

  • Save ArvidSilverlock/38e25c9ddba8d63be3314580cd08c28d to your computer and use it in GitHub Desktop.

Select an option

Save ArvidSilverlock/38e25c9ddba8d63be3314580cd08c28d to your computer and use it in GitHub Desktop.
--!native
--!optimize 2
type Buffer = {
buffer: buffer,
offset: number,
}
export type GIF = {
width: number,
height: number,
size: Vector2,
backgroundIndex: number,
globalPalette: buffer,
frames: { Frame },
}
export type Frame = {
left: number,
top: number,
position: Vector2,
width: number,
height: number,
size: Vector2,
isInterlaced: boolean,
localPalette: buffer,
palette: buffer,
paletteIndexes: buffer,
frameDelay: number,
transparentIndex: number,
disposalMethod: number,
}
local bitreader
local DecompressLZW
do -- bitreader
bitreader = {}
bitreader.__index = bitreader
local function nextChunk(reader, width)
if width > 24 then
reader._chunk += math.ldexp(buffer.readu32(reader._buffer, reader._byte), reader._bit)
reader._byte += 4
reader._bit += 32
elseif width > 16 then
reader._chunk += math.ldexp(
buffer.readu8(reader._buffer, reader._byte) + buffer.readu16(reader._buffer, reader._byte + 1) * 2 ^ 8,
reader._bit
)
reader._byte += 3
reader._bit += 24
elseif width > 8 then
reader._chunk += math.ldexp(buffer.readu16(reader._buffer, reader._byte), reader._bit)
reader._byte += 2
reader._bit += 16
else
reader._chunk += math.ldexp(buffer.readu8(reader._buffer, reader._byte), reader._bit)
reader._byte += 1
reader._bit += 8
end
end
function bitreader.new(b: buffer)
return setmetatable({
_buffer = b,
_byte = 0,
_bit = 0,
_chunk = 0,
}, bitreader)
end
function bitreader:UInt(width: number): number
if self._bit < width then nextChunk(self, width - self._bit) end
local value = self._chunk % 2 ^ width
self._chunk = self._chunk // 2 ^ width
self._bit -= width
return value
end
function bitreader:SetByteOffset(byte: number)
self._chunk = 0
self._bit = 0
self._byte = byte
end
function bitreader:GetByteOffset(byte: number)
return self._byte
end
end
do -- DecompressLZW
local AVERAGE_STACK_HEIGHT = 7
local AVERAGE_COMPRESSION = 1.4
local TRUE_MAX_VOCABULARY = 2 ^ 12
function DecompressLZW(b, initialCodeSize)
local clearCode = bit32.lshift(1, initialCodeSize - 1)
local endCode = clearCode + 1
-- kind of a linked listed, but in reverse
local vocabulary1 = table.create(TRUE_MAX_VOCABULARY)
local vocabulary2 = table.create(TRUE_MAX_VOCABULARY)
local codeSize, vocabularyMax
local nextCode, previousCode
local output, length = table.create(math.floor(buffer.len(b) / AVERAGE_COMPRESSION)), 0
local function resetVariables()
codeSize = initialCodeSize
vocabularyMax = bit32.lshift(1, codeSize)
nextCode = clearCode + 2
previousCode = nextCode - 1
end
resetVariables()
local reader = bitreader.new(b)
repeat
local code = reader:UInt(codeSize)
if code == clearCode then
table.clear(vocabulary1)
table.clear(vocabulary2)
resetVariables()
elseif code ~= endCode then
local valueStack
local stackIndex = 1
local value = code
if value > clearCode then
valueStack = table.create(AVERAGE_STACK_HEIGHT)
repeat
value, valueStack[stackIndex] = vocabulary1[value], vocabulary2[value]
stackIndex += 1
until value < clearCode
valueStack[stackIndex] = value
else
valueStack = { code }
end
if vocabulary1[previousCode] and vocabulary2[previousCode] == nil then
vocabulary2[previousCode] = value
if code == previousCode then valueStack[1] = value end
end
if nextCode < TRUE_MAX_VOCABULARY then
vocabulary1[nextCode] = code
if nextCode == vocabularyMax then
codeSize = math.min(codeSize + 1, 12)
vocabularyMax = bit32.lshift(1, codeSize)
end
previousCode = nextCode
nextCode += 1
end
table.insert(output, valueStack)
length += stackIndex
end
until code == endCode
local outputBuffer, outputOffset = buffer.create(length), 0
for _, value in output do
for index = 0, #value - 1 do
buffer.writeu8(outputBuffer, outputOffset + index, value[#value - index])
end
outputOffset += #value
end
return outputBuffer
end
end
local GIF_IDENTIFIER = "GIF89a"
local IMAGE_DESCRIPTOR = 44
local EXTENSION = 33
local TRAILER = 59
local function readBlock(b: Buffer, gif: GIF)
local blockTable = {}
while true do
local blockSize = buffer.readu8(b.buffer, b.offset)
b.offset += 1
if blockSize > 0 then
table.insert(blockTable, buffer.readstring(b.buffer, b.offset, blockSize))
b.offset += blockSize
else
break
end
end
return buffer.fromstring(table.concat(blockTable))
end
local function skipBlock(b: Buffer, gif: GIF)
repeat
local blockSize = buffer.readu8(b.buffer, b.offset)
b.offset += blockSize + 1
until blockSize == 0
end
local function getPalette(b: Buffer, gif: GIF, paletteSize: number, colourResolution: number)
local palette = buffer.create(paletteSize * 3)
local reader = bitreader.new(b.buffer)
reader:SetByteOffset(b.offset)
local max = bit32.lshift(1, colourResolution) - 1
for index = 0, paletteSize * 3 - 1 do
buffer.writeu8(palette, index, math.floor(reader:UInt(colourResolution) / max * 255))
end
b.offset = reader:GetByteOffset()
return palette
end
local function parseImage(b: Buffer, gif: GIF, frame: Frame)
frame.left = buffer.readu16(b.buffer, b.offset)
frame.top = buffer.readu16(b.buffer, b.offset + 2)
frame.width = buffer.readu16(b.buffer, b.offset + 4)
frame.height = buffer.readu16(b.buffer, b.offset + 6)
local field = buffer.readu8(b.buffer, b.offset + 8)
b.offset += 9
frame.position = Vector2.new(frame.left, frame.top)
frame.size = Vector2.new(frame.width, frame.height)
frame.isInterlaced = bit32.extract(field, 6, 1) == 1
local localPalettePresent = bit32.extract(field, 7, 1) == 1
if localPalettePresent then
local paletteSize = 2 ^ (bit32.extract(field, 0, 3) + 1)
frame.localPalette = getPalette(b, gif, paletteSize, 8)
end
local lzwCodeSize = buffer.readu8(b.buffer, b.offset) + 1
b.offset += 1
frame.paletteIndexes = DecompressLZW(readBlock(b, gif), lzwCodeSize)
end
local function runExtensions(b: Buffer, gif: GIF, frame)
local label = buffer.readu8(b.buffer, b.offset)
b.offset += 1
if label == 1 or label == 254 or label == 255 then -- Plain Text, Graphic Control, Comment, Application
skipBlock(b, gif)
else
local block = readBlock(b, gif)
if label == 249 then
local packedFields = buffer.readu8(block, 0)
frame.frameDelay = buffer.readu16(block, 1) / 100
frame.transparentIndex = if bit32.extract(packedFields, 0, 1) == 1 then buffer.readu8(block, 3) else nil
frame.disposalMethod = bit32.extract(packedFields, 2, 3)
else
error(`unrecognized extension label '{label}'`)
end
end
end
local function DecodeGif(gifData: string): (GIF, number)
local gif: GIF = {} :: any
local b: Buffer = { buffer = buffer.fromstring(gifData), offset = 0 }
assert(buffer.readstring(b.buffer, 0, 6) == GIF_IDENTIFIER, "wrong file type")
b.offset += 6
gif.width = buffer.readu16(b.buffer, b.offset)
gif.height = buffer.readu16(b.buffer, b.offset + 2)
gif.size = Vector2.new(gif.width, gif.height)
b.offset += 4
local field = buffer.readu8(b.buffer, b.offset)
gif.backgroundIndex = buffer.readu8(b.buffer, b.offset + 1) * 3
b.offset += 3
local globalPalettePresent = bit32.extract(field, 7, 1) == 1
if globalPalettePresent then
local paletteSize = 2 ^ (bit32.extract(field, 0, 3) + 1)
local colourResolution = bit32.extract(field, 4, 3) + 1
gif.globalPalette = getPalette(b, gif, paletteSize, colourResolution)
end
local lastWait = os.clock()
local yieldTime = 0
local currentFrame: Frame = {} :: any
gif.frames = {}
while true do
local blockType = buffer.readu8(b.buffer, b.offset)
b.offset += 1
if os.clock() - lastWait > 1 / 60 then
yieldTime += task.wait(1 / 60)
lastWait = os.clock()
end
if blockType == IMAGE_DESCRIPTOR then
parseImage(b, gif, currentFrame)
table.insert(gif.frames, currentFrame)
currentFrame = {} :: any
elseif blockType == EXTENSION then
runExtensions(b, gif, currentFrame)
elseif blockType == TRAILER then
break
else
warn(`unrecognized block type '{blockType}'`)
end
end
return gif, yieldTime
end
return DecodeGif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment