login

<     >

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}