math-act.lua /size: 26 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['math-act'] = {
2    version   = 1.001,
3    comment   = "companion to math-ini.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- Here we tweak some font properties (if needed). Per mid octover 2022 we also provide
10-- an lmtx emulation mode which means that we removed some other code. Some of that was
11-- experimental, some transitional, some is now obsolete). Using emulation mode also
12-- means that we are unlikely to test some aspects of the math engines extensively.
13
14local type, next = type, next
15local fastcopy, insert, remove, copytable = table.fastcopy, table.insert, table.remove, table.copy
16local formatters = string.formatters
17
18local trace_defining   = false  trackers.register("math.defining",   function(v) trace_defining   = v end)
19local trace_collecting = false  trackers.register("math.collecting", function(v) trace_collecting = v end)
20
21local report_math      = logs.reporter("mathematics","initializing")
22
23local context          = context
24local commands         = commands
25local mathematics      = mathematics
26local texsetdimen      = tex.setdimen
27local abs              = math.abs
28
29local helpers          = fonts.helpers
30local upcommand        = helpers.commands.up
31local rightcommand     = helpers.commands.right
32local charcommand      = helpers.commands.char
33local prependcommands  = helpers.prependcommands
34
35local sequencers       = utilities.sequencers
36local appendgroup      = sequencers.appendgroup
37local appendaction     = sequencers.appendaction
38
39local fontchars        = fonts.hashes.characters
40local fontproperties   = fonts.hashes.properties
41
42local mathfontparameteractions = sequencers.new {
43    name      = "mathparameters",
44    arguments = "target,original",
45}
46
47appendgroup("mathparameters","before") -- user
48appendgroup("mathparameters","system") -- private
49appendgroup("mathparameters","after" ) -- user
50
51function fonts.constructors.assignmathparameters(original,target)
52    local runner = mathfontparameteractions.runner
53    if runner then
54        runner(original,target)
55    end
56end
57
58function mathematics.initializeparameters(target,original)
59    local mathparameters = original.mathparameters
60    if mathparameters and next(mathparameters) then
61        mathparameters = mathematics.dimensions(mathparameters)
62        if not mathparameters.SpaceBeforeScript then
63            mathparameters.SpaceBeforeScript = mathparameters.SpaceAfterScript
64        end
65        if not mathparameters.SubscriptShiftDownWithSuperscript then
66            mathparameters.SubscriptShiftDownWithSuperscript = mathparameters.SubscriptShiftDown * 1.5
67        end
68        target.mathparameters = mathparameters
69    end
70end
71
72sequencers.appendaction("mathparameters","system","mathematics.initializeparameters")
73
74local how = {
75 -- RadicalKernBeforeDegree         = "horizontal",
76 -- RadicalKernAfterDegree          = "horizontal",
77    ScriptPercentScaleDown          = "unscaled",
78    ScriptScriptPercentScaleDown    = "unscaled",
79    RadicalDegreeBottomRaisePercent = "unscaled",
80    NoLimitSupFactor                = "unscaled",
81    NoLimitSubFactor                = "unscaled",
82}
83
84local function scaleparameters(mathparameters,parameters)
85    if mathparameters and next(mathparameters) and parameters then
86        local factor  = parameters.factor
87        local hfactor = parameters.hfactor
88        local vfactor = parameters.vfactor
89        for name, value in next, mathparameters do
90            local h = how[name]
91            if h == "unscaled" then
92                -- kept
93            elseif h == "horizontal" then
94                value = value * hfactor
95            elseif h == "vertical"then
96                value = value * vfactor
97            else
98                value = value * factor
99            end
100            mathparameters[name] = value
101        end
102    end
103end
104
105function mathematics.scaleparameters(target,original)
106    if not target.properties.math_is_scaled then
107        scaleparameters(target.mathparameters,target.parameters)
108        target.properties.math_is_scaled = true
109    end
110end
111
112-- -- AccentBaseHeight vs FlattenedAccentBaseHeight
113--
114-- function mathematics.checkaccentbaseheight(target,original)
115--     local mathparameters = target.mathparameters
116--     if mathparameters and mathparameters.AccentBaseHeight == 0 then
117--         mathparameters.AccentBaseHeight = target.parameters.x_height -- needs checking
118--     end
119-- end
120
121function mathematics.checkprivateparameters(target,original)
122    local mathparameters = target.mathparameters
123    if mathparameters then
124        local parameters = target.parameters
125        local properties = target.properties
126        if parameters then
127            local size = parameters.size
128            if size then
129                if not mathparameters.FractionDelimiterSize then
130                    mathparameters.FractionDelimiterSize = 1.01 * size
131                end
132                if not mathparameters.FractionDelimiterDisplayStyleSize then
133                    mathparameters.FractionDelimiterDisplayStyleSize = 2.40 * size
134                end
135            elseif properties then
136                report_math("invalid parameters in font %a",properties.fullname or "?")
137            else
138                report_math("invalid parameters in font")
139            end
140        elseif properties then
141            report_math("no parameters in font %a",properties.fullname or "?")
142        else
143            report_math("no parameters and properties in font")
144        end
145    end
146end
147
148function mathematics.overloadparameters(target,original)
149    local mathparameters = target.mathparameters
150    if mathparameters and next(mathparameters) then
151        local goodies = target.goodies
152        if goodies then
153            for i=1,#goodies do
154                local goodie = goodies[i]
155                local mathematics = goodie.mathematics
156                local parameters  = mathematics and mathematics.parameters
157                if parameters then
158                    if trace_defining then
159                        report_math("overloading math parameters in %a @ %p",target.properties.fullname,target.parameters.size)
160                    end
161                 -- for name, value in next, parameters do
162                 --     local tvalue = type(value)
163                 --     if tvalue == "string" then
164                 --         report_math("comment for math parameter %a: %s",name,value)
165                 --     else
166                 --         local oldvalue = mathparameters[name]
167                 --         local newvalue = oldvalue
168                 --         if oldvalue then
169                 --             if tvalue == "number" then
170                 --                 newvalue = value
171                 --             elseif tvalue == "function" then
172                 --                 newvalue = value(oldvalue,target,original)
173                 --             elseif not tvalue then
174                 --                 newvalue = nil
175                 --             end
176                 --             if trace_defining and oldvalue ~= newvalue then
177                 --                 report_math("overloading math parameter %a: %S => %S",name,oldvalue,newvalue)
178                 --             end
179                 --         else
180                 --          -- report_math("invalid math parameter %a",name)
181                 --         end
182                 --         mathparameters[name] = newvalue
183                 --     end
184                 -- end
185                    for name, value in next, parameters do
186                        local tvalue   = type(value)
187                        local oldvalue = mathparameters[name]
188                        local newvalue = oldvalue
189                        if tvalue == "number" then
190                            newvalue = value
191                        elseif tvalue == "string" then
192                            -- delay till all set
193                        elseif tvalue == "function" then
194                            newvalue = value(oldvalue,target,original)
195                        elseif not tvalue then
196                            newvalue = nil
197                        end
198                        if trace_defining and oldvalue ~= newvalue then
199                            report_math("overloading math parameter %a: %S => %S",name,oldvalue or 0,newvalue)
200                        end
201                        mathparameters[name] = newvalue
202                    end
203                    for name, value in next, parameters do
204                        local tvalue = type(value)
205                        if tvalue == "string" then
206                            local newvalue = mathparameters[value]
207                            if not newvalue then
208                                local code = loadstring("return " .. value,"","t",mathparameters)
209                                if type(code) == "function" then
210                                    local okay, v = pcall(code)
211                                    if okay then
212                                        newvalue = v
213                                    end
214                                end
215                            end
216                            if newvalue then
217                                -- split in number and string
218                                mathparameters[name] = newvalue
219                            elseif trace_defining then
220                                report_math("ignoring math parameter %a: %S",name,value)
221                            end
222                        end
223                    end
224                end
225            end
226        end
227    end
228end
229
230local mathtweaks   = { subsets = table.setmetatableindex("table") }
231mathematics.tweaks = mathtweaks
232
233local apply_tweaks = true
234
235directives.register("math.applytweaks", function(v)
236    apply_tweaks = v;
237end)
238
239local function applytweaks(when,target,original)
240    if apply_tweaks then
241        local goodies = original.goodies
242        if goodies then
243            local tweaked = target.tweaked or { }
244            if tweaked[when] then
245                if trace_defining then
246                    report_math("tweaking math of %a @ %p (%s: %s)",target.properties.fullname,target.parameters.size,when,"done")
247                end
248            else
249                for i=1,#goodies do
250                    local goodie = goodies[i]
251                    local mathematics = goodie.mathematics
252                    local tweaks = mathematics and mathematics.tweaks
253                    if type(tweaks) == "table" then
254                        tweaks = tweaks[when]
255                        if type(tweaks) == "table" then
256                            if trace_defining then
257                                report_math("tweaking math of %a @ %p (%s: %s)",target.properties.fullname,target.parameters.size,when,"okay")
258                            end
259                            for i=1,#tweaks do
260                                local tweak  = tweaks[i]
261                                local tvalue = type(tweak)
262                                if type(tweak) == "table" then
263                                    local action = mathtweaks[tweak.tweak or ""]
264                                    if action then
265                                        local feature  = tweak.feature
266                                        local features = target.specification.features.normal
267                                        if not feature or features[feature] == true then
268                                            local version = tweak.version
269                                            if version and version ~= target.tweakversion then
270                                                report_math("skipping tweak %a version %a",tweak.tweak,version)
271                                            elseif original then
272                                                action(target,original,tweak)
273                                            else
274                                                action(target,tweak)
275                                            end
276                                        end
277                                    end
278                                end
279                            end
280                        end
281                    end
282                end
283                tweaked[when] = true
284                target.tweaked = tweaked
285            end
286        end
287    else
288        report_math("not tweaking math of %a @ %p (%s)",target.properties.fullname,target.parameters.size,when)
289    end
290end
291
292function mathematics.tweakbeforecopyingfont(target,original)
293    local mathparameters = target.mathparameters -- why not hasmath
294    if mathparameters then
295        applytweaks("beforecopying",target,original)
296    end
297end
298
299function mathematics.tweakaftercopyingfont(target,original)
300    local mathparameters = target.mathparameters -- why not hasmath
301    if mathparameters then
302        applytweaks("aftercopying",target,original)
303    end
304end
305
306sequencers.appendaction("mathparameters","system","mathematics.overloadparameters")
307sequencers.appendaction("mathparameters","system","mathematics.scaleparameters")
308----------.appendaction("mathparameters","system","mathematics.checkaccentbaseheight")  -- should go in lfg instead
309sequencers.appendaction("mathparameters","system","mathematics.checkprivateparameters") -- after scaling !
310
311sequencers.appendaction("beforecopyingcharacters","system","mathematics.tweakbeforecopyingfont")
312sequencers.appendaction("aftercopyingcharacters", "system","mathematics.tweakaftercopyingfont")
313
314do
315
316    -- More than a year of testing, development, tweaking (and improving) fonts has resulted
317    -- in a math engine in \LUAMETATEX\ that is quite flexible. Basically we can drop italic
318    -- correction there. In \MKIV\ we can emulate this to some extend but we still need a bit
319    -- of mix because \LUAMETATEX\ lacks some features. A variant of the tweak below is now
320    -- also used in the plain code we ship. In \MKIV\ we dropped a few features that were a
321    -- prelude to this and, because most users switched to \LMTX, it is unlikely that other
322    -- tweaks wil be backported. There is also no need to adapt \LUATEX\ and eventually all
323    -- italic code might be removed from \LUAMETATEX\ (unless we want to be able to test the
324    -- alternative; I can live with a little ballast, especially because it took time to load
325    -- it).
326
327    local italics   = nil
328    local integrals = table.tohash {
329        0x0222B, 0x0222C, 0x0222D, 0x0222E, 0x0222F, 0x02230, 0x02231, 0x02232, 0x02233,
330        0x02A0B, 0x02A0C, 0x02A0D, 0x02A0E, 0x02A0F, 0x02A10, 0x02A11, 0x02A12, 0x02A13,
331        0x02A14, 0x02A15, 0x02A16, 0x02A17, 0x02A18, 0x02A19, 0x02A1A, 0x02A1B, 0x02A1C,
332        0x02320, 0x02321
333    }
334
335    function mathtweaks.emulatelmtx(target,original,parameters)
336        -- gaps are not known yet
337        if not italic then
338            italics = { }
339            local gaps = mathematics.gaps
340            for name, data in next, characters.blocks do
341                if data.math and data.italic then
342                    for i=data.first,data.last do
343                        italics[i] = true
344                        local g = gaps[i]
345                        if g then
346                            italics[g] = true
347                        end
348                    end
349                end
350            end
351--             table.save("temp.log", table.sortedkeys(italics))
352        end
353        --
354        local targetcharacters   = target.characters
355        local targetdescriptions = target.descriptions
356        local factor             = target.parameters.factor
357        local function getllx(u)
358            local d = targetdescriptions[u]
359            if d then
360                local b = d.boundingbox
361                if b then
362                    local llx = b[1]
363                    if llx < 0 then
364                        return - llx
365                    end
366                end
367            end
368            return false
369        end
370        -- beware: here we also do the weird ones
371        for u, c in next, targetcharacters do
372            local uc = c.unicode or u
373            if integrals[uc] then
374                -- skip this one
375            else
376                local accent = c.top_accent
377                local italic = c.italic
378                local width  = c.width  or 0
379                local llx    = getllx(u)
380                local bl, br, tl, tr
381                if llx then
382                    llx   = llx * factor
383                    width = width + llx
384                    bl    = - llx
385                    tl    = bl
386                    c.commands = { rightcommand[llx], charcommand[u] }
387                    if accent then
388                        accent = accent + llx
389                    end
390                end
391                if accent then
392                    if italics[uc] then
393                        c.top_accent = accent
394                    else
395                        c.top_accent = nil
396                    end
397                end
398                if italic and italic ~= 0 then
399                    width = width + italic
400                    br    = - italic
401                end
402                c.width = width
403                if italic then
404                    c.italic = nil
405                end
406                if bl or br or tl or tr then
407                    -- watch out: singular and _ because we are post copying / scaling
408                    c.mathkern = {
409                        bottom_left  = bl and { { height = 0,             kern = bl } } or nil,
410                        bottom_right = br and { { height = 0,             kern = br } } or nil,
411                        top_left     = tl and { { height = c.height or 0, kern = tl } } or nil,
412                        top_right    = tr and { { height = c.height or 0, kern = tr } } or nil,
413                    }
414                end
415            end
416        end
417    end
418
419    function mathtweaks.parameters(target,original,parameters)
420        local newparameters = parameters.list
421        local oldparameters = target.mathparameters
422        if newparameters and oldparameters then
423            newparameters = copytable(newparameters)
424            scaleparameters(newparameters,target.parameters)
425            for name, newvalue in next, newparameters do
426                oldparameters[name] = newvalue
427            end
428        end
429    end
430
431end
432
433local setmetatableindex  = table.setmetatableindex
434
435local getfontoffamily    = tex.getfontoffamily
436
437local fontcharacters     = fonts.hashes.characters
438local extensibles        = utilities.storage.allocate()
439fonts.hashes.extensibles = extensibles
440
441local chardata           = characters.data
442local extensibles        = mathematics.extensibles
443
444-- we use numbers at the tex end (otherwise we could stick to chars)
445
446local e_left       = extensibles.left
447local e_right      = extensibles.right
448local e_horizontal = extensibles.horizontal
449local e_mixed      = extensibles.mixed
450local e_unknown    = extensibles.unknown
451
452local unknown      = { e_unknown, false, false }
453
454local function extensiblecode(font,unicode)
455    local characters = fontcharacters[font]
456    local character = characters[unicode]
457    if not character then
458        return unknown
459    end
460    local first = character.next
461    local code = unicode
462    local next = first
463    while next do
464        code = next
465        character = characters[next]
466        next = character.next
467    end
468    local char = chardata[unicode]
469    if not char then
470        return unknown
471    end
472    if character.horiz_variants then
473        if character.vert_variants then
474            return { e_mixed, code, character }
475        else
476            local m = char.mathextensible
477            local e = m and extensibles[m]
478            return e and { e, code, character } or unknown
479        end
480    elseif character.vert_variants then
481        local m = char.mathextensible
482        local e = m and extensibles[m]
483        return e and { e, code, character } or unknown
484    elseif first then
485        -- assume accent (they seldom stretch .. sizes)
486        local m = char.mathextensible or char.mathstretch
487        local e = m and extensibles[m]
488        return e and { e, code, character } or unknown
489    else
490        return unknown
491    end
492end
493
494setmetatableindex(extensibles,function(extensibles,font)
495    local codes = { }
496    setmetatableindex(codes, function(codes,unicode)
497        local status = extensiblecode(font,unicode)
498        codes[unicode] = status
499        return status
500    end)
501    extensibles[font] = codes
502    return codes
503end)
504
505local function extensiblecode(family,unicode)
506    return extensibles[getfontoffamily(family or 0)][unicode][1]
507end
508
509-- left       : [head] ...
510-- right      : ... [head]
511-- horizontal : [head] ... [head]
512--
513-- abs(right["start"] - right["end"]) | right.advance | characters[right.glyph].width
514
515local function horizontalcode(family,unicode)
516    local font    = getfontoffamily(family or 0)
517    local data    = extensibles[font][unicode]
518    local kind    = data[1]
519    local loffset = 0
520    local roffset = 0
521    if kind == e_left then
522        local charlist = data[3].horiz_variants
523        if charlist then
524            local left = charlist[1]
525            loffset = abs((left["start"] or 0) - (left["end"] or 0))
526        end
527    elseif kind == e_right then
528        local charlist = data[3].horiz_variants
529        if charlist then
530            local right = charlist[#charlist]
531            roffset = abs((right["start"] or 0) - (right["end"] or 0))
532        end
533     elseif kind == e_horizontal then
534        local charlist = data[3].horiz_variants
535        if charlist then
536            local left  = charlist[1]
537            local right = charlist[#charlist]
538            loffset = abs((left ["start"] or 0) - (left ["end"] or 0))
539            roffset = abs((right["start"] or 0) - (right["end"] or 0))
540        end
541    end
542    return kind, loffset, roffset
543end
544
545mathematics.extensiblecode = extensiblecode
546mathematics.horizontalcode = horizontalcode
547
548interfaces.implement {
549    name      = "extensiblecode",
550    arguments = { "integer", "integer" },
551    actions   = { extensiblecode, context }
552}
553
554interfaces.implement {
555    name      = "horizontalcode",
556    arguments = { "integer", "integer" },
557    actions   = function(family,unicode)
558        local kind, loffset, roffset = horizontalcode(family,unicode)
559        texsetdimen("scratchleftoffset", loffset)
560        texsetdimen("scratchrightoffset",roffset)
561        context(kind)
562    end
563}
564
565local stack = { }
566
567function mathematics.registerfallbackid(n,id,name)
568    if trace_collecting then
569        report_math("resolved fallback font %i, name %a, id %a, used %a",
570            n,name,id,fontproperties[id].fontname)
571    end
572    stack[#stack][n] = id
573end
574
575interfaces.implement { -- will be shared with text
576    name      = "registerfontfallbackid",
577    arguments = { "integer", "integer", "string" },
578    actions   = mathematics.registerfallbackid,
579}
580
581function mathematics.resolvefallbacks(target,specification,fallbacks)
582    local definitions = fonts.collections.definitions[fallbacks]
583    if definitions then
584        local size = specification.size -- target.size
585        local list = { }
586        insert(stack,list)
587        context.pushcatcodes("prt") -- context.unprotect()
588        for i=1,#definitions do
589            local definition = definitions[i]
590            local name       = definition.font
591            local features   = definition.features or ""
592            local size       = size * (definition.rscale or 1)
593            context.font_fallbacks_register_math(i,name,features,size)
594            if trace_collecting then
595                report_math("registering fallback font %i, name %a, size %a, features %a",i,name,size,features)
596            end
597        end
598        context.popcatcodes()
599    end
600end
601
602function mathematics.finishfallbacks(target,specification,fallbacks)
603    local list = remove(stack)
604    if list and #list > 0 then
605        local definitions = fonts.collections.definitions[fallbacks]
606        if definitions and #definitions > 0 then
607            if trace_collecting then
608                report_math("adding fallback characters to font %a",specification.hash)
609            end
610            local definedfont = fonts.definers.internal
611            local copiedglyph = fonts.handlers.vf.math.copy_glyph
612            local fonts       = target.fonts
613            local size        = specification.size -- target.size
614            local characters  = target.characters
615            if not fonts then
616                fonts = { }
617                target.fonts = fonts
618            end
619            --
620            target.type = "virtual"
621            target.properties.virtualized = true
622            --
623            if #fonts == 0 then
624                fonts[1] = { id = 0, size = size } -- self, will be resolved later
625            end
626            local done = { }
627            for i=1,#definitions do
628                local definition = definitions[i]
629                local name   = definition.font
630                local start  = definition.start
631                local stop   = definition.stop
632                local gaps   = definition.gaps
633                local check  = definition.check
634                local force  = definition.force
635                local rscale = definition.rscale or 1
636                local offset = definition.offset or start
637                local id     = list[i]
638                if id then
639                    local index  = #fonts + 1
640                    fonts[index] = { id = id, size = size }
641                    local chars  = fontchars[id]
642                    local function remap(unic,unicode,gap)
643                        if check and not chars[unicode] then
644                            return
645                        end
646                        if force or (not done[unic] and not characters[unic]) then
647                            if trace_collecting then
648                                report_math("replacing math character %C by %C using vector %a and font id %a for %a%s%s",
649                                    unic,unicode,fallbacks,id,fontproperties[id].fontname,check and ", checked",gap and ", gap plugged")
650                            end
651                            characters[unic] = copiedglyph(target,characters,chars,unicode,index)
652                            done[unic] = true
653                        end
654                    end
655                    local step = offset - start
656                    for unicode = start, stop do
657                        remap(unicode + step,unicode,false)
658                    end
659                    if gaps then
660                        for unic, unicode in next, gaps do
661                            remap(unic,unicode,true)
662                            remap(unicode,unicode,true)
663                        end
664                    end
665                end
666            end
667        elseif trace_collecting then
668            report_math("no fallback characters added to font %a",specification.hash)
669        end
670    end
671end
672