SHARE
TWEET

libPNGimage

MoonlightOwl Jan 27th, 2015 (edited) 269 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  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); io.read()
  436.                
  437.                 if stype == 'IEND' then
  438.                         break
  439.                 end
  440.         end
  441.        
  442.         fh:close()
  443.  
  444.         -- to avoid 'too long without yielding' error
  445.         os.sleep(0.1)
  446.        
  447.         if ihdr.filter_method ~= 0 then
  448.                 error("Unsupported Filter Method: " .. ihdr.filter_method)
  449.         end
  450.        
  451.         if ihdr.interlace_method ~= 0 then
  452.                 error("Unsupported Interlace Method (Interlacing is currently unsupported): " .. ihdr.interlace_method)
  453.         end
  454.        
  455.         if ihdr.color_type ~= PNGImage.ColourTypes.TruecolourAlpha and ihdr.color_type ~= PNGImage.ColourTypes.Truecolour then
  456.                 error("Currently, only Truecolour and Truecolour+Alpha images are supported.")
  457.         end
  458.        
  459.         if ihdr.bit_depth ~= 8 then
  460.                 error("Currently, only images with a bit depth of 8 are supported.")
  461.         end
  462.        
  463.         --[[
  464.         local oh = io.open('before-decode.dat', 'wb')
  465.         oh:write(outss.str)
  466.         oh:close()
  467.         ]]--
  468.        
  469.         -- now parse the IDAT chunks
  470.         local out2 = __parse_IDAT_effective_bytes(outss, ihdr)
  471.        
  472.         if ihdr.color_type == PNGImage.ColourTypes.Truecolour then
  473.                 -- add an alpha layer so it effectively becomes RGBA, not RGB
  474.                 local inp = out2.str
  475.                 out2 = outssmt.OutStringStream()
  476.                
  477.                 for i=1, ihdr.width*ihdr.height do
  478.                         local b = ((i - 1)*3) + 1
  479.                         out2(inp:byte(b)) -- R
  480.                         out2(inp:byte(b + 1)) -- G
  481.                         out2(inp:byte(b + 2)) -- B
  482.                         out2(255) -- A
  483.                 end
  484.         end
  485.        
  486.         pngi.ihdr = ihdr
  487.         pngi.data = out2.str
  488.        
  489.         --[[
  490.         local oh = io.open('effective.dat', 'wb')
  491.         oh:write(out2.str)
  492.         oh:close()
  493.         ]]--
  494.        
  495.         return pngi
  496. end
  497.  
  498.  
  499. -- Warning: Co-ordinates are Zero-based but strings are 1-based
  500. function PNGImage:getByteOffsetForPixel(x, y)
  501.         return (((y * self.ihdr.width) + x) * 4) + 1
  502. end
  503.  
  504. function PNGImage:getPixel(x, y)
  505.         local off = self:getByteOffsetForPixel(x, y)
  506.         return self.data:byte(off, off + 3)
  507. end
  508.  
  509. function PNGImage:setPixel(x, y, col)
  510.         local off = self:getByteOffsetForPixel(x, y)
  511.         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)})
  512. end
  513.  
  514. function PNGImage:lineXAB(ax, y, bx, col)
  515.         for x=ax, bx do
  516.                 self:setPixel(x, y, col)
  517.         end
  518. end
  519.  
  520. function PNGImage:lineYAB(x, ay, by, col)
  521.         for y=ay, by do
  522.                 self:setPixel(x, y, col)
  523.         end
  524. end
  525.  
  526. function PNGImage:lineRectangleAB(ax, ay, bx, by, col)
  527.         self:lineXAB(ax, ay, bx, col)
  528.         self:lineXAB(ax, by, bx, col)
  529.         self:lineYAB(ax, ay, by, col)
  530.         self:lineYAB(bx, ay, by, col)
  531. end
  532.  
  533. function PNGImage:fillRectangleAB(ax, ay, bx, by, col)
  534.         for x=ax, bx do
  535.                 for y=ay, by do
  536.                         self:setPixel(x, y, col)
  537.                 end
  538.         end
  539. end
  540.  
  541. function PNGImage:saveToFile(fn)
  542.         local fh = io.open(fn, 'wb')
  543.         if not fh then
  544.                 error("Could not open for writing: " .. fn)
  545.         end
  546.         self:saveToFileHandle(fh)
  547.         fh:close()
  548. end
  549.  
  550. function PNGImage:getSize()
  551.         return self.ihdr.width, self.ihdr.height
  552. end
  553.  
  554. function PNGImage:generateRawIDATData(outbuf)
  555.         for y = 0, self.ihdr.height - 1 do
  556.                 outbuf(0) -- filter type is 0 (Filt(x) = Orig(x))
  557.                 for x = 0, self.ihdr.width - 1 do
  558.                         local r, g, b, a = self:getPixel(x, y)
  559.                         outbuf(r)
  560.                         outbuf(g)
  561.                         outbuf(b)
  562.                         outbuf(a)
  563.                 end
  564.         end
  565. end
  566.  
  567. local ZLIB_LITERAL_LIMIT = 65535
  568.  
  569. local function __raw_to_literalZLIB(inbuf)
  570.         local outstr = string.char(8, 29) -- zlib headers
  571.        
  572.         while inbuf.str:len() > 0 do
  573.                 if inbuf.str:len() > ZLIB_LITERAL_LIMIT then
  574.                         outstr = outstr .. string.char(0) -- LITERAL[00] FINAL[0]
  575.                 else
  576.                         outstr = outstr .. string.char(1) -- LITERAL[00] FINAL[1]
  577.                 end
  578.                 local min = math.min(ZLIB_LITERAL_LIMIT, inbuf.str:len())
  579.                 outstr = table.concat({outstr, __pack_msb_uint16(min), __pack_msb_uint16(bit.bnot(min)), inbuf.str:sub(1, min)})
  580.                 inbuf.str = inbuf.str:sub(min + 1)
  581.         end
  582.        
  583.         local adler32 = 1
  584.        
  585.         for i = 1, outstr:len() do
  586.                 adler32 = DeflateLua.adler32(outstr:byte(i), adler32)
  587.         end
  588.        
  589.         outstr = table.concat({outstr, string.char(__sep_msb_uint32(adler32))})
  590.        
  591.         return outstr
  592. end
  593.  
  594. function PNGImage:saveToFileHandle(fh)
  595.         -- basic PNG 'encoder'
  596.         -- most likely temporary
  597.         local expecting = "\137\080\078\071\013\010\026\010"
  598.         fh:write(expecting)
  599.        
  600.         local outbuf = outssmt.OutStringStream()
  601.        
  602.         __wbuf_msb_uint32(outbuf, self.ihdr.width)
  603.         __wbuf_msb_uint32(outbuf, self.ihdr.height)
  604.        
  605.         outbuf(8) -- bit depth
  606.         outbuf(6) -- colour type
  607.         outbuf(0) -- compression method
  608.         outbuf(0) -- filter method
  609.         outbuf(0) -- interlace method
  610.        
  611.         __write_msb_uint32(fh, outbuf.str:len()) -- length field
  612.         fh:write("IHDR") -- name of chunk
  613.         fh:write(outbuf.str) -- chunk data
  614.         __write_msb_uint32(fh, CRC32Lua.crc32_string("IHDR" .. outbuf.str)) -- chunk data CRC32
  615.         outbuf.str = "" -- reset buffer
  616.        
  617.         -- now onto the IDAT
  618.        
  619.         self:generateRawIDATData(outbuf)
  620.        
  621.         local zlibstr = __raw_to_literalZLIB(outbuf)
  622.        
  623.         local tmpstr = ""
  624.         while zlibstr:len() > 0 do
  625.                 local min = math.min(ZLIB_LITERAL_LIMIT, zlibstr:len())
  626.                 tmpstr = zlibstr:sub(1, min)
  627.                 zlibstr = zlibstr:sub(min + 1)
  628.                
  629.                 __write_msb_uint32(fh, tmpstr:len()) -- length field
  630.                 fh:write("IDAT") -- name of chunk
  631.                 fh:write(tmpstr) -- chunk data
  632.                 __write_msb_uint32(fh, CRC32Lua.crc32_string("IDAT" .. tmpstr)) -- chunk data CRC32
  633.                 tmpstr = "" -- reset
  634.         end
  635.        
  636.         __write_msb_uint32(fh, 0) -- length field
  637.         fh:write("IEND") -- name of chunk
  638.         -- fh:write(tmpstr) -- chunk data
  639.         __write_msb_uint32(fh, CRC32Lua.crc32_string("IEND")) -- chunk data CRC32
  640. end
  641.  
  642.  
  643. return PNGImage
RAW Paste Data
We use cookies for various purposes including analytics. By continuing to use Pastebin, you agree to our use of cookies as described in the Cookies Policy. OK, I Understand
 
Top