local signature = "\137\80\78\71\13\10\26\10" local assert, char, ipairs, insert, concat, abs, floor = assert, string.char, ipairs, table.insert, table.concat, math.abs, math.floor -- TODO move to modlib.bit eventually local function bit_xor(a, b) local res = 0 local bit = 1 for _ = 1, 32 do if a % 2 ~= b % 2 then res = res + bit end a = floor(a / 2) b = floor(b / 2) bit = bit * 2 end return res end -- Try to use `bit` library (if available) for a massive speed boost local bit = rawget(_G, "bit") if bit then local bxor = bit.bxor function bit_xor(a, b) local res = bxor(a, b) if res < 0 then -- convert signed to unsigned return res + 2^32 end return res end end local crc_table = {} for i = 0, 255 do local c = i for _ = 0, 7 do if c % 2 > 0 then c = bit_xor(0xEDB88320, floor(c / 2)) else c = floor(c / 2) end end crc_table[i] = c end local function update_crc(crc, text) for i = 1, #text do crc = bit_xor(crc_table[bit_xor(crc % 0x100, text:byte(i))], floor(crc / 0x100)) end return crc end local color_types = { [0] = { color = "grayscale" }, [2] = { color = "truecolor" }, [3] = { color = "palette", depth = 8 }, [4] = { color = "grayscale", alpha = true }, [6] = { color = "truecolor", alpha = true } } local set = modlib.table.set local allowed_bit_depths = { [0] = set{1, 2, 4, 8, 16}, [2] = set{8, 16}, [3] = set{1, 2, 4, 8}, [4] = set{8, 16}, [6] = set{8, 16} } local samples = { grayscale = 1, palette = 1, truecolor = 3 } local adam7_passes = { x_min = { 0, 4, 0, 2, 0, 1, 0 }, y_min = { 0, 0, 4, 0, 2, 0, 1 }, x_step = { 8, 8, 4, 4, 2, 2, 1 }, y_step = { 8, 8, 8, 4, 4, 2, 2 }, }; (...).decode_png = function(stream) local chunk_crc local function read(n) local text = stream:read(n) assert(#text == n) if chunk_crc then chunk_crc = update_crc(chunk_crc, text) end return text end local function byte() return read(1):byte() end local function _uint() return 0x1000000 * byte() + 0x10000 * byte() + 0x100 * byte() + byte() end local function uint() local val = _uint() assert(val < 2^31, "uint out of range") return val end local function check_crc() local crc = chunk_crc chunk_crc = nil if _uint() ~= bit_xor(crc, 0xFFFFFFFF) then error("CRC mismatch", 2) end end assert(read(8) == signature, "PNG signature expected") local IHDR_len = uint() assert(IHDR_len == 13, "invalid IHDR length") chunk_crc = 0xFFFFFFFF assert(read(4) == "IHDR", "IHDR chunk expected") local width = uint() assert(width > 0) local height = uint() assert(height > 0) local bit_depth = byte() local color_type_number = byte() local color_type = assert(color_types[color_type_number], "invalid color type") if color_type.color ~= "palette" then color_type.depth = bit_depth end assert(allowed_bit_depths[color_type_number][bit_depth], "disallowed bit depth for color type") local compression_method = byte() assert(compression_method == 0, "unsupported compression method") local filter_method = byte() assert(filter_method == 0, "unsupported filter method") local interlace_method = byte() assert(interlace_method <= 1, "unsupported interlace method") local adam7 = interlace_method == 1 check_crc() -- IHDR CRC local palette local alpha local source_gamma local idat_content = {} local idat_allowed = true local iend repeat local chunk_length = uint() chunk_crc = 0xFFFFFFFF local chunk_type = read(4) if chunk_type == "IDAT" then assert(idat_allowed, "no chunks inbetween IDAT chunks allowed") if color_type.color == "palette" then assert(palette, "PLTE chunk expected") end insert(idat_content, read(chunk_length)) else if next(idat_content) then -- Non-IDAT chunk, no IDAT chunks allowed anymore idat_allowed = false end if chunk_type == "PLTE" then assert(color_type.color ~= "grayscale") assert(not palette, "double PLTE chunk") assert(idat_allowed, "PLTE after IDAT chunks") palette = {} local entries = chunk_length / 3 assert(entries % 1 == 0 and entries >= 1 and entries <= 2^bit_depth, "invalid PLTE chunk length") for i = 1, entries do palette[i] = 0x10000 * byte() + 0x100 * byte() + byte() -- RGB end elseif chunk_type == "tRNS" then assert(not color_type.alpha, "unexpected tRNS chunk") color_type.transparency = true assert(idat_allowed, "tRNS after IDAT chunks") if color_type.color == "palette" then assert(palette, "PLTE chunk expected") alpha = {} for i = 1, chunk_length do alpha[i] = byte() end elseif color_type.color == "grayscale" then assert(chunk_length == 2) alpha = 0x100 * byte() + byte() else assert(color_type.color == "truecolor") assert(chunk_length == 6) alpha = 0 -- Read 16-bit RGB (6 bytes) for _ = 1, 6 do alpha = alpha * 0x100 + byte() end end elseif chunk_type == "gAMA" then assert(not palette, "gAMA after PLTE chunk") assert(idat_allowed, "gAMA after IDAT chunks") assert(chunk_length == 4) source_gamma = uint() / 1e5 elseif chunk_type == "IEND" then iend = true else -- Check whether the fifth bit of the first byte is set (upper vs. lowercase ASCII) local ancillary = floor(chunk_type:byte(1) % (2^6)) >= 2^5 if not ancillary then error(("unsupported critical chunk: %q"):format(chunk_type)) end read(chunk_length) end end check_crc() until iend assert(next(idat_content), "no IDAT chunk") idat_content = minetest.decompress(concat(idat_content), "deflate") --[[ For memory efficiency, we try to pack everything in a single number: Grayscale/lightness: AY Palette: ARGB Truecolor (8-bit): ARGB Truecolor (16-bit): RGB + A (64 bits required, packing non-mantissa bits isn't practical) => separate table with alpha values ]] local data = {} local alpha_data if color_type.color == "truecolor" and bit_depth == 16 and (color_type.alpha or color_type.transparency) then alpha_data = {} end if adam7 then -- Allocate space in list part in order to not fill the hash part later for i = 1, width * height do data[i] = false if alpha_data then alpha_data[i] = false end end end local bits_per_pixel = (samples[color_type.color] + (color_type.alpha and 1 or 0)) * bit_depth local bytes_per_pixel = math.ceil(bits_per_pixel / 8) local previous_scanline local idat_base_index = 1 local function read_scanline(x_min, x_step, y) local scanline_width = math.ceil((width - x_min) / x_step) local scanline_bytecount = math.ceil(scanline_width * bits_per_pixel / 8) local filtering = idat_content:byte(idat_base_index) local scanline = {} for i = 1, scanline_bytecount do local val = idat_content:byte(idat_base_index + i) local left = scanline[i - bytes_per_pixel] or 0 local up = previous_scanline and previous_scanline[i] or 0 local left_up = previous_scanline and previous_scanline[i - bytes_per_pixel] or 0 -- Undo lossless filter if filtering == 0 then -- None scanline[i] = val elseif filtering == 1 then -- Sub scanline[i] = (left + val) % 0x100 elseif filtering == 2 then -- Up scanline[i] = (up + val) % 0x100 elseif filtering == 3 then -- Average scanline[i] = (floor((left + up) / 2) + val) % 0x100 elseif filtering == 4 then -- Paeth local p = left + up - left_up local p_left = abs(p - left) local p_up = abs(p - up) local p_left_up = abs(p - left_up) local p_res if p_left <= p_up and p_left <= p_left_up then p_res = left elseif p_up <= p_left_up then p_res = up else p_res = left_up end scanline[i] = (p_res + val) % 0x100 else error("invalid filtering method: " .. filtering) end assert(scanline[i] >= 0 and scanline[i] <= 255 and scanline[i] % 1 == 0) end local bit = 0 local function sample() local byte_idx = 1 + floor(bit / 8) bit = bit + bit_depth local byte = scanline[byte_idx] if bit_depth == 16 then return byte * 0x100 + scanline[byte_idx + 1] end if bit_depth == 8 then return byte end assert(bit_depth == 1 or bit_depth == 2 or bit_depth == 4) local low = 2^(-bit % 8) return floor(byte / low) % (2^bit_depth) end for x = x_min, width - 1, x_step do local data_index = y * width + x + 1 if color_type.color == "palette" then local palette_index = sample() local rgb = assert(palette[palette_index + 1], "palette index out of range") -- Index alpha table if available local a = alpha and alpha[palette_index + 1] or 255 data[data_index] = a * 0x1000000 + rgb elseif color_type.color == "grayscale" then local Y = sample() local a = 2^bit_depth - 1 if color_type.alpha then a = sample() elseif alpha == Y then a = 0 -- Convert grayscale to transparency end data[data_index] = a * (2^bit_depth) + Y else assert(color_type.color == "truecolor") local r, g, b = sample(), sample(), sample() local rgb16 = r * 0x100000000 + g * 0x10000 + b local a = 2^bit_depth - 1 if color_type.alpha then a = sample() elseif alpha == rgb16 then a = 0 -- Convert color to transparency end if bit_depth == 8 then data[data_index] = a * 0x1000000 + r * 0x10000 + g * 0x100 + b else assert(bit_depth == 16) -- Pack only RGB in data, alpha goes in a different table -- 3 * 16 = 48 bytes can still be held accurately by the double mantissa data[data_index] = rgb16 if alpha_data then alpha_data[data_index] = a end end end end -- Each byte of the scanline must have been read from assert(bit >= #scanline * 8 - 7) previous_scanline = scanline idat_base_index = idat_base_index + scanline_bytecount + 1 end if adam7 then for pass = 1, 7 do local x_min, y_min = adam7_passes.x_min[pass], adam7_passes.y_min[pass] if x_min < width and y_min < height then -- Non-empty pass local x_step, y_step = adam7_passes.x_step[pass], adam7_passes.y_step[pass] previous_scanline = nil -- Filtering doesn't use scanlines of previous passes for y = y_min, height - 1, y_step do read_scanline(x_min, x_step, y) end end end else for y = 0, height - 1 do read_scanline(0, 1, y) end end return { width = width, height = height, color_type = color_type, source_gamma = source_gamma, data = data, alpha_data = alpha_data } end local function rescale_depth(sample, source_depth, target_depth) if source_depth == target_depth then return sample end return floor((sample * (2^target_depth - 1) / (2^source_depth - 1)) + 0.5) end -- In-place lossy (if bit depth = 16) conversion to ARGB8 (...).convert_png_to_argb8 = function(png) local color, transparency, depth = png.color_type.color, png.color_type.alpha or png.color_type.transparency, png.color_type.depth if color == "palette" or (color == "truecolor" and depth == 8) then return end for index, value in pairs(png.data) do if color == "grayscale" then local a, Y = rescale_depth(floor(value / (2^depth)), depth, 8), rescale_depth(value % (2^depth), depth, 8) png.data[index] = a * 0x1000000 + Y * 0x10000 + Y * 0x100 + Y -- R = G = B = Y else assert(color == "truecolor" and depth == 16) local r = rescale_depth(floor(value / 0x100000000), depth, 8) local g = rescale_depth(floor(value / 0x10000) % 0x10000, depth, 8) local b = rescale_depth(value % 0x10000, depth, 8) local a = 0xFF if transparency then a = rescale_depth(png.alpha_data[index], depth, 8) end png.data[index] = a * 0x1000000 + r * 0x10000 + g * 0x100 + b end end png.color_type = color_types[6] png.bit_depth = 8 png.alpha_data = nil end local function encode_png(width, height, data, compression, raw_write) local write = raw_write local function byte(value) write(char(value)) end local function _uint(value) local div = 0x1000000 for _ = 1, 4 do byte(floor(value / div) % 0x100) div = div / 0x100 end end local function uint(value) assert(value < 2^31) _uint(value) end local chunk_content local function chunk_write(text) insert(chunk_content, text) end local function chunk(type) chunk_content = {} write = chunk_write write(type) end local function end_chunk() write = raw_write local chunk_len = 0 for i = 2, #chunk_content do chunk_len = chunk_len + #chunk_content[i] end uint(chunk_len) write(concat(chunk_content)) local chunk_crc = 0xFFFFFFFF for _, text in ipairs(chunk_content) do chunk_crc = update_crc(chunk_crc, text) end _uint(bit_xor(chunk_crc, 0xFFFFFFFF)) end -- Signature write(signature) chunk"IHDR" uint(width) uint(height) -- Always use bit depth 8 byte(8) -- Always use color type "truecolor with alpha" byte(6) -- Compression method: deflate byte(0) -- Filter method: PNG filters byte(0) -- No interlace byte(0) end_chunk() chunk"IDAT" local data_rope = {} for y = 0, height - 1 do local base_index = y * width insert(data_rope, "\0") for x = 1, width do local colorspec = modlib.minetest.colorspec.from_any(data[base_index + x]) insert(data_rope, char(colorspec.r, colorspec.g, colorspec.b, colorspec.a)) end end write(minetest.compress(type(data) == "string" and data or concat(data_rope), "deflate", compression)) end_chunk() chunk"IEND" end_chunk() end (...).encode_png = minetest.encode_png or function(width, height, data, compression) local rope = {} encode_png(width, height, data, compression or 9, function(text) insert(rope, text) end) return concat(rope) end