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.
|
|
|
|
|
]]--
|
|
|
|
|
|
2023-03-22 02:59:44 +01:00
|
|
|
|
dofile("bidi.lua")
|
2023-03-17 03:39:41 +01:00
|
|
|
|
dofile("pixelops.lua")
|
2023-03-21 00:31:19 +01:00
|
|
|
|
dofile("unicodedata.lua")
|
2023-03-17 03:39:41 +01:00
|
|
|
|
dofile("utf8.lua")
|
|
|
|
|
|
2023-03-18 22:33:18 +01:00
|
|
|
|
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, properties)
|
|
|
|
|
properties = properties or {}
|
|
|
|
|
assert(
|
|
|
|
|
"table" == type(properties)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
-- Defaults
|
|
|
|
|
self.background_color = properties.background_color or { 0x00 }
|
|
|
|
|
self.foreground_color = properties.foreground_color or { 0xFF }
|
2023-03-18 23:45:23 +01:00
|
|
|
|
|
|
|
|
|
-- 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)
|
2023-03-18 22:33:18 +01:00
|
|
|
|
self.scanline_order = properties.scanline_order or "bottom-top"
|
2023-03-18 23:45:23 +01:00
|
|
|
|
|
|
|
|
|
-- tab size = 8 half-width spaces when using GNU Unifont
|
2023-03-18 22:33:18 +01:00
|
|
|
|
self.tabulator_size = properties.tabulator_size or 8 * 8
|
|
|
|
|
self.kerning = properties.kerning or false
|
|
|
|
|
|
|
|
|
|
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)
|
2023-09-04 19:07:23 +02:00
|
|
|
|
assert( "function" == type(iterator), "Are you using io.lines()?" )
|
2023-03-18 22:33:18 +01:00
|
|
|
|
for line in iterator do
|
|
|
|
|
assert("string" == type(line))
|
|
|
|
|
local codepoint_hex, bitmap_hex = line:match(
|
|
|
|
|
"([0123456789ABCDEF]+):([01234567890ABCDEF]+)"
|
|
|
|
|
)
|
2023-09-04 19:10:51 +02:00
|
|
|
|
local codepoint = tonumber(codepoint_hex, 16)
|
2023-03-18 22:33:18 +01:00
|
|
|
|
self[codepoint] = bitmap_hex
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Test: Glyphs are correctly loaded
|
|
|
|
|
local font = hexfont({})
|
|
|
|
|
assert(
|
|
|
|
|
font[0xFFFD] == "0000018003C006600C301998399C7F3E7E7E3E7C1FF80E70066003C001800000"
|
|
|
|
|
)
|
|
|
|
|
font = nil
|
|
|
|
|
|
2023-03-17 03:39:41 +01:00
|
|
|
|
-- 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",
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-18 22:33:18 +01:00
|
|
|
|
hexfont.bitmap_to_pixels = function(self, bitmap_hex)
|
2023-03-17 03:39:41 +01:00
|
|
|
|
-- bitmap_hex must be a string of uppercase hexadecimal digits
|
|
|
|
|
assert(
|
|
|
|
|
"string" == type(bitmap_hex) and
|
|
|
|
|
bitmap_hex:match("[0123456789ABCDEF]+") == bitmap_hex
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
-- background and foreground color must have equal color depth
|
|
|
|
|
assert(
|
2023-03-18 22:33:18 +01:00
|
|
|
|
"table" == type(self.background_color) and
|
|
|
|
|
"table" == type(self.foreground_color) and
|
|
|
|
|
#self.background_color == #self.foreground_color
|
2023-03-17 03:39:41 +01:00
|
|
|
|
)
|
|
|
|
|
local colormap = {
|
2023-03-18 22:33:18 +01:00
|
|
|
|
self.background_color,
|
|
|
|
|
self.foreground_color,
|
2023-03-17 03:39:41 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert(
|
2023-03-18 22:33:18 +01:00
|
|
|
|
"boolean" == type(self.kerning)
|
2023-03-17 03:39:41 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2023-03-18 22:33:18 +01:00
|
|
|
|
if self.kerning then
|
2023-03-17 03:39:41 +01:00
|
|
|
|
-- remove rightmost column if it is empty
|
|
|
|
|
local remove_rightmost_column = true
|
|
|
|
|
for h = 1, height do
|
2023-03-18 22:33:18 +01:00
|
|
|
|
if self.foreground_color == pixels[h][width] then
|
2023-03-17 03:39:41 +01:00
|
|
|
|
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 (
|
2023-03-18 22:33:18 +01:00
|
|
|
|
self.foreground_color == pixels[h][1] or
|
|
|
|
|
self.foreground_color == pixels[h][2]
|
2023-03-17 03:39:41 +01:00
|
|
|
|
) 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
|
|
|
|
|
|
2023-03-18 22:33:18 +01:00
|
|
|
|
hexfont.render_line = function(self, text)
|
2023-03-17 03:39:41 +01:00
|
|
|
|
assert(
|
|
|
|
|
"string" == type(text)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
-- background and foreground color must have equal color depth
|
|
|
|
|
assert(
|
2023-03-18 22:33:18 +01:00
|
|
|
|
"table" == type(self.background_color) and
|
|
|
|
|
"table" == type(self.foreground_color) and
|
|
|
|
|
#self.background_color == #self.foreground_color
|
2023-03-17 03:39:41 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert(
|
2023-03-18 22:33:18 +01:00
|
|
|
|
"number" == type(self.tabulator_size)
|
2023-03-17 03:39:41 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
local result = {}
|
|
|
|
|
for i = 1, 16 do
|
|
|
|
|
result[i] = {}
|
|
|
|
|
end
|
2023-03-22 02:59:44 +01:00
|
|
|
|
local codepoints = bidi.get_visual_reordering(
|
|
|
|
|
utf8.text_to_codepoints(text)
|
|
|
|
|
)
|
2023-03-17 03:39:41 +01:00
|
|
|
|
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
|
2023-03-18 22:33:18 +01:00
|
|
|
|
local bitmap = self:bitmap_to_pixels(bitmap_hex)
|
2023-03-17 03:39:41 +01:00
|
|
|
|
if 0x0009 == codepoint then -- HT (horizontal tab)
|
|
|
|
|
local result_width = #result[1]
|
|
|
|
|
local tab_stop = math.floor(
|
2023-03-18 22:33:18 +01:00
|
|
|
|
result_width / self.tabulator_size + 1
|
|
|
|
|
) * self.tabulator_size
|
2023-03-18 20:08:25 +01:00
|
|
|
|
result = pixelops.pad_right(
|
2023-03-17 03:39:41 +01:00
|
|
|
|
result,
|
|
|
|
|
tab_stop - result_width,
|
2023-03-18 22:33:18 +01:00
|
|
|
|
self.background_color
|
2023-03-17 03:39:41 +01:00
|
|
|
|
)
|
|
|
|
|
else
|
|
|
|
|
local result_width = #result[1]
|
|
|
|
|
local bitmap_width = #bitmap[1]
|
2023-03-21 00:31:19 +01:00
|
|
|
|
-- Hack: Overlay combining marks onto previous output.
|
|
|
|
|
-- <https://www.unicode.org/reports/tr44/#Canonical_Combining_Class_Values>
|
|
|
|
|
if (
|
|
|
|
|
unicodedata[codepoint] and -- ignore unknown codepoints
|
|
|
|
|
(
|
|
|
|
|
-- a nonspacing combining mark (zero advance width)
|
|
|
|
|
"Mn" == unicodedata[codepoint].general_category or
|
|
|
|
|
-- an enclosing combining mark
|
|
|
|
|
"Me" == unicodedata[codepoint].general_category
|
|
|
|
|
)
|
|
|
|
|
) then
|
2023-03-17 03:39:41 +01:00
|
|
|
|
for j = 1, 16 do
|
|
|
|
|
for k = 1, bitmap_width do
|
2023-03-18 22:33:18 +01:00
|
|
|
|
if self.foreground_color == bitmap[j][k] then
|
2023-03-17 03:39:41 +01:00
|
|
|
|
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
|
|
|
|
|
|
2023-03-18 22:33:18 +01:00
|
|
|
|
hexfont.render_text = function(self, text)
|
2023-03-17 03:39:41 +01:00
|
|
|
|
-- background and foreground color must have equal color depth
|
|
|
|
|
assert(
|
2023-03-18 22:33:18 +01:00
|
|
|
|
"table" == type(self.background_color) and
|
|
|
|
|
"table" == type(self.foreground_color) and
|
|
|
|
|
#self.background_color == #self.foreground_color
|
2023-03-17 03:39:41 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert(
|
2023-03-18 22:33:18 +01:00
|
|
|
|
"bottom-top" == self.scanline_order or
|
|
|
|
|
"top-bottom" == self.scanline_order
|
2023-03-17 03:39:41 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
local result
|
2023-03-18 23:15:27 +01:00
|
|
|
|
local max_width = 0
|
2023-03-21 17:24:44 +01:00
|
|
|
|
-- According to UAX #14, line breaks happen on:
|
|
|
|
|
-- • U+000A LINE FEED
|
|
|
|
|
-- • U+000D CARRIAGE RETURN (except as part of CRLF)
|
|
|
|
|
-- • U+0085 NEXT LINE
|
|
|
|
|
-- • U+2029 PARAGRAPH SEPARATOR
|
|
|
|
|
--
|
|
|
|
|
-- Hack: Replace all of those with LINE FEED.
|
|
|
|
|
-- FIXME: This makes CRLF into two newlines …
|
|
|
|
|
local codepoints = utf8.text_to_codepoints(text)
|
|
|
|
|
for i, codepoint in ipairs(codepoints) do
|
|
|
|
|
if (
|
|
|
|
|
0x000D == codepoints[i] or
|
|
|
|
|
0x0085 == codepoints[i] or
|
|
|
|
|
0x2029 == codepoints[i]
|
|
|
|
|
) then
|
|
|
|
|
codepoints[i] = 0x000A
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
-- FIXME: Code below should only operate on codepoints! Converting
|
|
|
|
|
-- back and forth makes it needlessly slow – but I do not know how
|
|
|
|
|
-- to split a table properly to get a single table for each line …
|
|
|
|
|
text = utf8.codepoints_to_text(codepoints)
|
2023-03-17 03:39:41 +01:00
|
|
|
|
for utf8_line in string.gmatch(text .. "\n", "([^\n]*)\n") do
|
2023-03-18 22:33:18 +01:00
|
|
|
|
local pixels = self:render_line(utf8_line)
|
2023-03-17 03:39:41 +01:00
|
|
|
|
assert( nil ~= pixels )
|
|
|
|
|
if nil == result then
|
|
|
|
|
result = pixels
|
|
|
|
|
else
|
2023-03-18 23:45:23 +01:00
|
|
|
|
for i = 1, #pixels do
|
|
|
|
|
result[#result+1] = pixels[i]
|
2023-03-17 03:39:41 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
2023-03-18 23:15:27 +01:00
|
|
|
|
local pixels_width = #pixels[1]
|
|
|
|
|
if pixels_width > max_width then
|
|
|
|
|
max_width = pixels_width
|
|
|
|
|
end
|
|
|
|
|
end
|
2023-03-18 23:34:47 +01:00
|
|
|
|
for _, scanline in ipairs(result) do
|
2023-03-18 23:26:39 +01:00
|
|
|
|
local scanline_width = #scanline
|
|
|
|
|
if scanline_width < max_width then
|
|
|
|
|
for i = 1, max_width - scanline_width do
|
|
|
|
|
scanline[scanline_width + i] = self.background_color
|
2023-03-18 23:15:27 +01:00
|
|
|
|
end
|
|
|
|
|
assert(
|
|
|
|
|
max_width == #scanline
|
|
|
|
|
)
|
|
|
|
|
end
|
2023-03-17 03:39:41 +01:00
|
|
|
|
end
|
2023-03-18 23:45:23 +01:00
|
|
|
|
-- flip image upside down for ”bottom-top” scanline order
|
|
|
|
|
-- (i.e. the first encoded pixel is the bottom left pixel)
|
|
|
|
|
if "bottom-top" == self.scanline_order then
|
|
|
|
|
result = pixelops.flip_vertically(result)
|
|
|
|
|
end
|
2023-03-17 03:39:41 +01:00
|
|
|
|
return result
|
|
|
|
|
end
|