unicode_text/hexfont.lua

361 lines
10 KiB
Lua
Raw Normal View History

2023-03-17 03:39:41 +01:00
#!/usr/bin/env lua5.1
--[[
Copyright © 2023 Nils Dagsson Moskopp (erle)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
Dieses Programm hat das Ziel, die Medienkompetenz der Leser zu
steigern. Gelegentlich packe ich sogar einen handfesten Buffer
Overflow oder eine Format String Vulnerability zwischen die anderen
Codezeilen und schreibe das auch nicht dran.
]]--
dofile("combining.lua")
dofile("pixelops.lua")
dofile("utf8.lua")
-- a lookup table was chosen for readability
-- DO NOT EVER REFUCKTOR IT INTO A FUNCTION!
local hex_to_bin = {
["0"] = "0000",
["1"] = "0001",
["2"] = "0010",
["3"] = "0011",
["4"] = "0100",
["5"] = "0101",
["6"] = "0110",
["7"] = "0111",
["8"] = "1000",
["9"] = "1001",
["A"] = "1010",
["B"] = "1011",
["C"] = "1100",
["D"] = "1101",
["E"] = "1110",
["F"] = "1111",
}
-- convert a binary bitmap to pixels accepted by tga_encoder
--
-- properties.background_color and properties.foreground_color must
-- have the same amount of entries. Use one entry for grayscale or
-- colormapped (palette) output, use three entries for RGB and four
-- entries for RGBA.
--
local bitmap_to_pixels = function(bitmap_hex, properties)
-- bitmap_hex must be a string of uppercase hexadecimal digits
assert(
"string" == type(bitmap_hex) and
bitmap_hex:match("[0123456789ABCDEF]+") == bitmap_hex
)
local properties = properties or {}
assert(
"table" == type(properties)
)
local background_color = properties.background_color or { 0 }
local foreground_color = properties.foreground_color or { 255 }
-- background and foreground color must have equal color depth
assert(
#background_color == #foreground_color
)
local colormap = {
background_color,
foreground_color,
}
local kerning = properties.kerning or false
assert(
"boolean" == type(kerning)
)
-- scanline order “bottom-top” was chosen as the default to match
-- the default scanline order of tga_encoder and to require users
-- using another file format encoder to care about scanline order
-- (users who “do not care about scanline order” might find their
-- glyphs upside down … the fault, naturally, lies with the user)
local scanline_order = properties.scanline_order or "bottom-top"
assert(
"bottom-top" == scanline_order or
"top-bottom" == scanline_order
)
local height = 16
local width = bitmap_hex:len() * 4 / height
assert(
16 == width or -- full-width character
8 == width -- half-width character
)
-- convert hexadecimal bitmap to binary bitmap
local bitmap_bin_table = {}
for i = 1, #bitmap_hex do
local character = bitmap_hex:sub(i,i)
bitmap_bin_table[i] = hex_to_bin[character]
end
bitmap_bin = table.concat(bitmap_bin_table)
-- decode binary bitmap with “top-bottom” scanline order
-- (i.e. the first encoded pixel is the top left pixel)
local pixels = {}
for scanline = 1, height do
pixels[scanline] = {}
for w = 1, width do
local i = ( ( scanline - 1 ) * width ) + w
local pixel
pixel = colormap[tonumber(bitmap_bin:sub(i,i)) + 1]
pixels[scanline][w] = pixel
end
end
-- flip image upside down for ”bottom-top” scanline order
-- (i.e. the first encoded pixel is the bottom left pixel)
if "bottom-top" == scanline_order then
pixels = pixelops.flip_vertical(pixels)
end
if kerning then
-- remove rightmost column if it is empty
local remove_rightmost_column = true
for h = 1, height do
if foreground_color == pixels[h][width] then
remove_rightmost_column = false
end
end
if remove_rightmost_column then
for h = 1, height do
pixels[h][width] = nil
end
end
-- remove leftmost column if it and the column to its right are
-- both empty, glyphs touch too often without the extra check
local remove_leftmost_column = true
for h = 1, height do
if (
foreground_color == pixels[h][1] or
foreground_color == pixels[h][2]
) then
remove_leftmost_column = false
end
end
if remove_leftmost_column then
for h = 1, height do
for w = 1, width do
pixels[h][w] = pixels[h][w+1]
end
end
end
end
return pixels
end
hexfont = setmetatable(
{},
{
__call = function(self, ...)
local new_hexfont = setmetatable(
{},
{
__index = self
}
)
new_hexfont:constructor(...)
return new_hexfont
end
}
)
local iter = function(table_)
local index = 0
local total = #table_
return function()
index = index + 1
if index <= total
then
return table_[index]
end
end
end
hexfont.constructor = function(self)
local minimal_hexfont = {
-- U+FFFD REPLACEMENT CHARACTER
"FFFD:0000018003C006600C301998399C7F3E7E7E3E7C1FF80E70066003C001800000"
}
self:load_glyphs(
iter(minimal_hexfont)
)
end
-- Usage:
-- hexfont.load_glyphs(io.lines("unifont.hex"))
-- hexfont.load_glyphs(io.lines("unifont_upper.hex"))
hexfont.load_glyphs = function(self, iterator)
assert( "function" == type(iterator) )
for line in iterator do
assert("string" == type(line))
local codepoint_hex, bitmap_hex = line:match(
"([0123456789ABCDEF]+):([01234567890ABCDEF]+)"
)
codepoint = tonumber(codepoint_hex, 16)
self[codepoint] = bitmap_hex
end
end
-- Test: Glyphs are correctly loaded
local font = hexfont()
assert(
font[0xFFFD] == "0000018003C006600C301998399C7F3E7E7E3E7C1FF80E70066003C001800000"
)
font = nil
hexfont.render_line = function(self, text, properties)
assert(
"string" == type(text)
)
properties = properties or {}
assert(
"table" == type(properties)
)
-- default colors are black (0) & white (255) in 1 bit color depth
local background_color = properties.background_color or { 0 }
local foreground_color = properties.foreground_color or { 255 }
-- background and foreground color must have equal color depth
assert(
#background_color == #foreground_color
)
local minimal_width = properties.minimal_width or 0
assert(
"number" == type(minimal_width)
)
local tabulator_size = properties.tabulator_size or 8 * 8
assert(
"number" == type(tabulator_size)
)
local result = {}
for i = 1, 16 do
result[i] = {}
end
local codepoints = utf8.text_to_codepoints(text)
-- FIXME: only works for LTR, should use UAX #9
for i = 1, #codepoints do
local codepoint = codepoints[i]
local bitmap_hex = self[codepoint]
-- use U+FFFD as fallback character
if nil == bitmap_hex then
bitmap_hex = self[0xFFFD]
end
local bitmap = bitmap_to_pixels(
bitmap_hex,
properties
)
if 0x0009 == codepoint then -- HT (horizontal tab)
local result_width = #result[1]
local tab_stop = math.floor(
result_width / tabulator_size + 1
) * tabulator_size
result = pixelops.pad_right(
2023-03-17 03:39:41 +01:00
result,
tab_stop - result_width,
background_color
)
else
local result_width = #result[1]
local bitmap_width = #bitmap[1]
if is_combining_character[codepoint] then
-- render combining glyph over previous glyph
-- FIXME: this is horrible, but seems to work
for j = 1, 16 do
for k = 1, bitmap_width do
if foreground_color == bitmap[j][k] then
result[j][result_width - bitmap_width + k] = bitmap[j][k]
end
end
end
else
-- append current glyph at right edge of result
for j = 1, 16 do
for k = 1, bitmap_width do
result[j][result_width + k] = bitmap[j][k]
end
end
end
end
end
return result
end
hexfont.render_text = function(self, text, properties)
local properties = properties or {}
assert(
"table" == type(properties)
)
local background_color = properties.background_color or { 0 }
local foreground_color = properties.foreground_color or { 255 }
-- background and foreground color must have equal color depth
assert(
#background_color == #foreground_color
)
local scanline_order = properties.scanline_order or "bottom-top"
assert(
"bottom-top" == scanline_order or
"top-bottom" == scanline_order
)
local result
-- TODO: implement UAX #14
for utf8_line in string.gmatch(text .. "\n", "([^\n]*)\n") do
local pixels = self:render_line(
utf8_line,
properties
)
assert( nil ~= pixels )
if nil == result then
result = pixels
else
local result_width = #result[1]
local pixels_width = #pixels[1]
if result_width > pixels_width then
pixels = pixelops.pad_right(
2023-03-17 03:39:41 +01:00
pixels,
result_width - pixels_width,
background_color
)
elseif result_width < pixels_width then
result = pixelops.pad_right(
2023-03-17 03:39:41 +01:00
result,
pixels_width - result_width,
background_color
)
end
assert(
#result[1] == #pixels[1]
)
if "bottom-top" == scanline_order then
for i = #pixels, 1, -1 do
table.insert(result, 1, pixels[i])
end
end
if "top-bottom" == scanline_order then
for i = 1, #pixels do
result[#result+1] = pixels[i]
end
end
end
end
return result
end