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