#!/usr/bin/env luajit --Converts sequence of images to Roland FA 06/08 screensaver format ("0001.BIN") --See here for details and instructions: http://www.fuxoft.cz/vyplody/roland_fa_screensaver/ --Version 2016.07.31 local bit32 = assert(require("bit")) log = print log = function() end local width, height = 480,272 local function in_stream(fin) local com = string.format("convert %s -resize %sx%s! -strip -compress none ppm:-",fin, width,height) --add e.g. "-dither FloydSteinberg -colors 256" to previous command to increase the compression ratio. Smaller numbers mean less colors in images and the better compression ratio local fd = io.popen(com) local txt = fd:read("*a") fd:close() local cor = coroutine.wrap( function () for str in txt:gmatch("(%w+)") do coroutine.yield(str) end coroutine.yield(false) end) local f = cor() if not f then return false end assert(f == "P3") assert(cor() == "480") assert(cor() == "272") assert(cor() == "255") return cor end local fd = assert(io.open("0001.BIN","w")) local write_bytes = function(n, val) assert (n >=1 and n<=4) local bytes = {} for i = 1, n do table.insert(bytes,bit32.band(val, 0xff)) val = bit32.rshift(val, 8) end for i = 1, n do local byte = table.remove(bytes) fd:write(string.char(byte)) end end fd:write("XMVh") write_bytes(2, width) write_bytes(2, height) fd:write("\x03\xc0\x10\x01") write_bytes(4, 0x12345678) local compression = {num = 0, sum = 0} local biggest = {size = -1} local images = {} local img = 1 while true do local time = os.time() local compressed if true then local fname = string.format("sequence/img%04d.png",img) local fetch = in_stream(fname) if not fetch then img = img - 1 goto done end print("Compressing image: "..img) local imgdata = {} local ptr = 0 for row = 0, height - 1 do for col = 0, width - 1 do local rgb = {} for i = 1,3 do local byte = tonumber(fetch()) assert(byte >= 0 and byte <=0xff) table.insert(rgb, byte/255) end local r = math.floor(rgb[1]*31 + 0.5) local g = math.floor(rgb[2]*63 + 0.5) local b = math.floor(rgb[3]*31 + 0.5) local word = (r*64 + g) * 32 + b assert(word <= 0xffff) imgdata[ptr] = bit32.rshift(bit32.band(word,0xff00),8) imgdata[ptr+1] = bit32.band(word,0xff) ptr = ptr + 2 end end assert(#imgdata == 2*width*height - 1) --compress local inter = {} local ptr = 0 repeat local found = {} for offset = -1, -4070, -1 do if ptr + offset < 0 then goto stop_search end if imgdata[ptr+offset] == imgdata[ptr] and imgdata[ptr+offset+1] == imgdata[ptr + 1] and imgdata[ptr+offset+2] == imgdata[ptr + 2] then for i = 3, 17 do if imgdata[ptr+offset+i] ~= imgdata[ptr+i] then --we found match (this byte does NOT match) if i <= (found.len or 0) then goto match_handled end found.len = i found.offset = offset goto match_handled end end --we found 18 matching bytes, end immediately, we cannot find anything better found.len = 18 found.offset = offset goto stop_search end :: match_handled :: end :: stop_search :: if found.len then local off2 = bit32.band(ptr+found.offset-18+4096, 0xfff) found.byte1=bit32.band(off2, 0xff) found.byte2=bit32.band(bit32.rshift(off2,4), 0xf0) + found.len - 3 table.insert(inter, found) local bytes = {} for i = 0, found.len - 1 do table.insert(bytes,string.format("%02X", imgdata[ptr+i])) end log(string.format("%s - from %d, %d b, %03X, %02X %02X", table.concat(bytes, " "), found.offset, found.len, off2, found.byte1, found.byte2)) ptr = ptr + found.len else --single byte table.insert(inter, imgdata[ptr]) log(string.format(" *%02X",imgdata[ptr])) ptr = ptr + 1 end until not(imgdata[ptr]) compressed = {0,3,252,0} --3fc00 for i = 1, #inter, 8 do local byte = 0 for j = 0, 7 do byte = bit32.rshift(byte,1) if type(inter[i+j]) == "number" then byte = byte + 128 end end table.insert(compressed, byte) for j = 0, 7 do local item = inter[i+j] if item then if type(item) == "number" then table.insert(compressed, item) else table.insert(compressed, item.byte1) table.insert(compressed, item.byte2) end end end end else local img2 = img%50 + 280 local orig = assert(io.open(string.format("original/img%04d.dat", img2))) local data = orig:read("*a") orig:close() assert(#data > 9999) compressed = {} for i = 1, #data do compressed[i] = string.byte(data:sub(i,i)) end end if #compressed > biggest.size then biggest.size = #compressed biggest.num = img end local ratio = #compressed/(width*height*2)*100 compression.num = compression.num + 1 compression.sum = compression.sum + ratio print(string.format("Compressed to %s, %s%%, %s secs, biggest=%s, avg.compr=%s%%",#compressed, math.floor(ratio + 0.5),os.time()-time,biggest.size, math.floor(compression.sum / compression.num + 0.5))) fd:write("XMVf") write_bytes(4,1) local prevsize = 0 if images[img-1] then prevsize = assert(images[img-1].size) end write_bytes(4,prevsize) local size = #compressed write_bytes(4,size) table.insert(images,{size = size}) for i,byte in ipairs(compressed) do write_bytes(1,byte) end --It seems there is a bug in some versions of LuaJit that can result in memory leak. The following line prevents this, just to be safe... collectgarbage() --print("memory: "..collectgarbage("count")) img = img + 1 end :: done :: fd:seek("set", 12) write_bytes(4, img) fd:close() print(img.." images finished OK!") print("(Ignore eventual errors above)") print(string.format("Biggest image: #%s, %s bytes", biggest.num, biggest.size))