VoxeLibre/init.lua
2022-05-16 02:48:51 +02:00

390 lines
12 KiB
Lua

tga_encoder = {}
local image = setmetatable({}, {
__call = function(self, ...)
local t = setmetatable({}, {__index = self})
t:constructor(...)
return t
end,
})
function image:constructor(pixels)
self.pixels = pixels
self.width = #pixels[1]
self.height = #pixels
end
function image:encode_colormap_spec()
self.data = self.data
.. string.char(0, 0) -- first entry index
.. string.char(0, 0) -- number of entries
.. string.char(0) -- bits per pixel
end
function image:encode_image_spec(properties)
local color_format = properties.color_format
assert(
"Y8" == color_format or -- (8 bit grayscale = 1 byte = 8 bits)
"A1R5G5B5" == color_format or -- (A1R5G5B5 = 2 bytes = 16 bits)
"B8G8R8" == color_format or -- (B8G8R8 = 3 bytes = 24 bits)
"B8G8R8A8" == color_format -- (B8G8R8A8 = 4 bytes = 32 bits)
)
pixel_depth_by_color_format = {
["Y8"] = 8,
["A1R5G5B5"] = 16,
["B8G8R8"] = 24,
["B8G8R8A8"] = 32,
}
local pixel_depth = pixel_depth_by_color_format[color_format]
assert( nil ~= pixel_depth)
self.data = self.data
.. string.char(0, 0) -- X-origin
.. string.char(0, 0) -- Y-origin
.. string.char(self.width % 256, math.floor(self.width / 256)) -- width
.. string.char(self.height % 256, math.floor(self.height / 256)) -- height
.. string.char(pixel_depth)
.. string.char(0) -- image descriptor
end
function image:encode_header(properties)
local color_format = properties.color_format
local compression = properties.compression
local image_type
if "Y8" == color_format and "RAW" == compression then
image_type = 3 -- grayscale
elseif (
"A1R5G5B5" == color_format or
"B8G8R8" == color_format or
"B8G8R8A8" == color_format
) then
if "RAW" == compression then
image_type = 2 -- RAW RGB(A)
elseif "RLE" == compression then
image_type = 10 -- RLE RGB
end
end
assert( nil ~= image_type )
self.data = self.data
.. string.char(0) -- image id
.. string.char(0) -- color map type
.. string.char(image_type)
self:encode_colormap_spec() -- color map specification
self:encode_image_spec(properties) -- image specification
end
function image:encode_data(properties)
local color_format = properties.color_format
local compression = properties.compression
if "Y8" == color_format and "RAW" == compression then
if 8 == self.pixel_depth then
self:encode_data_Y8_as_Y8_raw()
elseif 24 == self.pixel_depth then
self:encode_data_R8G8B8_as_Y8_raw()
end
elseif "A1R5G5B5" == color_format then
if "RAW" == compression then
self:encode_data_R8G8B8_as_A1R5G5B5_raw()
elseif "RLE" == compression then
self:encode_data_R8G8B8_as_A1R5G5B5_rle()
end
elseif "B8G8R8" == color_format then
if "RAW" == compression then
self:encode_data_R8G8B8_as_B8G8R8_raw()
elseif "RLE" == compression then
self:encode_data_R8G8B8_as_B8G8R8_rle()
end
elseif "B8G8R8A8" == color_format then
if "RAW" == compression then
self:encode_data_R8G8B8A8_as_B8G8R8A8_raw()
end
end
end
function image:encode_data_Y8_as_Y8_raw()
assert(8 == self.pixel_depth)
local raw_pixels = {}
for _, row in ipairs(self.pixels) do
for _, pixel in ipairs(row) do
local raw_pixel = string.char(pixel[1])
raw_pixels[#raw_pixels + 1] = raw_pixel
end
end
self.data = self.data .. table.concat(raw_pixels)
end
function image:encode_data_R8G8B8_as_Y8_raw()
assert(24 == self.pixel_depth)
local raw_pixels = {}
for _, row in ipairs(self.pixels) do
for _, pixel in ipairs(row) do
-- the HSP RGB to brightness formula is
-- sqrt( 0.299 r² + .587 g² + .114 b² )
-- see <https://alienryderflex.com/hsp.html>
local gray = math.floor(
math.sqrt(
0.299 * pixel[1]^2 +
0.587 * pixel[2]^2 +
0.114 * pixel[3]^2
) + 0.5
)
local raw_pixel = string.char(gray)
raw_pixels[#raw_pixels + 1] = raw_pixel
end
end
self.data = self.data .. table.concat(raw_pixels)
end
function image:encode_data_R8G8B8_as_A1R5G5B5_raw()
assert(24 == self.pixel_depth)
local raw_pixels = {}
-- Sample depth rescaling is done according to the algorithm presented in:
-- <https://www.w3.org/TR/2003/REC-PNG-20031110/#13Sample-depth-rescaling>
local max_sample_in = math.pow(2, 8) - 1
local max_sample_out = math.pow(2, 5) - 1
for _, row in ipairs(self.pixels) do
for _, pixel in ipairs(row) do
local colorword = 32768 +
((math.floor((pixel[1] * max_sample_out / max_sample_in) + 0.5)) * 1024) +
((math.floor((pixel[2] * max_sample_out / max_sample_in) + 0.5)) * 32) +
((math.floor((pixel[3] * max_sample_out / max_sample_in) + 0.5)) * 1)
local raw_pixel = string.char(colorword % 256, math.floor(colorword / 256))
raw_pixels[#raw_pixels + 1] = raw_pixel
end
end
self.data = self.data .. table.concat(raw_pixels)
end
function image:encode_data_R8G8B8_as_A1R5G5B5_rle()
assert(24 == self.pixel_depth)
local colorword = nil
local previous_r = nil
local previous_g = nil
local previous_b = nil
local raw_pixel = ''
local raw_pixels = {}
local count = 1
local packets = {}
local raw_packet = ''
local rle_packet = ''
-- Sample depth rescaling is done according to the algorithm presented in:
-- <https://www.w3.org/TR/2003/REC-PNG-20031110/#13Sample-depth-rescaling>
local max_sample_in = math.pow(2, 8) - 1
local max_sample_out = math.pow(2, 5) - 1
for _, row in ipairs(self.pixels) do
for _, pixel in ipairs(row) do
if pixel[1] ~= previous_r or pixel[2] ~= previous_g or pixel[3] ~= previous_b or count == 128 then
if nil ~= previous_r then
colorword = 32768 +
((math.floor((previous_r * max_sample_out / max_sample_in) + 0.5)) * 1024) +
((math.floor((previous_g * max_sample_out / max_sample_in) + 0.5)) * 32) +
((math.floor((previous_b * max_sample_out / max_sample_in) + 0.5)) * 1)
if 1 == count then
-- remember pixel verbatim for raw encoding
raw_pixel = string.char(colorword % 256, math.floor(colorword / 256))
raw_pixels[#raw_pixels + 1] = raw_pixel
if 128 == #raw_pixels then
raw_packet = string.char(#raw_pixels - 1)
packets[#packets + 1] = raw_packet
for i=1, #raw_pixels do
packets[#packets +1] = raw_pixels[i]
end
raw_pixels = {}
end
else
-- encode raw pixels, if any
if #raw_pixels > 0 then
raw_packet = string.char(#raw_pixels - 1)
packets[#packets + 1] = raw_packet
for i=1, #raw_pixels do
packets[#packets +1] = raw_pixels[i]
end
raw_pixels = {}
end
-- RLE encoding
rle_packet = string.char(128 + count - 1, colorword % 256, math.floor(colorword / 256))
packets[#packets +1] = rle_packet
end
end
count = 1
previous_r = pixel[1]
previous_g = pixel[2]
previous_b = pixel[3]
else
count = count + 1
end
end
end
colorword = 32768 +
((math.floor((previous_r * max_sample_out / max_sample_in) + 0.5)) * 1024) +
((math.floor((previous_g * max_sample_out / max_sample_in) + 0.5)) * 32) +
((math.floor((previous_b * max_sample_out / max_sample_in) + 0.5)) * 1)
if 1 == count then
raw_pixel = string.char(colorword % 256, math.floor(colorword / 256))
raw_pixels[#raw_pixels + 1] = raw_pixel
raw_packet = string.char(#raw_pixels - 1)
packets[#packets + 1] = raw_packet
for i=1, #raw_pixels do
packets[#packets +1] = raw_pixels[i]
end
raw_pixels = {}
else
-- encode raw pixels, if any
if #raw_pixels > 0 then
raw_packet = string.char(#raw_pixels - 1)
packets[#packets + 1] = raw_packet
for i=1, #raw_pixels do
packets[#packets +1] = raw_pixels[i]
end
raw_pixels = {}
end
-- RLE encoding
rle_packet = string.char(128 + count - 1, colorword % 256, math.floor(colorword / 256))
packets[#packets +1] = rle_packet
end
self.data = self.data .. table.concat(packets)
end
function image:encode_data_R8G8B8_as_B8G8R8_raw()
assert(24 == self.pixel_depth)
local raw_pixels = {}
for _, row in ipairs(self.pixels) do
for _, pixel in ipairs(row) do
local raw_pixel = string.char(pixel[3], pixel[2], pixel[1])
raw_pixels[#raw_pixels + 1] = raw_pixel
end
end
self.data = self.data .. table.concat(raw_pixels)
end
function image:encode_data_R8G8B8_as_B8G8R8_rle()
assert(24 == self.pixel_depth)
local previous_r = nil
local previous_g = nil
local previous_b = nil
local raw_pixel = ''
local raw_pixels = {}
local count = 1
local packets = {}
local raw_packet = ''
local rle_packet = ''
for _, row in ipairs(self.pixels) do
for _, pixel in ipairs(row) do
if pixel[1] ~= previous_r or pixel[2] ~= previous_g or pixel[3] ~= previous_b or count == 128 then
if nil ~= previous_r then
if 1 == count then
-- remember pixel verbatim for raw encoding
raw_pixel = string.char(previous_b, previous_g, previous_r)
raw_pixels[#raw_pixels + 1] = raw_pixel
if 128 == #raw_pixels then
raw_packet = string.char(#raw_pixels - 1)
packets[#packets + 1] = raw_packet
for i=1, #raw_pixels do
packets[#packets +1] = raw_pixels[i]
end
raw_pixels = {}
end
else
-- encode raw pixels, if any
if #raw_pixels > 0 then
raw_packet = string.char(#raw_pixels - 1)
packets[#packets + 1] = raw_packet
for i=1, #raw_pixels do
packets[#packets +1] = raw_pixels[i]
end
raw_pixels = {}
end
-- RLE encoding
rle_packet = string.char(128 + count - 1, previous_b, previous_g, previous_r)
packets[#packets +1] = rle_packet
end
end
count = 1
previous_r = pixel[1]
previous_g = pixel[2]
previous_b = pixel[3]
else
count = count + 1
end
end
end
if 1 == count then
raw_pixel = string.char(previous_b, previous_g, previous_r)
raw_pixels[#raw_pixels + 1] = raw_pixel
raw_packet = string.char(#raw_pixels - 1)
packets[#packets + 1] = raw_packet
for i=1, #raw_pixels do
packets[#packets +1] = raw_pixels[i]
end
raw_pixels = {}
else
-- encode raw pixels, if any
if #raw_pixels > 0 then
raw_packet = string.char(#raw_pixels - 1)
packets[#packets + 1] = raw_packet
for i=1, #raw_pixels do
packets[#packets +1] = raw_pixels[i]
end
raw_pixels = {}
end
-- RLE encoding
rle_packet = string.char(128 + count - 1, previous_b, previous_g, previous_r)
packets[#packets +1] = rle_packet
end
self.data = self.data .. table.concat(packets)
end
function image:encode_data_R8G8B8A8_as_B8G8R8A8_raw()
assert(32 == self.pixel_depth)
local raw_pixels = {}
for _, row in ipairs(self.pixels) do
for _, pixel in ipairs(row) do
local raw_pixel = string.char(pixel[3], pixel[2], pixel[1], pixel[4])
raw_pixels[#raw_pixels + 1] = raw_pixel
end
end
self.data = self.data .. table.concat(raw_pixels)
end
function image:encode_footer()
self.data = self.data
.. string.char(0, 0, 0, 0) -- extension area offset
.. string.char(0, 0, 0, 0) -- developer area offset
.. "TRUEVISION-XFILE"
.. "."
.. string.char(0)
end
function image:encode(properties)
self.data = ""
self:encode_header(properties) -- header
-- no color map and image id data
self:encode_data(properties) -- encode data
-- no extension or developer area
self:encode_footer() -- footer
end
function image:save(filename, properties)
local properties = properties or {}
properties.compression = properties.compression or "RAW"
self.pixel_depth = #self.pixels[1][1] * 8
if nil == properties.color_format then
if 8 == self.pixel_depth then
properties.color_format = "Y8"
elseif 24 == self.pixel_depth then
properties.color_format = "B8G8R8"
elseif 32 == self.pixel_depth then
properties.color_format = "B8G8R8A8"
end
end
assert( nil ~= properties.color_format )
self:encode(properties)
local f = assert(io.open(filename, "wb"))
f:write(self.data)
f:close()
end
tga_encoder.image = image