1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
|
--[[ 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)
local e = characters[char]
if not e then
e = {}
characters[char] = e
end
return e
end
local charwidth_lut = {}
local charwidth_ranges = {
[8] = {
0x20,0x7e, -- Latin
0xa0,0x052f, -- LGC
0x1e00,0x1fff, -- Latin, Greek
},
}
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
-- import "interesting" data from UnicodeData
local f = io.open(advtrains.modpath.."/UCD/UnicodeData.txt","r") or error("Failed to open UnicodeData")
for line in f:lines() do
local cp, decomp = string.match(line, "^(%x+);[^;]*;[^;]*;[^;]*;[^;]*;([^;]*);.+$")
if cp and decomp then
cp = tonumber(cp, 16)
decomp = string.split(decomp, " ")
end
if cp then
if decomp[1] then
local compatfmt = string.match(decomp[1], "^<([^>]+)>$")
if compatfmt then
table.remove(decomp, 1)
end
if not compatfmt then
if #decomp == 2 then
local base, modifier = tonumber(decomp[1], 16), tonumber(decomp[2], 16)
if base and modifier then
local bt = ensure_entry_exists(base)
if not bt.modifiers then
bt.modifiers = {}
end
bt.modifiers[modifier] = cp
end
end
end
end
end
end
f:close()
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 ucstombs(ucs)
local s = ""
for i = 1, #ucs do
local cp = math.floor(ucs[i])
local len
if cp < 0 or cp > 0x10ffff then
return ""
end
if cp < 0x80 then
len = 1
elseif cp < 0x0800 then
len = 2
elseif cp < 0x010000 then
len = 3
else
len = 4
end
if len == 1 then
s = s .. string.char(cp)
else
local t = {}
for i = len, 2, -1 do
local rem = cp % 64
cp = math.floor(cp/64)
t[i] = 128+rem
end
t[1] = cp + 256 - 2^(8-len)
s = s .. string.char(unpack(t))
end
end
return s
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 or 0
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
local 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 -- force 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 not seg.bchar then
lwidth = width+1 --force newline
end
if lwidth + seg.width <= width then
local line = lines[#lines]
lwidth = (line.width or 0) + seg.width
line.width = lwidth
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
lwidth = 0
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
lwidth = st[j-1]-tw
l.width = lwidth
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
lwidth = l.width
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)
local cs = {}
local i = 1
local lastchar
while i <= #cps do
local cp = cps[i]
local addchar = true
if lastchar then
local lcp = characters[lastchar]
if lcp and lcp.modifiers and lcp.modifiers[cp] then
lastchar = lcp.modifiers[cp]
cs[#cs] = lastchar
addchar = false
end
end
if addchar then
cs[#cs+1] = cp
lastchar = cp
end
i = i + 1
end
return cs
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.floor(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_%04x.png\\^[sheet\\:256x1\\:%d,0)",
math.floor(char/256),
char%256
)
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
local tilefmt = "advtrains_hud_bg.png^[resize:128x128^[colorize:#0000ff^(advtrains_hud_bg.png^[resize:128x64)^%s"
local sampletext = "Hello world\n\n"..
ucstombs{0xe1, 0x65, 0x0301, 0xef, 0x0301, 0x6f, 0x0303, 0x0301, 0x01d8, 0x20, -- Latin alphabet test
0x03b1, 0x0301, 0x0345, 0x03b2, 0x03b3, 0x03b4, 0x03ad, 0x20, -- Greek alphabet test
0x0430, 0x0308, 0x0431, 0x0432, 0x0433, 0x0301, 0x0434}
minetest.register_node("advtrains:textrender_demo",{
description = "Text rendering demo",
tiles = {string.format(tilefmt, render(sampletext, 128, 128, {color="#00ff00"}))},
groups = {cracky=3, not_in_creative_inventory=1}
})
minetest.register_on_mods_loaded(function() -- wait until the trainhud module is loaded
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)
return {
mbstoucs = mbstoucs,
ucstombs = ucstombs,
render = render,
texture_escape = texture_escape,
}
|