2021-08-27 10:25:04 (UTC-03:00)
Marcel Rodrigues <marcelgmr@gmail.com>
gif: groundwork for GIF decoder
diff --git a/README.md b/README.md index dedbccf..f85ad3d 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ surf:save_ppm("surf-aa.ppm", palette) ## Exporting to GIF (`anim.gif`) ```lua -gif = anim.gif.new_gifout(f, w, h, colors) -- f is the file name +gif = anim.gif.new_gif(f, w, h, colors) -- f is the file name gif:add_frame(surf1, delay) -- delay is in hundreths of a second gif:add_frame(surf2, delay) gif:add_frame(surf3, delay) diff --git a/gif.lua b/gif.lua index de18fc9..06fc021 100644 --- a/gif.lua +++ b/gif.lua @@ -9,6 +9,8 @@ local bnot = bit.bnot local bor, band = bit.bor, bit.band local lshift, rshift = bit.lshift, bit.rshift +-- == Encoder == + local write_num = bio.write_lei16 local function write_nums(f, ...) @@ -26,7 +28,7 @@ end local GIFout = {} GIFout.__index = GIFout -local function new_gifout(f, w, h, colors) +local function new_gif(f, w, h, colors) if type(f) == "string" then f = io.open(f, "wb") end local depth = get_depth(#colors) local self = setmetatable({f=f, w=w, h=h, d=depth, gct=colors}, GIFout) @@ -108,4 +110,121 @@ function GIFout:close() self.f:close() end -return {new_gifout=new_gifout} +-- == Decoder == + +local read_num = bio.read_lei16 + +local GIFin = {} +GIFin.__index = GIFin + +local function open_gif(f) + if type(f) == "string" then f = io.open(f, "rb") end + assert(f:read(6) == "GIF89a", "invalid signature") + local w, h = read_num(f), read_num(f) + local fdsz = bio.read_byte(f) + local has_gct = rshift(fdsz, 7) == 1 + local d = band(fdsz, 7) + 1 + local bg = bio.read_byte(f) + f:seek("cur", 1) -- ASPECT:u8 + local self = setmetatable({f=f, w=w, h=h, d=d, bg=bg}, GIFin) + self.gct = has_gct and self:read_color_table(d) or {} + if d == 1 then + self.surf = surf.new_bitmap(w, h) + else + self.surf = surf.new_bytemap(w, h) + end + return self +end + +function GIFin:read_color_table(d) + local ct = {} + local ncolors = lshift(1, d) + for i = 1, ncolors do + local r = bio.read_byte(self.f) + local g = bio.read_byte(self.f) + local b = bio.read_byte(self.f) + table.insert(ct, {r, g, b}) + end + return ct +end + +function GIFin:discard_sub_blocks() + repeat + local size = bio.read_byte(self.f) + self.f:seek("cur", size) + until size == 0 +end + +function GIFin:read_graphic_control_ext() + assert(bio.read_byte(self.f) == 0x04, "invalid GCE block size") + local rdit = bio.read_byte(self.f) + self.disposal = band(rshift(rdit, 2), 3) + self.input = band(rshift(rdit, 1), 1) == 1 + local transp = band(rdit, 1) == 1 + self.delay = read_num(self.f) + local tindex = bio.read_byte(self.f) + self.tindex = transp and tindex or -1 + self.f:seek("cur", 1) -- end-of-block +end + +function GIFin:read_application_ext() + assert(bio.read_byte(self.f) == 0x0B, "invalid APP block size") + local app_ip = self.f:read(8) + local app_auth_code = self.f:read(3) + if app_ip == "NETSCAPE" then + self.f:seek("cur", 2) -- always 0x03, 0x01 + self.loop = read_num(self.f) + self.f:seek("cur", 1) -- end-of-block + else + self:discard_sub_blocks() + end +end + +function GIFin:read_ext() + local label = bio.read_byte(self.f) + if label == 0xF9 then + self:read_graphic_control_ext() + elseif label == 0xFF then + self:read_application_ext() + else + error(("unknown extension: %02X"):format(label)) + end +end + +function GIFin:read_image() + local x, y = read_num(self.f), read_num(self.f) + local w, h = read_num(self.f), read_num(self.f) + local fisrz = bio.read_byte(self.f) + local has_lct = band(rshift(fisrz, 7), 1) == 1 + --~ assert(not has_lct, "unsupported GIF feature: Local Color Table") + local interlace = band(rshift(fisrz, 6), 1) == 1 + assert(not interlace, "unsupported GIF feature: Interlaced Frame") + local d = band(fisrz, 7) + 1 + local lct = has_lct and self:read_color_table(d) or {} + d = has_lct and d or self.d + lzw.decode(self.f, d, self.surf, x, y, w, h) -- IP (Appendix F) +end + +function GIFin:get_frame() + local sep = self.f:read(1) + while sep ~= "," do + if sep == ";" then + return 0 + elseif sep == "!" then + self:read_ext() + else + return -1 + end + sep = self.f:read(1) + end + if self:read_image() == -1 then + return -1 + end + return 1 +end + +function GIFin:close() + self.f:close() +end + +return {new_gif=new_gif, open_gif=open_gif} diff --git a/lzw.lua b/lzw.lua index 07f9ecb..2415b58 100644 --- a/lzw.lua +++ b/lzw.lua @@ -5,6 +5,8 @@ local bnot = bit.bnot local bor, band = bit.bor, bit.band local lshift, rshift = bit.lshift, bit.rshift +-- == Encoder == + local BUFout = {} BUFout.__index = BUFout @@ -101,4 +103,15 @@ local function encode(f, d, s, x, y, w, h) buf:end_key() end -return {encode=encode} +-- == Decoder == + +local function decode(f, d, s, x, y, w, h) + local code_size = f:read(1):byte(1) + assert(code_size == math.max(d, 2), "invalid code size") + repeat + local size = f:read(1):byte(1) + f:seek("cur", size) + until size == 0 +end + +return {encode=encode, decode=decode}