Use PNG instead of TGA

This commit is contained in:
Elias Fleckenstein 2021-05-02 12:55:04 +02:00
parent d9a670dcb8
commit ca9cd8cbe0
7 changed files with 305 additions and 119 deletions

@ -1,4 +0,0 @@
# tga_encoder
A TGA Encoder written in Lua without the use of external Libraries.
May be used as a Minetest mod.

@ -1,109 +0,0 @@
tga_encoder = {}
local LUA_ARGS_LIMIT = 1000
local image = setmetatable({}, {
__call = function(self, ...)
local t = setmetatable({}, {__index = self})
t:constructor(...)
return t
end,
})
function image:constructor(pixels)
self.bytes = {}
self.chunks = {self.bytes}
self.pixels = pixels
self.width = #pixels[1]
self.height = #pixels
self:encode()
end
function image:insert(byte)
table.insert(self.bytes, byte)
if #self.bytes == LUA_ARGS_LIMIT then
self.bytes = {}
table.insert(self.chunks, self.bytes)
end
end
function image:littleendian(size, value)
for i = 1, size do
local byte = value % 256
value = value - byte
value = value / 256
self:insert(byte)
end
end
function image:encode_colormap_spec()
-- first entry index
self:littleendian(2, 0)
-- number of entries
self:littleendian(2, 0)
-- number of bits per pixel
self:insert(0)
end
function image:encode_image_spec()
-- X- and Y- origin
self:littleendian(2, 0)
self:littleendian(2, 0)
-- width and height
self:littleendian(2, self.width)
self:littleendian(2, self.height)
-- pixel depth
self:insert(24)
-- image descriptor
self:insert(0)
end
function image:encode_header()
-- id length
self:insert(0) -- no image id info
-- color map type
self:insert(0) -- no color map
-- image type
self:insert(2) -- uncompressed true-color image
-- color map specification
self:encode_colormap_spec()
-- image specification
self:encode_image_spec()
end
function image:encode_data()
for _, row in ipairs(self.pixels) do
for _, pixel in ipairs(row) do
self:insert(pixel[3])
self:insert(pixel[2])
self:insert(pixel[1])
end
end
end
function image:encode()
-- encode header
self:encode_header()
-- no color map and image id data
-- encode data
self:encode_data()
-- no extension area
end
function image:get_data()
local data = ""
for _, bytes in ipairs(self.chunks) do
data = data .. string.char(unpack(bytes))
end
return data .. string.char(0, 0, 0, 0) .. string.char(0, 0, 0, 0) .. "TRUEVISION-XFILE." .. string.char(0)
end
function image:save(filename)
self.data = self.data or self:get_data()
local f = assert(io.open(filename, "w"))
f:write(self.data)
f:close()
end
tga_encoder.image = image

@ -1,3 +0,0 @@
name = tga_encoder
author = Fleckenstein
description = A TGA Encoder written in Lua without the use of external Libraries.

@ -0,0 +1,98 @@
bit32 = {}
local N = 32
local P = 2^N
function bit32.bnot(x)
x = x % P
return P - 1 - x
end
function bit32.band(x, y)
-- Common usecases, they deserve to be optimized
if y == 0xff then return x % 0x100 end
if y == 0xffff then return x % 0x10000 end
if y == 0xffffffff then return x % 0x100000000 end
x, y = x % P, y % P
local r = 0
local p = 1
for i = 1, N do
local a, b = x % 2, y % 2
x, y = math.floor(x / 2), math.floor(y / 2)
if a + b == 2 then
r = r + p
end
p = 2 * p
end
return r
end
function bit32.bor(x, y)
-- Common usecases, they deserve to be optimized
if y == 0xff then return x - (x%0x100) + 0xff end
if y == 0xffff then return x - (x%0x10000) + 0xffff end
if y == 0xffffffff then return 0xffffffff end
x, y = x % P, y % P
local r = 0
local p = 1
for i = 1, N do
local a, b = x % 2, y % 2
x, y = math.floor(x / 2), math.floor(y / 2)
if a + b >= 1 then
r = r + p
end
p = 2 * p
end
return r
end
function bit32.bxor(x, y)
x, y = x % P, y % P
local r = 0
local p = 1
for i = 1, N do
local a, b = x%2, y%2
x, y = math.floor(x/2), math.floor(y/2)
if a + b == 1 then
r = r + p
end
p = 2 * p
end
return r
end
function bit32.lshift(x, s_amount)
if math.abs(s_amount) >= N then return 0 end
x = x % P
if s_amount < 0 then
return math.floor(x * (2 ^ s_amount))
else
return (x * (2 ^ s_amount)) % P
end
end
function bit32.rshift(x, s_amount)
if math.abs(s_amount) >= N then return 0 end
x = x % P
if s_amount > 0 then
return math.floor(x * (2 ^ - s_amount))
else
return (x * (2 ^ -s_amount)) % P
end
end
function bit32.arshift(x, s_amount)
if math.abs(s_amount) >= N then return 0 end
x = x % P
if s_amount > 0 then
local add = 0
if x >= P/2 then
add = P - 2 ^ (N - s_amount)
end
return math.floor(x * (2 ^ -s_amount)) + add
else
return (x * (2 ^ -s_amount)) % P
end
end

@ -7,6 +7,11 @@ local worldpath = minetest.get_worldpath()
local map_textures_path = worldpath .. "/mcl_maps/" local map_textures_path = worldpath .. "/mcl_maps/"
local last_finished_id = storage:get_int("next_id") - 1 local last_finished_id = storage:get_int("next_id") - 1
dofile(modpath .. "/bit32.lua") -- taken from http://gitea.minetest.one/minetest-mods/turtle/src/branch/master/bit32.lua
bit = bit32
pngencoder = dofile(modpath .. "/pngencoder.lua") -- taken from https://github.com/wyozi/lua-pngencoder/blob/master/pngencoder.lua
minetest.mkdir(map_textures_path) minetest.mkdir(map_textures_path)
local function load_json_file(name) local function load_json_file(name)
@ -32,7 +37,7 @@ function mcl_maps.create_map(pos)
local meta = itemstack:get_meta() local meta = itemstack:get_meta()
local id = storage:get_int("next_id") local id = storage:get_int("next_id")
storage:set_int("next_id", id + 1) storage:set_int("next_id", id + 1)
local texture_file = "mcl_maps_map_texture_" .. id .. ".tga" local texture_file = "mcl_maps_map_texture_" .. id .. ".png"
local texture_path = map_textures_path .. texture_file local texture_path = map_textures_path .. texture_file
local texture = "[combine:140x140:0,0=mcl_maps_map_background.png:6,6=" .. texture_file local texture = "[combine:140x140:0,0=mcl_maps_map_background.png:6,6=" .. texture_file
meta:set_int("mcl_maps:id", id) meta:set_int("mcl_maps:id", id)
@ -121,7 +126,16 @@ function mcl_maps.create_map(pos)
end end
last_heightmap = heightmap last_heightmap = heightmap
end end
tga_encoder.image(pixels):save(texture_path) local image = pngencoder(128, 128, "rgb")
for _, row in ipairs(pixels) do
for _, pixel in ipairs(row) do
image:write(pixel)
end
end
assert(image.done)
local f = assert(io.open(texture_path, "w"))
f:write(table.concat(image.output))
f:close()
creating_maps[texture] = false creating_maps[texture] = false
end) end)
return itemstack return itemstack

@ -1,2 +1,2 @@
name = mcl_maps name = mcl_maps
depends = mcl_core, mcl_flowers, tga_encoder, tt, mcl_colors depends = mcl_core, mcl_flowers, tt, mcl_colors

@ -0,0 +1,190 @@
local Png = {}
Png.__index = Png
local DEFLATE_MAX_BLOCK_SIZE = 65535
local function putBigUint32(val, tbl, index)
for i=0,3 do
tbl[index + i] = bit.band(bit.rshift(val, (3 - i) * 8), 0xFF)
end
end
function Png:writeBytes(data, index, len)
index = index or 1
len = len or #data
for i=index,index+len-1 do
table.insert(self.output, string.char(data[i]))
end
end
function Png:write(pixels)
local count = #pixels -- Byte count
local pixelPointer = 1
while count > 0 do
if self.positionY >= self.height then
error("All image pixels already written")
end
if self.deflateFilled == 0 then -- Start DEFLATE block
local size = DEFLATE_MAX_BLOCK_SIZE;
if (self.uncompRemain < size) then
size = self.uncompRemain
end
local header = { -- 5 bytes long
bit.band((self.uncompRemain <= DEFLATE_MAX_BLOCK_SIZE and 1 or 0), 0xFF),
bit.band(bit.rshift(size, 0), 0xFF),
bit.band(bit.rshift(size, 8), 0xFF),
bit.band(bit.bxor(bit.rshift(size, 0), 0xFF), 0xFF),
bit.band(bit.bxor(bit.rshift(size, 8), 0xFF), 0xFF),
}
self:writeBytes(header)
self:crc32(header, 1, #header)
end
assert(self.positionX < self.lineSize and self.deflateFilled < DEFLATE_MAX_BLOCK_SIZE);
if (self.positionX == 0) then -- Beginning of line - write filter method byte
local b = {0}
self:writeBytes(b)
self:crc32(b, 1, 1)
self:adler32(b, 1, 1)
self.positionX = self.positionX + 1
self.uncompRemain = self.uncompRemain - 1
self.deflateFilled = self.deflateFilled + 1
else -- Write some pixel bytes for current line
local n = DEFLATE_MAX_BLOCK_SIZE - self.deflateFilled;
if (self.lineSize - self.positionX < n) then
n = self.lineSize - self.positionX
end
if (count < n) then
n = count;
end
assert(n > 0);
self:writeBytes(pixels, pixelPointer, n)
-- Update checksums
self:crc32(pixels, pixelPointer, n);
self:adler32(pixels, pixelPointer, n);
-- Increment positions
count = count - n;
pixelPointer = pixelPointer + n;
self.positionX = self.positionX + n;
self.uncompRemain = self.uncompRemain - n;
self.deflateFilled = self.deflateFilled + n;
end
if (self.deflateFilled >= DEFLATE_MAX_BLOCK_SIZE) then
self.deflateFilled = 0; -- End current block
end
if (self.positionX == self.lineSize) then -- Increment line
self.positionX = 0;
self.positionY = self.positionY + 1;
if (self.positionY == self.height) then -- Reached end of pixels
local footer = { -- 20 bytes long
0, 0, 0, 0, -- DEFLATE Adler-32 placeholder
0, 0, 0, 0, -- IDAT CRC-32 placeholder
-- IEND chunk
0x00, 0x00, 0x00, 0x00,
0x49, 0x45, 0x4E, 0x44,
0xAE, 0x42, 0x60, 0x82,
}
putBigUint32(self.adler, footer, 1)
self:crc32(footer, 1, 4)
putBigUint32(self.crc, footer, 5)
self:writeBytes(footer)
self.done = true
end
end
end
end
function Png:crc32(data, index, len)
self.crc = bit.bnot(self.crc)
for i=index,index+len-1 do
local byte = data[i]
for j=0,7 do -- Inefficient bitwise implementation, instead of table-based
local nbit = bit.band(bit.bxor(self.crc, bit.rshift(byte, j)), 1);
self.crc = bit.bxor(bit.rshift(self.crc, 1), bit.band((-nbit), 0xEDB88320));
end
end
self.crc = bit.bnot(self.crc)
end
function Png:adler32(data, index, len)
local s1 = bit.band(self.adler, 0xFFFF)
local s2 = bit.rshift(self.adler, 16)
for i=index,index+len-1 do
s1 = (s1 + data[i]) % 65521
s2 = (s2 + s1) % 65521
end
self.adler = bit.bor(bit.lshift(s2, 16), s1)
end
local function begin(width, height, colorMode)
-- Default to rgb
colorMode = colorMode or "rgb"
-- Determine bytes per pixel and the PNG internal color type
local bytesPerPixel, colorType
if colorMode == "rgb" then
bytesPerPixel, colorType = 3, 2
elseif colorMode == "rgba" then
bytesPerPixel, colorType = 4, 6
else
error("Invalid colorMode")
end
local state = setmetatable({ width = width, height = height, done = false, output = {} }, Png)
-- Compute and check data siezs
state.lineSize = width * bytesPerPixel + 1
-- TODO: check if lineSize too big
state.uncompRemain = state.lineSize * height
local numBlocks = math.ceil(state.uncompRemain / DEFLATE_MAX_BLOCK_SIZE)
-- 5 bytes per DEFLATE uncompressed block header, 2 bytes for zlib header, 4 bytes for zlib Adler-32 footer
local idatSize = numBlocks * 5 + 6
idatSize = idatSize + state.uncompRemain;
-- TODO check if idatSize too big
local header = { -- 43 bytes long
-- PNG header
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
-- IHDR chunk
0x00, 0x00, 0x00, 0x0D,
0x49, 0x48, 0x44, 0x52,
0, 0, 0, 0, -- 'width' placeholder
0, 0, 0, 0, -- 'height' placeholder
0x08, colorType, 0x00, 0x00, 0x00,
0, 0, 0, 0, -- IHDR CRC-32 placeholder
-- IDAT chunk
0, 0, 0, 0, -- 'idatSize' placeholder
0x49, 0x44, 0x41, 0x54,
-- DEFLATE data
0x08, 0x1D,
}
putBigUint32(width, header, 17)
putBigUint32(height, header, 21)
putBigUint32(idatSize, header, 34)
state.crc = 0
state:crc32(header, 13, 17)
putBigUint32(state.crc, header, 30)
state:writeBytes(header)
state.crc = 0
state:crc32(header, 38, 6); -- 0xD7245B6B
state.adler = 1
state.positionX = 0
state.positionY = 0
state.deflateFilled = 0
return state
end
return begin