mirror of
https://github.com/IgorTimofeev/MineOS.git
synced 2025-12-20 11:09:21 +01:00
643 lines
15 KiB
Lua
643 lines
15 KiB
Lua
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
|