local ffi = require "ffi"
local bit = require "bit"
local bio = require "bio"
local surf = require "surf"
local lzw = require "lzw"
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, ...)
local nums = {...}
for i, n in pairs(nums) do
write_num(f, n)
end
end
local function get_depth(n)
local m, e = math.frexp(n-1)
return math.max(e, 1)
end
local GIFout = {}
GIFout.__index = GIFout
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)
if depth == 1 then
self.back = surf.new_bitmap(w, h)
else
self.back = surf.new_bytemap(w, h)
end
f:write("GIF89a")
write_nums(f, w, h)
f:write(string.char(0xF0 + depth - 1, 0, 0)) -- FDSZ, BGINDEX, ASPECT
local i = 1
while i <= #colors do
f:write(string.char(unpack(colors[i])))
i = i + 1
end
while i <= 2^depth do -- GCT size must be a power of two
f:write(string.char(0, 0, 0)) -- fill unused colors as black
i = i + 1
end
self.n = 0 -- # of frames added
return self
end
function GIFout:set_loop(n)
n = n or 0
self.f:write("!")
self.f:write(string.char(0xFF, 0x0B))
self.f:write("NETSCAPE2.0")
self.f:write(string.char(0x03, 0x01))
write_num(self.f, n)
self.f:write(string.char(0))
end
function GIFout:set_delay(d)
self.f:write("!")
self.f:write(string.char(0xF9, 0x04, 0x04))
write_num(self.f, d)
self.f:write(string.char(0, 0))
end
function GIFout:get_bbox(frame)
local w, h = self.w, self.h
if self.n == 0 then return 0, 0, w, h end
local xmin, ymin = w, h
local xmax, ymax = 0, 0
local back = self.back
for y = 0, h-1 do
for x = 0, w-1 do
if frame:pget(x, y) ~= back:pget(x, y) then
if x < xmin then xmin = x end
if y < ymin then ymin = y end
if x > xmax then xmax = x end
if y > ymax then ymax = y end
end
end
end
if xmin == w or ymin == h then return 0, 0, 1, 1 end
return xmin, ymin, xmax-xmin+1, ymax-ymin+1
end
function GIFout:put_image(frame, x, y, w, h)
self.f:write(",")
write_nums(self.f, x, y, w, h)
self.f:write(string.char(0))
lzw.encode(self.f, self.d, frame, x, y, w, h) -- IP (Appendix F)
end
function GIFout:add_frame(frame, delay)
if delay then self:set_delay(delay) end
local x, y, w, h = self:get_bbox(frame)
self:put_image(frame, x, y, w, h)
self.n = self.n + 1
self.back:blit(x, y, frame, x, y, w, h)
end
function GIFout:close()
self.f:write(";")
self.f:close()
end
-- == 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}