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