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