font-ctx.lmt /size: 109 Kb    last modification: 2021-10-28 13:51
1if not modules then modules = { } end modules ['font-ctx'] = {
2    version   = 1.001,
3    comment   = "companion to font-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-- At some point I will clean up the code here so that at the tex end
10-- the table interface is used.
11--
12-- Todo: make a proper 'next id' mechanism (register etc) or wait till 'true'
13-- in virtual fonts indices is implemented.
14--
15-- Olders code can be found in the mkiv lua variant.
16
17local tostring, next, type, rawget, tonumber = tostring, next, type, rawget, tonumber
18
19local format, gmatch, match, find, lower, upper, gsub, byte, topattern = string.format, string.gmatch, string.match, string.find, string.lower, string.upper, string.gsub, string.byte, string.topattern
20local concat, serialize, sort, fastcopy, mergedtable = table.concat, table.serialize, table.sort, table.fastcopy, table.merged
21local sortedhash, sortedkeys, sequenced = table.sortedhash, table.sortedkeys, table.sequenced
22local parsers = utilities.parsers
23local settings_to_hash, hash_to_string, settings_to_array = parsers.settings_to_hash, parsers.hash_to_string, parsers.settings_to_array
24local formatcolumns = utilities.formatters.formatcolumns
25local mergehashes = utilities.parsers.mergehashes
26local formatters = string.formatters
27local basename = file.basename
28
29local utfchar, utfbyte = utf.char, utf.byte
30local round = math.round
31
32local context, commands = context, commands
33
34local P, R, S, C, Cc, Cf, Cg, Ct, Cs, lpegmatch = lpeg.P, lpeg.R, lpeg.S, lpeg.C, lpeg.Cc, lpeg.Cf, lpeg.Cg, lpeg.Ct, lpeg.Cs, lpeg.match
35
36local trace_features      = false  trackers.register("fonts.features",   function(v) trace_features   = v end)
37local trace_defining      = false  trackers.register("fonts.defining",   function(v) trace_defining   = v end)
38local trace_designsize    = false  trackers.register("fonts.designsize", function(v) trace_designsize = v end)
39local trace_usage         = false  trackers.register("fonts.usage",      function(v) trace_usage      = v end)
40local trace_mapfiles      = false  trackers.register("fonts.mapfiles",   function(v) trace_mapfiles   = v end)
41local trace_automode      = false  trackers.register("fonts.automode",   function(v) trace_automode   = v end)
42local trace_merge         = false  trackers.register("fonts.merge",      function(v) trace_merge      = v end)
43
44local report              = logs.reporter("fonts")
45local report_features     = logs.reporter("fonts","features")
46local report_cummulative  = logs.reporter("fonts","cummulative")
47local report_defining     = logs.reporter("fonts","defining")
48local report_status       = logs.reporter("fonts","status")
49local report_mapfiles     = logs.reporter("fonts","mapfiles")
50
51local setmetatableindex   = table.setmetatableindex
52
53local implement           = interfaces.implement
54
55local chardata            = characters.data
56
57local fonts               = fonts
58local handlers            = fonts.handlers
59local otf                 = handlers.otf -- brrr
60local afm                 = handlers.afm -- brrr
61local tfm                 = handlers.tfm -- brrr
62local names               = fonts.names
63local definers            = fonts.definers
64local specifiers          = fonts.specifiers
65local constructors        = fonts.constructors
66local loggers             = fonts.loggers
67local fontgoodies         = fonts.goodies
68local helpers             = fonts.helpers
69local hashes              = fonts.hashes
70local currentfont         = font.current
71local definefont          = font.define
72
73local getprivateslot      = helpers.getprivateslot
74
75local cleanname           = names.cleanname
76
77local encodings           = fonts.encodings
78----- aglunicodes         = encodings.agl.unicodes
79local aglunicodes         = nil -- delayed loading
80
81local nuts                = nodes.nuts
82local tonut               = nuts.tonut
83
84local nextchar            = nuts.traversers.char
85
86local getattr             = nuts.getattr
87local setattr             = nuts.setattr
88local getstate            = nuts.getstate
89local setsubtype          = nuts.setsubtype
90
91local texgetdimen         = tex.getdimen
92local texsetcount         = tex.setcount
93local texget              = tex.get
94
95local texdefinefont       = tex.definefont
96local texsp               = tex.sp
97
98local fontdata            = hashes.identifiers
99local characters          = hashes.characters
100local descriptions        = hashes.descriptions
101local properties          = hashes.properties
102local resources           = hashes.resources
103local unicodes            = hashes.unicodes
104local csnames             = hashes.csnames
105local lastmathids         = hashes.lastmathids
106local exheights           = hashes.exheights
107local emwidths            = hashes.emwidths
108local parameters          = hashes.parameters
109
110local designsizefilename  = fontgoodies.designsizes.filename
111
112local ctx_char            = context.char
113local ctx_safechar        = context.safechar
114local ctx_getvalue        = context.getvalue
115
116local otffeatures         = otf.features
117local otftables           = otf.tables
118
119local registerotffeature  = otffeatures.register
120
121local sequencers          = utilities.sequencers
122local appendgroup         = sequencers.appendgroup
123local appendaction        = sequencers.appendaction
124
125specifiers.contextsetups  = specifiers.contextsetups  or { }
126specifiers.contextnumbers = specifiers.contextnumbers or { }
127specifiers.contextmerged  = specifiers.contextmerged  or { }
128specifiers.synonyms       = specifiers.synonyms       or { }
129
130local setups   = specifiers.contextsetups
131local numbers  = specifiers.contextnumbers
132local merged   = specifiers.contextmerged
133local synonyms = specifiers.synonyms
134
135storage.register("fonts/setups" ,  setups ,  "fonts.specifiers.contextsetups" )
136storage.register("fonts/numbers",  numbers,  "fonts.specifiers.contextnumbers")
137storage.register("fonts/merged",   merged,   "fonts.specifiers.contextmerged")
138storage.register("fonts/synonyms", synonyms, "fonts.specifiers.synonyms")
139
140-- inspect(setups)
141
142setmetatableindex(setups, environment.initex and
143    function(t,k)
144        return type(k) == "number" and rawget(t,numbers[k]) or nil
145    end
146or
147    function(t,k)
148        local v = type(k) == "number" and rawget(t,numbers[k])
149        if v then
150            t[k] = v
151            return v
152        end
153    end
154)
155
156-- this will move elsewhere ...
157
158local function getfontname(tfmdata)
159    local p = type(tfmdata) == "number" and properties[tfmdata] or tfmdata.properties
160    return basename((p and (p.name or p.fullname or p.fontname)) or "unknown")
161end
162
163helpers.name = getfontname
164
165local addformatter = utilities.strings.formatters.add
166
167addformatter(formatters,"font:name",    [["'"..fontname(%s).."'"]],          { fontname  = helpers.name })
168addformatter(formatters,"font:features",[["'"..sequenced(%s," ",true).."'"]],{ sequenced = table.sequenced })
169
170-- ... like font-sfm or so
171
172constructors.resolvevirtualtoo = true -- context specific (due to resolver)
173constructors.sharefonts        = true -- experimental
174constructors.nofsharedfonts    = 0
175constructors.nofsharedhashes   = 0
176constructors.nofsharedvectors  = 0
177constructors.noffontsloaded    = 0
178
179-- we can get rid of the tfm instance when we have fast access to the
180-- scaled character dimensions at the tex end, e.g. a fontobject.width
181-- actually we already have some of that now as virtual keys in glyphs
182--
183-- flushing the kern and ligature tables from memory saves a lot (only
184-- base mode) but it complicates vf building where the new characters
185-- demand this data .. solution: functions that access them
186
187-- font.getcopy = font.getfont -- we always want the table that context uses
188
189do
190
191    -- Does this still make sense?
192
193    local shares       = { }
194    local hashes       = { }
195    local nofinstances = 0
196    local instances    = setmetatableindex(function(t,k)
197        nofinstances = nofinstances + 1
198        t[k] = nofinstances
199        return nofinstances
200    end)
201
202    function constructors.trytosharefont(target,tfmdata)
203        constructors.noffontsloaded = constructors.noffontsloaded + 1
204        if constructors.sharefonts then
205            local fonthash = target.specification.hash
206            if fonthash then
207                local properties = target.properties
208                local fullname   = target.fullname
209                local fontname   = target.fontname
210                local psname     = target.psname
211                -- for the moment here:
212                local instance = properties.instance
213                if instance then
214                    local format = tfmdata.properties.format
215                    if format == "opentype" then
216                        target.streamprovider = 1
217                    elseif format == "truetype" then
218                        target.streamprovider = 2
219                    else
220                        target.streamprovider = 0
221                    end
222                    if target.streamprovider > 0 then
223                        if fullname then
224                            fullname = fullname .. ":" .. instances[instance]
225                            target.fullname = fullname
226                        end
227                        if fontname then
228                            fontname = fontname .. ":" .. instances[instance]
229                            target.fontname = fontname
230                        end
231                        if psname then
232                            -- this one is used for the funny prefix in font names in pdf
233                            -- so it has to be kind of unique in order to avoid subset prefix
234                            -- clashes being reported
235                            psname = psname   .. ":" .. instances[instance]
236                            target.psname = psname
237                        end
238                    end
239                end
240                --
241                local sharedname = hashes[fonthash]
242                if sharedname then
243                    -- this is ok for context as we know that only features can mess with font definitions
244                    -- so a similar hash means that the fonts are similar too
245                    if trace_defining then
246                        report_defining("font %a uses backend resources of font %a (%s)",target.fullname,sharedname,"common hash")
247                    end
248                    target.fullname = sharedname
249                    properties.sharedwith = sharedname
250                    constructors.nofsharedfonts = constructors.nofsharedfonts + 1
251                    constructors.nofsharedhashes = constructors.nofsharedhashes + 1
252                else
253                    -- the one takes more time (in the worst case of many cjk fonts) but it also saves
254                    -- embedding time .. haha, this is interesting: when i got a clash on subset tag
255                    -- collision i saw in the source that these tags are also using a hash like below
256                    -- so maybe we should have an option to pass it from lua
257                    local characters = target.characters
258                    local n = 1
259                    local t = { target.psname }
260                    -- for the moment here:
261                    if instance then
262                        n = n + 1
263                        t[n] = instance
264                    end
265                    --
266                    local u = sortedkeys(characters)
267                    for i=1,#u do
268                        local k = u[i]
269                        n = n + 1 ; t[n] = k
270                        n = n + 1 ; t[n] = characters[k].index or k
271                    end
272                    local checksum   = md5.HEX(concat(t," "))
273                    local sharedname = shares[checksum]
274                    local fullname   = target.fullname
275                    if sharedname then
276                        if trace_defining then
277                            report_defining("font %a uses backend resources of font %a (%s)",fullname,sharedname,"common vector")
278                        end
279                        fullname = sharedname
280                        properties.sharedwith = sharedname
281                        constructors.nofsharedfonts = constructors.nofsharedfonts + 1
282                        constructors.nofsharedvectors = constructors.nofsharedvectors + 1
283                    else
284                        shares[checksum] = fullname
285                    end
286                    target.fullname  = fullname
287                    hashes[fonthash] = fullname
288                end
289            end
290        end
291    end
292
293end
294
295directives.register("fonts.checksharing",function(v)
296    if not v then
297        report_defining("font sharing in backend is disabled")
298    end
299    constructors.sharefonts = v
300end)
301
302function definers.resetnullfont()
303    -- resetting is needed because tikz misuses nullfont
304    local parameters = fonts.nulldata.parameters
305    --
306    parameters.slant         = 0 -- 1
307    parameters.space         = 0 -- 2
308    parameters.space_stretch = 0 -- 3
309    parameters.space_shrink  = 0 -- 4
310    parameters.x_height      = 0 -- 5
311    parameters.quad          = 0 -- 6
312    parameters.extra_space   = 0 -- 7
313    parameters.designsize    = 655360
314    --
315    constructors.enhanceparameters(parameters) -- official copies for us
316    --
317    definers.resetnullfont = function() end
318end
319
320-- This is some initialization code and we don't want (tracing) clutter in lmtx
321-- which is why we follow a different route there. I admit that is sounds freaky.
322-- Watch out: in lmtx the font dimen array is no longer resized automatically.
323
324implement {
325    name      = "resetnullfont",
326    onlyonce  = true,
327    permanent = false,
328    actions   = function()
329        for i=1,7 do
330            font.setfontdimen(0,i,0)
331        end
332        definers.resetnullfont()
333    end
334}
335
336-- this cannot be a feature initializer as there is no auto namespace
337-- so we never enter the loop then; we can store the defaults in the tma
338-- file (features.gpos.mkmk = 1 etc)
339
340local needsnodemode = { -- we will have node mode by default anyway
341 -- gsub_single              = true,
342    gsub_multiple            = true,
343 -- gsub_alternate           = true,
344 -- gsub_ligature            = true,
345    gsub_context             = true,
346    gsub_contextchain        = true,
347    gsub_reversecontextchain = true,
348 -- chainsub                 = true,
349 -- reversesub               = true,
350    gpos_mark2base           = true,
351    gpos_mark2ligature       = true,
352    gpos_mark2mark           = true,
353    gpos_cursive             = true,
354 -- gpos_single              = true,
355 -- gpos_pair                = true,
356    gpos_context             = true,
357    gpos_contextchain        = true,
358}
359
360otftables.scripts.auto = "automatic fallback to latn when no dflt present"
361
362-- setmetatableindex(otffeatures.descriptions,otftables.features)
363
364local function checkedscript(tfmdata,resources,features)
365    local latn   = false
366    local script = false
367    if resources.features then
368        for g, list in next, resources.features do
369            for f, scripts in next, list do
370                if scripts.dflt then
371                    script = "dflt"
372                    break
373                elseif scripts.latn then
374                    latn = true
375                end
376            end
377        end
378    end
379    if not script then
380        script = latn and "latn" or "dflt"
381    end
382    if trace_automode then
383        report_defining("auto script mode, using script %a in font %!font:name!",script,tfmdata)
384    end
385    features.script = script
386    return script
387end
388
389-- basemode combined with dynamics is somewhat tricky
390
391local function checkedmode(tfmdata,resources,features)
392    local sequences = resources.sequences
393    if sequences and #sequences > 0 then
394        local script    = features.script   or "dflt"
395        local language  = features.language or "dflt"
396        for feature, value in next, features do
397            if value then
398                local found = false
399                for i=1,#sequences do
400                    local sequence = sequences[i]
401                    local features = sequence.features
402                    if features then
403                        local scripts = features[feature]
404                        if scripts then
405                            local languages = scripts[script]
406                            if languages and languages[language] then
407                                if found then
408                                    -- more than one lookup
409                                    if trace_automode then
410                                        report_defining("forcing mode %a, font %!font:name!, feature %a, script %a, language %a, %s",
411                                            "node",tfmdata,feature,script,language,"multiple lookups")
412                                    end
413                                    features.mode = "node"
414                                    return "node"
415                                elseif needsnodemode[sequence.type] then
416                                    if trace_automode then
417                                        report_defining("forcing mode %a, font %!font:name!, feature %a, script %a, language %a, %s",
418                                            "node",tfmdata,feature,script,language,"no base support")
419                                    end
420                                    features.mode = "node"
421                                    return "node"
422                                else
423                                    -- at least one lookup
424                                    found = true
425                                end
426                            end
427                        end
428                    end
429                end
430            end
431        end
432    end
433    if trace_automode then
434        report_defining("forcing mode base, font %!font:name!",tfmdata)
435    end
436    features.mode = "base" -- new, or is this wrong?
437    return "base"
438end
439
440definers.checkedscript = checkedscript
441definers.checkedmode   = checkedmode
442
443-- We only set the checker and leave other settings of the mode feature as
444-- they are.
445
446local function modechecker(tfmdata,features,mode) -- we cannot adapt features as they are shared!
447    if trace_features then
448        report_features("fontname %!font:name!, features %!font:features!",tfmdata,features)
449    end
450    local rawdata   = tfmdata.shared.rawdata
451    local resources = rawdata and rawdata.resources
452    local script    = features.script
453    if resources then
454        if script == "auto" then
455            script = checkedscript(tfmdata,resources,features)
456        end
457        if mode == "auto" then
458            mode = checkedmode(tfmdata,resources,features)
459        end
460    else
461        report_features("missing resources for font %!font:name!",tfmdata)
462    end
463    return mode
464end
465
466registerotffeature {
467    name        = "mode",
468    modechecker = modechecker,
469}
470
471do
472
473    -- copying will end up here too
474
475    local beforecopyingcharacters = sequencers.new {
476        name      = "beforecopyingcharacters",
477        arguments = "target,original",
478    }
479
480    appendgroup(beforecopyingcharacters,"before") -- user
481    appendgroup(beforecopyingcharacters,"system") -- private
482    appendgroup(beforecopyingcharacters,"after" ) -- user
483
484    function constructors.beforecopyingcharacters(original,target)
485        local runner = beforecopyingcharacters.runner
486        if runner then
487            runner(original,target)
488        end
489    end
490
491    local aftercopyingcharacters = sequencers.new {
492        name      = "aftercopyingcharacters",
493        arguments = "target,original",
494    }
495
496    appendgroup(aftercopyingcharacters,"before") -- user
497    appendgroup(aftercopyingcharacters,"system") -- private
498    appendgroup(aftercopyingcharacters,"after" ) -- user
499
500    function constructors.aftercopyingcharacters(original,target)
501        local runner = aftercopyingcharacters.runner
502        if runner then
503            runner(original,target)
504        end
505    end
506
507end
508
509--[[ldx--
510<p>So far we haven't really dealt with features (or whatever we want
511to pass along with the font definition. We distinguish the following
512situations:</p>
513situations:</p>
514
515<code>
516name:xetex like specs
517name@virtual font spec
518name*context specification
519</code>
520--ldx]]--
521
522-- Currently fonts are scaled while constructing the font, so we have to do scaling
523-- of commands in the vf at that point using e.g. "local scale = g.parameters.factor
524-- or 1" after all, we need to work with copies anyway and scaling needs to be done
525-- at some point; however, when virtual tricks are used as feature (makes more
526-- sense) we scale the commands in fonts.constructors.scale (and set the factor
527-- there).
528
529local loadfont = definers.loadfont
530
531function definers.loadfont(specification,size,id) -- overloads the one in font-def
532    local variants = definers.methods.variants
533    local virtualfeatures = specification.features.virtual
534    if virtualfeatures and virtualfeatures.preset then
535        local variant = variants[virtualfeatures.preset]
536        if variant then
537            return variant(specification,size,id)
538        end
539    else
540        return loadfont(specification,size,id)
541    end
542end
543
544local function predefined(specification)
545    local variants = definers.methods.variants
546    local detail = specification.detail
547    if detail ~= "" and variants[detail] then
548        specification.features.virtual = { preset = detail }
549    end
550    return specification
551end
552
553definers.registersplit("@", predefined,"virtual")
554
555local normalize_features = otffeatures.normalize     -- should be general
556
557local function definecontext(name,t) -- can be shared
558    local number = setups[name] and setups[name].number or 0 -- hm, numbers[name]
559    if number == 0 then
560        number = #numbers + 1
561        numbers[number] = name
562    end
563    t.number = number
564    setups[name] = t
565    return number, t
566end
567
568-- {a,b,c} as table (so we don' need to parse again when it gets applied)
569-- we will update this ... when we have foo={a,b,c} then we can keep the table
570
571-- \definefontfeature[demo][a={b,c}]
572-- \definefontfeature[demo][a={b=12,c={34,35}}]
573
574local h = setmetatableindex(function(t,k)
575    local v = "," .. k .. ","
576    t[k] = v
577    return v
578end)
579
580-- local function removefromhash(hash,key)
581--     local pattern = h[key]
582--     for k in next, hash do
583--         if k ~= key and find(h[k],pattern) then -- if find(k,",") and ...
584--             hash[k] = nil
585--         end
586--     end
587-- end
588
589local function presetcontext(name,parent,features) -- will go to con and shared
590    if features == "" and find(parent,"=",1,true) then
591        features = parent
592        parent = ""
593    end
594    if not features or features == "" then
595        features = { }
596    elseif type(features) == "string" then
597        features = normalize_features(settings_to_hash(features))
598     -- if type(value) == "string" and find(value,"[=:]") then
599     --     local t = settings_to_hash_colon_too(value) -- clashes with foo=file:bar
600        for key, value in next, features do
601            if type(value) == "string" and find(value,"[=]") then
602                local t = settings_to_hash(value)
603                if next(t) then
604                    features[key] = sequenced(normalize_features(t,true),",")
605                end
606            end
607        end
608    else
609        features = normalize_features(features)
610    end
611    -- todo: synonyms, and not otf bound
612    if parent ~= "" then
613        for p in gmatch(parent,"[^, ]+") do
614            local s = setups[p]
615            if s then
616                for k, v in next, s do
617                    -- no, as then we cannot overload: e.g. math,mathextra
618                    -- reverted, so we only take from parent when not set
619                    if features[k] == nil then
620                        features[k] = v
621                    end
622                end
623            else
624                -- just ignore an undefined one .. i.e. we can refer to not yet defined
625            end
626        end
627    end
628    -- these are auto set so in order to prevent redundant definitions
629    -- we need to preset them (we hash the features and adding a default
630    -- setting during initialization may result in a different hash)
631    --
632    -- for k,v in next, triggers do
633    --     if features[v] == nil then -- not false !
634    --         local vv = default_features[v]
635    --         if vv then features[v] = vv end
636    --     end
637    -- end
638    --
639    for feature,value in next, features do
640        if value == nil then -- not false !
641            local default = default_features[feature]
642            if default ~= nil then
643                features[feature] = default
644            end
645        end
646    end
647    -- sparse 'm so that we get a better hash and less test (experimental
648    -- optimization)
649    local t = { } -- can we avoid t ?
650    for k,v in next, features do
651     -- if v then t[k] = v end
652        t[k] = v
653    end
654    -- the number is needed for dynamic features; maybe number should always be
655    -- renewed as we can redefine features ... i need a test
656    local number = setups[name] and setups[name].number or 0
657    if number == 0 then
658        number = #numbers + 1
659        numbers[number] = name
660    end
661    --
662    t.number = number
663    -- there is the special case of combined features as we have in math but maybe
664    -- this has to change some day ... otherwise we mess up dynamics (ok, we could
665    -- impose a limit there: no combined features)
666    --
667    -- done elsewhere (!)
668    --
669 -- removefromhash(setups,name) -- can have changed (like foo,extramath)
670    --
671    setups[name] = t
672    return number, t
673end
674
675local function adaptcontext(pattern,features)
676    local pattern = topattern(pattern,false,true)
677    for name in next, setups do
678        if find(name,pattern) then
679            presetcontext(name,name,features)
680        end
681    end
682end
683
684-- See for old code in the lua file. We now share code.
685
686local function contextnumber(name)
687    local t = setups[name]
688    return t and t.number or 0
689end
690
691local function mergecontext(currentnumber,extraname,option) -- number string number (used in scrp-ini
692    local extra = setups[extraname]
693    if extra then
694        local current = setups[numbers[currentnumber]]
695        local mergedfeatures = { }
696        local mergedname = nil
697        if option < 0 then
698            if current then
699                for k, v in next, current do
700                    if not extra[k] then
701                        mergedfeatures[k] = v
702                    end
703                end
704            end
705            mergedname = currentnumber .. "-" .. extraname
706        else
707            if current then
708                for k, v in next, current do
709                    mergedfeatures[k] = v
710                end
711            end
712            for k, v in next, extra do
713                mergedfeatures[k] = v
714            end
715            mergedname = currentnumber .. "+" .. extraname
716        end
717        local number = #numbers + 1
718        mergedfeatures.number = number
719        numbers[number] = mergedname
720        merged[number] = option
721        setups[mergedname] = mergedfeatures
722        return number
723    else
724        return currentnumber
725    end
726end
727
728local extrasets = { }
729
730setmetatableindex(extrasets,function(t,k)
731    local v = mergehashes(setups,k)
732    t[k] = v
733    return v
734end)
735
736local function mergecontextfeatures(currentname,extraname,how,mergedname) -- string string
737    local extra = setups[extraname] or extrasets[extraname]
738    if extra then
739        local current = setups[currentname]
740        local mergedfeatures = { }
741        if how == "+" then
742            if current then
743                for k, v in next, current do
744                    mergedfeatures[k] = v
745                end
746            end
747            for k, v in next, extra do
748                mergedfeatures[k] = v
749            end
750            if trace_merge then
751                report_features("merge %a, method %a, current %|T, extra %|T, result %|T",mergedname,"add",current or { },extra,mergedfeatures)
752            end
753        elseif how == "-" then
754            if current then
755                for k, v in next, current do
756                    mergedfeatures[k] = v
757                end
758            end
759            for k, v in next, extra do
760                -- only boolean features
761                if v == true then
762                    mergedfeatures[k] = false
763                end
764            end
765            if trace_merge then
766                report_features("merge %a, method %a, current %|T, extra %|T, result %|T",mergedname,"subtract",current or { },extra,mergedfeatures)
767            end
768        else -- =
769            for k, v in next, extra do
770                mergedfeatures[k] = v
771            end
772            if trace_merge then
773                report_features("merge %a, method %a, result %|T",mergedname,"replace",mergedfeatures)
774            end
775        end
776        local number = #numbers + 1
777        mergedfeatures.number = number
778        numbers[number] = mergedname
779        merged[number] = option
780        setups[mergedname] = mergedfeatures
781        return number
782    else
783        return numbers[currentname] or 0
784    end
785end
786
787local function registercontext(fontnumber,extraname,option)
788    local extra = setups[extraname]
789    if extra then
790        local mergedfeatures = { }
791        local mergedname     = nil
792        if option < 0 then
793            mergedname = fontnumber .. "-" .. extraname
794        else
795            mergedname = fontnumber .. "+" .. extraname
796        end
797        for k, v in next, extra do
798            mergedfeatures[k] = v
799        end
800        local number = #numbers + 1
801        mergedfeatures.number = number
802        numbers[number] = mergedname
803        merged[number] = option
804        setups[mergedname] = mergedfeatures
805        return number
806    else
807        return 0
808    end
809end
810
811local function registercontextfeature(mergedname,extraname,how)
812    local extra = setups[extraname]
813    if extra then
814        local mergedfeatures = { }
815        for k, v in next, extra do
816            mergedfeatures[k] = v
817        end
818        local number = #numbers + 1 -- we somehow end up with steps of 2
819        mergedfeatures.number = number
820        numbers[number] = mergedname
821        merged[number] = how == "=" and 1 or 2 -- 1=replace, 2=combine
822        setups[mergedname] = mergedfeatures
823        return number
824    else
825        report_features("unknown feature %a cannot be merged into %a using method %a",extraname,mergedname,how)
826        return 0
827    end
828end
829
830specifiers.presetcontext   = presetcontext
831specifiers.contextnumber   = contextnumber
832specifiers.mergecontext    = mergecontext
833specifiers.registercontext = registercontext
834specifiers.definecontext   = definecontext
835
836constructors.hashmethods.normal = function(list)
837    local s = { }
838    local n = 0
839    for k, v in next, list do
840        if not k then
841            -- no need to add to hash
842        elseif k == "number" or k == "features" then
843            -- no need to add to hash (maybe we need a skip list)
844        else
845            n = n + 1
846            if type(v) == "table" then
847                -- table.sequenced
848                local t = { }
849                local m = 0
850                for k, v in next, v do
851                    m = m + 1
852                    t[m] = format("%q=%q",k,v)
853                end
854                sort(t)
855                s[n] = format("%q={%s}",k,concat(t,","))
856            else
857                s[n] = format("%q=%q",k,v)
858            end
859        end
860    end
861    if n > 0 then
862        sort(s)
863        return concat(s,"+")
864    end
865end
866
867constructors.hashmethods.virtual = function(list)
868    local s = { }
869    local n = 0
870    for k, v in next, list do
871        n = n + 1
872        s[n] = format("%q=%q",k,v)
873    end
874    if n > 0 then
875        sort(s)
876        return concat(s,"+")
877    end
878end
879
880-- end of redefine
881
882-- local withcache = { } -- concat might be less efficient than nested tables
883--
884-- local function withset(name,what)
885--     local zero = texgetattribute(0)
886--     local hash = zero .. "+" .. name .. "*" .. what
887--     local done = withcache[hash]
888--     if not done then
889--         done = mergecontext(zero,name,what)
890--         withcache[hash] = done
891--     end
892--     texsetattribute(0,done)
893-- end
894--
895-- local function withfnt(name,what,font)
896--     local font = font or currentfont()
897--     local hash = font .. "*" .. name .. "*" .. what
898--     local done = withcache[hash]
899--     if not done then
900--         done = registercontext(font,name,what)
901--         withcache[hash] = done
902--     end
903--     texsetattribute(0,done)
904-- end
905
906function specifiers.showcontext(name)
907    return setups[name] or setups[numbers[name]] or setups[numbers[tonumber(name)]] or { }
908end
909
910-- we need a copy as we will add (fontclass) goodies to the features and
911-- that is bad for a shared table
912
913-- local function splitcontext(features) -- presetcontext creates dummy here
914--     return fastcopy(setups[features] or (presetcontext(features,"","") and setups[features]))
915-- end
916
917-- local function splitcontext(features) -- presetcontext creates dummy here
918--     local sf = setups[features]
919--     if not sf then
920--         local n -- number
921--         if find(features,",") then
922--             -- let's assume a combination which is not yet defined but just specified (as in math)
923--             n, sf = presetcontext(features,features,"")
924--         else
925--             -- we've run into an unknown feature and or a direct spec so we create a dummy
926--             n, sf = presetcontext(features,"","")
927--         end
928--     end
929--     return fastcopy(sf)
930-- end
931
932local function splitcontext(features) -- presetcontext creates dummy here
933    local n, sf
934    -- We can have: "a=yes,b=yes" "a,b" "a" "a=yes" etc.
935    if find(features,"[,=]") then
936        --
937        -- from elsewhere (!)
938        --
939        -- this will become:
940        --
941     -- if find(features,"^reset," then
942            setups[features] = nil
943     -- end
944        -- let's assume a combination which is not yet defined but just specified (as in math)
945        n, sf = presetcontext(features,features,"")
946    else
947        sf = setups[features]
948        if not sf then
949            -- we've run into an unknown feature and or a direct spec so we create a dummy
950            n, sf = presetcontext(features,"","")
951        end
952    end
953    return fastcopy(sf)
954end
955
956-- local splitter = lpeg.splitat("=")
957--
958-- local function splitcontext(features)
959--     local setup = setups[features]
960--     if setup then
961--         return setup
962--     elseif find(features,",",1,true) then
963--         -- This is not that efficient but handy anyway for quick and dirty tests
964--         -- beware, due to the way of caching setups you can get the wrong results
965--         -- when components change. A safeguard is to nil the cache.
966--         local merge = nil
967--         for feature in gmatch(features,"[^, ]+") do
968--             if find(feature,"=",1,true) then
969--                 local k, v = lpegmatch(splitter,feature)
970--                 if k and v then
971--                     if not merge then
972--                         merge = { k = v }
973--                     else
974--                         merge[k] = v
975--                     end
976--                 end
977--             else
978--                 local s = setups[feature]
979--                 if not s then
980--                     -- skip
981--                 elseif not merge then
982--                     merge = s
983--                 else
984--                     for k, v in next, s do
985--                         merge[k] = v
986--                     end
987--                 end
988--             end
989--         end
990--         setup = merge and presetcontext(features,"",merge) and setups[features]
991--         -- actually we have to nil setups[features] in order to permit redefinitions
992--         setups[features] = nil
993--     end
994--     return setup or (presetcontext(features,"","") and setups[features]) -- creates dummy
995-- end
996
997specifiers.splitcontext = splitcontext
998
999function specifiers.contexttostring(name,kind,separator,yes,no,strict,omit) -- not used
1000    return hash_to_string(
1001        mergedtable(handlers[kind].features.defaults or {},setups[name] or {}),
1002        separator, yes, no, strict, omit or { "number" }
1003    )
1004end
1005
1006local function starred(features) -- no longer fallbacks here
1007    local detail = features.detail
1008    if detail and detail ~= "" then
1009        features.features.normal = splitcontext(detail)
1010    else
1011        features.features.normal = { }
1012    end
1013    return features
1014end
1015
1016definers.registersplit('*',starred,"featureset")
1017
1018-- sort of xetex mode, but without [] and / as we have file: and name: etc
1019
1020local space      = P(" ")
1021local spaces     = space^0
1022local separator  = S(";,")
1023local equal      = P("=")
1024local sometext   = C((1-equal-space-separator)^1)
1025local truevalue  = P("+") * spaces * sometext                           * Cc(true)
1026local falsevalue = P("-") * spaces * sometext                           * Cc(false)
1027local somevalue  =                   sometext * spaces                  * Cc(true)
1028local keyvalue   =                   sometext * spaces * equal * spaces * sometext
1029local pattern    = Cf(Ct("") * (space + separator + Cg(falsevalue + truevalue + keyvalue + somevalue))^0, rawset)
1030
1031local function colonized(specification)
1032    specification.features.normal = normalize_features(lpegmatch(pattern,specification.detail))
1033    return specification
1034end
1035
1036definers.registersplit(":",colonized,"direct")
1037
1038-- define (two steps)
1039
1040local sizepattern, splitpattern, specialscale  do
1041
1042    -- todo: use lpeg.patterns
1043
1044    ----- space          = P(" ")
1045    ----- spaces         = space^0
1046    local digit          = R('09')
1047    local period         = P(".")
1048    local leftparent     = (P"(")
1049    local rightparent    = (P")")
1050    local leftbrace      = (P"{")
1051    local rightbrace     = (P"}")
1052    local withinparents  = leftparent * (1-rightparent)^0 * rightparent
1053    local withinbraces   = leftbrace  * (1-rightbrace )^0 * rightbrace
1054    local value          = C((withinparents + withinbraces + (1-space))^1)
1055 -- local dimension      = C((space/"" + P(1))^1)
1056    local dimension      = Cs((digit+period)^1 * (spaces/"") * (P(1)-digit)^0)
1057--     local scaler         = C((digit+period)^1)
1058    local scaler         = lpeg.patterns.unsigned/function(s) return round(tonumber(s)*1000) end
1059                         + C(lpeg.patterns.cardinal)
1060    local rest           = C(P(1)^0)
1061    local scale_none     =                      Cc(0)
1062    local scale_at       = (P("at") + P("@")) * Cc(1) * spaces * dimension -- dimension
1063    local scale_sa       = P("sa")            * Cc(2) * spaces * dimension -- number
1064    local scale_mo       = P("mo")            * Cc(3) * spaces * dimension -- number
1065    local scale_scaled   = P("scaled")        * Cc(4) * spaces * dimension -- number
1066    local scale_ht       = P("ht")            * Cc(5) * spaces * dimension -- dimension
1067    local scale_cp       = P("cp")            * Cc(6) * spaces * dimension -- dimension
1068
1069    specialscale = { [5] = "ht", [6] = "cp" }
1070
1071    sizepattern  = spaces * ((scale_at + scale_sa + scale_mo + scale_ht + scale_cp + scale_scaled) * (spaces * scaler)^0 + scale_none)
1072    splitpattern = spaces * value * spaces * rest
1073
1074end
1075
1076function helpers.splitfontpattern(str)
1077    local name, size = lpegmatch(splitpattern,str)
1078    local kind, size = lpegmatch(sizepattern,size) -- sx, sy
1079    return name, kind, size                        -- sx, sy
1080end
1081
1082function helpers.fontpatternhassize(str)
1083    local name, size = lpegmatch(splitpattern,str)
1084    local kind, size = lpegmatch(sizepattern,size)
1085    return size or false
1086end
1087
1088local specification -- still needed as local ?
1089
1090local getspecification = definers.getspecification
1091
1092-- we can make helper macros which saves parsing (but normaly not
1093-- that many calls, e.g. in mk a couple of 100 and in metafun 3500)
1094
1095local specifiers = { }
1096
1097do  -- else too many locals
1098
1099    local starttiming     = statistics.starttiming
1100    local stoptiming      = statistics.stoptiming
1101
1102    local setmacro        = tokens.setters.macro
1103    local ctxcatcodes     = tex.ctxcatcodes
1104    local texconditionals = tex.conditionals
1105
1106    local reported = setmetatableindex(function(t,k)
1107        local v = setmetatableindex(function(t,k)
1108            t[k] = true
1109            return false
1110        end)
1111        t[k] = v
1112        return v
1113    end)
1114
1115    implement {
1116        name      = "definefont_one",
1117        arguments = "string",
1118        actions   = function(str)
1119            starttiming(fonts)
1120            if trace_defining then
1121                report_defining("memory usage before: %s",statistics.memused())
1122                report_defining("start stage one: %s",str)
1123            end
1124            local fullname, size = lpegmatch(splitpattern,str)
1125            local lookup, name, sub, method, detail = getspecification(fullname)
1126            if not name then
1127                report_defining("strange definition %a",str)
1128             -- ctx_setdefaultfontname()
1129            elseif name == "unknown" then
1130             -- ctx_setdefaultfontname()
1131            else
1132             -- ctx_setsomefontname(name)
1133                setmacro("somefontname",name,"global")
1134            end
1135            -- we can also use a count for the size
1136            if size and size ~= "" then
1137                local mode, size, sx, sy = lpegmatch(sizepattern,size)
1138                if mode and size and size ~= "" then
1139                    texsetcount("scaledfontmode",mode)
1140                 -- ctx_setsomefontsize(size)
1141                    -- We use a catcodetable, just in case it's 1.2\exheight (a corner case that showed
1142                    -- up in the lmtx manual: marking that changed size in the mids of some verbatim).
1143                    setmacro(ctxcatcodes,"somefontsize",size)
1144                    if sx then
1145                        setmacro("somefontsizex",sx)
1146                    end
1147                    if sy then
1148                        setmacro("somefontsizey",sy)
1149                    end
1150                else
1151                    texsetcount("scaledfontmode",0)
1152                 -- ctx_setemptyfontsize()
1153                end
1154            elseif true then
1155                -- so we don't need to check in tex
1156                texsetcount("scaledfontmode",2)
1157             -- ctx_setemptyfontsize()
1158            else
1159                texsetcount("scaledfontmode",0)
1160             -- ctx_setemptyfontsize()
1161            end
1162            specification = definers.makespecification(str,lookup,name,sub,method,detail,size)
1163-- specification.original = str
1164            if trace_defining then
1165                report_defining("stop stage one")
1166            end
1167        end
1168    }
1169
1170    local function nice_cs(cs)
1171        return (gsub(cs,".->", ""))
1172    end
1173
1174    local n               = 0
1175    local busy            = false
1176    local combinefeatures = false
1177
1178    directives.register("fonts.features.combine",function(v)
1179        combinefeatures = v
1180    end)
1181
1182    implement {
1183        name      = "definefont_two",
1184        arguments = {
1185         -- "boolean", "string", "string", "dimension", "integer", "string",
1186         -- "string", "string", "string", "integer", "dimension", "string",
1187         -- "string", "string", "string", "integer",
1188            "boolean", "argument", "argument", "dimension", "integer",
1189            "argument", "argument", "argument", "argument", "integer",
1190            "dimension", "argument", "argument", "argument", "argument",
1191            "integer",
1192        },
1193        actions   = function (
1194                        global,          -- \ifx\fontclass\empty\s!false\else\s!true\fi
1195                        cs,              -- {#csname}%
1196                        str,             -- \somefontfile
1197                        size,            -- \d_font_scaled_font_size
1198                        inheritancemode, -- \c_font_feature_inheritance_mode
1199                        classfeatures,   -- \m_font_class_features
1200                        fontfeatures,    -- \m_font_features
1201                        classfallbacks,  -- \m_font_class_fallbacks
1202                        fontfallbacks,   -- \m_font_fallbacks
1203                        mathsize,        -- \fontface
1204                        textsize,        -- \d_font_scaled_text_face
1205                        classgoodies,    -- \m_font_class_goodies
1206                        goodies,         -- \m_font_goodies
1207                        classdesignsize, -- \m_font_class_designsize
1208                        fontdesignsize,  -- \m_font_designsize
1209                        scaledfontmode   -- \scaledfontmode
1210                    )
1211            if trace_defining then
1212                report_defining("start stage two: %s, size %s, features %a & %a, mode %a",str,size,classfeatures,fontfeatures,inheritancemode)
1213            end
1214            -- name is now resolved and size is scaled cf sa/mo
1215            local lookup, name, sub, method, detail = getspecification(str or "")
1216            -- new (todo: inheritancemode)
1217            local designsize = fontdesignsize ~= "" and fontdesignsize or classdesignsize or ""
1218            local designname = designsizefilename(name,designsize,size)
1219            if designname and designname ~= "" then
1220                if trace_defining or trace_designsize then
1221                    report_defining("remapping name %a, specification %a, size %a, designsize %a",name,designsize,size,designname)
1222                end
1223                -- we don't catch detail here
1224                local o_lookup, o_name, o_sub, o_method, o_detail = getspecification(designname)
1225                if o_lookup and o_lookup ~= "" then lookup = o_lookup end
1226                if o_method and o_method ~= "" then method = o_method end
1227                if o_detail and o_detail ~= "" then detail = o_detail end
1228                name = o_name
1229                sub = o_sub
1230            end
1231            if texconditionals["c_font_compact"] then
1232                size = 655360
1233            end
1234            -- so far
1235            -- some settings can have been overloaded
1236            if lookup and lookup ~= "" then
1237                specification.lookup = lookup
1238            end
1239            if relativeid and relativeid ~= "" then -- experimental hook
1240                local id = tonumber(relativeid) or 0
1241                specification.relativeid = id > 0 and id
1242            end
1243            --
1244            specification.name      = name
1245            specification.size      = size
1246            specification.sub       = (sub and sub ~= "" and sub) or specification.sub
1247            specification.mathsize  = mathsize
1248            specification.textsize  = textsize
1249            specification.goodies   = goodies
1250            specification.cs        = cs
1251            specification.global    = global
1252            specification.scalemode = scaledfontmode -- context specific
1253            if detail and detail ~= "" then
1254                specification.method = method or "*"
1255                specification.detail = detail
1256            elseif specification.detail and specification.detail ~= "" then
1257                -- already set
1258            elseif inheritancemode == 0 then
1259                -- nothing
1260            elseif inheritancemode == 1 then
1261                -- fontonly
1262                if fontfeatures and fontfeatures ~= "" then
1263                    specification.method = "*"
1264                    specification.detail = fontfeatures
1265                end
1266                if fontfallbacks and fontfallbacks ~= "" then
1267                    specification.fallbacks = fontfallbacks
1268                end
1269            elseif inheritancemode == 2 then
1270                -- classonly
1271                if classfeatures and classfeatures ~= "" then
1272                    specification.method = "*"
1273                    specification.detail = classfeatures
1274                end
1275                if classfallbacks and classfallbacks ~= "" then
1276                    specification.fallbacks = classfallbacks
1277                end
1278            elseif inheritancemode == 3 then
1279                -- fontfirst
1280                if combinefeatures then
1281                    if classfeatures and classfeatures ~= "" then
1282                        specification.method = "*"
1283                        if fontfeatures and fontfeatures ~= "" and fontfeatures ~= classfeatures then
1284                            specification.detail = classfeatures .. "," .. fontfeatures
1285                        else
1286                            specification.detail = classfeatures
1287                        end
1288                    elseif fontfeatures and fontfeatures ~= "" then
1289                        specification.method = "*"
1290                        specification.detail = fontfeatures
1291                    end
1292                else
1293                    if fontfeatures and fontfeatures ~= "" then
1294                        specification.method = "*"
1295                        specification.detail = fontfeatures
1296                    elseif classfeatures and classfeatures ~= "" then
1297                        specification.method = "*"
1298                        specification.detail = classfeatures
1299                    end
1300                end
1301                if fontfallbacks and fontfallbacks ~= "" then
1302                    specification.fallbacks = fontfallbacks
1303                elseif classfallbacks and classfallbacks ~= "" then
1304                    specification.fallbacks = classfallbacks
1305                end
1306            elseif inheritancemode == 4 then
1307                -- classfirst
1308                if combinefeatures then
1309                    if fontfeatures and fontfeatures ~= "" then
1310                        specification.method = "*"
1311                        if classfeatures and classfeatures ~= "" and classfeatures ~= fontfeatures then
1312                            specification.detail = fontfeatures .. "," .. classfeatures
1313                        else
1314                            specification.detail = fontfeatures
1315                        end
1316                    elseif classfeatures and classfeatures ~= "" then
1317                        specification.method = "*"
1318                        specification.detail = classfeatures
1319                    end
1320                else
1321                    if classfeatures and classfeatures ~= "" then
1322                        specification.method = "*"
1323                        specification.detail = classfeatures
1324                    elseif fontfeatures and fontfeatures ~= "" then
1325                        specification.method = "*"
1326                        specification.detail = fontfeatures
1327                    end
1328                end
1329                if classfallbacks and classfallbacks ~= "" then
1330                    specification.fallbacks = classfallbacks
1331                elseif fontfallbacks and fontfallbacks ~= "" then
1332                    specification.fallbacks = fontfallbacks
1333                end
1334            end
1335            --
1336            local tfmdata = definers.read(specification,size) -- id not yet known (size in spec?)
1337            --
1338            local lastfontid = 0
1339            local tfmtype    = type(tfmdata)
1340            if tfmtype == "table" then
1341                -- setting the extra characters will move elsewhere
1342                local characters = tfmdata.characters
1343                local parameters = tfmdata.parameters
1344                local properties = tfmdata.properties
1345                -- we use char0 as signal; cf the spec pdf can handle this (no char in slot)
1346                characters[0] = nil
1347             -- characters[0x00A0] = { width = parameters.space }
1348             -- characters[0x2007] = { width = characters[0x0030] and characters[0x0030].width or parameters.space } -- figure
1349             -- characters[0x2008] = { width = characters[0x002E] and characters[0x002E].width or parameters.space } -- period
1350                --
1351                local fallbacks = specification.fallbacks or ""
1352                local mathsize  = (mathsize == 1 or mathsize == 2 or mathsize == 3) and mathsize or nil -- can be unset so we test 1 2 3
1353                if fallbacks ~= "" and mathsize and not busy then
1354                    busy = true
1355                    -- We need this ugly hack in order to resolve fontnames (at the \TEX end). Originally
1356                    -- math was done in Lua after loading (plugged into aftercopying).
1357                    --
1358                    -- After tl 2017 I'll also do text fallbacks this way (although backups there are done
1359                    -- in a completely different way.)
1360                    if trace_defining then
1361                        report_defining("defining %a, id %a, target %a, features %a / %a, fallbacks %a / %a, step %a",
1362                            name,id,nice_cs(cs),classfeatures,fontfeatures,classfallbacks,fontfallbacks,1)
1363                    end
1364                    mathematics.resolvefallbacks(tfmdata,specification,fallbacks)
1365                    context(function()
1366                        -- We're doing math fallbacks.
1367                        busy = false
1368                        mathematics.finishfallbacks(tfmdata,specification,fallbacks)
1369                        tfmdata.original = specification.specification
1370                        local id = definefont(tfmdata,properties.id)
1371                        csnames[id] = specification.cs
1372                        properties.id = id -- already set
1373                        definers.register(tfmdata,id) -- to be sure, normally already done
1374                        texdefinefont(global,cs,id)
1375                     -- texdefinefont(cs,id,global and "global")
1376                        constructors.finalize(tfmdata)
1377                        if trace_defining then
1378                            report_defining("defining %a, id %a, target %a, features %a / %a, fallbacks %a / %a, step %a",
1379                                name,id,nice_cs(cs),classfeatures,fontfeatures,classfallbacks,fontfallbacks,2)
1380                        end
1381                        -- resolved (when designsize is used):
1382                        local size = round(tfmdata.parameters.size or 655360)
1383                        setmacro("somefontsize",size.."sp")
1384                     -- ctx_setsomefontsize(size .. "sp")
1385                        texsetcount("scaledfontsize",size)
1386                        lastfontid = id
1387                        --
1388                        if trace_defining then
1389                            report_defining("memory usage after: %s",statistics.memused())
1390                            report_defining("stop stage two")
1391                        end
1392                        --
1393                        texsetcount("global","lastfontid",lastfontid)
1394                        specifiers[lastfontid] = { str, size }
1395                        if not mathsize then
1396                            -- forget about it (can't happen here)
1397                        elseif mathsize == 0 then
1398                            -- can't happen (here)
1399                        else
1400                            -- maybe only 1 2 3 (we already test for this)
1401                            lastmathids[mathsize] = lastfontid
1402                        end
1403                        stoptiming(fonts)
1404                    end)
1405                    return
1406                else
1407                    tfmdata.original = specification.specification
1408                    local id = definefont(tfmdata,properties.id)
1409                    csnames[id] = specification.cs
1410                    properties.id = id -- already set
1411                    definers.register(tfmdata,id) -- to be sure, normally already done
1412                    texdefinefont(global,cs,id)
1413                 -- texdefinefont(cs,id,global and "global")
1414                    constructors.finalize(tfmdata)
1415                    if trace_defining then
1416                        report_defining("defining %a, id %a, target %a, features %a / %a, fallbacks %a / %a, step %a",
1417                            name,id,nice_cs(cs),classfeatures,fontfeatures,classfallbacks,fontfallbacks,"-")
1418                    end
1419                    -- resolved (when designsize is used):
1420                    local size = round(tfmdata.parameters.size or 655360)
1421                    setmacro("somefontsize",size.."sp")
1422                 -- ctx_setsomefontsize(size .. "sp")
1423                    texsetcount("scaledfontsize",size)
1424                    lastfontid = id
1425                end
1426            elseif tfmtype == "number" then
1427                if trace_defining then
1428                    report_defining("reusing %s, id %a, target %a, features %a / %a, fallbacks %a / %a, goodies %a / %a, designsize %a / %a",
1429                        name,tfmdata,nice_cs(cs),classfeatures,fontfeatures,classfallbacks,fontfallbacks,classgoodies,goodies,classdesignsize,fontdesignsize)
1430                end
1431                csnames[tfmdata] = specification.cs
1432                texdefinefont(global,cs,tfmdata)
1433             -- texdefinefont(cs,tfmdata,global and "global")
1434                -- resolved (when designsize is used):
1435                local size = round(fontdata[tfmdata].parameters.size or 0)
1436             -- ctx_setsomefontsize(size .. "sp")
1437                setmacro("somefontsize",size.."sp")
1438                texsetcount("scaledfontsize",size)
1439                lastfontid = tfmdata
1440            else
1441                local nice = nice_cs(cs)
1442                if not reported[name][nice] then
1443                    report_defining("unable to define %a as %a",name,nice)
1444                end
1445                lastfontid = -1
1446                texsetcount("scaledfontsize",0)
1447             -- ctx_letvaluerelax(cs) -- otherwise the current definition takes the previous one
1448            end
1449            if trace_defining then
1450                report_defining("memory usage after: %s",statistics.memused())
1451                report_defining("stop stage two")
1452            end
1453            --
1454            texsetcount("global","lastfontid",lastfontid)
1455            specifiers[lastfontid] = { str, size }
1456            if not mathsize then
1457                -- forget about it
1458            elseif mathsize == 0 then
1459                -- can't happen (here)
1460            else
1461                -- maybe only 1 2 3
1462                lastmathids[mathsize] = lastfontid
1463            end
1464            --
1465            stoptiming(fonts)
1466        end
1467    }
1468
1469    implement {
1470         name      = "specifiedfontspec",
1471         arguments = "integer",
1472         actions   = function(id)
1473            local f = specifiers[id]
1474            if f then
1475                context(f[1])
1476            end
1477        end
1478    }
1479
1480    implement {
1481         name      = "specifiedfontsize",
1482         arguments = "integer",
1483         actions   = function(id)
1484            local f = specifiers[id]
1485            if f then
1486                context(f[2])
1487            end
1488        end
1489    }
1490
1491    implement {
1492         name      = "specifiedfont",
1493         arguments = { "integer", "number" },
1494         actions   = function(id,size)
1495            local f = specifiers[id]
1496            if f and size then
1497                context("%s at %0.2p",f[1],size * f[2]) -- we round to 2 decimals (as at the tex end)
1498            end
1499        end
1500    }
1501    --
1502
1503    local function define(specification)
1504        --
1505        local name = specification.name
1506        if not name or name == "" then
1507            return -1
1508        else
1509            starttiming(fonts)
1510            --
1511            -- following calls expect a few properties to be set:
1512            --
1513            local lookup, name, sub, method, detail = getspecification(name or "")
1514            --
1515            specification.name          = (name ~= "" and name) or specification.name
1516            --
1517            specification.lookup        = specification.lookup or (lookup ~= "" and lookup) or "file"
1518            specification.size          = specification.size                                or 655260
1519            specification.sub           = specification.sub    or (sub    ~= "" and sub)    or ""
1520            specification.method        = specification.method or (method ~= "" and method) or "*"
1521            specification.detail        = specification.detail or (detail ~= "" and detail) or ""
1522            --
1523            if type(specification.size) == "string" then
1524                specification.size = texsp(specification.size) or 655260
1525            end
1526            --
1527            specification.specification = "" -- not used
1528            specification.resolved      = ""
1529            specification.forced        = ""
1530            specification.features      = { } -- via detail, maybe some day
1531            --
1532            -- we don't care about mathsize textsize goodies fallbacks
1533            --
1534            local cs = specification.cs
1535            if cs == "" then
1536                cs = nil
1537                specification.cs = nil
1538                specification.global = false
1539            elseif specification.global == nil then
1540                specification.global = false
1541            end
1542            --
1543            local tfmdata = definers.read(specification,specification.size)
1544            if not tfmdata then
1545                return -1, nil
1546            elseif type(tfmdata) == "number" then
1547                if cs then
1548                    texdefinefont(specification.global,cs,tfmdata)
1549                 -- texdefinefont(cs,tfmdata,specification.global and "global")
1550                    csnames[tfmdata] = cs
1551                end
1552                stoptiming(fonts)
1553                return tfmdata, fontdata[tfmdata]
1554            else
1555                local id = definefont(tfmdata)
1556                tfmdata.properties.id = id
1557                definers.register(tfmdata,id)
1558                if cs then
1559                    texdefinefont(specification.global,cs,id)
1560                 -- texdefinefont(cs,id,specification.global and "global")
1561                    csnames[id] = cs
1562                end
1563                constructors.finalize(tfmdata)
1564                stoptiming(fonts)
1565                return id, tfmdata
1566            end
1567        end
1568    end
1569
1570    definers.define = define
1571
1572    -- local id, cs = fonts.definers.internal { }
1573    -- local id, cs = fonts.definers.internal { number = 2 }
1574    -- local id, cs = fonts.definers.internal { name = "dejavusans" }
1575
1576    local n = 0
1577
1578    function definers.internal(specification,cs)
1579        specification = specification or { }
1580        local name    = specification.name
1581        local size    = tonumber(specification.size)
1582        local number  = tonumber(specification.number)
1583        local id      = nil
1584        if not size then
1585            size = texgetdimen("bodyfontsize")
1586        end
1587        if number then
1588            id = number
1589        elseif name and name ~= "" then
1590            local cs = cs or specification.cs
1591            if not cs then
1592                n  = n + 1 -- beware ... there can be many and they are often used once
1593             -- cs = formatters["internal font %s"](n)
1594                cs = "internal font " .. n
1595            else
1596                specification.cs = cs
1597            end
1598            id = define {
1599                name = name,
1600                size = size,
1601                cs   = cs,
1602            }
1603        end
1604        if not id then
1605            id = currentfont()
1606        end
1607        return id, csnames[id]
1608    end
1609
1610    local function read(name,size)
1611        return (define { name = name, size = size } or 0)
1612    end
1613
1614    callbacks.register('define_font', read, "definition of fonts (tfmdata preparation)")
1615
1616    -- here
1617
1618    local infofont = 0
1619
1620    function fonts.infofont()
1621        if infofont == 0 then
1622            infofont = define { name = "dejavusansmono", size = texsp("6pt") }
1623        end
1624        return infofont
1625    end
1626
1627    -- abstract interfacing
1628
1629    implement { name = "tf", actions = function() setmacro("fontalternative","tf") end }
1630    implement { name = "bf", actions = function() setmacro("fontalternative","bf") end }
1631    implement { name = "it", actions = function() setmacro("fontalternative","it") end }
1632    implement { name = "sl", actions = function() setmacro("fontalternative","sl") end }
1633    implement { name = "bi", actions = function() setmacro("fontalternative","bi") end }
1634    implement { name = "bs", actions = function() setmacro("fontalternative","bs") end }
1635
1636end
1637
1638-- Not ok, we can best use a database for this. The problem is that we
1639-- have delayed definitions and so we never know what style is taken
1640-- as start.
1641
1642function constructors.calculatescale(tfmdata,scaledpoints,relativeid,specification)
1643    local parameters = tfmdata.parameters
1644    local units      = parameters.units or 1000
1645    if specification then
1646        local scalemode = specification.scalemode
1647        local special   = scalemode and specialscale[scalemode]
1648        if special == "ht" then
1649            local height = parameters.ascender / units
1650            scaledpoints = scaledpoints / height
1651        elseif special == "cp" then
1652            local glyph  = tfmdata.descriptions[utfbyte("X")]
1653            local height = (glyph and glyph.height or parameters.ascender) / units
1654            scaledpoints = scaledpoints / height
1655        end
1656    end
1657    if scaledpoints < 0 then
1658        scaledpoints = (- scaledpoints/1000) * (tfmdata.designsize or parameters.designsize) -- already in sp
1659    end
1660    -- a temp hack till we have upgraded all mechanisms
1661    local delta = round(scaledpoints/units)
1662    local size  = round(scaledpoints)
1663    texsetcount("c_font_scaled_points",size)
1664    --
1665    return size, delta
1666end
1667
1668local designsizes = constructors.designsizes
1669
1670-- called quite often when in mp labels
1671-- otf.normalizedaxis
1672
1673function constructors.hashinstance(specification,force)
1674    local hash      = specification.hash
1675    local size      = specification.size
1676    local fallbacks = specification.fallbacks
1677    if force or not hash then
1678        hash = constructors.hashfeatures(specification)
1679        specification.hash = hash
1680    end
1681    if size < 1000 and designsizes[hash] then
1682        size = round(constructors.scaled(size,designsizes[hash]))
1683    else
1684        size = round(size)
1685    end
1686    specification.size = size
1687    if fallbacks then
1688        return hash .. ' @ ' .. size .. ' @ ' .. fallbacks
1689    else
1690        local scalemode = specification.scalemode
1691        local special   = scalemode and specialscale[scalemode]
1692        if special then
1693            return hash .. ' @ ' .. size .. ' @ ' .. special
1694        else
1695            return hash .. ' @ ' .. size
1696        end
1697    end
1698end
1699
1700-- We overload the (generic) resolver:
1701
1702local resolvers    = definers.resolvers
1703local hashfeatures = constructors.hashfeatures
1704
1705function definers.resolve(specification) -- overload function in font-con.lua
1706    if not specification.resolved or specification.resolved == "" then -- resolved itself not per se in mapping hash
1707        local r = resolvers[specification.lookup]
1708        if r then
1709            r(specification)
1710        end
1711    end
1712    if specification.forced == "" then
1713        specification.forced = nil
1714    else
1715        specification.forced = specification.forced
1716    end
1717    -- goodies are a context specific thing and are not always defined
1718    -- as feature, so we need to make sure we add them here before
1719    -- hashing because otherwise we get funny goodies applied
1720    local goodies = specification.goodies
1721    if goodies and goodies ~= "" then
1722        -- this adapts the features table so it has best be a copy
1723        local normal = specification.features.normal
1724        if not normal then
1725            specification.features.normal = { goodies = goodies }
1726        elseif not normal.goodies then
1727            local g = normal.goodies
1728            if g and g ~= "" then
1729                normal.goodies = formatters["%s,%s"](g,goodies)
1730            else
1731                normal.goodies = goodies
1732            end
1733        end
1734    end
1735    -- so far for goodie hacks
1736    local hash = hashfeatures(specification)
1737    local name = specification.name or "badfont"
1738    local sub  = specification.sub
1739    if sub and sub ~= "" then
1740        specification.hash = lower(name .. " @ " .. sub .. ' @ ' .. hash)
1741    else
1742        specification.hash = lower(name .. " @ "        .. ' @ ' .. hash)
1743    end
1744    --
1745    return specification
1746end
1747
1748-- we need an 'do after the banner hook'
1749
1750-- => commands
1751
1752local pattern = P("P")
1753              * (lpeg.patterns.hexdigit^4 / function(s) return tonumber(s,16) end)
1754              * P(-1)
1755
1756local function nametoslot(name) -- also supports PXXXXX (4+ positions)
1757    local t = type(name)
1758    if t == "string" then
1759        local unic = unicodes[true]
1760        local slot = unic[name]
1761        if slot then
1762            return slot
1763        end
1764        --
1765        local slot = unic[gsub(name,"_"," ")] or unic[gsub(name,"_","-")] or
1766                     unic[gsub(name,"-"," ")] or unic[gsub(name,"-","_")] or
1767                     unic[gsub(name," ","_")] or unic[gsub(name," ","-")]
1768        if slot then
1769            return slot
1770        end
1771        --
1772        if not aglunicodes then
1773            aglunicodes = encodings.agl.unicodes
1774        end
1775        local char = characters[true]
1776        local slot = aglunicodes[name]
1777        if slot and char[slot] then
1778            return slot
1779        end
1780        local slot = lpegmatch(pattern,name)
1781        if slot and char[slot] then
1782            return slot
1783        end
1784        -- not in font
1785    elseif t == "number" then
1786        if characters[true][name] then
1787            return slot
1788        else
1789            -- not in font
1790        end
1791    end
1792end
1793
1794local found = { }
1795
1796local function descriptiontoslot(name)
1797    local t = type(name)
1798    if t == "string" then
1799        -- slow
1800        local list = sortedkeys(chardata) -- can be a cache with weak tables
1801        local slot = found[name]
1802        local char = characters[true]
1803        if slot then
1804            return char[slot] and slot or nil
1805        end
1806        local NAME = upper(name)
1807        for i=1,#list do
1808            slot = list[i]
1809            local c = chardata[slot]
1810            local d = c.description
1811            if d == NAME then
1812                found[name] = slot
1813                return char[slot] and slot or nil
1814            end
1815        end
1816        for i=1,#list do
1817            slot = list[i]
1818            local c = chardata[slot]
1819            local s = c.synonyms
1820            if s then
1821                for i=1,#s do
1822                    local si = s[i]
1823                    if si == name then
1824                        found[name] = si
1825                        return char[slot] and slot or nil
1826                    end
1827                end
1828            end
1829        end
1830        for i=1,#list do
1831            slot = list[i]
1832            local c = chardata[slot]
1833            local d = c.description
1834            if d and find(d,NAME) then
1835                found[name] = slot
1836                return char[slot] and slot or nil
1837            end
1838        end
1839        for i=1,#list do
1840            slot = list[i]
1841            local c = chardata[slot]
1842            local s = c.synonyms
1843            if s then
1844                for i=1,#s do
1845                    local si = s[i]
1846                    if find(s[i],name) then
1847                        found[name] = si
1848                        return char[slot] and slot or nil
1849                    end
1850                end
1851            end
1852        end
1853        -- not in font
1854    elseif t == "number" then
1855        if characters[true][name] then
1856            return slot
1857        else
1858            -- not in font
1859        end
1860    end
1861end
1862
1863local function indextoslot(font,index)
1864    if not index then
1865        index = font
1866        font  = true
1867    end
1868    local r = resources[font]
1869    if r then
1870        local indices = r.indices
1871        if not indices then
1872            indices = { }
1873            local c = characters[font]
1874            for unicode, data in next, c do
1875                local di = data.index
1876                if di then
1877                    indices[di] = unicode
1878                end
1879            end
1880            r.indices = indices
1881        end
1882        return indices[tonumber(index)]
1883    end
1884end
1885
1886do -- else too many locals
1887
1888    local entities = characters.entities
1889    local lowered  = { } -- delayed initialization
1890
1891    setmetatableindex(lowered,function(t,k)
1892        for k, v in next, entities do
1893            local l = lower(k)
1894            if not entities[l] then
1895                lowered[l] = v
1896            end
1897        end
1898        setmetatableindex(lowered,nil)
1899        return lowered[k]
1900    end)
1901
1902    local methods = {
1903        -- entity
1904        e = function(name)
1905                return entities[name] or lowered[name] or name
1906            end,
1907        -- hexadecimal unicode
1908        x = function(name)
1909                local n = tonumber(name,16)
1910                return n and utfchar(n) or name
1911            end,
1912        -- decimal unicode
1913        d = function(name)
1914                local n = tonumber(name)
1915                return n and utfchar(n) or name
1916            end,
1917        -- hexadecimal index (slot)
1918        s = function(name)
1919                local n = tonumber(name,16)
1920                local n = n and indextoslot(n)
1921                return n and utfchar(n) or name
1922            end,
1923        -- decimal index
1924        i = function(name)
1925                local n = tonumber(name)
1926                local n = n and indextoslot(n)
1927                return n and utfchar(n) or name
1928            end,
1929        -- name
1930        n = function(name)
1931                local n = nametoslot(name)
1932                return n and utfchar(n) or name
1933            end,
1934        -- unicode description (synonym)
1935        u = function(name)
1936                local n = descriptiontoslot(name,false)
1937                return n and utfchar(n) or name
1938            end,
1939        -- all
1940        a = function(name)
1941                local n = nametoslot(name) or descriptiontoslot(name)
1942                return n and utfchar(n) or name
1943            end,
1944        -- char
1945        c = function(name)
1946                return name
1947            end,
1948    }
1949
1950    -- -- nicer:
1951    --
1952    -- setmetatableindex(methods,function(t,k) return methods.c end)
1953    --
1954    -- local splitter = (C(1) * P(":") + Cc("c")) * C(P(1)^1) / function(method,name)
1955    --     return methods[method](name)
1956    -- end
1957    --
1958    -- -- more efficient:
1959
1960    local splitter = C(1) * P(":") * C(P(1)^1) / function(method,name)
1961        local action = methods[method]
1962        return action and action(name) or name
1963    end
1964
1965    local function tochar(str)
1966        local t = type(str)
1967        if t == "number" then
1968            return utfchar(str)
1969        elseif t == "string" then
1970            return lpegmatch(splitter,str) or str
1971        else
1972            return str
1973        end
1974    end
1975
1976    helpers.nametoslot        = nametoslot
1977    helpers.descriptiontoslot = descriptiontoslot
1978    helpers.indextoslot       = indextoslot
1979    helpers.tochar            = tochar
1980
1981    -- interfaces:
1982
1983    implement {
1984        name      = "fontchar",
1985        actions   = { nametoslot, ctx_char },
1986        arguments = "string",
1987    }
1988
1989    implement {
1990        name      = "fontcharbyindex",
1991        actions   = { indextoslot, ctx_char },
1992        arguments = "integer",
1993    }
1994
1995    implement {
1996        name      = "tochar",
1997        actions   = { tochar, ctx_safechar },
1998        arguments = "string",
1999    }
2000
2001end
2002
2003-- this will change ...
2004
2005function loggers.reportdefinedfonts()
2006    if trace_usage then
2007        local t, tn = { }, 0
2008        for id, data in sortedhash(fontdata) do
2009            local properties = data.properties or { }
2010            local parameters = data.parameters or { }
2011            tn = tn + 1
2012            t[tn] = {
2013  formatters["%03i"](id                    or 0),
2014  formatters["%p"  ](parameters.size       or 0),
2015                     properties.format     or "unknown",
2016                     properties.name       or "",
2017                     properties.psname     or "",
2018                     properties.fullname   or "",
2019                     properties.sharedwith or "",
2020            }
2021        end
2022        formatcolumns(t,"  ")
2023        --
2024        logs.startfilelogging(report,"defined fonts")
2025        for k=1,tn do
2026            report(t[k])
2027        end
2028        logs.stopfilelogging()
2029    end
2030end
2031
2032logs.registerfinalactions(loggers.reportdefinedfonts)
2033
2034function loggers.reportusedfeatures()
2035    -- numbers, setups, merged
2036    if trace_usage then
2037        local t, n = { }, #numbers
2038        for i=1,n do
2039            local name = numbers[i]
2040            local setup = setups[name]
2041            local n = setup.number
2042            setup.number = nil -- we have no reason to show this
2043            t[i] = { i, name, sequenced(setup,false,true) } -- simple mode
2044            setup.number = n -- restore it (normally not needed as we're done anyway)
2045        end
2046        formatcolumns(t,"  ")
2047        logs.startfilelogging(report,"defined featuresets")
2048        for k=1,n do
2049            report(t[k])
2050        end
2051        logs.stopfilelogging()
2052    end
2053end
2054
2055logs.registerfinalactions(loggers.reportusedfeatures)
2056
2057-- maybe move this to font-log.lua:
2058
2059statistics.register("font engine", function()
2060    local elapsed   = statistics.elapsedseconds(fonts)
2061    local nofshared = constructors.nofsharedfonts or 0
2062    local nofloaded = constructors.noffontsloaded or 0
2063    if nofshared > 0 then
2064        return format("otf %0.3f, afm %0.3f, tfm %0.3f, %s instances, %s shared in backend, %s common vectors, %s common hashes, load time %s",
2065            otf.version,afm.version,tfm.version,nofloaded,
2066            nofshared,constructors.nofsharedvectors,constructors.nofsharedhashes,
2067            elapsed)
2068    elseif nofloaded > 0 and elapsed then
2069        return format("otf %0.3f, afm %0.3f, tfm %0.3f, %s instances, load time %s",
2070            otf.version,afm.version,tfm.version,nofloaded,
2071            elapsed)
2072    else
2073        return format("otf %0.3f, afm %0.3f, tfm %0.3f",
2074            otf.version,afm.version,tfm.version)
2075    end
2076end)
2077
2078-- experimental mechanism for Mojca:
2079--
2080-- fonts.definetypeface {
2081--     name         = "mainbodyfont-light",
2082--     preset       = "antykwapoltawskiego-light",
2083-- }
2084--
2085-- fonts.definetypeface {
2086--     name         = "mojcasfavourite",
2087--     preset       = "antykwapoltawskiego",
2088--     normalweight = "light",
2089--     boldweight   = "bold",
2090--     width        = "condensed",
2091-- }
2092
2093local Shapes = {
2094    serif = "Serif",
2095    sans  = "Sans",
2096    mono  = "Mono",
2097}
2098
2099local ctx_startfontclass       = context.startfontclass
2100local ctx_stopfontclass        = context.stopfontclass
2101local ctx_definefontsynonym    = context.definefontsynonym
2102local ctx_dofastdefinetypeface = context.dofastdefinetypeface
2103
2104function fonts.definetypeface(name,t)
2105    if type(name) == "table" then
2106        -- {name=abc,k=v,...}
2107        t = name
2108    elseif t then
2109        if type(t) == "string" then
2110            -- "abc", "k=v,..."
2111            t = settings_to_hash(name)
2112        else
2113            -- "abc", {k=v,...}
2114        end
2115        t.name = t.name or name
2116    else
2117        -- "name=abc,k=v,..."
2118        t = settings_to_hash(name)
2119    end
2120    local p = t.preset and fonts.typefaces[t.preset] or { }
2121    local name         = t.name         or "unknowntypeface"
2122    local shortcut     = t.shortcut     or p.shortcut or "rm"
2123    local size         = t.size         or p.size     or "default"
2124    local shape        = t.shape        or p.shape    or "serif"
2125    local fontname     = t.fontname     or p.fontname or "unknown"
2126    local normalweight = t.normalweight or t.weight or p.normalweight or p.weight or "normal"
2127    local boldweight   = t.boldweight   or t.weight or p.boldweight   or p.weight or "normal"
2128    local normalwidth  = t.normalwidth  or t.width  or p.normalwidth  or p.width  or "normal"
2129    local boldwidth    = t.boldwidth    or t.width  or p.boldwidth    or p.width  or "normal"
2130    Shape = Shapes[shape] or "Serif"
2131    ctx_startfontclass { name }
2132        ctx_definefontsynonym( { formatters["%s"]          (Shape) }, { formatters["spec:%s-%s-regular-%s"] (fontname, normalweight, normalwidth) } )
2133        ctx_definefontsynonym( { formatters["%sBold"]      (Shape) }, { formatters["spec:%s-%s-regular-%s"] (fontname, boldweight,   boldwidth  ) } )
2134        ctx_definefontsynonym( { formatters["%sBoldItalic"](Shape) }, { formatters["spec:%s-%s-italic-%s"]  (fontname, boldweight,   boldwidth  ) } )
2135        ctx_definefontsynonym( { formatters["%sItalic"]    (Shape) }, { formatters["spec:%s-%s-italic-%s"]  (fontname, normalweight, normalwidth) } )
2136    ctx_stopfontclass()
2137    local settings = sequenced({ features= t.features },",")
2138    ctx_dofastdefinetypeface(name, shortcut, shape, size, settings)
2139end
2140
2141implement {
2142    name      = "definetypeface",
2143    actions   = fonts.definetypeface,
2144    arguments = "2 strings"
2145}
2146
2147function fonts.current() -- todo: also handle name
2148    return fontdata[currentfont()] or fontdata[0]
2149end
2150
2151function fonts.currentid()
2152    return currentfont() or 0
2153end
2154
2155-- for the moment here, this will become a chain of extras that is
2156-- hooked into the ctx registration (or scaler or ...)
2157
2158local dimenfactors = number.dimenfactors
2159
2160function helpers.dimenfactor(unit,id)
2161    if unit == "ex" then
2162        return id and exheights[id] or 282460 -- lm 10pt
2163    elseif unit == "em" then
2164        return id and emwidths [id] or 655360 -- lm 10pt
2165    else
2166        local du = dimenfactors[unit]
2167        return du and 1/du or tonumber(unit) or 1
2168    end
2169end
2170
2171local function digitwidth(font) -- max(quad/2,wd(0..9))
2172    local tfmdata = fontdata[font]
2173    local parameters = tfmdata.parameters
2174    local width = parameters.digitwidth
2175    if not width then
2176        width = round(parameters.quad/2) -- maybe tex.scale
2177        local characters = tfmdata.characters
2178        for i=48,57 do
2179            local wd = round(characters[i].width)
2180            if wd > width then
2181                width = wd
2182            end
2183        end
2184        parameters.digitwidth = width
2185    end
2186    return width
2187end
2188
2189helpers.getdigitwidth = digitwidth
2190helpers.setdigitwidth = digitwidth
2191
2192--
2193
2194function helpers.getparameters(tfmdata)
2195    local p = { }
2196    local m = p
2197    local parameters = tfmdata.parameters
2198    while true do
2199        for k, v in next, parameters do
2200            m[k] = v
2201        end
2202        parameters = getmetatable(parameters)
2203        parameters = parameters and parameters.__index
2204        if type(parameters) == "table" then
2205            m = { }
2206            p.metatable = m
2207        else
2208            break
2209        end
2210    end
2211    return p
2212end
2213
2214if environment.initex then
2215
2216    local function names(t)
2217        local nt = #t
2218        if nt > 0 then
2219            local n = { }
2220            for i=1,nt do
2221                n[i] = t[i].name
2222            end
2223            return concat(n," ")
2224        else
2225            return "-"
2226        end
2227    end
2228
2229    statistics.register("font processing", function()
2230        local l = { }
2231        for what, handler in table.sortedpairs(handlers) do
2232            local features = handler and handler.features
2233            if features then
2234                l[#l+1] = format("[%s (base initializers: %s) (base processors: %s) (base manipulators: %s) (node initializers: %s) (node processors: %s) (node manipulators: %s)]",
2235                    what,
2236                    names(features.initializers.base),
2237                    names(features.processors  .base),
2238                    names(features.manipulators.base),
2239                    names(features.initializers.node),
2240                    names(features.processors  .node),
2241                    names(features.manipulators.node)
2242                )
2243            end
2244        end
2245        return concat(l, " | ")
2246    end)
2247
2248end
2249
2250-- redefinition
2251
2252-- local hashes      = fonts.hashes
2253-- local emwidths    = hashes.emwidths
2254-- local exheights   = hashes.exheights
2255
2256setmetatableindex(dimenfactors, function(t,k)
2257    if k == "ex" then
2258        return 1/exheights[currentfont()]
2259    elseif k == "em" then
2260        return 1/emwidths[currentfont()]
2261    elseif k == "pct" or k == "%" then
2262        return 1/(texget("hsize")/100)
2263    else
2264     -- error("wrong dimension: " .. (s or "?")) -- better a message
2265        return false
2266    end
2267end)
2268
2269dimenfactors.ex   = nil
2270dimenfactors.em   = nil
2271dimenfactors["%"] = nil
2272dimenfactors.pct  = nil
2273
2274--[[ldx--
2275<p>Before a font is passed to <l n='tex'/> we scale it. Here we also need
2276to scale virtual characters.</p>
2277--ldx]]--
2278
2279do
2280
2281    -- can become luat-tex.lua
2282
2283    local texsetglyphdata = tex.setglyphdata
2284    local texgetglyphdata = tex.getglyphdata
2285
2286    if not texsetglyphdata then
2287
2288        local texsetattribute = tex.setattribute
2289        local texgetattribute = tex.getattribute
2290
2291        texsetglyphdata = function(n) return texsetattribute(0,n) end
2292        texgetglyphdata = function()  return texgetattribute(0)   end
2293
2294        tex.setglyphdata = texsetglyphdata
2295        tex.getglyphdata = texgetglyphdata
2296
2297    end
2298
2299    -- till here
2300
2301    local setmacro = tokens.setters.macro
2302
2303    function constructors.currentfonthasfeature(n)
2304        local f = fontdata[currentfont()]
2305        if not f then return end f = f.shared
2306        if not f then return end f = f.rawdata
2307        if not f then return end f = f.resources
2308        if not f then return end f = f.features
2309        return f and (f.gpos[n] or f.gsub[n])
2310    end
2311
2312    local ctx_doifelse = commands.doifelse
2313    local ctx_doif     = commands.doif
2314
2315    implement {
2316        name      = "doifelsecurrentfonthasfeature",
2317        actions   = { constructors.currentfonthasfeature, ctx_doifelse },
2318        arguments = "string"
2319    }
2320
2321    local f_strip  = formatters["%0.2fpt"] -- normally this value is changed only once
2322    local stripper = lpeg.patterns.stripzeros
2323
2324    local cache = { }
2325
2326    local hows = {
2327        ["+"] = "add",
2328        ["-"] = "subtract",
2329        ["="] = "replace",
2330    }
2331
2332    local function setfeature(how,parent,name,font) -- 0/1 test temporary for testing
2333        if not how or how == 0 then
2334            if trace_features and texgetglyphdata() ~= 0 then
2335                report_cummulative("font %!font:name!, reset",fontdata[font or true])
2336            end
2337            texsetglyphdata(0)
2338        elseif how == true or how == 1 then
2339            local hash = "feature > " .. parent
2340            local done = cache[hash]
2341            if trace_features and done then
2342                report_cummulative("font %!font:name!, revive %a : %!font:features!",fontdata[font or true],parent,setups[numbers[done]])
2343            end
2344            texsetglyphdata(done or 0)
2345        else
2346            local full = parent .. how .. name
2347            local hash = "feature > " .. full
2348            local done = cache[hash]
2349            if not done then
2350                local n = setups[full]
2351                if n then
2352                    -- already defined
2353                else
2354                    n = mergecontextfeatures(parent,name,how,full)
2355                end
2356                done = registercontextfeature(hash,full,how)
2357                cache[hash] = done
2358                if trace_features then
2359                    report_cummulative("font %!font:name!, %s %a : %!font:features!",fontdata[font or true],hows[how],full,setups[numbers[done]])
2360                end
2361            end
2362            texsetglyphdata(done)
2363        end
2364    end
2365
2366    local function resetfeature()
2367        if trace_features and texgetglyphdata() ~= 0 then
2368            report_cummulative("font %!font:name!, reset",fontdata[true])
2369        end
2370        texsetglyphdata(0)
2371    end
2372
2373    local function setfontfeature(tag)
2374        texsetglyphdata(contextnumber(tag))
2375    end
2376
2377    local function resetfontfeature()
2378        texsetglyphdata(0)
2379    end
2380
2381    implement {
2382        name      = "nbfs",
2383        arguments = "dimen",
2384        actions   = function(d)
2385            context(lpegmatch(stripper,f_strip(d/65536)))
2386        end
2387    }
2388
2389    implement {
2390        name      = "featureattribute",
2391        arguments = "string",
2392        actions   = { contextnumber, context }
2393    }
2394
2395    implement {
2396        name      = "setfontfeature",
2397        arguments = "string",
2398        actions   = setfontfeature,
2399    }
2400
2401    implement {
2402        name      = "resetfontfeature",
2403     -- arguments = { 0, 0 },
2404        actions   = resetfontfeature,
2405    }
2406
2407    implement {
2408        name      = "setfontofid",
2409        arguments = "integer",
2410        actions   = function(id)
2411            ctx_getvalue(csnames[id])
2412        end
2413    }
2414
2415    implement {
2416        name      = "definefontfeature",
2417        arguments = "3 strings",
2418        actions   = presetcontext,
2419    }
2420
2421    implement {
2422        name      = "doifelsefontfeature",
2423        arguments = "string",
2424        actions   = function(name) ctx_doifelse(contextnumber(name) > 1) end,
2425    }
2426
2427    implement {
2428        name      = "doifunknownfontfeature",
2429        arguments = "string",
2430        actions   = function(name) ctx_doif(contextnumber(name) == 0) end,
2431    }
2432
2433    implement {
2434        name      = "adaptfontfeature",
2435        arguments = "2 strings",
2436        actions   = adaptcontext
2437    }
2438
2439    local function registerlanguagefeatures()
2440        local specifications = languages.data.specifications
2441        for i=1,#specifications do
2442            local specification = specifications[i]
2443            local language = specification.opentype
2444            if language then
2445                local script = specification.opentypescript or specification.script
2446                if script then
2447                    local context = specification.context
2448                    if type(context) == "table" then
2449                        for i=1,#context do
2450                            definecontext(context[i], { language = language, script = script})
2451                        end
2452                    elseif type(context) == "string" then
2453                       definecontext(context, { language = language, script = script})
2454                    end
2455                end
2456            end
2457        end
2458    end
2459
2460    constructors.setfeature   = setfeature
2461    constructors.resetfeature = resetfeature
2462
2463    implement { name = "resetfeature",    actions = resetfeature }
2464    implement { name = "addfeature",      actions = setfeature, arguments = { "'+'",  "string", "string" } }
2465    implement { name = "subtractfeature", actions = setfeature, arguments = { "'-'",  "string", "string" } }
2466    implement { name = "replacefeature",  actions = setfeature, arguments = { "'='",  "string", "string" } }
2467    implement { name = "revivefeature",   actions = setfeature, arguments = { true, "string" } }
2468
2469    implement {
2470        name      = "featurelist",
2471        actions   = { fonts.specifiers.contexttostring, context },
2472        arguments = { "string", "'otf'", "string", "'yes'", "'no'", true }
2473    }
2474
2475    implement {
2476        name    = "registerlanguagefeatures",
2477        actions = registerlanguagefeatures,
2478    }
2479
2480end
2481
2482-- a fontkern plug:
2483
2484-- nodes.injections.installnewkern(nuts.pool.fontkern)
2485
2486do
2487
2488    local report = logs.reporter("otf","variants")
2489
2490    local function replace(tfmdata,feature,value)
2491        local characters = tfmdata.characters
2492        local variants   = tfmdata.resources.variants
2493        if variants then
2494            local t = { }
2495            for k, v in sortedhash(variants) do
2496                t[#t+1] = formatters["0x%X (%i)"](k,k)
2497            end
2498            value = tonumber(value) or 0xFE00 -- 917762
2499            report("fontname : %s",tfmdata.properties.fontname)
2500            report("available: % t",t)
2501            local v = variants[value]
2502            if v then
2503                report("using    : %X (%i)",value,value)
2504                for k, v in next, v do
2505                    local c = characters[v]
2506                    if c then
2507                        characters[k] = c
2508                    end
2509                end
2510            else
2511                report("unknown  : %X (%i)",value,value)
2512            end
2513        end
2514    end
2515
2516    registerotffeature {
2517        name         = 'variant',
2518        description  = 'unicode variant',
2519        manipulators = {
2520            base = replace,
2521            node = replace,
2522        }
2523    }
2524
2525end
2526
2527-- here (todo: closure)
2528
2529-- make a closure (200 limit):
2530
2531do
2532
2533    local trace_analyzing = false  trackers.register("otf.analyzing", function(v) trace_analyzing = v end)
2534
2535    local analyzers       = fonts.analyzers
2536    local methods         = analyzers.methods
2537
2538    local unsetvalue      = attributes.unsetvalue
2539
2540    local a_color         = attributes.private('color')
2541    local a_colormodel    = attributes.private('colormodel')
2542    local m_color         = attributes.list[a_color] or { }
2543
2544    local glyph_code      = nodes.nodecodes.glyph
2545
2546    local states          = analyzers.states
2547
2548    local colornames = {
2549        [states.init] = "font:1",
2550        [states.medi] = "font:2",
2551        [states.fina] = "font:3",
2552        [states.isol] = "font:4",
2553        [states.mark] = "font:5",
2554        [states.rest] = "font:6",
2555        [states.rphf] = "font:1",
2556        [states.half] = "font:2",
2557        [states.pref] = "font:3",
2558        [states.blwf] = "font:4",
2559        [states.pstf] = "font:5",
2560    }
2561
2562    -- todo: traversers
2563    -- todo: check attr_list so that we can use the same .. helper: setcolorattr
2564
2565    local function markstates(head)
2566        if head then
2567            head = tonut(head)
2568            local model = getattr(head,a_colormodel) or 1
2569            for glyph in nextchar, head do
2570                local a = getstate(glyph)
2571                if a then
2572                    local name = colornames[a]
2573                    if name then
2574                        local color = m_color[name]
2575                        if color then
2576                            setattr(glyph,a_colormodel,model)
2577                            setattr(glyph,a_color,color)
2578                        end
2579                    end
2580                end
2581            end
2582        end
2583    end
2584
2585    local function analyzeprocessor(head,font,attr)
2586        local tfmdata = fontdata[font]
2587        local script, language = otf.scriptandlanguage(tfmdata,attr)
2588        local action = methods[script]
2589        if not action then
2590            return head, false
2591        end
2592        if type(action) == "function" then
2593            local head, done = action(head,font,attr)
2594            if done and trace_analyzing then
2595                markstates(head)
2596            end
2597            return head, done
2598        end
2599        action = action[language]
2600        if action then
2601            local head, done = action(head,font,attr)
2602            if done and trace_analyzing then
2603                markstates(head)
2604            end
2605            return head, done
2606        else
2607            return head, false
2608        end
2609    end
2610
2611    registerotffeature { -- adapts
2612        name       = "analyze",
2613        processors = {
2614            node     = analyzeprocessor,
2615        }
2616    }
2617
2618
2619    function methods.nocolor(head,font,attr)
2620        for n, c, f in nextchar, head do
2621            if not font or f == font then
2622                setattr(n,a_color,unsetvalue)
2623            end
2624        end
2625        return head, true
2626    end
2627
2628end
2629
2630
2631local function purefontname(name)
2632    if type(name) == "number" then
2633        name = getfontname(name)
2634    end
2635    if type(name) == "string" then
2636       return basename(name)
2637    end
2638end
2639
2640implement {
2641    name      = "purefontname",
2642    actions   = { purefontname, context },
2643    arguments = "string",
2644}
2645
2646local sharedstorage = storage.shared
2647
2648local list    = sharedstorage.bodyfontsizes        or { }
2649local unknown = sharedstorage.unknownbodyfontsizes or { }
2650
2651sharedstorage.bodyfontsizes        = list
2652sharedstorage.unknownbodyfontsizes = unknown
2653
2654implement {
2655    name      = "registerbodyfontsize",
2656    arguments = "string",
2657    actions   = function(size)
2658        list[size] = true
2659    end
2660}
2661
2662interfaces.implement {
2663    name      = "registerunknownbodysize",
2664    arguments = "string",
2665    actions   = function(size)
2666        if not unknown[size] then
2667            interfaces.showmessage("fonts",14,size)
2668        end
2669        unknown[size] = true
2670    end,
2671}
2672
2673implement {
2674    name      = "getbodyfontsizes",
2675    arguments = "string",
2676    actions   = function(separator)
2677        context(concat(sortedkeys(list),separator))
2678    end
2679}
2680
2681implement {
2682    name      = "processbodyfontsizes",
2683    arguments = "string",
2684    actions   = function(command)
2685        local keys = sortedkeys(list)
2686        if command then
2687            local action = context[command]
2688            for i=1,#keys do
2689                action(keys[i])
2690            end
2691        else
2692            context(concat(keys,","))
2693        end
2694    end
2695}
2696
2697implement {
2698    name      = "cleanfontname",
2699    actions   = { cleanname, context },
2700    arguments = "string"
2701}
2702
2703implement {
2704    name      = "fontlookupinitialize",
2705    actions   = names.lookup,
2706    arguments = "string",
2707}
2708
2709implement {
2710    name      = "fontlookupnoffound",
2711    actions   = { names.noflookups, context },
2712}
2713
2714implement {
2715    name      = "fontlookupgetkeyofindex",
2716    actions   = { names.getlookupkey, context },
2717    arguments = { "string", "integer"}
2718}
2719
2720implement {
2721    name      = "fontlookupgetkey",
2722    actions   = { names.getlookupkey, context },
2723    arguments = "string"
2724}
2725
2726-- this might move to a runtime module:
2727
2728function commands.showchardata(n)
2729    local tfmdata = fontdata[currentfont()]
2730    if tfmdata then
2731        if type(n) == "string" then
2732            n = utfbyte(n)
2733        end
2734        local chr = tfmdata.characters[n]
2735        if chr then
2736            report_status("%s @ %s => %U => %c => %s",tfmdata.properties.fullname,tfmdata.parameters.size,n,n,serialize(chr,false))
2737        end
2738    end
2739end
2740
2741function commands.showfontparameters(tfmdata)
2742    -- this will become more clever
2743    local tfmdata = tfmdata or fontdata[currentfont()]
2744    if tfmdata then
2745        local parameters        = tfmdata.parameters
2746        local mathparameters    = tfmdata.mathparameters
2747        local properties        = tfmdata.properties
2748        local hasparameters     = parameters     and next(parameters)
2749        local hasmathparameters = mathparameters and next(mathparameters)
2750        if hasparameters then
2751            report_status("%s @ %s => text parameters => %s",properties.fullname,parameters.size,serialize(parameters,false))
2752        end
2753        if hasmathparameters then
2754            report_status("%s @ %s => math parameters => %s",properties.fullname,parameters.size,serialize(mathparameters,false))
2755        end
2756        if not hasparameters and not hasmathparameters then
2757            report_status("%s @ %s => no text parameters and/or math parameters",properties.fullname,parameters.size)
2758        end
2759    end
2760end
2761
2762implement {
2763    name    = "currentdesignsize",
2764    actions = function()
2765        context(parameters[currentfont()].designsize)
2766    end
2767}
2768
2769implement {
2770    name      = "doifelsefontpresent",
2771    actions   = { names.exists, commands.doifelse },
2772    arguments = "string"
2773}
2774
2775-- we use 0xFE000+ and 0xFF000+ in math and for runtime (text) extensions we
2776-- use 0xFD000+
2777
2778constructors.privateslots = constructors.privateslots or { }
2779
2780storage.register("fonts/constructors/privateslots", constructors.privateslots, "fonts.constructors.privateslots")
2781
2782do
2783
2784    local privateslots    = constructors.privateslots
2785    local lastprivateslot = 0xFD000
2786
2787    constructors.privateslots = setmetatableindex(privateslots,function(t,k)
2788        local v = lastprivateslot
2789        lastprivateslot = lastprivateslot + 1
2790        t[k] = v
2791        return v
2792    end)
2793
2794    implement {
2795        name      = "getprivateglyphslot",
2796        actions   = function(name) context(privateslots[name]) end,
2797        arguments = "string",
2798    }
2799
2800end
2801
2802-- an extra helper
2803
2804function helpers.getcoloredglyphs(tfmdata)
2805    if type(tfmdata) == "number" then
2806        tfmdata = fontdata[tfmdata]
2807    end
2808    if not tfmdata then
2809        tfmdata = fontdata[true]
2810    end
2811    local characters   = tfmdata.characters
2812    local descriptions = tfmdata.descriptions
2813    local collected    = { }
2814    for unicode, character in next, characters do
2815        local description = descriptions[unicode]
2816        if description and (description.colors or character.svg) then
2817            collected[#collected+1] = unicode
2818        end
2819    end
2820    table.sort(collected)
2821    return collected
2822end
2823
2824-- for the font manual
2825
2826statistics.register("body font sizes", function()
2827    if next(unknown) then
2828        return formatters["defined: % t, undefined: % t"](sortedkeys(list),sortedkeys(unknown))
2829    end
2830end)
2831
2832statistics.register("used fonts",function()
2833    if trace_usage then
2834        local filename = file.nameonly(environment.jobname) .. "-fonts-usage.lua"
2835        if next(fontdata) then
2836            local files = { }
2837            local list  = { }
2838            for id, tfmdata in sortedhash(fontdata) do
2839                local filename = tfmdata.properties.filename
2840                if filename then
2841                    local filedata = files[filename]
2842                    if filedata then
2843                        filedata.instances = filedata.instances + 1
2844                    else
2845                        local rawdata  = tfmdata.shared and tfmdata.shared.rawdata
2846                        local metadata = rawdata and rawdata.metadata
2847                        files[filename] = {
2848                            instances = 1,
2849                            filename  = filename,
2850                            version   = metadata and metadata.version,
2851                            size      = rawdata and rawdata.size,
2852                        }
2853                    end
2854                else
2855                    -- what to do
2856                end
2857            end
2858            for k, v in sortedhash(files) do
2859                list[#list+1] = v
2860            end
2861            table.save(filename,list)
2862        else
2863            os.remove(filename)
2864        end
2865    end
2866end)
2867
2868-- new
2869
2870do
2871
2872    local settings_to_array = utilities.parsers.settings_to_array
2873
2874    implement {
2875        name      = "definefontcolorpalette",
2876        arguments = "2 strings",
2877        actions   = function(name,set)
2878            otf.registerpalette(name,settings_to_array(set))
2879        end
2880    }
2881
2882end
2883
2884do
2885
2886    local pattern = C((1-S("* "))^1) -- strips all after * or ' at'
2887
2888    implement {
2889        name      = "truefontname",
2890        arguments = "string",
2891        actions   = function(s)
2892         -- context(match(s,"[^* ]+") or s)
2893            context(lpegmatch(pattern,s) or s)
2894        end
2895    }
2896
2897end
2898
2899do
2900
2901    local function getinstancespec(id)
2902        local data      = fontdata[id or true]
2903        local shared    = data.shared
2904        local resources = shared and shared.rawdata.resources
2905        if resources then
2906            local instancespec = data.properties.instance
2907            if instancespec then
2908                local variabledata = resources.variabledata
2909                if variabledata then
2910                    local instances = variabledata.instances
2911                    if instances then
2912                        for i=1,#instances do
2913                            local instance = instances[i]
2914                            if cleanname(instance.subfamily)== instancespec then
2915                                local values = table.copy(instance.values)
2916                                local axis = variabledata.axis
2917                                for i=1,#values do
2918                                    for j=1,#axis do
2919                                        if values[i].axis == axis[j].tag then
2920                                            values[i].name = axis[j].name
2921                                            break
2922                                        end
2923                                    end
2924                                end
2925                                return values
2926                            end
2927                        end
2928                    end
2929                end
2930            end
2931        end
2932    end
2933
2934    helpers.getinstancespec = getinstancespec
2935
2936    -- expandable
2937
2938    implement {
2939        name      = "currentfontinstancespec",
2940        public    = true,
2941        actions   = function()
2942            local t = getinstancespec() -- current font
2943            if t then
2944                for i=1,#t do
2945                    if i > 1 then
2946                        context.space()
2947                    end
2948                    local ti = t[i]
2949                    context("%s=%s",ti.name,ti.value)
2950                end
2951            end
2952        end
2953    }
2954
2955end
2956
2957-- for the moment here (and not in font-con.lua):
2958
2959do
2960
2961    local identical     = table.identical
2962    local copy          = table.copy
2963    local fontdata      = fonts.hashes.identifiers
2964    local addcharacters = font.addcharacters
2965
2966    -- This helper is mostly meant to add last-resort (virtual) characters
2967    -- or runtime generated fonts (so we forget about features and such). It
2968    -- will probably take a while before it get used.
2969
2970    local trace_adding  = false
2971    local report_adding = logs.reporter("fonts","add characters")
2972
2973    trackers.register("fonts.addcharacters",function(v) trace_adding = v end)
2974
2975    function constructors.addcharacters(id,list)
2976        local newchar = list.characters
2977        if newchar then
2978            local data    = fontdata[id]
2979            local newfont = list.fonts
2980            local oldchar = data.characters
2981            local oldfont = data.fonts
2982            addcharacters(id, {
2983                characters = newchar,
2984                fonts      = newfont,
2985                nomath     = not data.properties.hasmath,
2986            })
2987            -- this is just for tracing, as the assignment only uses the fonts list
2988            -- and doesn't store it otherwise
2989            if newfont then
2990                if oldfont then
2991                    local oldn = #oldfont
2992                    local newn = #newfont
2993                    for n=1,newn do
2994                        local ok = false
2995                        local nf = newfont[n]
2996                        for o=1,oldn do
2997                            if identical(nf,oldfont[o]) then
2998                                ok = true
2999                                break
3000                            end
3001                        end
3002                        if not ok then
3003                            oldn = oldn + 1
3004                            oldfont[oldn] = newfont[i]
3005                        end
3006                    end
3007                else
3008                    data.fonts = newfont
3009                end
3010            end
3011            -- this is because we need to know what goes on and also might
3012            -- want to access character data
3013            for u, c in next, newchar do
3014                if trace_adding then
3015                    report_adding("adding character %U to font %!font:name!",u,id)
3016                end
3017                oldchar[u] = c
3018            end
3019        end
3020    end
3021
3022    implement {
3023        name      = "addfontpath",
3024        arguments = "string",
3025        actions   = function(list)
3026            names.addruntimepath(settings_to_array(list))
3027        end
3028    }
3029
3030end
3031
3032-- moved here
3033
3034do
3035
3036    local getfontoffamily = tex.getfontoffamily
3037    local new_glyph       = nodes.pool.glyph
3038    local fontproperties  = fonts.hashes.properties
3039
3040    local function getprivateslot(id,name)
3041        if not name then
3042            name = id
3043            id   = currentfont()
3044        end
3045        local properties = fontproperties[id]
3046        local privates   = properties and properties.privates
3047        return privates and privates[name]
3048    end
3049
3050    local function getprivatenode(tfmdata,name)
3051        if type(tfmdata) == "number" then
3052            tfmdata = fontdata[tfmdata]
3053        end
3054        local properties = tfmdata.properties
3055        local font = properties.id
3056        local slot = getprivateslot(font,name)
3057        if slot then
3058            -- todo: set current attribibutes
3059            local char   = tfmdata.characters[slot]
3060            local tonode = char.tonode
3061            if tonode then
3062                return tonode(font,char)
3063            else
3064                return new_glyph(font,slot)
3065            end
3066        end
3067    end
3068
3069    local function getprivatecharornode(tfmdata,name)
3070        if type(tfmdata) == "number" then
3071            tfmdata = fontdata[tfmdata]
3072        end
3073        local properties = tfmdata.properties
3074        local font = properties.id
3075        local slot = getprivateslot(font,name)
3076        if slot then
3077            -- todo: set current attributes
3078            local char   = tfmdata.characters[slot]
3079            local tonode = char.tonode
3080            if tonode then
3081                return "node", tonode(tfmdata,char)
3082            else
3083                return "char", slot
3084            end
3085        end
3086    end
3087
3088    helpers.getprivateslot       = getprivateslot
3089    helpers.getprivatenode       = getprivatenode
3090    helpers.getprivatecharornode = getprivatecharornode
3091
3092    implement {
3093        name      = "getprivatechar",
3094        arguments = "string",
3095        actions   = function(name)
3096            local p = getprivateslot(name)
3097            if p then
3098                context(utfchar(p))
3099            end
3100        end
3101    }
3102
3103    implement {
3104        name      = "getprivatemathchar",
3105        arguments = "string",
3106        actions   = function(name)
3107            local p = getprivateslot(getfontoffamily(0),name)
3108            if p then
3109                context(utfchar(p))
3110            end
3111        end
3112    }
3113
3114    implement {
3115        name      = "getprivateslot",
3116        arguments = "string",
3117        actions   = function(name)
3118            local p = getprivateslot(name)
3119            if p then
3120                context(p)
3121            end
3122        end
3123    }
3124
3125end
3126
3127-- handy, for now here:
3128
3129function fonts.helpers.collectanchors(tfmdata)
3130
3131    local resources = tfmdata.resources -- todo: use shared
3132
3133    if not resources or resources.anchors then
3134        return resources.anchors
3135    end
3136
3137    local anchors = { }
3138
3139    local function set(unicode,target,class,anchor)
3140        local a = anchors[unicode]
3141        if not a then
3142            anchors[unicode] = { [target] = { anchor } }
3143            return
3144        end
3145        local t = a[target]
3146        if not t then
3147            a[target] = { anchor }
3148            return
3149        end
3150        local x = anchor[1]
3151        local y = anchor[2]
3152        for k, v in next, t do
3153            if v[1] == x and v[2] == y then
3154                return
3155            end
3156        end
3157        t[#t+1] = anchor
3158    end
3159
3160    local function getanchors(steps,target)
3161        for i=1,#steps do
3162            local step     = steps[i]
3163            local coverage = step.coverage
3164            for unicode, data in next, coverage do
3165                local class  = data[1]
3166                local anchor = data[2]
3167                if anchor[1] ~= 0 or anchor[2] ~= 0 then
3168                    set(unicode,target,class,anchor)
3169                end
3170            end
3171        end
3172    end
3173
3174    local function getcursives(steps)
3175        for i=1,#steps do
3176            local step     = steps[i]
3177            local coverage = step.coverage
3178            for unicode, data in next, coverage do
3179                local class  = data[1]
3180                local en = data[2]
3181                local ex = data[3]
3182                if en then
3183                    set(unicode,"entry",class,en)
3184                end
3185                if ex then
3186                    set(unicode,"exit", class,ex)
3187                end
3188            end
3189        end
3190    end
3191
3192    local function collect(list)
3193        if list then
3194            for i=1,#list do
3195                local entry = list[i]
3196                local steps = entry.steps
3197                local kind  = entry.type
3198                if kind == "gpos_mark2mark" then
3199                    getanchors(steps,"mark")
3200                elseif kind == "gpos_mark2base" then
3201                    getanchors(steps,"base")
3202                elseif kind == "gpos_mark2ligature" then
3203                    getanchors(steps,"ligature")
3204                elseif kind == "gpos_cursive" then
3205                    getcursives(steps)
3206                end
3207            end
3208        end
3209    end
3210
3211    collect(resources.sequences)
3212    collect(resources.sublookups)
3213
3214    local function sorter(a,b)
3215        if a[1] == b[1] then
3216            return a[2] < b[2]
3217        else
3218            return a[1] < b[1]
3219        end
3220    end
3221
3222    for unicode, old in next, anchors do
3223        for target, list in next, old do
3224            sort(list,sorter)
3225        end
3226    end
3227
3228    resources.anchors = anchors
3229
3230    return anchors
3231
3232end
3233