typo-itc.lmt /size: 22 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['typo-itc'] = {
2    version   = 1.001,
3    comment   = "companion to typo-itc.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
9local tonumber = tonumber
10local tand = math.tand
11
12local trace_italics       = false  trackers.register("typesetters.italics", function(v) trace_italics = v end)
13
14local report_italics      = logs.reporter("nodes","italics")
15
16typesetters.italics       = typesetters.italics or { }
17local italics             = typesetters.italics
18
19typesetters.corrections   = typesetters.corrections or { }
20local corrections         = typesetters.corrections
21
22local settings_to_hash    = utilities.parsers.settings_to_hash
23
24local nodecodes           = nodes.nodecodes
25local glyph_code          <const> = nodecodes.glyph
26local kern_code           <const> = nodecodes.kern
27local glue_code           <const> = nodecodes.glue
28local disc_code           <const> = nodecodes.disc
29local math_code           <const> = nodecodes.math
30
31local font_kern_code      <const> = nodes.kerncodes.fontkern
32local italic_kern_code    <const> = nodes.kerncodes.italiccorrection
33local left_kern_code      <const> = nodes.kerncodes.leftcorrectionkern
34local right_kern_code     <const> = nodes.kerncodes.rightcorrectionkern
35local spaceskip_code      <const> = nodes.gluecodes.spaceskip
36local xspaceskip_code     <const> = nodes.gluecodes.xspaceskip
37
38local enableaction        = nodes.tasks.enableaction
39
40local nuts                = nodes.nuts
41local tonut               = nodes.tonut
42local nodepool            = nuts.pool
43
44local getprev             = nuts.getprev
45local getnext             = nuts.getnext
46local getid               = nuts.getid
47local getsubtype          = nuts.getsubtype
48local getchar             = nuts.getchar
49local getdisc             = nuts.getdisc
50local setattrlist         = nuts.setattrlist
51local setdisc             = nuts.setdisc
52local isglyph             = nuts.isglyph
53local isnextglyph         = nuts.isnextglyph
54local issimilarglyph      = nuts.issimilarglyph
55local isitalicglyph       = nuts.isitalicglyph
56local firstitalicglyph    = nuts.firstitalicglyph
57local hasglyphoption      = nuts.hasglyphoption
58local hasdiscoption       = nuts.hasdiscoption
59local setkern             = nuts.setkern
60local getkern             = nuts.getkern
61local setglue             = nuts.setglue
62local getglue             = nuts.getglue
63local getheight           = nuts.getheight
64local getoptions          = nuts.getoptions
65local getslant            = nuts.getslant
66local getcornerkerns      = nuts.getcornerkerns
67local xscaled             = nuts.xscaled
68local yscaled             = nuts.yscaled
69
70local insertnodeafter     = nuts.insertafter
71local insertnodebefore    = nuts.insertbefore
72local beginofmath         = nuts.beginofmath
73local endofmath           = nuts.endofmath
74local findnode            = nuts.findnode
75
76local traverseitalic      = nuts.traverseitalic
77
78local new_correction_kern = nodepool.italiccorrection
79
80local fonthashes          = fonts.hashes
81local fontdata            = fonthashes.identifiers
82local exheights           = fonthashes.exheights
83local chardata            = fonthashes.characters
84
85local ispunctuation       = characters.is_punctuation
86
87local implement           = interfaces.implement
88
89local no_correction_code     <const> = tex.glyphoptioncodes.noitaliccorrection
90local math_check_italic_code <const> = tex.userglyphoptioncodes.mathcheckitalic
91local text_check_italic_code <const> = tex.userglyphoptioncodes.textcheckitalic
92local disc_check_italic_code <const> = tex.userdiscoptioncodes .textcheckitalic
93
94local mathokay = false
95local textokay = false
96local kernokay = false
97
98-- We don't drive this by attributes because most likely it is a document property.
99
100local exfactormath = 1.25 -- this need checking because now we go unscaled
101local exfactortext = 0.50 -- idem
102local spacefactor  = 0
103
104directives.register("typesetters.italics.threshold", function(v)
105    exfactortext = v == true and 0.50 or tonumber(v)
106end)
107
108directives.register("typesetters.italics.threshold.math", function(v)
109    exfactormath = v == true and 1.25 or tonumber(v)
110end)
111
112-- function typesetters.italics.forcevariant(variant)
113-- end
114
115-- We use the same key as the tex font handler. So, if a value has already be set, we
116-- use that one. Contrary to \MKIV\ we don't cache the kern value because we share the
117-- character data in compact mode as well as in slant mode. We do however cache the
118-- intermediate (partial) italic value. Instead of attributes we now set bits in the
119-- glyph and discretionary options which is more efficient and we had these anyway.
120
121local function getmultiplier(tfmdata,current)
122    local multiplier
123    local slant = getslant(current)
124    if slant and slant ~= 0 then
125        multiplier = slant/1000
126    else
127        local properties  = tfmdata.properties
128        local italicangle = properties.useditalicangle or 0
129        if italicangle == 0 then
130            slant = properties.usedslant or 0
131            if slant and slant ~= 0 then
132                multiplier = slant/1000
133            else
134                -- We take the median of all valid italic/slanted on my disk.
135                italicangle = -12
136                multiplier = tand(12)
137            end
138        else
139            multiplier = tand(-italicangle)
140        end
141    end
142    return multiplier
143end
144
145local function getleftitalic(font,char,current)
146    local tfmdata   = fontdata[font]
147    local character = tfmdata.characters[char]
148    if character then
149        local italic = character.italic
150        if not italic then
151            local multiplier = getmultiplier(tfmdata,current)
152            if multiplier ~= 0 then
153                local lsb = character._lsb_
154                if lsb == false then
155                    return 0, 0
156                end
157                local description = tfmdata.descriptions[char]
158                if not lsb then
159                    if description then
160                        local boundingbox = description.boundingbox
161                        if boundingbox then
162                            lsb = boundingbox[1]
163                            character._lsb_ = lsb
164                        else
165                            character._lsb_ = false
166                            return 0, 0
167                        end
168                    else
169                        character._lsb_ = false
170                        return 0, 0
171                    end
172                end
173                if lsb ~= 0 then
174                    local factor = tfmdata.parameters.hfactor
175                    local italic = xscaled(current,lsb * factor)
176                    local extra  = xscaled(current,description.height * multiplier * spacefactor * factor)
177                    if trace_italics then
178                        report_italics("setting left italic correction of %C of font %a to %p",char,font,italic)
179                    end
180                    return -italic, extra
181                end
182            end
183        end
184    end
185    return 0, 0
186end
187
188-- bb3 > width : shape sticks out : we we need to add
189-- bb3 < width : we could have a negative correction
190
191local function getrightitalic(font,char,current)
192    local tfmdata   = fontdata[font]
193    local character = tfmdata.characters[char]
194    if character then
195        local italic = character.italic
196        if italic then
197            return italic, 0
198        else
199            local multiplier = getmultiplier(tfmdata,current)
200            if multiplier ~= 0 then
201                local rsb = character._rsb_
202                if rsb == false then
203                    return 0, 0
204                end
205                local description = tfmdata.descriptions[char]
206                if not rsb then
207                    if description then
208                        local boundingbox = description.boundingbox
209                        if boundingbox then
210                            rsb = boundingbox[3] - description.width
211                            character._rsb_ = rsb
212                        else
213                            character._rsb_ = false
214                            return 0, 0
215                        end
216                    else
217                        character._rsb_ = false
218                        return 0, 0
219                    end
220                end
221                if rsb > 0 then
222                    local factor = tfmdata.parameters.hfactor
223                    local italic = xscaled(current,rsb * factor)
224                    local extra  = xscaled(current,description.height * multiplier * spacefactor * factor)
225                    if trace_italics then
226                        report_italics("setting right italic correction of %C of font %a to %p",char,font,italic)
227                    end
228                    return italic, extra
229                else
230                    -- < 0 indicates no overshoot or a very small auto italic
231                end
232            end
233            return 0, 0
234        end
235    else
236        return 0, 0
237    end
238end
239
240-- The left and right correction kerns will be done in a second pass because we have
241-- to deal with the left side anyway due to lack of a next glyph at injection time.
242
243-- callback.register("italic_correction", function(node,kern,subtype)
244--     if node and subtype == italic_kern_code then
245--         node = tonut(node)
246--         local char, id = isglyph(node)
247--         if char then
248--             kern = getrightitalic(id,char,node)
249--         elseif id == disc_code then
250--             -- for now we forget about it
251--         end
252--     end
253--     return kern
254-- end)
255
256-- This means that we need to make sure that we have the sub type set so that we
257-- handle it later. We keep the zero value because that way we ignore successive
258-- corrections.
259
260callback.register("italic_correction", function(node,kern,subtype)
261    if not kernokay then
262        enableaction("processors","typesetters.corrections.handler")
263        kernokay = true
264        if trace_italics then
265            report_italics("enabling kern italics")
266        end
267    end
268    return kern
269end)
270
271local function report_ignored(previous,current,why)
272    report_italics("%s correction between %C and %C, %s","ignoring",getchar(previous),getchar(current),why)
273end
274
275local function report_obeyed(previous,current,why)
276    report_italics("%s correction between %C and %C, %s","inserting",getchar(previous),getchar(current),why)
277end
278
279local function report_math(kern,mathchar,char,why)
280     report_italics("%s italic %p between math %C and punctuation %C, %s",kern,mathchar,char,why)
281end
282
283local function injection_okay(previous,current)
284    if hasglyphoption(current,no_correction_code) then
285        if trace_italics then
286            report_ignored(previous,current,"disabled")
287        end
288        return false
289    else
290        if exfactortext and exfactortext > 0 then
291            -- hm, check previous .. probably not ok yet
292            while current and getid(current) ~= glyph_code do
293                current = getprev(current)
294            end
295            if current then
296                local char, id = isglyph(current)
297                if char then
298                    local height   = getheight(current) -- chardata[id][char].height or 0
299                    local exheight = yscaled(current, exheights[id])
300                    if height <= exfactortext*exheight then
301                        if trace_italics then
302                            report_ignored(previous,current,"threshold")
303                        end
304                        return false
305                    end
306                end
307            end
308        end
309        if trace_italics then
310            report_obeyed(previous,current,"enabled")
311        end
312        return true
313    end
314end
315
316local function correction_kern(kern,n)
317    local k = new_correction_kern(kern)
318    if n then
319        setattrlist(k,n)
320    end
321    return k
322end
323
324-- The code below now assumes the non-italic math model to be used so instead of
325-- nilling a kern (italic correction) we now compensate for the right corner kern.
326--
327-- [math: <glyph><kern>]<glyph> is now just [math: <glyph>]<glyph>
328
329----- function domath(head,last)
330local function domath(head,current)
331    if exfactormath and exfactormath > 0 then
332        local current = endofmath(current)
333        local next    = getnext(current)
334        if not hasglyphoption(next,no_correction_code) then -- check node and id too
335            local char, id = isglyph(next)
336            if char and ispunctuation[char] then
337                local glyph = getprev(current)
338                if hasglyphoption(glyph,math_check_italic_code) then -- check node and id too
339                    local ll, lr = getcornerkerns(glyph)
340                    if lr ~= 0 then
341                        -- unscaled
342                        local height   = chardata[id][char].height or 0
343                        local exheight = yscaled(current, exheights[id])
344                        if height < exfactormath*exheight then
345                            insertnodeafter(head,current,correction_kern(lr,glyph))
346                            if trace_italics then
347                                report_math(lr,c,char,"injecting")
348                            end
349                        end
350                    end
351                end
352            end
353        end
354    end
355    return current
356end
357
358local function mathhandler(head)
359    local current = head
360    while current do
361        current = beginofmath(current)
362        if current then
363            current = domath(head,current)
364            current = getnext(current)
365        end
366    end
367    return head
368end
369
370local function injectafter(head,glyph,font,char,where)
371    local italic, extra = getrightitalic(font,char,glyph)
372    if italic ~= 0 then
373        -- we could have a .01 pt threshold but correction doesn't happen often
374        local kern = correction_kern(italic,glyph)
375        insertnodeafter(head,glyph,kern)
376        if extra > 0 then
377            -- todo helper: isspace
378            local next = getnext(kern)
379            if next and getid(next) == glue_code then
380                local subtype = getsubtype(next)
381                if subtype == spaceskip_code or subtype == xspaceskip_code then
382                    local width, stretch, shrink = getglue(next)
383                    setglue(next,width+extra,stretch,shrink)
384                end
385            end
386        end
387        if trace_italics then
388            report_italics("inserting %p between %s italic %C ",italic,where,char)
389        end
390    end
391end
392
393local function fixleftitalic(current)
394    local prev = getnext(current)
395    local char, id = isglyph(prev)
396    if char then
397        local italic, extra = getleftitalic(id,char,prev)
398        setkern(current,italic)
399    end
400    return current
401end
402
403local function fixrightitalic(current)
404    local prev = getprev(current)
405    local char, id = isglyph(prev)
406    if char then
407        local italic, extra = getrightitalic(id,char,prev)
408        setkern(current,italic)
409    end
410    return current
411end
412
413local function texthandler(head)
414
415    -- yes : slantedglyph end
416    -- nop : slantedglyph kern
417    -- nop : slantedglyph      similarglyph
418    -- nop : slantedglyph      samefont
419    -- nop : slantedglyph glue similarglyph
420    -- nop : slantedglyph glue samefont
421    -- yes : slantedglyph glue
422
423 -- local current = head
424
425 -- local current = firstitalicglyph(head,true) -- no discretionaries (yet) as we assume a leading glyph
426    local current = firstitalicglyph(head)
427
428    if not current then
429        return head
430    end
431
432    local currentdisc  = nil
433
434    local prevglyph    = nil
435    local prevchar     = nil
436    local prevfont     = nil
437
438    local prehead      = nil
439    local pretail      = nil
440
441    local postglyph    = nil
442    local posthead     = nil
443    local posttail     = nil
444    local postfont     = nil
445    local postchar     = nil
446
447    local replaceglyph = nil
448    local replacehead  = nil
449    local replacetail  = nil
450    local replacefont  = nil
451    local replacechar  = nil
452
453    local function flush()
454        if prevglyph then
455            injectafter(head,prevglyph,prevfont,prevchar,"glyph")
456            prevglyph    = nil
457            replaceglyph = nil
458            postglyph    = nil
459        else
460            local updatedisc = false
461            if replaceglyph then
462                injectafter(replacehead,replaceglyph,replacefont,replacechar,"replace")
463                replaceglyph = nil
464                updatedisc   = true
465            end
466            if postglyph then
467                injectafter(posthead,postglyph,postfont,postchar,"post")
468                postglyph  = nil
469                updatedisc = true
470            end
471            if updatedisc then
472                setdisc(currentdisc,prehead,posthead,replacehead)
473            end
474        end
475    end
476
477    if false then
478--     if true then
479        local p = getprev(current)
480        if not p or getid(p) ~= glue_code then
481            local char, id = isglyph(current) -- isitalicglyph(current)
482            if char then
483                local italic = getleftitalic(id,char,current)
484                if italic ~= 0 and current == head then
485                    head = insertnodebefore(head,current,correction_kern(italic,current))
486                end
487            end
488        end
489    end
490
491    while current do
492        local nxt, char, id = isnextglyph(current)
493        if char then
494            if not hasglyphoption(current,text_check_italic_code) then
495                prevglyph    = nil
496                replaceglyph = nil
497                postglyph    = nil
498            elseif not prevglyph then
499                -- nothing to do
500                local data = isitalicglyph(current)
501                if data then
502                    prevglyph    = current
503                    prevchar     = char
504                    prevfont     = id
505                    replaceglyph = nil
506                    postglyph    = nil
507                end
508            elseif issimilarglyph(current,prevglyph) then
509                -- nothing to do
510                prevglyph    = current
511                prevchar     = char
512                prevfont     = id
513                replaceglyph = nil
514                postglyph    = nil
515            else
516                local fine = injection_okay(prevglyph,current)
517                if fine then
518                    flush()
519                else
520                    prevglyph    = nil
521                    replaceglyph = nil
522                    postglyph    = nil
523                end
524             -- local data = fine and (italicsdata[id] or getslant(current))
525                local data = isitalicglyph(current)
526                if data then
527                    prevglyph = current
528                    prevchar  = char
529                    prevfont  = id
530                end
531            end
532        elseif id == disc_code then
533            prevglyph    = nil
534            replaceglyph = nil
535            postglyph    = nil
536            if hasdiscoption(current,disc_check_italic_code) then
537             -- if not hasglyphoption(current,no_correction_code) then -- checked in the glyph itself
538                    prehead, posthead, replacehead, pretail, posttail, replacetail = getdisc(current,true)
539                    if replacetail then
540                        local char, id = isglyph(replacetail)
541                        if char then
542                            local data = isitalicglyph(replacetail)
543                            if data then
544                                replaceglyph = replacetail
545                                replacechar  = char
546                                replacefont  = id
547                            end
548                        end
549                    end
550                    if posttail then
551                        local char, id = isglyph(posttail)
552                        if char then
553                            local data = isitalicglyph(replacetail)
554                            if data then
555                                postglyph = posttail
556                                postchar  = char
557                                postfont  = id
558                            end
559                        end
560                    end
561                    currentdisc = current
562             -- end
563            end
564        elseif id == kern_code then
565            local subtype = getsubtype(current)
566--             if subtype == italic_kern_code or subtype == right_kern_code then
567--                 fixrightitalic(current)
568--                 prevglyph    = nil
569--                 replaceglyph = nil
570--                 postglyph    = nil
571--             elseif subtype == left_kern_code then
572--                 fixleftitalic(current)
573--                 prevglyph    = nil
574--                 replaceglyph = nil
575--                 postglyph    = nil
576--             end
577            if subtype == italic_kern_code or subtype == right_kern_code or subtype == left_kern_code then
578                prevglyph    = nil
579                replaceglyph = nil
580                postglyph    = nil
581            end
582        elseif id == glue_code then
583            -- selective correction
584        elseif id == math_code then
585            flush()
586            current = endofmath(current)
587            if mathokay then
588                current = domath(head,current)
589         -- else
590         --     current = endofmath(current)
591            end
592            prevglyph    = nil
593            replaceglyph = nil
594            postglyph    = nil
595        else
596            flush()
597        end
598        current = nxt
599    end
600 -- if lastattr and lastattr > 1 then -- more control is needed here
601    flush()
602    return head
603end
604
605function corrections.handler(head)
606    if kernokay then
607        for current, subtype in traverseitalic(head) do
608            if subtype == italic_kern_code or subtype == right_kern_code then
609                fixrightitalic(current)
610            elseif subtype == left_kern_code then
611                fixleftitalic(current)
612            end
613        end
614    end
615    return head
616end
617
618local function enable()
619    enableaction("processors","typesetters.italics.handler")
620    enable = false
621end
622
623function italics.handler(head)
624    if textokay then
625        return texthandler(head)
626    else
627        -- fast loop for math
628        if mathokay then
629            return mathhandler(head)
630        end
631        return head
632    end
633end
634
635local enabletext = function()
636    if enable then
637        enable()
638    end
639    if trace_italics then
640        report_italics("enabling text/text italics")
641    end
642    enabletext = false
643    textokay   = true
644end
645
646local enablemath = function()
647    if enable then
648        enable()
649    end
650    if trace_italics then
651        report_italics("enabling math/text italics")
652    end
653    enablemath = false
654    mathokay   = true
655end
656
657function italics.enabletext() -- math and text
658    if enabletext then
659        enabletext()
660    end
661end
662
663function italics.enablemath() -- only math
664    if enablemath then
665        enablemath()
666    end
667end
668
669-- maybe a table and also thresholds
670
671local function setitaliccorrection(factor)
672    factor = tonumber(factor) or 0
673    if enabletext then
674        enabletext()
675    end
676    spacefactor = factor
677end
678
679implement {
680    name      = "setitaliccorrection",
681    actions   = setitaliccorrection,
682    arguments = "argument",
683}
684