FluttyProger

libPNGImage.lua

Aug 3rd, 2018
84
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 15.77 KB | None | 0 0
  1. local PNGImage = {}
  2.  
  3. --
  4. -- libPNGimage by TehSomeLuigi
  5. -- Revision:
  6. PNGImage.rev = 1
  7. --
  8. -- A library to load, edit and save PNGs for OpenComputers
  9. --
  10.  
  11. --[[
  12.    
  13.     Feel free to use however you wish.
  14.     This header must however be preserved should this be redistributed, even
  15.     if in a modified form.
  16.    
  17.     This software comes with no warranties whatsoever.
  18.    
  19.     2014 TehSomeLuigi
  20.  
  21. ]]--
  22.  
  23.  
  24. --local DEFLATE = require("libDEFLATE")
  25. local DeflateLua = require("deflatelua")
  26. local CRC32Lua = require("crc32lua")
  27.  
  28. local bit = require("bit32")
  29.  
  30. local PNGImagemetatable = {}
  31. PNGImagemetatable.__index = PNGImage
  32.  
  33.  
  34.  
  35.  
  36. PNGImage.ColourTypes = {
  37.     Greyscale=0,
  38.     Truecolour=2,
  39.     IndexedColour=3,
  40.     GreyscaleAlpha=4,
  41.     TruecolourAlpha=6
  42. }
  43.  
  44.  
  45.  
  46.  
  47.  
  48.  
  49. local band = bit.band
  50. local rshift = bit.rshift
  51.  
  52.  
  53.  
  54. -- Unpack 32-bit unsigned integer (most-significant-byte, MSB, first)
  55. -- from byte string.
  56. local function __unpack_msb_uint32(s)
  57.     local a,b,c,d = s:byte(1,#s)
  58.     local num = (((a*256) + b) * 256 + c) * 256 + d
  59.     return num
  60. end
  61.  
  62. local function __write_msb_uint32(fh, int)
  63.     -- MSB B2 B1 LSB
  64.    
  65.     local msb = rshift(band(0xFF000000, int), 24)
  66.     local b2 = rshift(band(0x00FF0000, int), 16)
  67.     local b1 = rshift(band(0x0000FF00, int), 8)
  68.     local lsb = band(0x000000FF, int)
  69.    
  70.     fh:write(string.char(msb))
  71.     fh:write(string.char(b2))
  72.     fh:write(string.char(b1))
  73.     fh:write(string.char(lsb))
  74. end
  75.  
  76. local function __wbuf_msb_uint32(buf, int)
  77.     -- MSB B2 B1 LSB
  78.    
  79.     local msb = rshift(band(0xFF000000, int), 24)
  80.     local b2 = rshift(band(0x00FF0000, int), 16)
  81.     local b1 = rshift(band(0x0000FF00, int), 8)
  82.     local lsb = band(0x000000FF, int)
  83.    
  84.     buf(msb)
  85.     buf(b2)
  86.     buf(b1)
  87.     buf(lsb)
  88. end
  89.  
  90. local function __sep_msb_uint32(int)
  91.     -- MSB B2 B1 LSB
  92.    
  93.     local msb = rshift(band(0xFF000000, int), 24)
  94.     local b2 = rshift(band(0x00FF0000, int), 16)
  95.     local b1 = rshift(band(0x0000FF00, int), 8)
  96.     local lsb = band(0x000000FF, int)
  97.    
  98.     return msb, b2, b1, lsb
  99. end
  100.  
  101. local function __pack_msb_uint16(int)
  102.     return string.char(rshift(band(int, 0xFF00), 8), band(int, 0xFF))
  103. end
  104.  
  105. local function __pack_lsb_uint16(int)
  106.     return string.char(band(int, 0xFF), rshift(band(int, 0xFF00), 8))
  107. end
  108.  
  109. -- Read 32-bit unsigned integer (most-significant-byte, MSB, first) from file.
  110. local function __read_msb_uint32(fh)
  111.     return __unpack_msb_uint32(fh:read(4))
  112. end
  113.  
  114. -- Read unsigned byte (integer) from file
  115. local function __read_byte(fh)
  116.     return fh:read(1):byte()
  117. end
  118.  
  119.  
  120.  
  121. local function getBitWidthPerPixel(ihdr)
  122.     if ihdr.color_type == PNGImage.ColourTypes.Greyscale then -- Greyscale
  123.         return ihdr.bit_depth
  124.     end
  125.     if ihdr.color_type == PNGImage.ColourTypes.Truecolour then -- Truecolour
  126.         return ihdr.bit_depth * 3
  127.     end
  128.     if ihdr.color_type == PNGImage.ColourTypes.IndexedColour then -- Indexed-colour
  129.         return ihdr.bit_depth
  130.     end
  131.     if ihdr.color_type == PNGImage.ColourTypes.GreyscaleAlpha then -- Greyscale + Alpha
  132.         return ihdr.bit_depth * 2
  133.     end
  134.     if ihdr.color_type == PNGImage.ColourTypes.TruecolourAlpha then -- Truecolour + Alpha
  135.         return ihdr.bit_depth * 4
  136.     end
  137. end
  138.  
  139. local function getByteWidthPerScanline(ihdr)
  140.     return math.ceil((ihdr.width * getBitWidthPerPixel(ihdr)) / 8)
  141. end
  142.  
  143. local outssmt = {}
  144.  
  145. function outssmt:__call(write)
  146.     self.str = self.str .. string.char(write)
  147. end
  148.  
  149. function outssmt.OutStringStream()
  150.     local outss = {str=""}
  151.     setmetatable(outss, outssmt)
  152.     return outss
  153. end
  154.  
  155.  
  156.  
  157.  
  158. local function __parse_IHDR(fh, len)
  159.     if len ~= 13 then
  160.         error("PNG IHDR Corrupt - should be 13 bytes long")
  161.     end
  162.    
  163.     local ihdr = {}
  164.    
  165.     ihdr.width = __read_msb_uint32(fh)
  166.     ihdr.height = __read_msb_uint32(fh)
  167.     ihdr.bit_depth = __read_byte(fh)
  168.     ihdr.color_type = __read_byte(fh)
  169.     ihdr.compression_method = __read_byte(fh)
  170.     ihdr.filter_method = __read_byte(fh)
  171.     ihdr.interlace_method = __read_byte(fh)
  172.    
  173.     --[[
  174.     print("width=", ihdr.width)
  175.     print("height=", ihdr.height)
  176.     print("bit_depth=", ihdr.bit_depth)
  177.     print("color_type=", ihdr.color_type)
  178.     print("compression_method=", ihdr.compression_method)
  179.     print("filter_method=", ihdr.filter_method)
  180.     print("interlace_method=", ihdr.interlace_method)
  181.     ]]--
  182.    
  183.     return ihdr
  184. end
  185.  
  186. --[[
  187. local function __parse_IDAT(fh, len, commeth)
  188.     if commeth ~= 0 then
  189.         error("Only zlib/DEFLATE compression supported")
  190.     end
  191.    
  192.     local d, msg = DEFLATE.inflate(fh, len);
  193.    
  194.     if not d then
  195.         return nil, msg
  196.     end
  197.    
  198.     local oh = io.open('dump.dat', 'wb')
  199.     oh:write(d.dat)
  200.     oh:close()
  201.    
  202.     return true
  203. end
  204. ]]--
  205.  
  206. local function __parse_IDAT(fh, len, commeth, outss)
  207.     if commeth ~= 0 then
  208.         error("Only zlib/DEFLATE compression supported")
  209.     end
  210.    
  211.    
  212.     --local oh = io.open('dump.dat', 'wb')
  213.     --oh:write(d.dat)
  214.    
  215.     --local ph = io.open('pass.dat', 'wb')
  216.    
  217.     local input = fh:read(len)
  218.    
  219.     --ph:write(input)
  220.     --ph:close()
  221.    
  222.     local cfg = {input=input, output=outss, disable_crc=true}
  223.    
  224.     DeflateLua.inflate_zlib(cfg)
  225.    
  226.     --oh:close()
  227.    
  228. --  if not d then
  229. --      return nil, msg
  230. --  end
  231.    
  232.     return true
  233. end
  234.  
  235.  
  236.  
  237. local function getPNGStdByteAtXY(ihdr, oss, x, y)
  238.     local bpsl = getByteWidthPerScanline(ihdr) -- don't include filterType byte -- we don't store that after it has been read
  239.     if (x <= 0) or (y <= 0) then
  240.         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
  241.     end
  242.     local offset_by_y = (y - 1) * bpsl
  243.     -- now read it!
  244.     local idx = offset_by_y + x
  245.     return oss.str:sub(idx, idx):byte()
  246. end
  247.  
  248.  
  249. local function __paeth_predictor(a, b, c)
  250.     local p = a + b - c
  251.     local pa = math.abs(p - a)
  252.     local pb = math.abs(p - b)
  253.     local pc = math.abs(p - c)
  254.     if pa <= pb and pa <= pc then
  255.         return a
  256.     elseif pb <= pc then
  257.         return b
  258.     else
  259.         return c
  260.     end
  261. end
  262.  
  263.  
  264.  
  265. local function __parse_IDAT_effective_bytes(outss, ihdr)
  266.     local bpsl = getByteWidthPerScanline(ihdr)
  267.     local bypsl = math.ceil(getBitWidthPerPixel(ihdr) / 8)
  268.    
  269.     if outss.str:len() == 0 then
  270.         error("Empty string: outss")
  271.     end
  272.    
  273.     local bys = DeflateLua.stringToBytestream(outss.str)
  274.    
  275.     if not bys then
  276.         error("Did not get a bytestream from string", bys, outss)
  277.     end
  278.    
  279.     local out2 = outssmt.OutStringStream() -- __callable table with metatable that stores what you give it
  280.    
  281.     local y = 0
  282.    
  283.     -- x the byte being filtered;
  284.     -- 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);
  285.     -- b the byte corresponding to x in the previous scanline;
  286.     -- 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).
  287.    
  288.     while true do
  289.         local filterType = bys:read()
  290.        
  291.         if filterType == nil then
  292.             break
  293.         end
  294.        
  295.         y = y + 1
  296.        
  297.         for x = 1, bpsl do
  298.             --[..c..][..b..]
  299.             --[..a..][..x <--- what is being processed (x)
  300.             local a = getPNGStdByteAtXY(ihdr, out2, x - bypsl, y)
  301.             local b = getPNGStdByteAtXY(ihdr, out2, x, y - 1)
  302.             local c = getPNGStdByteAtXY(ihdr, out2, x - bypsl, y - 1)
  303.            
  304.             local outVal = 0
  305.            
  306.             if filterType == 0 then
  307.                 -- Recon(x) = Filt(x)
  308.                 outVal = bys:read()
  309.             elseif filterType == 1 then
  310.                 -- Recon(x) = Filt(x) + Recon(a)
  311.                 outVal = bys:read() + a
  312.             elseif filterType == 2 then
  313.                 -- Recon(x) = Filt(x) + Recon(b)
  314.                 outVal = bys:read() + b
  315.             elseif filterType == 3 then
  316.                 -- Recon(x) = Filt(x) + floor((Recon(a) + Recon(b)) / 2)
  317.                 outVal = bys:read() + math.floor((a + b) / 2)
  318.             elseif filterType == 4 then
  319.                 -- Recon(x) = Filt(x) + PaethPredictor(Recon(a), Recon(b), Recon(c))
  320.                 outVal = bys:read() + __paeth_predictor(a, b, c)
  321.             else
  322.                 error("Unsupported Filter Type: " .. tostring(filterType))
  323.             end
  324.            
  325.             outVal = outVal % 256
  326.            
  327.             out2(outVal)
  328.         end
  329.     end
  330.    
  331.     return out2
  332. end
  333.  
  334.  
  335.  
  336. local function __newPNGImage()
  337.     local pngi = {}
  338.     setmetatable(pngi, PNGImagemetatable)
  339.     return pngi
  340. end
  341.  
  342. function PNGImage.newFromFile(fn)
  343.     local fh = io.open(fn, 'rb')
  344.     if not fh then
  345.         error("Could not open PNG file")
  346.     end
  347.     return PNGImage.newFromFileHandle(fh)
  348. end
  349.  
  350. function PNGImage.newFromScratch(width, height, bkcol)
  351.     local pngi = __newPNGImage()
  352.    
  353.     local width = tonumber(width)
  354.     local height = tonumber(height)
  355.    
  356.     if (not width) or (width < 1) or (math.floor(width) ~= width) then
  357.         error("Invalid param #1 (width) to PNGImage.newFromScratch - integer (>=0) expected")
  358.     end
  359.     if (not height) or (height < 1) or (math.floor(height) ~= height) then
  360.         error("Invalid param #2 (height) to PNGImage.newFromScratch - integer (>=0) expected")
  361.     end
  362.    
  363.     local bkg = {0, 0, 0, 0} -- Transparency
  364.    
  365.     if type(bkcol) == "table" then
  366.         if #bkcol == 3 then
  367.             bkg = {bkcol[1], bkcol[2], bkcol[3], 255} -- Opaque colour
  368.         elseif #bkcol == 4 then
  369.             bkg = {bkcol[1], bkcol[2], bkcol[3], bkcol[4]} -- Defined
  370.         else
  371.             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")
  372.         end
  373.     elseif bkcol ~= nil then
  374.         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")
  375.     end
  376.    
  377.     bkg[1] = tonumber(bkg[1])
  378.     if bkg[1] == nil then
  379.         error("PNGImage.newFromScratch: bkg[R] is not numeric")
  380.     end
  381.     bkg[2] = tonumber(bkg[2])
  382.     if bkg[2] == nil then
  383.         error("PNGImage.newFromScratch: bkg[G] is not numeric")
  384.     end
  385.     bkg[3] = tonumber(bkg[3])
  386.     if bkg[3] == nil then
  387.         error("PNGImage.newFromScratch: bkg[B] is not numeric")
  388.     end
  389.     bkg[4] = tonumber(bkg[4])
  390.     if bkg[4] == nil then
  391.         error("PNGImage.newFromScratch: bkg[A] is not numeric")
  392.     end
  393.    
  394.     pngi.ihdr = {
  395.         width = width,
  396.         height = height,
  397.         bit_depth = 8,
  398.         color_type = 6,
  399.         compression_method = 0,
  400.         filter_method = 0,
  401.         interlace_method = 0
  402.     }
  403.    
  404.     -- do this for every pixel.. i.e string.rep(str, w*h)
  405.     pngi.data = string.byte(bkg[1], bkg[2], bkg[3], bkg[4]):rep(width * height)
  406.    
  407.     return pngi
  408. end
  409.  
  410. function PNGImage.newFromFileHandle(fh)
  411.     local pngi = __newPNGImage()
  412.     local expecting = "\137\080\078\071\013\010\026\010"
  413.     if fh:read(8) ~= expecting then -- check the 8-byte PNG header exists
  414.         error("Not a PNG file")
  415.     end
  416.    
  417.     local ihdr
  418.    
  419.     local outss = outssmt.OutStringStream()
  420.    
  421.     while true do
  422.         local len = __read_msb_uint32(fh)
  423.         local stype = fh:read(4)
  424.        
  425.         if stype == 'IHDR' then
  426.             ihdr, msg = __parse_IHDR(fh, len)
  427.         elseif stype == 'IDAT' then
  428.             local res, msg = __parse_IDAT(fh, len, ihdr.compression_method, outss)
  429.         else
  430.             fh:read(len) -- dummy read
  431.         end
  432.        
  433.         local crc = __read_msb_uint32(fh)
  434.        
  435.         -- print("chunk:", "type=", stype, "len=", len, "crc=", crc)
  436.        
  437.         if stype == 'IEND' then
  438.             break
  439.         end
  440.     end
  441.    
  442.     fh:close()
  443.    
  444.    
  445.     if ihdr.filter_method ~= 0 then
  446.         error("Unsupported Filter Method: " .. ihdr.filter_method)
  447.     end
  448.    
  449.     if ihdr.interlace_method ~= 0 then
  450.         error("Unsupported Interlace Method (Interlacing is currently unsupported): " .. ihdr.interlace_method)
  451.     end
  452.    
  453.     if ihdr.color_type ~= PNGImage.ColourTypes.TruecolourAlpha and ihdr.color_type ~= PNGImage.ColourTypes.Truecolour then
  454.         error("Currently, only Truecolour and Truecolour+Alpha images are supported.")
  455.     end
  456.    
  457.     if ihdr.bit_depth ~= 8 then
  458.         error("Currently, only images with a bit depth of 8 are supported.")
  459.     end
  460.    
  461.     --[[
  462.     local oh = io.open('before-decode.dat', 'wb')
  463.     oh:write(outss.str)
  464.     oh:close()
  465.     ]]--
  466.    
  467.     -- now parse the IDAT chunks
  468.     local out2 = __parse_IDAT_effective_bytes(outss, ihdr)
  469.    
  470.     if ihdr.color_type == PNGImage.ColourTypes.Truecolour then
  471.         -- add an alpha layer so it effectively becomes RGBA, not RGB
  472.         local inp = out2.str
  473.         out2 = outssmt.OutStringStream()
  474.        
  475.         for i=1, ihdr.width*ihdr.height do
  476.             local b = ((i - 1)*3) + 1
  477.             out2(inp:byte(b)) -- R
  478.             out2(inp:byte(b + 1)) -- G
  479.             out2(inp:byte(b + 2)) -- B
  480.             out2(255) -- A
  481.         end
  482.     end
  483.    
  484.     pngi.ihdr = ihdr
  485.     pngi.data = out2.str
  486.    
  487.     --[[
  488.     local oh = io.open('effective.dat', 'wb')
  489.     oh:write(out2.str)
  490.     oh:close()
  491.     ]]--
  492.    
  493.     return pngi
  494. end
  495.  
  496.  
  497. -- Warning: Co-ordinates are Zero-based but strings are 1-based
  498. function PNGImage:getByteOffsetForPixel(x, y)
  499.     return (((y * self.ihdr.width) + x) * 4) + 1
  500. end
  501.  
  502. function PNGImage:getPixel(x, y)
  503.     local off = self:getByteOffsetForPixel(x, y)
  504.     return self.data:byte(off, off + 3)
  505. end
  506.  
  507. function PNGImage:setPixel(x, y, col)
  508.     local off = self:getByteOffsetForPixel(x, y)
  509.     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)})
  510. end
  511.  
  512. function PNGImage:lineXAB(ax, y, bx, col)
  513.     for x=ax, bx do
  514.         self:setPixel(x, y, col)
  515.     end
  516. end
  517.  
  518. function PNGImage:lineYAB(x, ay, by, col)
  519.     for y=ay, by do
  520.         self:setPixel(x, y, col)
  521.     end
  522. end
  523.  
  524. function PNGImage:lineRectangleAB(ax, ay, bx, by, col)
  525.     self:lineXAB(ax, ay, bx, col)
  526.     self:lineXAB(ax, by, bx, col)
  527.     self:lineYAB(ax, ay, by, col)
  528.     self:lineYAB(bx, ay, by, col)
  529. end
  530.  
  531. function PNGImage:fillRectangleAB(ax, ay, bx, by, col)
  532.     for x=ax, bx do
  533.         for y=ay, by do
  534.             self:setPixel(x, y, col)
  535.         end
  536.     end
  537. end
  538.  
  539. function PNGImage:saveToFile(fn)
  540.     local fh = io.open(fn, 'wb')
  541.     if not fh then
  542.         error("Could not open for writing: " .. fn)
  543.     end
  544.     self:saveToFileHandle(fh)
  545.     fh:close()
  546. end
  547.  
  548. function PNGImage:getSize()
  549.     return self.ihdr.width, self.ihdr.height
  550. end
  551.  
  552. function PNGImage:generateRawIDATData(outbuf)
  553.     for y = 0, self.ihdr.height - 1 do
  554.         outbuf(0) -- filter type is 0 (Filt(x) = Orig(x))
  555.         for x = 0, self.ihdr.width - 1 do
  556.             local r, g, b, a = self:getPixel(x, y)
  557.             outbuf(r)
  558.             outbuf(g)
  559.             outbuf(b)
  560.             outbuf(a)
  561.         end
  562.     end
  563. end
  564.  
  565. local ZLIB_LITERAL_LIMIT = 65535
  566.  
  567. local function __raw_to_literalZLIB(inbuf)
  568.     local outstr = string.char(8, 29) -- zlib headers
  569.    
  570.     while inbuf.str:len() > 0 do
  571.         if inbuf.str:len() > ZLIB_LITERAL_LIMIT then
  572.             outstr = outstr .. string.char(0) -- LITERAL[00] FINAL[0]
  573.         else
  574.             outstr = outstr .. string.char(1) -- LITERAL[00] FINAL[1]
  575.         end
  576.         local min = math.min(ZLIB_LITERAL_LIMIT, inbuf.str:len())
  577.         outstr = table.concat({outstr, __pack_msb_uint16(min), __pack_msb_uint16(bit.bnot(min)), inbuf.str:sub(1, min)})
  578.         inbuf.str = inbuf.str:sub(min + 1)
  579.     end
  580.    
  581.     local adler32 = 1
  582.    
  583.     for i = 1, outstr:len() do
  584.         adler32 = DeflateLua.adler32(outstr:byte(i), adler32)
  585.     end
  586.    
  587.     outstr = table.concat({outstr, string.char(__sep_msb_uint32(adler32))})
  588.    
  589.     return outstr
  590. end
  591.  
  592. function PNGImage:saveToFileHandle(fh)
  593.     -- basic PNG 'encoder'
  594.     -- most likely temporary
  595.     local expecting = "\137\080\078\071\013\010\026\010"
  596.     fh:write(expecting)
  597.    
  598.     local outbuf = outssmt.OutStringStream()
  599.    
  600.     __wbuf_msb_uint32(outbuf, self.ihdr.width)
  601.     __wbuf_msb_uint32(outbuf, self.ihdr.height)
  602.    
  603.     outbuf(8) -- bit depth
  604.     outbuf(6) -- colour type
  605.     outbuf(0) -- compression method
  606.     outbuf(0) -- filter method
  607.     outbuf(0) -- interlace method
  608.    
  609.     __write_msb_uint32(fh, outbuf.str:len()) -- length field
  610.     fh:write("IHDR") -- name of chunk
  611.     fh:write(outbuf.str) -- chunk data
  612.     __write_msb_uint32(fh, CRC32Lua.crc32_string("IHDR" .. outbuf.str)) -- chunk data CRC32
  613.     outbuf.str = "" -- reset buffer
  614.    
  615.     -- now onto the IDAT
  616.    
  617.     self:generateRawIDATData(outbuf)
  618.    
  619.     local zlibstr = __raw_to_literalZLIB(outbuf)
  620.    
  621.     local tmpstr = ""
  622.     while zlibstr:len() > 0 do
  623.         local min = math.min(ZLIB_LITERAL_LIMIT, zlibstr:len())
  624.         tmpstr = zlibstr:sub(1, min)
  625.         zlibstr = zlibstr:sub(min + 1)
  626.        
  627.         __write_msb_uint32(fh, tmpstr:len()) -- length field
  628.         fh:write("IDAT") -- name of chunk
  629.         fh:write(tmpstr) -- chunk data
  630.         __write_msb_uint32(fh, CRC32Lua.crc32_string("IDAT" .. tmpstr)) -- chunk data CRC32
  631.         tmpstr = "" -- reset
  632.     end
  633.    
  634.     __write_msb_uint32(fh, 0) -- length field
  635.     fh:write("IEND") -- name of chunk
  636.     -- fh:write(tmpstr) -- chunk data
  637.     __write_msb_uint32(fh, CRC32Lua.crc32_string("IEND")) -- chunk data CRC32
  638. end
  639.  
  640.  
  641.  
  642. return PNGImage
Add Comment
Please, Sign In to add comment