node-fnt.lmt /size: 18 Kb    last modification: 2021-10-28 13:51
1if not modules then modules = { } end modules ['node-fnt'] = {
2    version   = 1.001,
3    comment   = "companion to font-ini.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files",
7}
8
9if not context then os.exit() end -- generic function in node-dum
10
11local next, type = next, type
12local concat, keys = table.concat, table.keys
13
14local nodes, node, fonts = nodes, node, fonts
15
16local trace_characters  = false  trackers.register("nodes.characters", function(v) trace_characters = v end)
17local trace_fontrun     = false  trackers.register("nodes.fontrun",    function(v) trace_fontrun    = v end)
18local trace_variants    = false  trackers.register("nodes.variants",   function(v) trace_variants   = v end)
19
20-- bad namespace for directives
21
22local force_discrun     = true   directives.register("nodes.discrun",      function(v) force_discrun     = v end)
23local force_boundaryrun = true   directives.register("nodes.boundaryrun",  function(v) force_boundaryrun = v end)
24local force_basepass    = true   directives.register("nodes.basepass",     function(v) force_basepass    = v end)
25local keep_redundant    = false  directives.register("nodes.keepredundant",function(v) keep_redundant    = v end)
26
27local report_fonts      = logs.reporter("fonts","processing")
28
29local fonthashes        = fonts.hashes
30local fontdata          = fonthashes.identifiers
31local fontvariants      = fonthashes.variants
32local fontmodes         = fonthashes.modes
33
34local otf               = fonts.handlers.otf
35
36local starttiming       = statistics.starttiming
37local stoptiming        = statistics.stoptiming
38
39local nodecodes         = nodes.nodecodes
40local boundarycodes     = nodes.boundarycodes
41
42local handlers          = nodes.handlers
43
44local nuts              = nodes.nuts
45
46local getid             = nuts.getid
47local getsubtype        = nuts.getsubtype
48local getreplace        = nuts.getreplace
49local getnext           = nuts.getnext
50local getprev           = nuts.getprev
51local getboth           = nuts.getboth
52local getdata           = nuts.getdata
53local getglyphdata      = nuts.getglyphdata
54
55local setchar           = nuts.setchar
56local setlink           = nuts.setlink
57local setnext           = nuts.setnext
58local setprev           = nuts.setprev
59
60local isglyph           = nuts.isglyph -- unchecked
61local ischar            = nuts.ischar  -- checked
62
63local nextboundary      = nuts.traversers.boundary
64local nextdisc          = nuts.traversers.disc
65local nextchar          = nuts.traversers.char
66
67local flushnode         = nuts.flush
68
69local disc_code         = nodecodes.disc
70local boundary_code     = nodecodes.boundary
71
72local wordboundary_code = boundarycodes.word
73
74local protectglyphs     = nuts.protectglyphs
75local unprotectglyphs   = nuts.unprotectglyphs
76
77local setmetatableindex = table.setmetatableindex
78
79-- some tests with using an array of dynamics[id] and processes[id] demonstrated
80-- that there was nothing to gain (unless we also optimize other parts)
81--
82-- maybe getting rid of the intermediate shared can save some time
83
84local run = 0
85
86local setfontdynamics = { }
87local fontprocesses   = { }
88
89setmetatableindex(setfontdynamics, function(t,font)
90    local tfmdata = fontdata[font]
91    local shared = tfmdata.shared
92    local f = shared and shared.dynamics and otf.setdynamics or false
93    if f then
94        local v = { }
95        t[font] = v
96        setmetatableindex(v,function(t,k)
97            local v = f(font,k)
98            t[k] = v
99            return v
100        end)
101        return v
102    else
103        t[font] = false
104        return false
105    end
106end)
107
108setmetatableindex(fontprocesses, function(t,font)
109    local tfmdata = fontdata[font]
110    local shared = tfmdata.shared -- we need to check shared, only when same features
111    local processes = shared and shared.processes
112    if processes and #processes > 0 then
113        t[font] = processes
114        return processes
115    else
116        t[font] = false
117        return false
118    end
119end)
120
121fonts.hashes.setdynamics = setfontdynamics
122fonts.hashes.processes   = fontprocesses
123
124-- if we forget about basemode we don't need to test too much here and we can consider running
125-- over sub-ranges .. this involves a bit more initializations but who cares .. in that case we
126-- also need to use the stop criterium (we already use head too) ... we cannot use traverse
127-- then, so i'll test it on some local clone first ... the only pitfall is changed directions
128-- inside a run which means that we need to keep track of this which in turn complicates matters
129-- in a way i don't like
130
131-- we need to deal with the basemode fonts here and can only run over ranges as we otherwise get
132-- luatex craches due to all kind of asserts in the disc/lig builder
133
134-- there is no gain in merging used (dynamic 0) and dynamics apart from a bit less code
135
136local ligaturing = nuts.ligaturing
137local kerning    = nuts.kerning
138
139local function start_trace(head)
140    run = run + 1
141    report_fonts()
142    report_fonts("checking node list, run %s",run)
143    report_fonts()
144    local n = head
145    while n do
146        local char, id = isglyph(n)
147        if char then
148            local font = id
149            local dynamic = getglyphdata(n) or 0
150            report_fonts("font %03i, dynamic %03i, glyph %C",font,dynamic,char)
151        elseif id == disc_code then
152            report_fonts("[disc] %s",nodes.listtoutf(n,true,false,n))
153        elseif id == boundary_code then
154            report_fonts("[boundary] %i:%i",getsubtype(n),getdata(n))
155        else
156            report_fonts("[%s]",nodecodes[id])
157        end
158        n = getnext(n)
159    end
160end
161
162local function stop_trace(u,usedfonts,d,dynamicfonts,b,basefonts,r,redundant)
163    report_fonts()
164    report_fonts("statics : %s",u > 0 and concat(keys(usedfonts)," ") or "none")
165    report_fonts("dynamics: %s",d > 0 and concat(keys(dynamicfonts)," ") or "none")
166    report_fonts("built-in: %s",b > 0 and b or "none")
167    report_fonts("removed : %s",r > 0 and r or "none")
168    report_fonts()
169end
170
171do
172
173    local usedfonts
174    local dynamicfonts
175    local basefonts  -- could be reused
176    local basefont
177    local prevfont
178    local prevdynamic
179    local variants
180    local redundant  -- could be reused
181    local firstnone
182    local lastfont
183    local lastproc
184    local lastnone
185
186    local d, u, b, r
187
188    local function protectnone()
189        protectglyphs(firstnone,lastnone)
190        firstnone = nil
191    end
192
193    local function setnone(n)
194        if firstnone then
195            protectnone()
196        end
197        if basefont then
198            basefont[2] = getprev(n)
199            basefont = false
200        end
201        if not firstnone then
202            firstnone = n
203        end
204        lastnone = n
205    end
206
207    local function setbase(n)
208        if firstnone then
209            protectnone()
210        end
211        if force_basepass then
212            if basefont then
213                basefont[2] = getprev(n)
214            end
215            b = b + 1
216            basefont = { n, false }
217            basefonts[b] = basefont
218        end
219    end
220
221    local function setnode(n,font,dynamic) -- we could use prevfont and prevdynamic when we set then first
222        if firstnone then
223            protectnone()
224        end
225        if basefont then
226            basefont[2] = getprev(n)
227            basefont = false
228        end
229        if dynamic > 0 then
230            local used = dynamicfonts[font]
231            if not used then
232                used = { }
233                dynamicfonts[font] = used
234            end
235            if not used[dynamic] then
236                local fd = setfontdynamics[font]
237                if fd then
238                    used[dynamic] = fd[dynamic]
239                    d = d + 1
240                end
241            end
242        else
243            local used = usedfonts[font]
244            if not used then
245                lastfont = font
246                lastproc = fontprocesses[font]
247                if lastproc then
248                    usedfonts[font] = lastproc
249                    u = u + 1
250                end
251            end
252        end
253    end
254
255    function handlers.characters(head,groupcode,direction)
256        -- either next or not, but definitely no already processed list
257        starttiming(nodes)
258
259        usedfonts    = { }
260        dynamicfonts = { }
261        basefonts    = { }
262        basefont     = nil
263        prevfont     = nil
264        prevdynamic  = 0
265        variants     = nil
266        redundant    = nil
267        firstnone    = nil
268        lastfont     = nil
269        lastproc     = nil
270        lastnone     = nil
271
272        local fontmode = nil -- base none or other
273
274        d, u, b, r = 0, 0, 0, 0
275
276        if trace_fontrun then
277            start_trace(head)
278        end
279
280        -- There is no gain in checking for a single glyph and then having a fast path. On the
281        -- metafun manual (with some 2500 single char lists) the difference is just noise.
282
283        for n, char, font, dynamic in nextchar, head do
284
285            if font ~= prevfont then
286                prevfont = font
287                fontmode = fontmodes[font]
288                if fontmode == "none" then
289                    prevdynamic = 0
290                    variants    = false
291                    setnone(n)
292                elseif fontmode == "base" then
293                    prevdynamic = 0
294                    variants    = false
295                    setbase(n)
296                else
297                 -- local dynamic = getglyphdata(n) or 0 -- zero dynamic is reserved for fonts in context
298                    prevdynamic = dynamic
299                    variants    = fontvariants[font]
300                    setnode(n,font,dynamic)
301                end
302            elseif fontmode == "node" then
303                local dynamic = getglyphdata(n) or 0 -- zero dynamic is reserved for fonts in context
304                if dynamic ~= prevdynamic then
305                    prevdynamic = dynamic
306                    variants    = fontvariants[font]
307                    setnode(n,font,dynamic)
308                end
309            elseif firstnone then
310                lastnone = n
311            end
312
313            if variants then
314                if (char >= 0xFE00 and char <= 0xFE0F) or (char >= 0xE0100 and char <= 0xE01EF) then
315         -- if variants and char >= 0xFE00 then
316         --     if char < 0xFE0F or (char >= 0xE0100 and char <= 0xE01EF) then
317                    local hash = variants[char]
318                    if hash then
319                        local p = getprev(n)
320                        if p then
321                            local char    = ischar(p) -- checked
322                            local variant = hash[char]
323                            if variant then
324                                if trace_variants then
325                                    report_fonts("replacing %C by %C",char,variant)
326                                end
327                                setchar(p,variant)
328                                if redundant then
329                                    r = r + 1
330                                    redundant[r] = n
331                                else
332                                    r = 1
333                                    redundant = { n }
334                                end
335                            end
336                        end
337                    elseif keep_redundant then
338                        -- go on, can be used for tracing
339                    elseif redundant then
340                        r = r + 1
341                        redundant[r] = n
342                    else
343                        r = 1
344                        redundant = { n }
345                    end
346                end
347            end
348
349        end
350
351        if firstnone then
352            protectnone()
353        end
354
355        if force_boundaryrun then
356
357            -- we can inject wordboundaries and then let the hyphenator do its work
358            -- but we need to get rid of those nodes in order to build ligatures
359            -- and kern (a rather context thing)
360
361            for b, subtype in nextboundary, head do
362                if subtype == wordboundary_code then
363                    if redundant then
364                        r = r + 1
365                        redundant[r] = b
366                    else
367                        r = 1
368                        redundant = { b }
369                    end
370                end
371            end
372
373        end
374
375        if redundant then
376            for i=1,r do
377                local r = redundant[i]
378                local p, n = getboth(r)
379                if r == head then
380                    head = n
381                    setprev(n)
382                else
383                    setlink(p,n)
384                end
385                if b > 0 then
386                    for i=1,b do
387                        local bi = basefonts[i]
388                        local b1 = bi[1]
389                        local b2 = bi[2]
390                        if b1 == b2 then
391                            if b1 == r then
392                                bi[1] = false
393                                bi[2] = false
394                            end
395                        elseif b1 == r then
396                            bi[1] = n
397                        elseif b2 == r then
398                            bi[2] = p
399                        end
400                    end
401                end
402                flushnode(r)
403            end
404        end
405
406        if force_discrun then
407            -- basefont is not supported in disc only runs ... it would mean a lot of
408            -- ranges .. we could try to run basemode as a separate processor run but not
409            -- for now (we can consider it when the new node code is tested
410            for disc in nextdisc, head do
411                -- doing only replace is good enough because pre and post are normally used
412                -- for hyphens and these come from fonts that part of the hyphenated word
413                local r = getreplace(disc)
414                if r then
415                    local prevfont    = nil
416                    local prevdynamic = nil
417                    local none        = false
418                          firstnone   = nil
419                          basefont    = nil
420                    for n, char, font, dynamic in nextchar, r do
421                     -- local dynamic = getglyphdata(n) or 0 -- zero dynamic is reserved for fonts in context
422                        if font ~= prevfont or dynamic ~= prevdynamic then
423                            prevfont    = font
424                            prevdynamic = dynamic
425                            local fontmode = fontmodes[font]
426                            if fontmode == "none" then
427                                setnone(n)
428                            elseif fontmode == "base" then
429                                -- so the replace gets an extra treatment ... so be it
430                                setbase(n)
431                            else
432                                setnode(n,font,dynamic)
433                            end
434                        elseif firstnone then
435                         -- lastnone = n
436                            lastnone = nil
437                        end
438                        -- we assume one font for now (and if there are more and we get into issues then
439                        -- we can always remove the break)
440                        break
441                    end
442                    if firstnone then
443                        protectnone()
444                    end
445                end
446            end
447
448        end
449
450        if trace_fontrun then
451            stop_trace(u,usedfonts,d,dynamicfonts,b,basefonts,r,redundant)
452        end
453
454        -- in context we always have at least 2 processors
455        if u == 0 then
456            -- skip
457        elseif u == 1 then
458            for i=1,#lastproc do
459                head = lastproc[i](head,lastfont,0,direction)
460            end
461        else
462            for font, processors in next, usedfonts do -- unordered
463                for i=1,#processors do
464                    head = processors[i](head,font,0,direction,u) -- u triggers disc optimizer
465                end
466            end
467        end
468        if d == 0 then
469            -- skip
470        elseif d == 1 then
471            local font, dynamics = next(dynamicfonts)
472            for dynamic, processors in next, dynamics do -- unordered, dynamic can switch in between
473                for i=1,#processors do
474                    head = processors[i](head,font,dynamic,direction)
475                end
476            end
477        else
478            for font, dynamics in next, dynamicfonts do
479                for dynamic, processors in next, dynamics do -- unordered, dynamic can switch in between
480                    for i=1,#processors do
481                        head = processors[i](head,font,dynamic,direction,d) -- d triggers disc optimizer
482                    end
483                end
484            end
485        end
486        if b == 0 then
487            -- skip
488        elseif b == 1 then
489            -- only one font
490            local range = basefonts[1]
491            local start = range[1]
492            local stop  = range[2]
493            if (start or stop) and (start ~= stop) then
494                local front = head == start
495                if stop then
496                    start = ligaturing(start,stop)
497                    start = kerning(start,stop)
498                elseif start then -- safeguard
499                    start = ligaturing(start)
500                    start = kerning(start)
501                end
502                if front and head ~= start then
503                    head = start
504                end
505            end
506        else
507            -- multiple fonts
508            for i=1,b do
509                local range = basefonts[i]
510                local start = range[1]
511                local stop  = range[2]
512                if start then -- and start ~= stop but that seldom happens
513                    local front = head == start
514                    local prev  = getprev(start)
515                    local next  = getnext(stop)
516                    if stop then
517                        start, stop = ligaturing(start,stop)
518                        start, stop = kerning(start,stop)
519                    else
520                        start = ligaturing(start)
521                        start = kerning(start)
522                    end
523                    -- is done automatically
524                    if prev then
525                        setlink(prev,start)
526                    end
527                    if next then
528                        setlink(stop,next)
529                    end
530                    -- till here
531                    if front and head ~= start then
532                        head = start
533                    end
534                end
535            end
536        end
537
538        stoptiming(nodes)
539
540        if trace_characters then
541            nodes.report(head)
542        end
543
544        return head
545    end
546
547end
548
549handlers.protectglyphs   = protectglyphs
550handlers.unprotectglyphs = unprotectglyphs
551