2021-04-10 23:05:40 (UTC-03:00)
Marcel Rodrigues <marcelgmr@gmail.com>
add GIF encoding
diff --git a/gif.lua b/gif.lua new file mode 100644 index 0000000..16c48d6 --- /dev/null +++ b/gif.lua @@ -0,0 +1,88 @@ +local ffi = require "ffi" +local bit = require "bit" + +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 + +local function write_num(f, n) + f:write(string.char(band(n, 0xFF), rshift(n, 8))) +end + +local function write_nums(f, ...) + local nums = {...} + for i, n in pairs(nums) do + write_num(f, n) + end +end + +local GIFout = {} +GIFout.__index = GIFout + +local function new_gifout(f, w, h, depth, gct, frame, back) + if type(f) == "string" then f = io.open(f, "wb") end + local self = setmetatable({f=f, w=w, h=h, d=depth, gct=gct}, GIFout) + -- TODO: use ByteMap (NYI) if depth > 1 + assert(depth == 1) + self.frame = frame or surf.new_bitmap(w, h) + self.back = back or surf.new_bitmap(w, h) + f:write("GIF89a") + write_nums(f, w, h) + f:write(string.char(0xF0 + depth - 1, 0, 0)) -- FDSZ, BGINDEX, ASPECT + f:write(ffi.string(gct, 3 * 2 ^ depth)) + -- TODO: write Netscape Application Extension (loop) here + self.n = 0 -- # of frames added + return self +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() + 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 frame, back = self.frame, 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(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, self.frame, x, y, w, h) -- IP (Appendix F) +end + +function GIFout:add_frame(delay) + if delay then self:set_delay(delay) end + local x, y, w, h = self:get_bbox() + self:put_image(x, y, w, h) + self.n = self.n + 1 + self.frame, self.back = self.back, self.frame +end + +function GIFout:close() + self.f:write(";") + self.f:close() +end + +return {new_gifout=new_gifout} diff --git a/lzw.lua b/lzw.lua new file mode 100644 index 0000000..07f9ecb --- /dev/null +++ b/lzw.lua @@ -0,0 +1,104 @@ +local ffi = require "ffi" +local bit = require "bit" + +local bnot = bit.bnot +local bor, band = bit.bor, bit.band +local lshift, rshift = bit.lshift, bit.rshift + +local BUFout = {} +BUFout.__index = BUFout + +local function new_buffer(f) + local self = setmetatable({f=f, offset=0, partial=0}, BUFout) + self.buf = ffi.new("char[255]") + return self +end + +function BUFout:put_key(key, size) + local offset, partial = self.offset, self.partial + local f, buf = self.f, self.buf + local byte_offset, bit_offset = math.floor(offset / 8), offset % 8 + partial = bor(partial, lshift(key, bit_offset)) + local bits_to_write = bit_offset + size + while bits_to_write >= 8 do + buf[byte_offset] = band(partial, 0xFF) + byte_offset = byte_offset + 1 + if byte_offset == 0xFF then -- flush + f:write(string.char(0xFF)) + f:write(ffi.string(buf, 0xFF)) + byte_offset = 0 + end + partial = rshift(partial, 8) + bits_to_write = bits_to_write - 8 + end + self.offset = (offset + size) % (0xFF * 8) + self.partial = partial +end + +function BUFout:end_key() + local offset, partial = self.offset, self.partial + local f, buf = self.f, self.buf + local byte_offset = math.floor(offset / 8) + if offset % 8 ~= 0 then + buf[byte_offset] = band(partial, 0xFF) + byte_offset = byte_offset + 1 + end + if byte_offset > 0 then + f:write(string.char(byte_offset)) + f:write(ffi.string(buf, byte_offset)) + end + f:write(string.char(0)) + self.offset, self.partial = 0, 0 +end + + +local function new_trie(degree) + local children = {} + for key = 0, degree-1 do + children[key] = {key=key, children={}} + end + return {children=children} +end + +local function encode(f, d, s, x, y, w, h) + local buf = new_buffer(f) + local code_size = math.max(d, 2) + f:write(string.char(code_size)) + local degree = 2 ^ code_size + local root = new_trie(degree) + local clear, stop = degree, degree + 1 + local nkeys = degree + 2 -- skip clear code and stop code + local node = root + local key_size = code_size + 1 + buf:put_key(clear, key_size) + for j = y, y+h-1 do + for i = x, x+w-1 do + local index = band(s:pget(i, j), degree-1) + local child = node.children[index] + if child ~= nil then + node = child + else + buf:put_key(node.key, key_size) + if nkeys < 0x1000 then + if nkeys == 2 ^ key_size then + key_size = key_size + 1 + end + node.children[index] = {key=nkeys, children={}} + nkeys = nkeys + 1 + else + buf:put_key(clear, key_size) + root = new_trie(degree) + node = root + nkeys = degree + 2 + key_size = code_size + 1 + end + node = root.children[index] + end + end + end + buf:put_key(node.key, key_size) + buf:put_key(stop, key_size) + buf:end_key() +end + +return {encode=encode}