math-act.lua /size: 25 Kb    last modification: 2021-10-28 13:50
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).
10
11local type, next = type, next
12local fastcopy, insert, remove = table.fastcopy, table.insert, table.remove
13local formatters = string.formatters
14
15local trace_defining   = false  trackers.register("math.defining",   function(v) trace_defining   = v end)
16local trace_collecting = false  trackers.register("math.collecting", function(v) trace_collecting = v end)
17
18local report_math      = logs.reporter("mathematics","initializing")
19
20local context          = context
21local commands         = commands
22local mathematics      = mathematics
23local texsetdimen      = tex.setdimen
24local abs              = math.abs
25
26local helpers          = fonts.helpers
27local upcommand        = helpers.commands.up
28local rightcommand     = helpers.commands.right
29local charcommand      = helpers.commands.char
30local prependcommands  = helpers.prependcommands
31
32local sequencers       = utilities.sequencers
33local appendgroup      = sequencers.appendgroup
34local appendaction     = sequencers.appendaction
35
36local fontchars        = fonts.hashes.characters
37local fontproperties   = fonts.hashes.properties
38
39local mathfontparameteractions = sequencers.new {
40    name      = "mathparameters",
41    arguments = "target,original",
42}
43
44appendgroup("mathparameters","before") -- user
45appendgroup("mathparameters","system") -- private
46appendgroup("mathparameters","after" ) -- user
47
48function fonts.constructors.assignmathparameters(original,target)
49    local runner = mathfontparameteractions.runner
50    if runner then
51        runner(original,target)
52    end
53end
54
55function mathematics.initializeparameters(target,original)
56    local mathparameters = original.mathparameters
57    if mathparameters and next(mathparameters) then
58        mathparameters = mathematics.dimensions(mathparameters)
59        if not mathparameters.SpaceBeforeScript then
60            mathparameters.SpaceBeforeScript = mathparameters.SpaceAfterScript
61        end
62        target.mathparameters = mathparameters
63    end
64end
65
66sequencers.appendaction("mathparameters","system","mathematics.initializeparameters")
67
68local how = {
69 -- RadicalKernBeforeDegree         = "horizontal",
70 -- RadicalKernAfterDegree          = "horizontal",
71    ScriptPercentScaleDown          = "unscaled",
72    ScriptScriptPercentScaleDown    = "unscaled",
73    RadicalDegreeBottomRaisePercent = "unscaled",
74    NoLimitSupFactor                = "unscaled",
75    NoLimitSubFactor                = "unscaled",
76}
77
78function mathematics.scaleparameters(target,original)
79    if not target.properties.math_is_scaled then
80        local mathparameters = target.mathparameters
81        if mathparameters and next(mathparameters) then
82            local parameters = target.parameters
83            local factor  = parameters.factor
84            local hfactor = parameters.hfactor
85            local vfactor = parameters.vfactor
86            for name, value in next, mathparameters do
87                local h = how[name]
88                if h == "unscaled" then
89                    -- kept
90                elseif h == "horizontal" then
91                    value = value * hfactor
92                elseif h == "vertical"then
93                    value = value * vfactor
94                else
95                    value = value * factor
96                end
97               mathparameters[name] = value
98            end
99        end
100        target.properties.math_is_scaled = true
101    end
102end
103
104-- AccentBaseHeight vs FlattenedAccentBaseHeight
105
106function mathematics.checkaccentbaseheight(target,original)
107    local mathparameters = target.mathparameters
108    if mathparameters and mathparameters.AccentBaseHeight == 0 then
109        mathparameters.AccentBaseHeight = target.parameters.x_height -- needs checking
110    end
111end
112
113function mathematics.checkprivateparameters(target,original)
114    local mathparameters = target.mathparameters
115    if mathparameters then
116        local parameters = target.parameters
117        local properties = target.properties
118        if parameters then
119            local size = parameters.size
120            if size then
121                if not mathparameters.FractionDelimiterSize then
122                    mathparameters.FractionDelimiterSize = 1.01 * size
123                end
124                if not mathparameters.FractionDelimiterDisplayStyleSize then
125                    mathparameters.FractionDelimiterDisplayStyleSize = 2.40 * size
126                end
127            elseif properties then
128                report_math("invalid parameters in font %a",properties.fullname or "?")
129            else
130                report_math("invalid parameters in font")
131            end
132        elseif properties then
133            report_math("no parameters in font %a",properties.fullname or "?")
134        else
135            report_math("no parameters and properties in font")
136        end
137    end
138end
139
140function mathematics.overloadparameters(target,original)
141    local mathparameters = target.mathparameters
142    if mathparameters and next(mathparameters) then
143        local goodies = target.goodies
144        if goodies then
145            for i=1,#goodies do
146                local goodie = goodies[i]
147                local mathematics = goodie.mathematics
148                local parameters  = mathematics and mathematics.parameters
149                if parameters then
150                    if trace_defining then
151                        report_math("overloading math parameters in %a @ %p",target.properties.fullname,target.parameters.size)
152                    end
153                    for name, value in next, parameters do
154                        local tvalue = type(value)
155                        if tvalue == "string" then
156                            report_math("comment for math parameter %a: %s",name,value)
157                        else
158                            local oldvalue = mathparameters[name]
159                            local newvalue = oldvalue
160                            if oldvalue then
161                                if tvalue == "number" then
162                                    newvalue = value
163                                elseif tvalue == "function" then
164                                    newvalue = value(oldvalue,target,original)
165                                elseif not tvalue then
166                                    newvalue = nil
167                                end
168                                if trace_defining and oldvalue ~= newvalue then
169                                    report_math("overloading math parameter %a: %S => %S",name,oldvalue,newvalue)
170                                end
171                            else
172                                report_math("invalid math parameter %a",name)
173                            end
174                            mathparameters[name] = newvalue
175                        end
176                    end
177                end
178            end
179        end
180    end
181end
182
183local function applytweaks(when,target,original)
184    local goodies = original.goodies
185    if goodies then
186        for i=1,#goodies do
187            local goodie = goodies[i]
188            local mathematics = goodie.mathematics
189            local tweaks = mathematics and mathematics.tweaks
190            if type(tweaks) == "table" then
191                tweaks = tweaks[when]
192                if type(tweaks) == "table" then
193                    if trace_defining then
194                        report_math("tweaking math of %a @ %p (%s)",target.properties.fullname,target.parameters.size,when)
195                    end
196                    for i=1,#tweaks do
197                        local tweak= tweaks[i]
198                        local tvalue = type(tweak)
199                        if tvalue == "function" then
200                            tweak(target,original)
201                        end
202                    end
203                end
204            end
205        end
206    end
207end
208
209function mathematics.tweakbeforecopyingfont(target,original)
210    local mathparameters = target.mathparameters -- why not hasmath
211    if mathparameters then
212        applytweaks("beforecopying",target,original)
213    end
214end
215
216function mathematics.tweakaftercopyingfont(target,original)
217    local mathparameters = target.mathparameters -- why not hasmath
218    if mathparameters then
219        applytweaks("aftercopying",target,original)
220    end
221end
222
223sequencers.appendaction("mathparameters","system","mathematics.scaleparameters")
224sequencers.appendaction("mathparameters","system","mathematics.checkaccentbaseheight")  -- should go in lfg instead
225sequencers.appendaction("mathparameters","system","mathematics.checkprivateparameters") -- after scaling !
226sequencers.appendaction("mathparameters","system","mathematics.overloadparameters")
227
228sequencers.appendaction("beforecopyingcharacters","system","mathematics.tweakbeforecopyingfont")
229sequencers.appendaction("aftercopyingcharacters", "system","mathematics.tweakaftercopyingfont")
230
231-- no, it's a feature now (see good-mth):
232--
233-- sequencers.appendaction("aftercopyingcharacters", "system","mathematics.overloaddimensions")
234
235-- a couple of predefined tweaks:
236
237local tweaks       = { }
238mathematics.tweaks = tweaks
239
240-- function tweaks.fixbadprime(target,original)
241--     target.characters[0xFE325] = target.characters[0x2032]
242-- end
243
244-- these could go to math-fbk
245
246-- local virtualized = mathematics.virtualized
247--
248-- local function accent_to_extensible(target,newchr,original,oldchr,height,depth,swap)
249--     local characters = target.characters
250--  -- if not characters[newchr] then -- xits needs an enforce
251--     local addprivate = fonts.helpers.addprivate
252--         local olddata = characters[oldchr]
253--         if olddata then
254--             if swap then
255--                 swap = characters[swap]
256--                 height = swap.depth
257--                 depth  = 0
258--             else
259--                 height = height or 0
260--                 depth  = depth  or 0
261--             end
262--             local correction = swap and { "down", (olddata.height or 0) - height } or { "down", olddata.height }
263--             local newdata = {
264--                 commands = { correction, { "slot", 1, oldchr } },
265--                 width    = olddata.width,
266--                 height   = height,
267--                 depth    = depth,
268--             }
269--             characters[newchr] = newdata
270--             local nextglyph = olddata.next
271--             while nextglyph do
272--                 local oldnextdata = characters[nextglyph]
273--                 local newnextdata = {
274--                     commands = { correction, { "slot", 1, nextglyph } },
275--                     width    = oldnextdata.width,
276--                     height   = height,
277--                     depth    = depth,
278--                 }
279--                 local newnextglyph = addprivate(target,formatters["original-%H"](nextglyph),newnextdata)
280--                 newdata.next = newnextglyph
281--                 local nextnextglyph = oldnextdata.next
282--                 if nextnextglyph == nextglyph then
283--                     break
284--                 else
285--                     olddata   = oldnextdata
286--                     newdata   = newnextdata
287--                     nextglyph = nextnextglyph
288--                 end
289--             end
290--             local hv = olddata.horiz_variants
291--             if hv then
292--                 hv = fastcopy(hv)
293--                 newdata.horiz_variants = hv
294--                 for i=1,#hv do
295--                     local hvi = hv[i]
296--                     local oldglyph = hvi.glyph
297--                     local olddata = characters[oldglyph]
298--                     local newdata = {
299--                         commands = { correction, { "slot", 1, oldglyph } },
300--                         width    = olddata.width,
301--                         height   = height,
302--                         depth    = depth,
303--                     }
304--                     hvi.glyph = addprivate(target,formatters["original-%H"](oldglyph),newdata)
305--                 end
306--             end
307--         end
308--  -- end
309-- end
310
311-- function tweaks.fixoverline(target,original)
312--     local height, depth = 0, 0
313--     local mathparameters = target.mathparameters
314--     if mathparameters then
315--         height = mathparameters.OverbarVerticalGap
316--         depth  = mathparameters.UnderbarVerticalGap
317--     else
318--         height = target.parameters.xheight/4
319--         depth  = height
320--     end
321--     accent_to_extensible(target,0x203E,original,0x0305,height,depth)
322--     -- also crappy spacing for our purpose: push to top of baseline
323--     accent_to_extensible(target,0xFE3DE,original,0x23DE,height,depth,0x23DF)
324--     accent_to_extensible(target,0xFE3DC,original,0x23DC,height,depth,0x23DD)
325--     accent_to_extensible(target,0xFE3B4,original,0x23B4,height,depth,0x23B5)
326--     -- for symmetry
327--     target.characters[0xFE3DF] = original.characters[0x23DF]
328--     target.characters[0xFE3DD] = original.characters[0x23DD]
329--     target.characters[0xFE3B5] = original.characters[0x23B5]
330--  -- inspect(fonts.helpers.expandglyph(target.characters,0x203E))
331--  -- inspect(fonts.helpers.expandglyph(target.characters,0x23DE))
332-- end
333
334-- sequencers.appendaction("aftercopyingcharacters", "system","mathematics.tweaks.fixoverline") -- for the moment always
335
336-- helpers
337
338local setmetatableindex  = table.setmetatableindex
339
340local getfontoffamily    = tex.getfontoffamily
341
342local fontcharacters     = fonts.hashes.characters
343local extensibles        = utilities.storage.allocate()
344fonts.hashes.extensibles = extensibles
345
346local chardata           = characters.data
347local extensibles        = mathematics.extensibles
348
349-- we use numbers at the tex end (otherwise we could stick to chars)
350
351local e_left       = extensibles.left
352local e_right      = extensibles.right
353local e_horizontal = extensibles.horizontal
354local e_mixed      = extensibles.mixed
355local e_unknown    = extensibles.unknown
356
357local unknown      = { e_unknown, false, false }
358
359local function extensiblecode(font,unicode)
360    local characters = fontcharacters[font]
361    local character = characters[unicode]
362    if not character then
363        return unknown
364    end
365    local first = character.next
366    local code = unicode
367    local next = first
368    while next do
369        code = next
370        character = characters[next]
371        next = character.next
372    end
373    local char = chardata[unicode]
374    if not char then
375        return unknown
376    end
377    if character.horiz_variants then
378        if character.vert_variants then
379            return { e_mixed, code, character }
380        else
381            local m = char.mathextensible
382            local e = m and extensibles[m]
383            return e and { e, code, character } or unknown
384        end
385    elseif character.vert_variants then
386        local m = char.mathextensible
387        local e = m and extensibles[m]
388        return e and { e, code, character } or unknown
389    elseif first then
390        -- assume accent (they seldom stretch .. sizes)
391        local m = char.mathextensible or char.mathstretch
392        local e = m and extensibles[m]
393        return e and { e, code, character } or unknown
394    else
395        return unknown
396    end
397end
398
399setmetatableindex(extensibles,function(extensibles,font)
400    local codes = { }
401    setmetatableindex(codes, function(codes,unicode)
402        local status = extensiblecode(font,unicode)
403        codes[unicode] = status
404        return status
405    end)
406    extensibles[font] = codes
407    return codes
408end)
409
410local function extensiblecode(family,unicode)
411    return extensibles[getfontoffamily(family or 0)][unicode][1]
412end
413
414-- left       : [head] ...
415-- right      : ... [head]
416-- horizontal : [head] ... [head]
417--
418-- abs(right["start"] - right["end"]) | right.advance | characters[right.glyph].width
419
420local function horizontalcode(family,unicode)
421    local font    = getfontoffamily(family or 0)
422    local data    = extensibles[font][unicode]
423    local kind    = data[1]
424    local loffset = 0
425    local roffset = 0
426    if kind == e_left then
427        local charlist = data[3].horiz_variants
428        if charlist then
429            local left = charlist[1]
430            loffset = abs((left["start"] or 0) - (left["end"] or 0))
431        end
432    elseif kind == e_right then
433        local charlist = data[3].horiz_variants
434        if charlist then
435            local right = charlist[#charlist]
436            roffset = abs((right["start"] or 0) - (right["end"] or 0))
437        end
438     elseif kind == e_horizontal then
439        local charlist = data[3].horiz_variants
440        if charlist then
441            local left  = charlist[1]
442            local right = charlist[#charlist]
443            loffset = abs((left ["start"] or 0) - (left ["end"] or 0))
444            roffset = abs((right["start"] or 0) - (right["end"] or 0))
445        end
446    end
447    return kind, loffset, roffset
448end
449
450mathematics.extensiblecode = extensiblecode
451mathematics.horizontalcode = horizontalcode
452
453interfaces.implement {
454    name      = "extensiblecode",
455    arguments = { "integer", "integer" },
456    actions   = { extensiblecode, context }
457}
458
459interfaces.implement {
460    name      = "horizontalcode",
461    arguments = { "integer", "integer" },
462    actions   = function(family,unicode)
463        local kind, loffset, roffset = horizontalcode(family,unicode)
464        texsetdimen("scratchleftoffset", loffset)
465        texsetdimen("scratchrightoffset",roffset)
466        context(kind)
467    end
468}
469
470-- experiment
471
472-- check: when true, only set when present in font
473-- force: when false, then not set when already set
474
475-- todo: tounicode
476
477-- function mathematics.injectfallbacks(target,original)
478--     local properties = original.properties
479--     if properties and properties.hasmath then
480--         local specification = target.specification
481--         if specification then
482--             local fallbacks = specification.fallbacks
483--             if fallbacks then
484--                 local definitions = fonts.collections.definitions[fallbacks]
485--                 if definitions then
486--                     if trace_collecting then
487--                         report_math("adding fallback characters to font %a",specification.hash)
488--                     end
489--                     local definedfont = fonts.definers.internal
490--                     local copiedglyph = fonts.handlers.vf.math.copy_glyph
491--                     local fonts       = target.fonts
492--                     local size        = specification.size -- target.size
493--                     local characters  = target.characters
494--                     if not fonts then
495--                         fonts = { }
496--                         target.fonts = fonts
497-- if not CONTEXTLMTXMODE or CONTEXTLMTXMODE == 0 then
498--                         target.type = "virtual"
499--                         target.properties.virtualized = true
500-- end
501--                     end
502--                     if #fonts == 0 then
503--                         fonts[1] = { id = 0, size = size } -- sel, will be resolved later
504--                     end
505--                     local done = { }
506--                     for i=1,#definitions do
507--                         local definition = definitions[i]
508--                         local name   = definition.font
509--                         local start  = definition.start
510--                         local stop   = definition.stop
511--                         local gaps   = definition.gaps
512--                         local check  = definition.check
513--                         local force  = definition.force
514--                         local rscale = definition.rscale or 1
515--                         local offset = definition.offset or start
516--                         local id     = definedfont { name = name, size = size * rscale }
517--                         local index  = #fonts + 1
518--                         fonts[index] = { id = id, size = size }
519--                         local chars  = fontchars[id]
520--                         local function remap(unic,unicode,gap)
521--                          -- local unic = unicode + offset - start
522--                             if check and not chars[unicode] then
523--                                 -- not in font
524--                             elseif force or (not done[unic] and not characters[unic]) then
525--                                 if trace_collecting then
526--                                     report_math("remapping math character, vector %a, font %a, character %C%s%s",
527--                                         fallbacks,name,unic,check and ", checked",gap and ", gap plugged")
528--                                 end
529--                                 characters[unic] = copiedglyph(target,characters,chars,unicode,index)
530--                                 done[unic] = true
531--                             end
532--                         end
533--                         for unicode = start, stop do
534--                             local unic = unicode + offset - start
535--                             remap(unic,unicode,false)
536--                         end
537--                         if gaps then
538--                             for unic, unicode in next, gaps do
539--                                 remap(unic,unicode,true)
540--                             end
541--                         end
542--                     end
543--                 end
544--             end
545--         end
546--     end
547-- end
548--
549-- sequencers.appendaction("aftercopyingcharacters", "system","mathematics.finishfallbacks")
550
551local stack = { }
552
553function mathematics.registerfallbackid(n,id,name)
554    if trace_collecting then
555        report_math("resolved fallback font %i, name %a, id %a, used %a",
556            n,name,id,fontproperties[id].fontname)
557    end
558    stack[#stack][n] = id
559end
560
561interfaces.implement { -- will be shared with text
562    name      = "registerfontfallbackid",
563    arguments = { "integer", "integer", "string" },
564    actions   = mathematics.registerfallbackid,
565}
566
567function mathematics.resolvefallbacks(target,specification,fallbacks)
568    local definitions = fonts.collections.definitions[fallbacks]
569    if definitions then
570        local size = specification.size -- target.size
571        local list = { }
572        insert(stack,list)
573        context.pushcatcodes("prt") -- context.unprotect()
574        for i=1,#definitions do
575            local definition = definitions[i]
576            local name       = definition.font
577            local features   = definition.features or ""
578            local size       = size * (definition.rscale or 1)
579            context.font_fallbacks_register_math(i,name,features,size)
580            if trace_collecting then
581                report_math("registering fallback font %i, name %a, size %a, features %a",i,name,size,features)
582            end
583        end
584        context.popcatcodes()
585    end
586end
587
588function mathematics.finishfallbacks(target,specification,fallbacks)
589    local list = remove(stack)
590    if list and #list > 0 then
591        local definitions = fonts.collections.definitions[fallbacks]
592        if definitions and #definitions > 0 then
593            if trace_collecting then
594                report_math("adding fallback characters to font %a",specification.hash)
595            end
596            local definedfont = fonts.definers.internal
597            local copiedglyph = fonts.handlers.vf.math.copy_glyph
598            local fonts       = target.fonts
599            local size        = specification.size -- target.size
600            local characters  = target.characters
601            if not fonts then
602                fonts = { }
603                target.fonts = fonts
604            end
605            --
606            target.type = "virtual"
607            target.properties.virtualized = true
608            --
609            if #fonts == 0 then
610                fonts[1] = { id = 0, size = size } -- self, will be resolved later
611            end
612            local done = { }
613            for i=1,#definitions do
614                local definition = definitions[i]
615                local name   = definition.font
616                local start  = definition.start
617                local stop   = definition.stop
618                local gaps   = definition.gaps
619                local check  = definition.check
620                local force  = definition.force
621                local rscale = definition.rscale or 1
622                local offset = definition.offset or start
623                local id     = list[i]
624                if id then
625                    local index  = #fonts + 1
626                    fonts[index] = { id = id, size = size }
627                    local chars  = fontchars[id]
628                    local function remap(unic,unicode,gap)
629                        if check and not chars[unicode] then
630                            return
631                        end
632                        if force or (not done[unic] and not characters[unic]) then
633                            if trace_collecting then
634                                report_math("replacing math character %C by %C using vector %a and font id %a for %a%s%s",
635                                    unic,unicode,fallbacks,id,fontproperties[id].fontname,check and ", checked",gap and ", gap plugged")
636                            end
637                            characters[unic] = copiedglyph(target,characters,chars,unicode,index)
638                            done[unic] = true
639                        end
640                    end
641                    local step = offset - start
642                    for unicode = start, stop do
643                        remap(unicode + step,unicode,false)
644                    end
645                    if gaps then
646                        for unic, unicode in next, gaps do
647                            remap(unic,unicode,true)
648                            remap(unicode,unicode,true)
649                        end
650                    end
651                end
652            end
653        elseif trace_collecting then
654            report_math("no fallback characters added to font %a",specification.hash)
655        end
656    end
657end
658