This repository has been archived on 2024-04-19. You can view files and clone it, but cannot push or open issues or pull requests.
FREAX/usr/local/bin/pngview
2022-07-18 19:33:40 +02:00

716 lines
22 KiB
Text

local png = {}
do -- png.lua
--[[
BSD 2-Clause License
Copyright (c) 2018, Kartik Singh
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--]]
local floor = math.floor
local ceil = math.ceil
local min = math.min
local max = math.max
local abs = math.abs
-- utility functions
function memoize(f)
local cache = {}
return function(...)
local key = table.concat({...}, "-")
if not cache[key] then
cache[key] = f(...)
end
return cache[key]
end
end
function int(bytes)
local n = 0
for i = 1, #bytes do
n = 256*n + bytes:sub(i, i):byte()
end
return n
end
int = memoize(int)
function bint(bits)
return tonumber(bits, 2) or 0
end
bint = memoize(bint)
function bits(b, width)
local s = ""
if type(b) == "number" then
for i = 1, width do
s = b%2 .. s
b = floor(b/2)
end
else
for i = 1, #b do
s = s .. bits(b:sub(i, i):byte(), 8):reverse()
end
end
return s
end
bits = memoize(bits)
function fill(bytes, len)
return bytes:rep(floor(len / #bytes)) .. bytes:sub(1, len % #bytes)
end
function zip(t1, t2)
local zipped = {}
for i = 1, max(#t1, #t2) do
zipped[#zipped + 1] = {t1[i], t2[i]}
end
return zipped
end
function unzip(zipped)
local t1, t2 = {}, {}
for i = 1, #zipped do
t1[#t1 + 1] = zipped[i][1]
t2[#t2 + 1] = zipped[i][2]
end
return t1, t2
end
function map(f, t)
local mapped = {}
for i = 1, #t do
mapped[#mapped + 1] = f(t[i], i)
end
return mapped
end
function filter(pred, t)
local filtered = {}
for i = 1, #t do
if pred(t[i], i) then
filtered[#filtered + 1] = t[i]
end
end
return filtered
end
function find(key, t)
if type(key) == "function" then
for i = 1, #t do
if key(t[i]) then
return i
end
end
return nil
else
return find(function(x) return x == key end, t)
end
end
function slice(t, i, j, step)
local sliced = {}
for k = i < 1 and 1 or i, i < 1 and #t + i or j or #t, step or 1 do
sliced[#sliced + 1] = t[k]
end
return sliced
end
function range(i, j)
local r = {}
for k = j and i or 0, j or i - 1 do
r[#r + 1] = k
end
return r
end
-- streams
function byte_stream(raw)
local stream = {}
local curr = 0
function stream:read(n)
local b = raw:sub(curr + 1, curr + n)
curr = curr + n
return b
end
function stream:seek(n, whence)
if n == "beg" then
curr = 0
elseif n == "end" then
curr = #raw
elseif whence == "beg" then
curr = n
else
curr = curr + n
end
return self
end
function stream:is_empty()
return curr >= #raw
end
function stream:pos()
return curr
end
function stream:raw()
return raw
end
return stream
end
function bit_stream(raw, offset)
local stream = {}
local curr = 0
offset = offset or 0
function stream:read(n, reverse)
local start = floor(curr/8) + offset + 1
local b = bits(raw:sub(start, start + ceil(n/8))):sub(curr%8 + 1, curr%8 + n)
curr = curr + n
return reverse and b or b:reverse()
end
function stream:seek(n)
if n == "beg" then
curr = 0
elseif n == "end" then
curr = #raw
else
curr = curr + n
end
return self
end
function stream:is_empty()
return curr >= 8*#raw
end
function stream:pos()
return curr
end
return stream
end
function output_stream()
local stream, buffer = {}, {}
local curr = 0
function stream:write(bytes)
for i = 1, #bytes do
buffer[#buffer + 1] = bytes:sub(i, i)
end
curr = curr + #bytes
end
function stream:back_read(offset, n)
local read = {}
for i = curr - offset + 1, curr - offset + n do
read[#read + 1] = buffer[i]
end
return table.concat(read)
end
function stream:back_copy(dist, len)
local start, copied = curr - dist + 1, {}
for i = start, min(start + len, curr) do
copied[#copied + 1] = buffer[i]
end
self:write(fill(table.concat(copied), len))
end
function stream:pos()
return curr
end
function stream:raw()
return table.concat(buffer)
end
return stream
end
-- inflate
local CL_LENS_ORDER = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15}
local MAX_BITS = 15
local PT_WIDTH = 8
function cl_code_lens(stream, hclen)
local code_lens = {}
for i = 1, hclen do
code_lens[#code_lens + 1] = bint(stream:read(3))
end
return code_lens
end
function code_tree(lens, alphabet)
alphabet = alphabet or range(#lens)
local using = filter(function(x, i) return lens[i] and lens[i] > 0 end, alphabet)
lens = filter(function(x) return x > 0 end, lens)
local tree = zip(lens, using)
table.sort(tree, function(a, b)
if a[1] == b[1] then
return a[2] < b[2]
else
return a[1] < b[1]
end
end)
return unzip(tree)
end
function codes(lens)
local codes = {}
local code = 0
for i = 1, #lens do
codes[#codes + 1] = bits(code, lens[i])
if i < #lens then
code = (code + 1)*2^(lens[i + 1] - lens[i])
end
end
return codes
end
function handle_long_codes(codes, alphabet, pt)
local i = find(function(x) return #x > PT_WIDTH end, codes)
local long = slice(zip(codes, alphabet), i)
i = 0
repeat
local prefix = long[i + 1][1]:sub(1, PT_WIDTH)
local same = filter(function(x) return x[1]:sub(1, PT_WIDTH) == prefix end, long)
same = map(function(x) return {x[1]:sub(PT_WIDTH + 1), x[2]} end, same)
pt[prefix] = {rest = prefix_table(unzip(same)), unused = 0}
i = i + #same
until i == #long
end
function prefix_table(codes, alphabet)
local pt = {}
if #codes[#codes] > PT_WIDTH then
handle_long_codes(codes, alphabet, pt)
end
for i = 1, #codes do
local code = codes[i]
if #code > PT_WIDTH then
break
end
local entry = {value = alphabet[i], unused = PT_WIDTH - #code}
if entry.unused == 0 then
pt[code] = entry
else
for i = 0, 2^entry.unused - 1 do
pt[code .. bits(i, entry.unused)] = entry
end
end
end
return pt
end
function huffman_decoder(lens, alphabet)
local base_codes = prefix_table(codes(lens), alphabet)
return function(stream)
local codes = base_codes
local entry
repeat
entry = codes[stream:read(PT_WIDTH, true)]
stream:seek(-entry.unused)
codes = entry.rest
until not codes
return entry.value
end
end
function code_lens(stream, decode, n)
local lens = {}
repeat
local value = decode(stream)
if value < 16 then
lens[#lens + 1] = value
elseif value == 16 then
for i = 1, bint(stream:read(2)) + 3 do
lens[#lens + 1] = lens[#lens]
end
elseif value == 17 then
for i = 1, bint(stream:read(3)) + 3 do
lens[#lens + 1] = 0
end
elseif value == 18 then
for i = 1, bint(stream:read(7)) + 11 do
lens[#lens + 1] = 0
end
end
until #lens == n
return lens
end
function code_trees(stream)
local hlit = bint(stream:read(5)) + 257
local hdist = bint(stream:read(5)) + 1
local hclen = bint(stream:read(4)) + 4
local cl_decode = huffman_decoder(code_tree(cl_code_lens(stream, hclen), CL_LENS_ORDER))
local ll_decode = huffman_decoder(code_tree(code_lens(stream, cl_decode, hlit)))
local d_decode = huffman_decoder(code_tree(code_lens(stream, cl_decode, hdist)))
return ll_decode, d_decode
end
function extra_bits(value)
if value >= 4 and value <= 29 then
return floor(value/2) - 1
elseif value >= 265 and value <= 284 then
return ceil(value/4) - 66
else
return 0
end
end
extra_bits = memoize(extra_bits)
function decode_len(value, bits)
assert(value >= 257 and value <= 285, "value out of range")
assert(#bits == extra_bits(value), "wrong number of extra bits")
if value <= 264 then
return value - 254
elseif value == 285 then
return 258
end
local len = 11
for i = 1, #bits - 1 do
len = len + 2^(i+2)
end
return floor(bint(bits) + len + ((value - 1) % 4)*2^#bits)
end
decode_len = memoize(decode_len)
function a(n)
if n <= 3 then
return n + 2
else
return a(n-1) + 2*a(n-2) - 2*a(n-3)
end
end
a = memoize(a)
function decode_dist(value, bits)
assert(value >= 0 and value <= 29, "value out of range")
assert(#bits == extra_bits(value), "wrong number of extra bits")
return bint(bits) + a(value - 1)
end
decode_dist = memoize(decode_dist)
function inflate(stream)
local ostream = output_stream()
repeat
local bfinal, btype = bint(stream:read(1)), bint(stream:read(2))
assert(btype == 2, "compression method not supported")
local ll_decode, d_decode = code_trees(stream)
while true do
local value = ll_decode(stream)
if value < 256 then
ostream:write(string.char(value))
elseif value == 256 then
break
else
local len = decode_len(value, stream:read(extra_bits(value)))
value = d_decode(stream)
local dist = decode_dist(value, stream:read(extra_bits(value)))
ostream:back_copy(dist, len)
end
end
os.sleep(0)
--write(".")
until bfinal == 1
return ostream:raw()
end
-- chunk processing
local CHANNELS = {}
CHANNELS[0] = 1
CHANNELS[2] = 3
CHANNELS[3] = 1
CHANNELS[4] = 2
CHANNELS[6] = 4
function process_header(stream, image)
stream:seek(8)
image.width = int(stream:read(4))
image.height = int(stream:read(4))
image.bit_depth = int(stream:read(1))
image.color_type= int(stream:read(1))
image.channels = CHANNELS[image.color_type]
image.compression_method = int(stream:read(1))
image.filter_method = int(stream:read(1))
image.interlace_method = int(stream:read(1))
assert(image.interlace_method == 0, "interlacing not supported")
stream:seek(4)
end
function process_data(stream, image)
local chunk_len = int(stream:read(4))
stream:seek(4)
assert(int(stream:read(2)) % 31 == 0, "invalid zlib header")
stream:seek(-2)
local dstream = output_stream()
repeat
dstream:write(stream:read(chunk_len))
stream:seek(4)
chunk_len = int(stream:read(4))
until stream:read(4) ~= "IDAT"
stream:seek(-8)
local bstream = bit_stream(dstream:raw(), 2)
image.data = inflate(bstream)
end
function process_palette(stream, image)
local chunk_len = int(stream:read(4))
stream:seek(4)
assert(chunk_len % 3 == 0, "invalid palette")
image.palette = {}
for i = 0, chunk_len - 1, 3 do image.palette[i/3] = {
r = int(stream:read(1)),
g = int(stream:read(1)),
b = int(stream:read(1))
} end
stream:seek(4)
end
function process_chunk(stream, image)
local chunk_len = int(stream:read(4))
local chunk_type = stream:read(4)
stream:seek(-8)
if chunk_type == "IHDR" then
process_header(stream, image)
elseif chunk_type == "IDAT" then
process_data(stream, image)
elseif chunk_type == "IEND" then
stream:seek("end")
elseif chunk_type == "PLTE" then
process_palette(stream, image)
else
stream:seek(chunk_len + 12)
end
end
-- reconstruction
function paeth(a, b, c)
local p = a + b - c
local pa, pb, pc = abs(p - a), abs(p - b), abs(p - c)
if pa <= pb and pa <= pc then
return a
elseif pb <= pc then
return b
else
return c
end
end
function scanlines(image)
assert(image.bit_depth % 8 == 0, "bit depth not supported")
local stream = byte_stream(image.data)
local pixel_width = image.channels * image.bit_depth/8
local scanline_width = image.width * pixel_width
local ostream = output_stream()
return function()
local lstream = output_stream()
if not stream:is_empty() then
local filter_method = int(stream:read(1))
for i = 1, scanline_width do
local x = int(stream:read(1))
local a = int(ostream:back_read(pixel_width, 1))
local b = int(ostream:back_read(scanline_width, 1))
local c = int(ostream:back_read(scanline_width + pixel_width, 1))
if i <= pixel_width then
a, c = 0, 0
end
local byte
if filter_method == 0 then
byte = string.char(x)
elseif filter_method == 1 then
byte = string.char((x + a) % 256)
elseif filter_method == 2 then
byte = string.char((x + b) % 256)
elseif filter_method == 3 then
byte = string.char((x + floor((a + b)/2)) % 256)
elseif filter_method == 4 then
byte = string.char((x + paeth(a, b, c)) % 256)
end
lstream:write(byte)
ostream:write(byte)
end
end
return lstream:raw()
end
end
function pixel(stream, color_type, bit_depth)
assert(bit_depth % 8 == 0, "bit depth not supported")
local channels = CHANNELS[color_type]
local function read_value()
return int(stream:read(bit_depth/8))
end
if color_type == 0 then
return {
v = read_value()
}
elseif color_type == 2 then
return {
r = read_value(),
g = read_value(),
b = read_value()
}
elseif color_type == 3 then
return {
v = int(stream:read(bit_depth/8))
}
elseif color_type == 4 then
return {
v = read_value(),
a = read_value()
}
elseif color_type == 6 then
return {
r = read_value(),
g = read_value(),
b = read_value(),
a = read_value()
}
end
end
function png.pixels(image)
local i = 0
local next_scanline = scanlines(image)
local scanline = byte_stream(next_scanline())
return function()
if scanline:is_empty() then
return
end
local p = pixel(scanline, image.color_type, image.bit_depth)
local x = i % image.width
local y = floor(i / image.width)
i = i + 1
if scanline:is_empty() then
scanline = byte_stream(next_scanline())
end
return p, x, y
end
end
-- exports
local PNG_HEADER = string.char(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a)
function png.load_from_file(filename)
local file = io.open(filename, "rb")
local data = file:read("*all")
file:close()
return png.load(data)
end
function png.load(data)
local stream = byte_stream(data)
assert(stream:read(8) == PNG_HEADER, "PNG header not found")
local image = {}
repeat
process_chunk(stream, image)
until stream:is_empty()
return image
end
end -- png.lua
if not term.getGraphicsMode or not term.drawPixels then error("This requires CraftOS-PC v2.1 or later.") end
local args = {...}
if #args < 1 then error("Usage: pngview <image.png>") end
local image = png.load_from_file(shell.resolve(args[1]))
if image.data == nil then error("data is nil") end
local w, h = term.getSize(2)
if image.width > w or image.height > h then error("Image is too big") end
os.queueEvent("nosleep")
os.pullEvent()
term.setGraphicsMode(2)
term.clear()
if image.color_type == 0 or image.color_type == 4 then for i = 0, 2^image.bit_depth-1 do term.setPaletteColor(i, i/(2^image.bit_depth-1), i/(2^image.bit_depth-1), i/(2^image.bit_depth-1)) end
elseif image.color_type == 3 then for i = 0, #image.palette do term.setPaletteColor(i, image.palette[i].r/255, image.palette[i].g/255, image.palette[i].b/255) end
elseif image.color_type == 2 or image.color_type == 6 then
local palette = {}
--local bpp = 3 + (bit.band(image.color_type, 4) / 4) --?
local data = {}
for p, x, y in png.pixels(image) do
local idx
for i,v in ipairs(palette) do if v.r == p.r and v.g == p.g and v.b == p.b then idx = i; break end end
if idx == nil then
if #palette >= 256 then
term.setGraphicsMode(false)
error("Image has too many colors")
end
idx = #palette + 1
palette[idx] = p
end
if data[y] == nil then data[y] = {} end
data[y][x] = idx-1
--if x == 0 then os.sleep(0) end
end
for i,v in ipairs(palette) do term.setPaletteColor(i-1, v.r / 255, v.g / 255, v.b / 255) end
term.drawPixels(0, 0, data)
read()
term.setGraphicsMode(false)
for i = 0, 15 do term.setPaletteColor(2^i, term.nativePaletteColor(2^i)) end
return
else error("Image not supported") end
term.clear()
--local start = os.epoch("utc")
local pixels = {}
for y = 0, image.height - 1 do
if image.color_type == 4 then
local str = string.sub(image.data, y * (image.width * 2 + 1) + 1, (y + 1) * (image.width * 2 + 1))
pixels[y] = str
for i = 1, #str, 2 do pixels[y] = pixels[y] .. string.sub(str, i, i) end
else
if image.bit_depth == 8 then pixels[y] = string.sub(image.data, y * (image.width + 2) + 1, (y + 1) * (image.width + 2))
else
pixels[y] = ""
for x = 1, image.width / (8 / image.bit_depth) do
for i = 1, 8 / image.bit_depth do
pixels[y] = pixels[y] .. string.char(bit32.band(bit32.rshift(string.byte(image.data, y * math.floor(image.width / (8 / image.bit_depth) + 1) + x), ((8/image.bit_depth)-i) * image.bit_depth), 2^image.bit_depth - 1))
end
end
end
end
end
term.drawPixels(0, 0, pixels)
--print("Render took " .. os.epoch("utc") - start .. " ms")
read()
term.setGraphicsMode(0)
for i = 0, 15 do term.setPaletteColor(2^i, term.nativePaletteColor(2^i)) end