typo-krn.lmt /size: 24 Kb    last modification: 2024-01-16 09:03
1if not modules then modules = { } end modules ['typo-krn'] = {
2    version   = 1.001,
3    comment   = "companion to typo-krn.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
9-- glue is still somewhat suboptimal
10-- components: better split on tounicode
11--
12-- maybe ignore when properties[n].injections.cursivex (or mark)
13
14local next, type, tonumber = next, type, tonumber
15
16local nodes              = nodes
17local fonts              = fonts
18
19local enableaction       = nodes.tasks.enableaction
20
21local nuts               = nodes.nuts
22local nodepool           = nuts.pool
23
24-- check what is used
25
26local find_node_tail     = nuts.tail
27local insertnodebefore   = nuts.insertbefore
28local insertnodeafter    = nuts.insertafter
29local endofmath          = nuts.endofmath
30local copy_node          = nuts.copy
31local findattribute      = nuts.findattribute
32local unsetattributes    = nuts.unsetattributes
33
34local getnext            = nuts.getnext
35local getprev            = nuts.getprev
36local getid              = nuts.getid
37local getfont            = nuts.getfont
38local getxscale          = nuts.getxscale
39local getsubtype         = nuts.getsubtype
40local getchar            = nuts.getchar
41local getdisc            = nuts.getdisc
42local getglue            = nuts.getglue
43local getkern            = nuts.getkern
44local getglyphdata       = nuts.getglyphdata
45
46local isglyph            = nuts.isglyph
47
48local setfield           = nuts.setfield
49local getattr            = nuts.getattr
50local setattr            = nuts.setattr
51local setlink            = nuts.setlink
52local setdisc            = nuts.setdisc
53local setglue            = nuts.setglue
54local setkern            = nuts.setkern
55local setchar            = nuts.setchar
56local setglue            = nuts.setglue -- todo
57local setattrlist        = nuts.setattrlist
58
59local texsetattribute    = tex.setattribute
60local unsetvalue         = attributes.unsetvalue
61
62local new_kern           = nodepool.kern -- fontkern
63local new_glue           = nodepool.glue
64
65local nodecodes          = nodes.nodecodes
66local kerncodes          = nodes.kerncodes
67local gluecodes          = nodes.gluecodes
68local disccodes          = nodes.disccodes
69local listcodes          = nodes.listcodes
70
71local glyph_code         = nodecodes.glyph
72local kern_code          = nodecodes.kern
73local disc_code          = nodecodes.disc
74local glue_code          = nodecodes.glue
75local hlist_code         = nodecodes.hlist
76local vlist_code         = nodecodes.vlist
77local math_code          = nodecodes.math
78
79local boxlist_code       = listcodes.box
80local unknownlist_code   = listcodes.unknown
81
82local discretionarydisc_code = disccodes.discretionary
83local automaticdisc_code     = disccodes.automatic
84
85local fontkern_code      = kerncodes.fontkern
86local userkern_code      = kerncodes.userkern
87
88local userskip_code      = gluecodes.userskip
89local spaceskip_code     = gluecodes.spaceskip
90local xspaceskip_code    = gluecodes.xspaceskip
91
92local fonthashes         = fonts.hashes
93local chardata           = fonthashes.characters
94local quaddata           = fonthashes.quads
95local markdata           = fonthashes.marks
96local fontproperties     = fonthashes.properties
97local fontdescriptions   = fonthashes.descriptions
98local fontfeatures       = fonthashes.features
99
100local tracers            = nodes.tracers
101local setcolor           = tracers.colors.set
102local resetcolor         = tracers.colors.reset
103
104local setattrlist        = nuts.setattrlist
105
106local v_max              = interfaces.variables.max
107local v_auto             = interfaces.variables.auto
108
109typesetters              = typesetters or { }
110local typesetters        = typesetters
111
112local kerns              = typesetters.kerns or { }
113typesetters.kerns        = kerns
114
115local report             = logs.reporter("kerns")
116local trace_ligatures    = false  trackers.register("typesetters.kerns.ligatures",         function(v) trace_ligatures   = v end)
117local trace_ligatures_d  = false  trackers.register("typesetters.kerns.ligatures.details", function(v) trace_ligatures_d = v end)
118
119kerns.mapping            = kerns.mapping or { }
120kerns.factors            = kerns.factors or { }
121local a_kerns            = attributes.private("kern")
122
123local contextsetups      = fonts.specifiers.contextsetups
124
125storage.register("typesetters/kerns/mapping", kerns.mapping, "typesetters.kerns.mapping")
126storage.register("typesetters/kerns/factors", kerns.factors, "typesetters.kerns.factors")
127
128local mapping = kerns.mapping
129local factors = kerns.factors
130
131-- one must use liga=no and mode=base and kern=yes
132-- use more helpers
133-- make sure it runs after all others
134-- there will be a width adaptor field in nodes so this will change
135-- todo: interchar kerns / disc nodes / can be made faster
136-- todo: use insertbefore etc
137
138local gluefactor = 4 -- assumes quad = .5 enspace
139
140-- red   : kept by dynamic feature
141-- green : kept by static feature
142-- blue  : keep by goodie
143
144function kerns.keepligature(n) -- might become default
145    local f = getfont(n)
146    local a = getglyphdata(n) or 0
147    if trace_ligatures then
148        local c = getchar(n)
149        local d = fontdescriptions[f][c].name
150        if a > 0 and contextsetups[a].keepligatures == v_auto then
151            if trace_ligatures_d then
152                report("font %!font:name!, glyph %a, slot %X -> ligature %s, by %s feature %a",f,d,c,"kept","dynamic","keepligatures")
153            end
154            setcolor(n,"darkred")
155            return true
156        end
157        local k = fontfeatures[f].keepligatures
158        if k == v_auto then
159            if trace_ligatures_d then
160                report("font %!font:name!, glyph %a, slot %X -> ligature %s, by %s feature %a",f,d,c,"kept","static","keepligatures")
161            end
162            setcolor(n,"darkgreen")
163            return true
164        end
165        if not k then
166            if trace_ligatures_d then
167                report("font %!font:name!, glyph %a, slot %X -> ligature %s, by %s feature %a",f,d,c,"split","static","keepligatures")
168            end
169            resetcolor(n)
170            return false
171        end
172        local k = fontproperties[f].keptligatures
173        if not k then
174            report("font %!font:name!, glyph %a, slot %X -> ligature %s, %s goodie specification",f,d,c,"split","no")
175            resetcolor(n)
176            return false
177        end
178        if k and k[c] then
179            report("font %!font:name!, glyph %a, slot %X -> ligature %s, %s goodie specification",f,d,c,"kept","by")
180            setcolor(n,"darkblue")
181            return true
182        else
183            report("font %!font:name!, glyph %a, slot %X -> ligature %s, %s goodie specification",f,d,c,"split","by")
184            resetcolor(n)
185            return false
186        end
187    else
188        if a > 0 and contextsetups[a].keepligatures == v_auto then
189            return true
190        end
191        local k = fontfeatures[f].keepligatures
192        if k == v_auto then
193            return true
194        end
195        if not k then
196            return false
197        end
198        local k = fontproperties[f].keptligatures
199        if not k then
200            return false
201        end
202        if k and k[c] then
203            return true
204        end
205    end
206end
207
208-- can be optimized .. the prev thing .. but hardly worth the effort
209
210local function kern_injector(fillup,kern,template)
211    local n
212    if fillup then
213        n = new_glue(kern)
214        setfield(n,"stretch",kern)
215        setfield(n,"stretchorder",1)
216    else
217        n = new_kern(kern)
218    end
219    setattrlist(n,template)
220    return n
221end
222
223-- a simple list injector, no components and such .. just disable ligatures in
224-- kern mode .. maybe not even hyphenate ... anyway, the next one is for simple
225-- sublists .. beware: we can have char -1
226
227local function inject_begin(boundary,prev,keeptogether,krn,ok,scale) -- prev is a glyph
228    local char, id = isglyph(boundary)
229    if char then
230        if keeptogether and keeptogether(boundary,prev) then
231            -- keep 'm
232        else
233            local prevchar, prevfont = isglyph(prev)
234            if prevchar and prevchar > 0 and prevfont == id then
235                local data  = chardata[id][prevchar]
236                local kerns = data and data.kerns
237                local kern  = new_kern(scale*((kerns and kerns[char] or 0) + quaddata[id]*krn))
238                setattrlist(kern,boundary)
239                setlink(kern,boundary)
240                return kern, true
241            end
242        end
243    elseif id == kern_code then
244        if getsubtype(boundary) == fontkern_code then
245            local inject = true
246            if keeptogether then
247                local next = getnext(boundary)
248                if not next or (getid(next) == glyph_code and keeptogether(prev,next)) then
249                    inject = false
250                end
251            end
252            if inject then
253                -- not yet ok, as injected kerns can be overlays (from node-inj.lua)
254                setkern(boundary,getkern(boundary) + scale*quaddata[getfont(prev)]*krn) -- ,userkern_code)
255                return boundary, true
256            end
257        end
258    end
259    return boundary, ok
260end
261
262local function inject_end(boundary,next,keeptogether,krn,ok,scale)
263    local tail = find_node_tail(boundary)
264    local char, id = isglyph(tail)
265    if char then
266        if keeptogether and keeptogether(tail,two) then
267            -- keep 'm
268        else
269            -- base mode
270            local nextchar, nextfont = isglyph(tail)
271            if nextchar and nextchar > 0 and nextfont == id then
272                local data  = chardata[id][nextchar]
273                local kerns = data and data.kerns
274                local kern  = new_kern(scale*((kerns and kerns[char] or 0) + quaddata[id]*krn))
275                setattrlist(kern,boundary)
276                setlink(tail,kern)
277                return boundary, true
278            end
279        end
280    elseif id == kern_code then
281        if getsubtype(tail) == fontkern_code then
282            local inject = true
283            if keeptogether then
284                local prev = getprev(tail)
285                if getid(prev) == glyph_code and keeptogether(prev,two) then
286                    inject = false
287                end
288            end
289            if inject then
290                -- not yet ok, as injected kerns can be overlays (from node-inj.lua)
291                setkern(tail,getkern(tail) + scale*quaddata[getfont(next)]*krn) -- ,userkern_code)
292                return boundary, true
293            end
294        end
295    end
296    return boundary, ok
297end
298
299local function process_list(head,keeptogether,krn,font,okay)
300    local start = head
301    local prev  = nil
302    local pid   = nil
303    local kern  = 0
304    local mark  = font and markdata[font]
305    local scale = 1
306    while start  do
307        local char, id = isglyph(start)
308        if char then
309            if not font then
310                font  = id -- getfont(start)
311                mark  = markdata[font]
312                kern  = quaddata[font]*krn
313                scale = getxscale(start)
314            end
315            if prev then
316                if mark[char] then
317                    -- skip
318                elseif pid == kern_code then
319                    if getsubtype(prev) == fontkern_code then
320                        local inject = true
321                        if keeptogether then
322                            local prevprev = getprev(prev)
323                            if getid(prevprev) == glyph_code and keeptogether(prevprev,start) then
324                                inject = false
325                            end
326                        end
327                        if inject then
328                            -- not yet ok, as injected kerns can be overlays (from node-inj.lua)
329                            setkern(prev,getkern(prev) + scale*kern) -- ,userkern_code)
330                            okay = true
331                        end
332                    end
333                elseif pid == glyph_code then
334                    if keeptogether and keeptogether(prev,start) then
335                        -- keep 'm
336                    else
337                        local prevchar = getchar(prev)
338                        local data     = chardata[font][prevchar]
339                        local kerns    = data and data.kerns
340                     -- if kerns then
341                     --     print("it happens indeed, basemode kerns not yet injected")
342                     -- end
343                        local kern = new_kern(scale*((kerns and kerns[char] or 0) + kern))
344                        setattrlist(kern,start)
345                        insertnodebefore(head,start,kern)
346                        okay = true
347                    end
348                end
349            end
350            pid = glyph_code
351        else
352            pid = id
353        end
354        prev  = start
355        start = getnext(start)
356    end
357    return head, okay, prev
358end
359
360local function closest_bound(b,get)
361    b = get(b)
362    if b and getid(b) ~= glue_code then
363        while b do
364            if not getattr(b,a_kerns) then
365                break
366            else
367                local c, f = isglyph(b)
368                if c then
369                    return b, f
370                else
371                    b = get(b)
372                end
373            end
374        end
375    end
376end
377
378function kerns.handler(head)
379    local _, start = findattribute(head, a_kerns)
380    if start then
381        local lastfont     = nil
382        local keepligature = kerns.keepligature
383        local keeptogether = kerns.keeptogether
384        local fillup       = false
385        local bound        = false
386        local prev         = nil
387        local previd       = nil
388        local prevchar     = nil
389        local prevfont     = nil
390        local prevmark     = nil
391        local done         = false
392        local scale        = 1
393        while start do
394            -- fontkerns don't get the attribute but they always sit between glyphs so
395            -- are always valid bound .. disc nodes also sometimes don't get them
396            local attr = getattr(start,a_kerns)
397            if attr and attr > 0 then
398                local char, id = isglyph(start)
399                local krn = mapping[attr]
400                if krn == v_max then
401                    krn    = .25
402                    fillup = true
403                else
404                    fillup = false
405                end
406                if not krn or krn == 0 then
407                    bound = false
408                elseif char then -- id == glyph_code
409                    local font = id -- more readable
410                    local mark = markdata[font]
411                    scale = getxscale(start)
412                    if keepligature and keepligature(start) then
413                        -- keep 'm
414                    else
415                        -- beware, these are not kerned so we mighty need a kern only pass
416                        -- maybe some day .. anyway, one should disable ligaturing
417                        local data = chardata[font][char]
418                        if data then
419                            local unicode = data.unicode -- can be cached
420                            if type(unicode) == "table" then
421                                char = unicode[1]
422                                local s = start
423                                setchar(s,char)
424                                for i=2,#unicode do
425                                    local n = copy_node(s)
426                                    if i == 2 then
427                                        setattr(n,a_kerns,attr) -- we took away the attr
428                                    end
429                                    setchar(n,unicode[i])
430                                    insertnodeafter(head,s,n)
431                                    s = n
432                                end
433                            end
434                        end
435                    end
436                    if not bound then
437                        -- yet
438                    elseif mark[char] then
439                        -- skip
440                    elseif previd == kern_code then
441                        if getsubtype(prev) == fontkern_code then
442                            local inject = true
443                            if keeptogether then
444                                if previd == glyph_code and keeptogether(prev,start) then
445                                    inject = false
446                                end
447                            end
448                            if inject then
449                                -- not yet ok, as injected kerns can be overlays (from node-inj.lua)
450                                setkern(prev,getkern(prev) + scale*quaddata[font]*krn) -- ,userkern_code)
451                            end
452                        end
453                    elseif previd == glyph_code then
454                        if prevfont == font then
455                            if keeptogether and keeptogether(prev,start) then
456                                -- keep 'm
457                            else
458                                -- hm, only basemode ... will go away ...
459                                local data  = chardata[font][prevchar]
460                                local kerns = data and data.kerns
461                                local kern  = (kerns and kerns[char] or 0) + quaddata[font]*krn
462                                insertnodebefore(head,start,kern_injector(fillup,scale*kern,start))
463                            end
464                        else
465                            insertnodebefore(head,start,kern_injector(fillup,scale*quaddata[font]*krn,start))
466                        end
467                    end
468                    prev     = start
469                    prevchar = char
470                    prevfont = font
471                    prevmark = mark
472                    previd   = glyph_code -- id
473                    bound    = true
474                elseif id == disc_code then
475                    local prev, next, pglyph, nglyph -- delayed till needed
476                    local subtype = getsubtype(start)
477                 -- if subtype == automaticdisc_code then
478                 --     -- this is kind of special, as we have already injected the
479                 --     -- previous kern
480                 --     local prev   = getprev(start)
481                 --     local pglyph = prev and getid(prev) == glyph_code
482                 --     languages.expand(start,pglyph and prev)
483                 --     -- we can have a different start now
484                 -- elseif subtype ~= discretionarydisc_code then
485                 --     prev    = getprev(start)
486                 --     pglyph  = prev and getid(prev) == glyph_code
487                 --     languages.expand(start,pglyph and prev)
488                 -- end
489                    local pre, post, replace = getdisc(start)
490                    local indeed = false
491                    if pre then
492                        local okay = false
493                        if not prev then
494                            prev   = getprev(start)
495                            pglyph = prev and getid(prev) == glyph_code
496                        end
497                        if pglyph then
498                            pre, okay = inject_begin(pre,prev,keeptogether,krn,okay,scale)
499                        end
500                        pre, okay = process_list(pre,keeptogether,krn,false,okay)
501                        if okay then
502                            indeed = true
503                        end
504                    end
505                    if post then
506                        local okay = false
507                        if not next then
508                            next   = getnext(start)
509                            nglyph = next and getid(next) == glyph_code
510                        end
511                        if nglyph then
512                            post, okay = inject_end(post,next,keeptogether,krn,okay,scale)
513                        end
514                        post, okay = process_list(post,keeptogether,krn,false,okay)
515                        if okay then
516                            indeed = true
517                        end
518                    end
519                    if replace then
520                        local okay = false
521                        if not prev then
522                            prev    = getprev(start)
523                            pglyph  = prev and getid(prev) == glyph_code
524                        end
525                        if pglyph then
526                            replace, okay = inject_begin(replace,prev,keeptogether,krn,okay,scale)
527                        end
528                        if not next then
529                            next   = getnext(start)
530                            nglyph = next and getid(next) == glyph_code
531                        end
532                        if nglyph then
533                            replace, okay = inject_end(replace,next,keeptogether,krn,okay,scale)
534                        end
535                        replace, okay = process_list(replace,keeptogether,krn,false,okay)
536                        if okay then
537                            indeed = true
538                        end
539                    elseif prevfont then
540                        replace = new_kern(scale*quaddata[prevfont]*krn)
541                        setattrlist(replace,start)
542                        indeed  = true
543                    end
544                    if indeed then
545                        setdisc(start,pre,post,replace)
546                    end
547                    bound = false
548                elseif id == kern_code then
549                    bound  = getsubtype(start) == fontkern_code
550                    prev   = start
551                    previd = id
552                elseif id == glue_code then
553                    local subtype = getsubtype(start)
554                    if subtype == userskip_code or subtype == xspaceskip_code or subtype == spaceskip_code then
555                        local width, stretch, shrink, stretchorder, shrinkorder = getglue(start)
556                        if width > 0 then
557                            local w = width + gluefactor * width * krn
558                            stretch = stretch * w / width
559                            shrink  = shrink  * w / width
560                            if fillup then
561                                stretch = 2 * stretch
562                                shrink  = 2 * shrink
563                                stretchorder = 1
564                             -- shrinkorder  = 1 ?
565                            end
566                            setglue(start,w,stretch,shrink,stretchorder,shrinkorder)
567                        end
568                    end
569                    bound = false
570                elseif id == hlist_code or id == vlist_code then
571                    local subtype = getsubtype(start)
572                    if subtype == unknownlist_code or subtype == boxlist_code then
573                        -- special case
574                        local b, f = closest_bound(start,getprev)
575                        if b then
576                            insertnodebefore(head,start,kern_injector(fillup,scale*quaddata[f]*krn,start))
577                        end
578                        local b, f = closest_bound(start,getnext)
579                        if b then
580                            insertnodeafter(head,start,kern_injector(fillup,scale*quaddata[f]*krn,start))
581                        end
582                    end
583                    bound = false
584                elseif id == math_code then
585                    start = endofmath(start)
586                    bound = false
587                end
588                if start then
589                    start = getnext(start)
590                end
591                done = true
592            else
593                local id = getid(start)
594                if id == kern_code then
595                    bound  = getsubtype(start) == fontkern_code
596                    prev   = start
597                    previd = id
598                    start  = getnext(start)
599                else
600                    bound = false
601                    start = getnext(start)
602                end
603            end
604        end
605        if done then
606    --         unsetattributes(a_kerns, head)
607        end
608    end
609    return head
610end
611
612local enabled = false
613
614function kerns.set(factor)
615    if factor ~= v_max then
616        factor = tonumber(factor) or 0
617    end
618    if factor == v_max or factor ~= 0 then
619        if not enabled then
620            enableaction("processors","typesetters.kerns.handler")
621            enabled = true
622        end
623        local a = factors[factor]
624        if not a then
625            a = #mapping + 1
626            factors[factors], mapping[a] = a, factor
627        end
628        factor = a
629    else
630        factor = unsetvalue
631    end
632    texsetattribute(a_kerns,factor)
633    return factor
634end
635
636-- interface
637
638interfaces.implement {
639    name      = "setcharacterkerning",
640    actions   = kerns.set,
641    arguments = "string"
642}
643
644