aboutsummaryrefslogtreecommitdiff
path: root/advtrains/textrender.lua
diff options
context:
space:
mode:
Diffstat (limited to 'advtrains/textrender.lua')
-rw-r--r--advtrains/textrender.lua303
1 files changed, 303 insertions, 0 deletions
diff --git a/advtrains/textrender.lua b/advtrains/textrender.lua
new file mode 100644
index 0000000..f23cd3f
--- /dev/null
+++ b/advtrains/textrender.lua
@@ -0,0 +1,303 @@
+--[[ Simple text rendering (= texture generating) library
+Definitions:
+* A string ("str") refers to a regular Lua string
+* A codepoint ("cp") refers to a Unicode codepoint
+* A character ("char") refers to the smallest unit of rendering
+* A segment ("seg") refers to a unit of text that should not be broken across two or more lines unless the renderer is explicitly given the option to do so
+* A sequence ("seq") is a Lua table t where:
+ * Every element with a numeric index are of the same type
+ * Numeric indices are continuous positive integers starting from 1
+ * String indices can exist to indicate certain properties of the sequence
+ (Note that a "numeric index" refers to an index of a table that is a number. This includes floating-point numbers, inf, -inf, and NaN, if present)
+For simplicity, the above definitions may differ from that of Unicode
+Codepoints and characters differ in that characters can be composed out of multiple codepoints (e.g. the character "รก" can be represented with U+00E1 or with a sequence of U+0061 followed by U+0301)
+
+Exposed API:
+* mbstoucs(str) a utility function that converts a string to a sequence of Unicode codepoints
+* texture_escape(str) a utility function that escapes the texture string
+]]
+
+-- Lookup tables
+local characters = {}
+do -- prevent LUT creation code from polluting the local environment
+ local function ensure_entry_exists(char)
+ if not characters[char] then
+ characters[char] = {}
+ end
+ end
+ local charwidth_lut = {}
+ local charwidth_ranges = {
+ [8] = {
+ 0x20,0x7e, -- Latin
+ 0xa0,0x052f, -- LGC
+ },
+ }
+ for k, v in pairs(charwidth_lut) do
+ ensure_entry_exists(k)
+ characters[k].width = v
+ end
+ for k, v in pairs(charwidth_ranges) do
+ for i = 1, #v, 2 do
+ for j = v[i], v[i+1] do
+ ensure_entry_exists(j)
+ if not characters[j].width then
+ characters[j].width = k
+ end
+ end
+ end
+ end
+ local charprops = {
+ can_break_lines = {0x20,},
+ force_linebreak = {0x0a,},
+ }
+ for k, v in pairs(charprops) do
+ for i = 1, #v do
+ local char = v[i]
+ ensure_entry_exists(char)
+ characters[char][k] = true
+ end
+ end
+end
+
+local function texture_escape(str)
+ return string.gsub(str, "[%[%()^:]", "\\%1")
+end
+
+-- Functions
+local function mbstoucs(str)
+ if type(str) ~= "string" then
+ return {}
+ end
+ local i = 1
+ local strlen = #str
+ local cps = {}
+ while i <= strlen do
+ local bytes = {string.byte(str, i, math.max(i+3, strlen))}
+ local nbytes
+ local codepoint
+ local msb = bytes[1]
+ if msb < 128 then
+ nbytes = 1
+ elseif msb >= 192 and bytes[2] and bytes[2] >= 128 and bytes[2] < 192 then
+ if msb < 224 then
+ nbytes = 2
+ elseif bytes[3] and bytes[3] >= 128 and bytes[3] < 192 then
+ if msb < 240 then
+ nbytes = 3
+ elseif msb < 148 and bytes[4] and bytes[4] >= 128 and bytes[4] < 192 then
+ nbytes = 4
+ end
+ end
+ end
+ if not nbytes then
+ return {} -- invalid MB character encountered
+ elseif nbytes == 1 then
+ codepoint = msb
+ else
+ codepoint = msb%(2^(7-nbytes))
+ for i = 2, nbytes do
+ codepoint = codepoint * 64 + bytes[i] - 128
+ end
+ end
+ cps[#cps+1] = codepoint
+ i = i + nbytes
+ end
+ return cps
+end
+
+local function basechar(char)
+ if type(char) == "table" then
+ return char[1]
+ else
+ return char
+ end
+end
+
+local function charwidth(char)
+ return (characters[basechar(char)] or {}).width
+end
+
+-- Splits the text into segments
+local function split_into_segments(cs)
+ local sl = {}
+ for i = 1, #cs do
+ local char = cs[i]
+ local props = characters[basechar(char)] or {}
+ local cwidth = charwidth(char)
+ if props.can_break_lines then
+ sl[#sl+1] = {bchar = char, bwidth = cwidth, width = cwidth, seq = {}, size = {}}
+ elseif props.force_linebreak then
+ sl[#sl+1] = {width = 0, seq = {}, size = {}}
+ else
+ if not sl[1] then
+ sl[1] = {width = cwidth, seq = {char}, size = {cwidth}}
+ else
+ seg = sl[#sl]
+ local seq, size = seg.seq, seg.size
+ seq[#seq+1] = char
+ size[#size+1] = (size[#size] or 0) + cwidth
+ seg.width = seg.width + cwidth
+ end
+ end
+ end
+ return sl
+end
+
+local function split_lines(cs, width, height, options)
+ local lines = {}
+ local nlines = options.lines
+ local lwidth = width+1 -- for newline for the first line
+ local wrap_overflow = options.wrap_overflow
+ local align = options.align
+ local sl = split_into_segments(cs)
+ for i = 1, #sl do
+ seg = sl[i]
+ local seq = seg.seq
+ if lwidth + seg.width <= width then
+ local line = lines[#lines]
+ line.width = (line.width or 0) + seg.width
+ if seg.bchar then
+ line[#line+1] = seg.bchar
+ end
+ local seq = seg.seq
+ for i = 1, #seq do
+ line[#line+1] = seq[i]
+ end
+ elseif lwidth > 0 then
+ -- create a new line
+ local line = seq
+ line.width = seg.size[#line]
+ lines[#lines+1] = line
+ else -- now we need to deal with segments longer than the given width
+ if wrap_overflow then
+ -- forcibly break the sequence
+ local st = seg.size
+ local ci = 1
+ local tw = 0
+ while ci <= #seq do
+ local l = {}
+ local j = ci
+ while j <= #seq and st[j]-tw <= width do
+ l[j-ci+1] = seq[j]
+ j = j+1
+ end
+ if l[1] then
+ l.width = st[j-1]-tw
+ lines[#lines+1] = l
+ end
+ ci = j
+ end
+ else
+ -- only add the sequence that will get rendered
+ local minx = align*(seq.width-width)
+ local maxx = minx+width
+ local l = {width = 0}
+ local st = seg.size
+ local i = 1
+ while st[i] and st[i] < minx do
+ i = i + 1
+ end
+ while st[i] and st[i] <= maxx do
+ l[#l+1] = seq[i]
+ l.width = l.width + charwidth(seq[i])
+ end
+ lines[#lines+1] = l
+ end
+ end
+ if #lines > nlines then
+ for i = nlines+1, #lines do
+ lines[i] = nil
+ end
+ break
+ end
+ end
+ return lines
+end
+
+local function cpstocs(cps)
+ return cps -- TODO: at least implement support for combining diacritics
+end
+
+local function check_options(width, height, options)
+ local w, h = tonumber(width), tonumber(height)
+ if not (w and h) then
+ return nil
+ end
+ if w < 8 or h < 18 then
+ return nil
+ end
+ local opt = options
+ if type(options) ~= "table" then
+ opt = {}
+ end
+ local ret = {}
+ -- check text alignment option
+ ret.align = math.min(math.max(tonumber(opt.align) or 0, 0),1)
+ -- check "wrap overflow text segment" option
+ ret.wrap_overflow = opt.wrap_overflow and true or false -- convert to boolean
+ -- check line count option
+ local maxlines = math.max(h/18)
+ ret.lines = math.min(maxlines, tonumber(opt.lines) or maxlines)
+ -- check text color option
+ ret.color = string.match(tostring(options.color) or "", "^#%x%x%x%x%x%x$") or "#000000"
+ return w, h, ret
+end
+
+local function get_char_texture(char)
+ if type(char) == "number" then
+ return string.format("(advtrains_unifont.png\\^[sheet\\:258x260\\:%d,%d)",
+ char%256+2,
+ math.floor(char/256+4)
+ )
+ end
+end
+
+local function render(str, ...)
+ local width, height, opts = check_options(...)
+ if not (width and height and opts) then
+ return nil
+ end
+ local lines = split_lines(cpstocs(mbstoucs(str)), width, height, opts)
+ local tst = {string.format("(([combine:%dx%d", width, height)}
+ local vpad = (height-18*#lines)/2
+ local align = opts.align
+ for i = 1, #lines do
+ local y = vpad+18*(i-1)
+ local line = lines[i]
+ if line.width then
+ local x = (width-line.width)*align
+ for i = 1, #line do
+ local char = line[i]
+ local cw = charwidth(char)
+ if cw > 0 then
+ tst[#tst+1] = string.format("%d,%d=%s", x, y, get_char_texture(char))
+ x = x + cw
+ end
+ end
+ end
+ end
+ return table.concat(tst,":")..string.format(")^[makealpha:0,0,0^[multiply:%s)", opts.color)
+end
+
+if minetest then
+ minetest.register_node("advtrains:textrender_demo",{
+ description = "Text rendering demo",
+ tiles = {"advtrains_hud_bg.png^[resize:128x128^[colorize:#0000ff^(advtrains_hud_bg.png^[resize:128x64)^"..render("Hello\n\n World", 128, 128, {color="#00ff00"})},
+ groups = {cracky=3, not_in_creative_inventory=1}
+ })
+ minetest.register_on_mods_loaded(function()
+ minetest.register_craft{
+ output = "advtrains:textrender_demo",
+ recipe = {
+ {"default:paper","default:paper","default:paper"},
+ {"default:paper","advtrains:hud_demo","default:paper"},
+ {"default:paper","default:paper","default:paper"},
+ }
+ }
+ end)
+end
+
+return {
+ mbstoucs = mbstoucs,
+ texture_escape = texture_escape,
+} \ No newline at end of file