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