font-def.lmt /size: 19 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['font-def'] = {
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-- We can overload some of the definers.functions so we don't local them.
10
11local lower, gsub = string.lower, string.gsub
12local tostring, next = tostring, next
13local lpegmatch = lpeg.match
14local suffixonly, removesuffix, basename = file.suffix, file.removesuffix, file.basename
15local formatters = string.formatters
16local sortedhash, sortedkeys, setmetatableindex = table.sortedhash, table.sortedkeys, table.setmetatableindex
17
18local allocate = utilities.storage.allocate
19
20local trace_defining     = false  trackers  .register("fonts.defining", function(v) trace_defining     = v end)
21local directive_embedall = false  directives.register("fonts.embedall", function(v) directive_embedall = v end)
22
23trackers.register("fonts.loading", "fonts.defining", "otf.loading", "afm.loading", "tfm.loading")
24
25local report_defining = logs.reporter("fonts","defining")
26
27-- Here we deal with defining fonts. We do so by intercepting the default loader
28-- that only handles TFM files. Although, we started out that way but in the
29-- meantime we can hardly speak of TFM any more.
30
31local nextfont      = font.nextid
32
33local fonts         = fonts
34local fontdata      = fonts.hashes.identifiers
35local readers       = fonts.readers
36local definers      = fonts.definers
37local specifiers    = fonts.specifiers
38local constructors  = fonts.constructors
39local fontgoodies   = fonts.goodies
40
41readers.sequence    = allocate { 'otf', 'ttf', 'afm', 'tfm', 'lua' } -- dfont ttc
42
43local variants      = allocate()
44specifiers.variants = variants
45
46definers.methods    = definers.methods or { }
47
48local internalized  = allocate() -- internal tex numbers (private)
49
50local loadedfonts   = constructors.loadedfonts
51local designsizes   = constructors.designsizes
52
53-- not in generic (some day I'll make two defs, one for context, one for generic)
54
55local resolvefile   = fontgoodies and fontgoodies.filenames and fontgoodies.filenames.resolve or function(s) return s end
56
57-- We hardly gain anything when we cache the final (pre scaled) TFM table. But it
58-- can be handy for debugging, so we no longer carry this code along. Also, we now
59-- have quite some reference to other tables so we would end up with lots of
60-- catches.
61--
62-- We can prefix a font specification by "name:" or "file:". The first case will
63-- result in a lookup in the synonym table.
64--
65--   [ name: | file: ] identifier [ separator [ specification ] ]
66--
67-- The following function split the font specification into components and prepares
68-- a table that will move along as we proceed.
69
70-- beware, we discard additional specs
71--
72-- method:name method:name(sub) method:name(sub)*spec method:name*spec
73-- name name(sub) name(sub)*spec name*spec
74-- name@spec*oeps
75
76local function makespecification(specification,lookup,name,sub,method,detail,size)
77    size = size or 655360
78    if not lookup or lookup == "" then
79        lookup = definers.defaultlookup
80    end
81    if trace_defining then
82        report_defining("specification %a, lookup %a, name %a, sub %a, method %a, detail %a",
83            specification, lookup, name, sub, method, detail)
84    end
85    local t = {
86        lookup        = lookup,        -- forced type
87        specification = specification, -- full specification
88        size          = size,          -- size in scaled points or -1000*n
89        name          = name,          -- font or filename
90        sub           = sub,           -- subfont (eg in ttc)
91        method        = method,        -- specification method
92        detail        = detail,        -- specification
93        resolved      = "",            -- resolved font name
94        forced        = "",            -- forced loader
95        features      = { },           -- preprocessed features
96    }
97    return t
98end
99
100definers.makespecification = makespecification
101
102do
103
104    local splitter, splitspecifiers = nil, "" -- not so nice
105
106    local P, C, S, Cc, Cs = lpeg.P, lpeg.C, lpeg.S, lpeg.Cc, lpeg.Cs
107
108    local left   = P("(")
109    local right  = P(")")
110    local colon  = P(":")
111    local space  = P(" ")
112    local lbrace = P("{")
113    local rbrace = P("}")
114
115    definers.defaultlookup = "file"
116
117    local prefixpattern = P(false)
118
119    local function addspecifier(symbol)
120        splitspecifiers     = splitspecifiers .. symbol
121        local method        = S(splitspecifiers)
122        local lookup        = C(prefixpattern) * colon
123        local sub           = left * C(P(1-left-right-method)^1) * right
124        local specification = C(method) * C(P(1)^1)
125        local name          = Cs((lbrace/"") * (1-rbrace)^1 * (rbrace/"") + (1-sub-specification)^1)
126        splitter = P((lookup + Cc("")) * name * (sub + Cc("")) * (specification + Cc("")))
127    end
128
129    local function addlookup(str)
130        prefixpattern = prefixpattern + P(str)
131    end
132
133    definers.addlookup = addlookup
134
135    addlookup("file")
136    addlookup("name")
137    addlookup("spec")
138
139    local function getspecification(str)
140        return lpegmatch(splitter,str or "") -- weird catch
141    end
142
143    definers.getspecification = getspecification
144
145    function definers.registersplit(symbol,action,verbosename)
146        addspecifier(symbol)
147        variants[symbol] = action
148        if verbosename then
149            variants[verbosename] = action
150        end
151    end
152
153    function definers.analyze(specification, size)
154        -- can be optimized with locals
155        local lookup, name, sub, method, detail = getspecification(specification or "")
156        return makespecification(specification, lookup, name, sub, method, detail, size)
157    end
158
159end
160
161-- We can resolve the filename using the next function:
162
163definers.resolvers = definers.resolvers or { }
164local resolvers    = definers.resolvers
165
166-- todo: reporter
167
168function resolvers.file(specification)
169    local name = resolvefile(specification.name) -- catch for renames
170    local suffix = lower(suffixonly(name))
171    if fonts.formats[suffix] then
172        specification.forced     = suffix
173        specification.forcedname = name
174        specification.name       = removesuffix(name)
175    else
176        -- maybe when no suffix and not found, then still run over suffixes
177        specification.name       = name -- can be resolved
178    end
179end
180
181function resolvers.name(specification)
182    local resolve = fonts.names.resolve
183    if resolve then
184        local resolved, sub, subindex, instance = resolve(specification.name,specification.sub,specification) -- we pass specification for overloaded versions
185        if resolved then
186            specification.resolved = resolved
187            specification.sub      = sub
188            specification.subindex = subindex
189            -- new, needed for experiments
190            if instance then
191                specification.instance = instance
192                local features = specification.features
193                if not features then
194                    features = { }
195                    specification.features = features
196                end
197                local normal = features.normal
198                if not normal then
199                    normal = { }
200                    features.normal = normal
201                end
202                normal.instance = instance
203            end
204            --
205            local suffix = lower(suffixonly(resolved))
206            if fonts.formats[suffix] then
207                specification.forced     = suffix
208                specification.forcedname = resolved
209                specification.name       = removesuffix(resolved)
210            else
211                specification.name       = resolved
212            end
213        end
214    else
215        resolvers.file(specification)
216    end
217end
218
219function resolvers.spec(specification)
220    local resolvespec = fonts.names.resolvespec
221    if resolvespec then
222        local resolved, sub, subindex = resolvespec(specification.name,specification.sub,specification) -- we pass specification for overloaded versions
223        if resolved then
224            specification.resolved   = resolved
225            specification.sub        = sub
226            specification.subindex   = subindex
227            specification.forced     = lower(suffixonly(resolved))
228            specification.forcedname = resolved
229            specification.name       = removesuffix(resolved)
230        end
231    else
232        resolvers.name(specification)
233    end
234end
235
236function definers.resolve(specification)
237    if not specification.resolved or specification.resolved == "" then -- resolved itself not per se in mapping hash
238        local r = resolvers[specification.lookup]
239        if r then
240            r(specification)
241        end
242    end
243    if specification.forced == "" then
244        specification.forced     = nil
245        specification.forcedname = nil
246    end
247    specification.hash = lower(specification.name .. ' @ ' .. constructors.hashfeatures(specification))
248    if specification.sub and specification.sub ~= "" then
249        specification.hash = specification.sub .. ' @ ' .. specification.hash
250    end
251    return specification
252end
253
254-- The main read function either uses a forced reader (as determined by a lookup) or
255-- tries to resolve the name using the list of readers.
256--
257-- We need to cache when possible. We do cache raw tfm data (from TFM, AFM or OTF).
258-- After that we can cache based on specificstion (name) and size, that is, TeX only
259-- needs a number for an already loaded fonts. However, it may make sense to cache
260-- fonts before they're scaled as well (store TFM's with applied methods and
261-- features). However, there may be a relation between the size and features (esp in
262-- virtual fonts) so let's not do that now.
263--
264-- Watch out, here we do load a font, but we don't prepare the specification yet.
265
266function definers.applypostprocessors(tfmdata)
267    local postprocessors = tfmdata.postprocessors
268    if postprocessors then
269        local properties = tfmdata.properties
270        for i=1,#postprocessors do
271            local extrahash = postprocessors[i](tfmdata) -- after scaling etc
272            if type(extrahash) == "string" and extrahash ~= "" then
273                -- e.g. a reencoding needs this
274                extrahash = gsub(lower(extrahash),"[^a-z]","-")
275                properties.fullname = formatters["%s-%s"](properties.fullname,extrahash)
276            end
277        end
278    end
279    return tfmdata
280end
281
282-- function definers.applypostprocessors(tfmdata)
283--     return tfmdata
284-- end
285
286local function checkfeatures(tfmdata)
287    local resources = tfmdata.resources
288    local shared    = tfmdata.shared
289    if resources and shared then
290        local features     = resources.features
291        local usedfeatures = shared.features
292        if features and usedfeatures then
293            local usedlanguage = usedfeatures.language or "dflt"
294            local usedscript   = usedfeatures.script or "dflt"
295            local function check(what)
296                if what then
297                    local foundlanguages = { }
298                    for feature, scripts in next, what do
299                        if usedscript == "auto" or scripts["*"] then
300                            -- ok
301                        elseif not scripts[usedscript] then
302                         -- report_defining("font %!font:name!, feature %a, no script %a",
303                         --     tfmdata,feature,usedscript)
304                        else
305                            for script, languages in next, scripts do
306                                if languages["*"] then
307                                    -- ok
308                                elseif not languages[usedlanguage] then
309                                    report_defining("font %!font:name!, feature %a, script %a, no language %a",
310                                        tfmdata,feature,script,usedlanguage)
311                                end
312                            end
313                        end
314                        for script, languages in next, scripts do
315                            for language in next, languages do
316                                foundlanguages[language] = true
317                            end
318                        end
319                    end
320                    if false then
321                        foundlanguages["*"] = nil
322                        foundlanguages = sortedkeys(foundlanguages)
323                        for feature, scripts in sortedhash(what) do
324                            for script, languages in next, scripts do
325                                if not languages["*"] then
326                                    for i=1,#foundlanguages do
327                                        local language = foundlanguages[i]
328                                        if not languages[language] then
329                                            report_defining("font %!font:name!, feature %a, script %a, no language %a",
330                                                tfmdata,feature,script,language)
331                                        end
332                                    end
333                                end
334                            end
335                        end
336                    end
337                end
338            end
339            check(features.gsub)
340            check(features.gpos)
341        end
342    end
343end
344
345local reported = setmetatableindex(function(t,k)
346    if k then
347        t[k] = true
348    end
349    return false
350end)
351
352function definers.loadfont(specification)
353    local hash = constructors.hashinstance(specification)
354    -- todo: also hash by instance / factors
355    local tfmdata = loadedfonts[hash] -- hashes by size !
356    local name    = specification.name
357    if not tfmdata then
358        -- normally context will not end up here often (if so there is an issue somewhere)
359        local forced = specification.forced or ""
360        local id = nextfont(true)
361        specification.id = id
362        if forced ~= "" then
363            local reader = readers[lower(forced)] -- normally forced is already lowered
364            tfmdata = reader and reader(specification)
365            if not tfmdata and not reported[name] then
366                report_defining("forced type %a of %a not found",forced,name)
367            end
368        else
369            local sequence = readers.sequence -- can be overloaded so only a shortcut here
370            for s=1,#sequence do
371                local reader = sequence[s]
372                if readers[reader] then -- we skip not loaded readers
373                    if trace_defining then
374                        report_defining("trying (reader sequence driven) type %a for %a with file %a",reader,specification.name,specification.filename)
375                    end
376                    tfmdata = readers[reader](specification)
377                    if tfmdata then
378                        break
379                    else
380                        specification.filename = nil
381                    end
382                end
383            end
384        end
385        if tfmdata then
386            tfmdata = definers.applypostprocessors(tfmdata)
387            loadedfonts[hash] = tfmdata
388            designsizes[specification.hash] = tfmdata.parameters.designsize
389            checkfeatures(tfmdata)
390        end
391    end
392    if not tfmdata and not reported[name] then
393        report_defining("font with asked name %a is not found using lookup %a",name,specification.lookup)
394    end
395    return tfmdata
396end
397
398function constructors.readanddefine(name,size) -- no id -- maybe a dummy first
399    local specification = definers.analyze(name,size)
400    local method = specification.method
401    if method and variants[method] then
402        specification = variants[method](specification)
403    end
404    specification = definers.resolve(specification)
405    local hash = constructors.hashinstance(specification)
406    local id = definers.registered(hash)
407    if not id then
408        local tfmdata = definers.loadfont(specification)
409        if tfmdata then
410            tfmdata.properties.hash = hash
411            id = font.define(tfmdata)
412            definers.register(tfmdata,id)
413        else
414            id = 0  -- signal
415        end
416    end
417    return fontdata[id], id
418end
419
420-- So far the specifiers. Now comes the real definer. Here we cache based on id's.
421-- Here we also intercept the virtual font handler.
422--
423-- In the previously defined reader (the one resulting in a TFM table) we cached the
424-- (scaled) instances. Here we cache them again, but this time based on id. We could
425-- combine this in one cache but this does not gain much. By the way, passing id's
426-- back to in the callback was introduced later in the development.
427
428function definers.registered(hash)
429    local id = internalized[hash]
430    return id, id and fontdata[id]
431end
432
433function definers.register(tfmdata,id)
434    if tfmdata and id then
435        local hash = tfmdata.properties.hash
436        if not hash then
437            report_defining("registering font, id %a, name %a, invalid hash",id,tfmdata.properties.filename or "?")
438        elseif not internalized[hash] then
439            internalized[hash] = id
440            if trace_defining then
441                report_defining("registering font, id %s, hash %a",id,hash)
442            end
443            fontdata[id] = tfmdata
444        end
445    end
446end
447
448function definers.read(specification,size,id,compact) -- id can be optional, name can already be table
449    statistics.starttiming(fonts)
450    if type(specification) == "string" then
451        specification = definers.analyze(specification,size)
452    end
453    local method = specification.method
454    if method and variants[method] then
455        specification = variants[method](specification)
456    end
457    specification = definers.resolve(specification)
458--     local hash    = constructors.hashinstance(specification,false,true)
459    local hash, extend, squeeze, slant, weight, auto = constructors.hashinstance(specification,false,compact)
460    local tfmdata = definers.registered(hash) -- id
461    local name    = specification.name
462    if tfmdata then
463        if trace_defining then
464            report_defining("already hashed: %s",hash)
465        end
466    else
467        tfmdata = definers.loadfont(specification) -- can be overloaded
468        if tfmdata then
469            tfmdata.original = specification.specification
470            if trace_defining then
471                report_defining("loaded and hashed: %s",hash)
472            end
473            tfmdata.properties.hash = hash
474            if id then
475                definers.register(tfmdata,id)
476            end
477        else
478            if trace_defining then
479                report_defining("not loaded and hashed: %s",hash)
480            end
481        end
482    end
483    if not tfmdata then -- or id?
484        if not reported[name] then
485            report_defining( "unknown font %a, loading aborted",name)
486        end
487    elseif trace_defining and type(tfmdata) == "table" then
488        local properties = tfmdata.properties or { }
489        local parameters = tfmdata.parameters or { }
490        report_defining("using %a font with id %a, name %a, size %a, fullname %a, filename %a",
491            properties.format or "unknown", id or "-", properties.name, parameters.size,
492            properties.fullname, basename(properties.filename))
493    end
494    statistics.stoptiming(fonts)
495    if compact and weight ~= 0 then
496        local parameters = (tfmtype == "table" and tfmdata or fontdata[tfmdata]).parameters
497        local amount = 65536 * weight
498        if auto then
499            if not squeeze or squeeze == 1 then
500                local xheight = parameters.xheight
501                squeeze = xheight / (xheight + amount)
502            end
503            if not extend or extend == 1 then
504                local emwidth = parameters.quad
505                extend = emwidth / (emwidth + 2 * amount)
506            end
507        end
508        parameters.hshift = amount -- * extend -- weight
509    end
510    return tfmdata, extend, squeeze, slant, weight
511end
512
513function font.getfont(id)
514    return fontdata[id] -- otherwise issues
515end
516