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