From 4333ccd5d4f79b3dae931108dfd52dddb5657fb6 Mon Sep 17 00:00:00 2001 From: Igor Timofeev Date: Wed, 5 Aug 2015 03:46:16 +0300 Subject: [PATCH] Create libPNGimage.lua --- libPNGimage.lua | 641 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 641 insertions(+) create mode 100644 libPNGimage.lua diff --git a/libPNGimage.lua b/libPNGimage.lua new file mode 100644 index 00000000..c3612808 --- /dev/null +++ b/libPNGimage.lua @@ -0,0 +1,641 @@ +local PNGImage = {} + +-- +-- libPNGimage by TehSomeLuigi +-- Revision: +PNGImage.rev = 1 +-- +-- A library to load, edit and save PNGs for OpenComputers +-- + +--[[ + + Feel free to use however you wish. + This header must however be preserved should this be redistributed, even + if in a modified form. + + This software comes with no warranties whatsoever. + + 2014 TehSomeLuigi + +]]-- + + +--local DEFLATE = require("libDEFLATE") +local DeflateLua = require("deflatelua") +local CRC32Lua = require("crc32lua") + +local bit = require("bit32") + +local PNGImagemetatable = {} +PNGImagemetatable.__index = PNGImage + + + + +PNGImage.ColourTypes = { + Greyscale=0, + Truecolour=2, + IndexedColour=3, + GreyscaleAlpha=4, + TruecolourAlpha=6 +} + + + + + + +local band = bit.band +local rshift = bit.rshift + + + +-- Unpack 32-bit unsigned integer (most-significant-byte, MSB, first) +-- from byte string. +local function __unpack_msb_uint32(s) + local a,b,c,d = s:byte(1,#s) + local num = (((a*256) + b) * 256 + c) * 256 + d + return num +end + +local function __write_msb_uint32(fh, int) + -- MSB B2 B1 LSB + + local msb = rshift(band(0xFF000000, int), 24) + local b2 = rshift(band(0x00FF0000, int), 16) + local b1 = rshift(band(0x0000FF00, int), 8) + local lsb = band(0x000000FF, int) + + fh:write(string.char(msb)) + fh:write(string.char(b2)) + fh:write(string.char(b1)) + fh:write(string.char(lsb)) +end + +local function __wbuf_msb_uint32(buf, int) + -- MSB B2 B1 LSB + + local msb = rshift(band(0xFF000000, int), 24) + local b2 = rshift(band(0x00FF0000, int), 16) + local b1 = rshift(band(0x0000FF00, int), 8) + local lsb = band(0x000000FF, int) + + buf(msb) + buf(b2) + buf(b1) + buf(lsb) +end + +local function __sep_msb_uint32(int) + -- MSB B2 B1 LSB + + local msb = rshift(band(0xFF000000, int), 24) + local b2 = rshift(band(0x00FF0000, int), 16) + local b1 = rshift(band(0x0000FF00, int), 8) + local lsb = band(0x000000FF, int) + + return msb, b2, b1, lsb +end + +local function __pack_msb_uint16(int) + return string.char(rshift(band(int, 0xFF00), 8), band(int, 0xFF)) +end + +local function __pack_lsb_uint16(int) + return string.char(band(int, 0xFF), rshift(band(int, 0xFF00), 8)) +end + +-- Read 32-bit unsigned integer (most-significant-byte, MSB, first) from file. +local function __read_msb_uint32(fh) + return __unpack_msb_uint32(fh:read(4)) +end + +-- Read unsigned byte (integer) from file +local function __read_byte(fh) + return fh:read(1):byte() +end + + + +local function getBitWidthPerPixel(ihdr) + if ihdr.color_type == PNGImage.ColourTypes.Greyscale then -- Greyscale + return ihdr.bit_depth + end + if ihdr.color_type == PNGImage.ColourTypes.Truecolour then -- Truecolour + return ihdr.bit_depth * 3 + end + if ihdr.color_type == PNGImage.ColourTypes.IndexedColour then -- Indexed-colour + return ihdr.bit_depth + end + if ihdr.color_type == PNGImage.ColourTypes.GreyscaleAlpha then -- Greyscale + Alpha + return ihdr.bit_depth * 2 + end + if ihdr.color_type == PNGImage.ColourTypes.TruecolourAlpha then -- Truecolour + Alpha + return ihdr.bit_depth * 4 + end +end + +local function getByteWidthPerScanline(ihdr) + return math.ceil((ihdr.width * getBitWidthPerPixel(ihdr)) / 8) +end + +local outssmt = {} + +function outssmt:__call(write) + self.str = self.str .. string.char(write) +end + +function outssmt.OutStringStream() + local outss = {str=""} + setmetatable(outss, outssmt) + return outss +end + + + + +local function __parse_IHDR(fh, len) + if len ~= 13 then + error("PNG IHDR Corrupt - should be 13 bytes long") + end + + local ihdr = {} + + ihdr.width = __read_msb_uint32(fh) + ihdr.height = __read_msb_uint32(fh) + ihdr.bit_depth = __read_byte(fh) + ihdr.color_type = __read_byte(fh) + ihdr.compression_method = __read_byte(fh) + ihdr.filter_method = __read_byte(fh) + ihdr.interlace_method = __read_byte(fh) + + --[[ + print("width=", ihdr.width) + print("height=", ihdr.height) + print("bit_depth=", ihdr.bit_depth) + print("color_type=", ihdr.color_type) + print("compression_method=", ihdr.compression_method) + print("filter_method=", ihdr.filter_method) + print("interlace_method=", ihdr.interlace_method) + ]]-- + + return ihdr +end + +--[[ +local function __parse_IDAT(fh, len, commeth) + if commeth ~= 0 then + error("Only zlib/DEFLATE compression supported") + end + + local d, msg = DEFLATE.inflate(fh, len); + + if not d then + return nil, msg + end + + local oh = io.open('dump.dat', 'wb') + oh:write(d.dat) + oh:close() + + return true +end +]]-- + +local function __parse_IDAT(fh, len, commeth, outss) + if commeth ~= 0 then + error("Only zlib/DEFLATE compression supported") + end + + + --local oh = io.open('dump.dat', 'wb') + --oh:write(d.dat) + + --local ph = io.open('pass.dat', 'wb') + + local input = fh:read(len) + + --ph:write(input) + --ph:close() + + local cfg = {input=input, output=outss, disable_crc=true} + + DeflateLua.inflate_zlib(cfg) + + --oh:close() + +-- if not d then +-- return nil, msg +-- end + + return true +end + + + +local function getPNGStdByteAtXY(ihdr, oss, x, y) + local bpsl = getByteWidthPerScanline(ihdr) -- don't include filterType byte -- we don't store that after it has been read + if (x <= 0) or (y <= 0) then + return 0 -- this is what the spec says we should return when the coordinate is out of bounds -- in this part of the code, the coordinates are ONE-BASED like in good Lua + end + local offset_by_y = (y - 1) * bpsl + -- now read it! + local idx = offset_by_y + x + return oss.str:sub(idx, idx):byte() +end + + +local function __paeth_predictor(a, b, c) + local p = a + b - c + local pa = math.abs(p - a) + local pb = math.abs(p - b) + local pc = math.abs(p - c) + if pa <= pb and pa <= pc then + return a + elseif pb <= pc then + return b + else + return c + end +end + + + +local function __parse_IDAT_effective_bytes(outss, ihdr) + local bpsl = getByteWidthPerScanline(ihdr) + local bypsl = math.ceil(getBitWidthPerPixel(ihdr) / 8) + + if outss.str:len() == 0 then + error("Empty string: outss") + end + + local bys = DeflateLua.stringToBytestream(outss.str) + + if not bys then + error("Did not get a bytestream from string", bys, outss) + end + + local out2 = outssmt.OutStringStream() -- __callable table with metatable that stores what you give it + + local y = 0 + + -- x the byte being filtered; + -- a the byte corresponding to x in the pixel immediately before the pixel containing x (or the byte immediately before x, when the bit depth is less than 8); + -- b the byte corresponding to x in the previous scanline; + -- c the byte corresponding to b in the pixel immediately before the pixel containing b (or the byte immediately before b, when the bit depth is less than 8). + + while true do + local filterType = bys:read() + + if filterType == nil then + break + end + + y = y + 1 + + for x = 1, bpsl do + --[..c..][..b..] + --[..a..][..x <--- what is being processed (x) + local a = getPNGStdByteAtXY(ihdr, out2, x - bypsl, y) + local b = getPNGStdByteAtXY(ihdr, out2, x, y - 1) + local c = getPNGStdByteAtXY(ihdr, out2, x - bypsl, y - 1) + + local outVal = 0 + + if filterType == 0 then + -- Recon(x) = Filt(x) + outVal = bys:read() + elseif filterType == 1 then + -- Recon(x) = Filt(x) + Recon(a) + outVal = bys:read() + a + elseif filterType == 2 then + -- Recon(x) = Filt(x) + Recon(b) + outVal = bys:read() + b + elseif filterType == 3 then + -- Recon(x) = Filt(x) + floor((Recon(a) + Recon(b)) / 2) + outVal = bys:read() + math.floor((a + b) / 2) + elseif filterType == 4 then + -- Recon(x) = Filt(x) + PaethPredictor(Recon(a), Recon(b), Recon(c)) + outVal = bys:read() + __paeth_predictor(a, b, c) + else + error("Unsupported Filter Type: " .. tostring(filterType)) + end + + outVal = outVal % 256 + + out2(outVal) + end + end + + return out2 +end + + + +local function __newPNGImage() + local pngi = {} + setmetatable(pngi, PNGImagemetatable) + return pngi +end + +function PNGImage.newFromFile(fn) + local fh = io.open(fn, 'rb') + if not fh then + error("Could not open PNG file") + end + return PNGImage.newFromFileHandle(fh) +end + +function PNGImage.newFromScratch(width, height, bkcol) + local pngi = __newPNGImage() + + local width = tonumber(width) + local height = tonumber(height) + + if (not width) or (width < 1) or (math.floor(width) ~= width) then + error("Invalid param #1 (width) to PNGImage.newFromScratch - integer (>=0) expected") + end + if (not height) or (height < 1) or (math.floor(height) ~= height) then + error("Invalid param #2 (height) to PNGImage.newFromScratch - integer (>=0) expected") + end + + local bkg = {0, 0, 0, 0} -- Transparency + + if type(bkcol) == "table" then + if #bkcol == 3 then + bkg = {bkcol[1], bkcol[2], bkcol[3], 255} -- Opaque colour + elseif #bkcol == 4 then + bkg = {bkcol[1], bkcol[2], bkcol[3], bkcol[4]} -- Defined + else + error("Invalid format for param #3 (bkcol) to PNGImage.newFromScratch: nil or table expected, but the table must be of format {r, g, b} or {r, g, b, a} -- Invalid table") + end + elseif bkcol ~= nil then + error("Invalid format for param #3 (bkcol) to PNGImage.newFromScratch: nil or table expected, but the table must be of format {r, g, b} or {r, g, b, a} -- Parameter is not nil or table") + end + + bkg[1] = tonumber(bkg[1]) + if bkg[1] == nil then + error("PNGImage.newFromScratch: bkg[R] is not numeric") + end + bkg[2] = tonumber(bkg[2]) + if bkg[2] == nil then + error("PNGImage.newFromScratch: bkg[G] is not numeric") + end + bkg[3] = tonumber(bkg[3]) + if bkg[3] == nil then + error("PNGImage.newFromScratch: bkg[B] is not numeric") + end + bkg[4] = tonumber(bkg[4]) + if bkg[4] == nil then + error("PNGImage.newFromScratch: bkg[A] is not numeric") + end + + pngi.ihdr = { + width = width, + height = height, + bit_depth = 8, + color_type = 6, + compression_method = 0, + filter_method = 0, + interlace_method = 0 + } + + -- do this for every pixel.. i.e string.rep(str, w*h) + pngi.data = string.byte(bkg[1], bkg[2], bkg[3], bkg[4]):rep(width * height) + + return pngi +end + +function PNGImage.newFromFileHandle(fh) + local pngi = __newPNGImage() + local expecting = "\137\080\078\071\013\010\026\010" + if fh:read(8) ~= expecting then -- check the 8-byte PNG header exists + error("Not a PNG file") + end + + local ihdr + + local outss = outssmt.OutStringStream() + + while true do + local len = __read_msb_uint32(fh) + local stype = fh:read(4) + + if stype == 'IHDR' then + ihdr, msg = __parse_IHDR(fh, len) + elseif stype == 'IDAT' then + local res, msg = __parse_IDAT(fh, len, ihdr.compression_method, outss) + else + fh:read(len) -- dummy read + end + + local crc = __read_msb_uint32(fh) + + -- print("chunk:", "type=", stype, "len=", len, "crc=", crc) + + if stype == 'IEND' then + break + end + end + + fh:close() + + + if ihdr.filter_method ~= 0 then + error("Unsupported Filter Method: " .. ihdr.filter_method) + end + + if ihdr.interlace_method ~= 0 then + error("Unsupported Interlace Method (Interlacing is currently unsupported): " .. ihdr.interlace_method) + end + + if ihdr.color_type ~= PNGImage.ColourTypes.TruecolourAlpha and ihdr.color_type ~= PNGImage.ColourTypes.Truecolour then + error("Currently, only Truecolour and Truecolour+Alpha images are supported.") + end + + if ihdr.bit_depth ~= 8 then + error("Currently, only images with a bit depth of 8 are supported.") + end + + --[[ + local oh = io.open('before-decode.dat', 'wb') + oh:write(outss.str) + oh:close() + ]]-- + + -- now parse the IDAT chunks + local out2 = __parse_IDAT_effective_bytes(outss, ihdr) + + if ihdr.color_type == PNGImage.ColourTypes.Truecolour then + -- add an alpha layer so it effectively becomes RGBA, not RGB + local inp = out2.str + out2 = outssmt.OutStringStream() + + for i=1, ihdr.width*ihdr.height do + local b = ((i - 1)*3) + 1 + out2(inp:byte(b)) -- R + out2(inp:byte(b + 1)) -- G + out2(inp:byte(b + 2)) -- B + out2(255) -- A + end + end + + pngi.ihdr = ihdr + pngi.data = out2.str + + --[[ + local oh = io.open('effective.dat', 'wb') + oh:write(out2.str) + oh:close() + ]]-- + + return pngi +end + + +-- Warning: Co-ordinates are Zero-based but strings are 1-based +function PNGImage:getByteOffsetForPixel(x, y) + return (((y * self.ihdr.width) + x) * 4) + 1 +end + +function PNGImage:getPixel(x, y) + local off = self:getByteOffsetForPixel(x, y) + return self.data:byte(off, off + 3) +end + +function PNGImage:setPixel(x, y, col) + local off = self:getByteOffsetForPixel(x, y) + self.data = table.concat({self.data:sub(1, off - 1), string.char(col[1], col[2], col[3], col[4]), self.data:sub(off + 4)}) +end + +function PNGImage:lineXAB(ax, y, bx, col) + for x=ax, bx do + self:setPixel(x, y, col) + end +end + +function PNGImage:lineYAB(x, ay, by, col) + for y=ay, by do + self:setPixel(x, y, col) + end +end + +function PNGImage:lineRectangleAB(ax, ay, bx, by, col) + self:lineXAB(ax, ay, bx, col) + self:lineXAB(ax, by, bx, col) + self:lineYAB(ax, ay, by, col) + self:lineYAB(bx, ay, by, col) +end + +function PNGImage:fillRectangleAB(ax, ay, bx, by, col) + for x=ax, bx do + for y=ay, by do + self:setPixel(x, y, col) + end + end +end + +function PNGImage:saveToFile(fn) + local fh = io.open(fn, 'wb') + if not fh then + error("Could not open for writing: " .. fn) + end + self:saveToFileHandle(fh) + fh:close() +end + +function PNGImage:getSize() + return self.ihdr.width, self.ihdr.height +end + +function PNGImage:generateRawIDATData(outbuf) + for y = 0, self.ihdr.height - 1 do + outbuf(0) -- filter type is 0 (Filt(x) = Orig(x)) + for x = 0, self.ihdr.width - 1 do + local r, g, b, a = self:getPixel(x, y) + outbuf(r) + outbuf(g) + outbuf(b) + outbuf(a) + end + end +end + +local ZLIB_LITERAL_LIMIT = 65535 + +local function __raw_to_literalZLIB(inbuf) + local outstr = string.char(8, 29) -- zlib headers + + while inbuf.str:len() > 0 do + if inbuf.str:len() > ZLIB_LITERAL_LIMIT then + outstr = outstr .. string.char(0) -- LITERAL[00] FINAL[0] + else + outstr = outstr .. string.char(1) -- LITERAL[00] FINAL[1] + end + local min = math.min(ZLIB_LITERAL_LIMIT, inbuf.str:len()) + outstr = table.concat({outstr, __pack_msb_uint16(min), __pack_msb_uint16(bit.bnot(min)), inbuf.str:sub(1, min)}) + inbuf.str = inbuf.str:sub(min + 1) + end + + local adler32 = 1 + + for i = 1, outstr:len() do + adler32 = DeflateLua.adler32(outstr:byte(i), adler32) + end + + outstr = table.concat({outstr, string.char(__sep_msb_uint32(adler32))}) + + return outstr +end + +function PNGImage:saveToFileHandle(fh) + -- basic PNG 'encoder' + -- most likely temporary + local expecting = "\137\080\078\071\013\010\026\010" + fh:write(expecting) + + local outbuf = outssmt.OutStringStream() + + __wbuf_msb_uint32(outbuf, self.ihdr.width) + __wbuf_msb_uint32(outbuf, self.ihdr.height) + + outbuf(8) -- bit depth + outbuf(6) -- colour type + outbuf(0) -- compression method + outbuf(0) -- filter method + outbuf(0) -- interlace method + + __write_msb_uint32(fh, outbuf.str:len()) -- length field + fh:write("IHDR") -- name of chunk + fh:write(outbuf.str) -- chunk data + __write_msb_uint32(fh, CRC32Lua.crc32_string("IHDR" .. outbuf.str)) -- chunk data CRC32 + outbuf.str = "" -- reset buffer + + -- now onto the IDAT + + self:generateRawIDATData(outbuf) + + local zlibstr = __raw_to_literalZLIB(outbuf) + + local tmpstr = "" + while zlibstr:len() > 0 do + local min = math.min(ZLIB_LITERAL_LIMIT, zlibstr:len()) + tmpstr = zlibstr:sub(1, min) + zlibstr = zlibstr:sub(min + 1) + + __write_msb_uint32(fh, tmpstr:len()) -- length field + fh:write("IDAT") -- name of chunk + fh:write(tmpstr) -- chunk data + __write_msb_uint32(fh, CRC32Lua.crc32_string("IDAT" .. tmpstr)) -- chunk data CRC32 + tmpstr = "" -- reset + end + + __write_msb_uint32(fh, 0) -- length field + fh:write("IEND") -- name of chunk + -- fh:write(tmpstr) -- chunk data + __write_msb_uint32(fh, CRC32Lua.crc32_string("IEND")) -- chunk data CRC32 +end + + +return PNGImage