font-def.lmt /size: 19 Kb    last modification: 2024-01-16 09:02
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    t[k] = true
347    return false
348end)
349
350function definers.loadfont(specification)
351    local hash = constructors.hashinstance(specification)
352    -- todo: also hash by instance / factors
353    local tfmdata = loadedfonts[hash] -- hashes by size !
354    local name    = specification.name
355    if not tfmdata then
356        -- normally context will not end up here often (if so there is an issue somewhere)
357        local forced = specification.forced or ""
358local id = nextfont(true)
359specification.id = id
360        if forced ~= "" then
361            local reader = readers[lower(forced)] -- normally forced is already lowered
362            tfmdata = reader and reader(specification)
363            if not tfmdata and not reported[name] then
364                report_defining("forced type %a of %a not found",forced,name)
365            end
366        else
367            local sequence = readers.sequence -- can be overloaded so only a shortcut here
368            for s=1,#sequence do
369                local reader = sequence[s]
370                if readers[reader] then -- we skip not loaded readers
371                    if trace_defining then
372                        report_defining("trying (reader sequence driven) type %a for %a with file %a",reader,specification.name,specification.filename)
373                    end
374                    tfmdata = readers[reader](specification)
375                    if tfmdata then
376                        break
377                    else
378                        specification.filename = nil
379                    end
380                end
381            end
382        end
383        if tfmdata then
384            tfmdata = definers.applypostprocessors(tfmdata)
385            loadedfonts[hash] = tfmdata
386            designsizes[specification.hash] = tfmdata.parameters.designsize
387            checkfeatures(tfmdata)
388        end
389    end
390    if not tfmdata and not reported[name] then
391        report_defining("font with asked name %a is not found using lookup %a",name,specification.lookup)
392    end
393    return tfmdata
394end
395
396function constructors.readanddefine(name,size) -- no id -- maybe a dummy first
397    local specification = definers.analyze(name,size)
398    local method = specification.method
399    if method and variants[method] then
400        specification = variants[method](specification)
401    end
402    specification = definers.resolve(specification)
403    local hash = constructors.hashinstance(specification)
404    local id = definers.registered(hash)
405    if not id then
406        local tfmdata = definers.loadfont(specification)
407        if tfmdata then
408            tfmdata.properties.hash = hash
409            id = font.define(tfmdata)
410            definers.register(tfmdata,id)
411        else
412            id = 0  -- signal
413        end
414    end
415    return fontdata[id], id
416end
417
418-- So far the specifiers. Now comes the real definer. Here we cache based on id's.
419-- Here we also intercept the virtual font handler.
420--
421-- In the previously defined reader (the one resulting in a TFM table) we cached the
422-- (scaled) instances. Here we cache them again, but this time based on id. We could
423-- combine this in one cache but this does not gain much. By the way, passing id's
424-- back to in the callback was introduced later in the development.
425
426function definers.registered(hash)
427    local id = internalized[hash]
428    return id, id and fontdata[id]
429end
430
431function definers.register(tfmdata,id)
432    if tfmdata and id then
433        local hash = tfmdata.properties.hash
434        if not hash then
435            report_defining("registering font, id %a, name %a, invalid hash",id,tfmdata.properties.filename or "?")
436        elseif not internalized[hash] then
437            internalized[hash] = id
438            if trace_defining then
439                report_defining("registering font, id %s, hash %a",id,hash)
440            end
441            fontdata[id] = tfmdata
442        end
443    end
444end
445
446function definers.read(specification,size,id,compact) -- id can be optional, name can already be table
447    statistics.starttiming(fonts)
448    if type(specification) == "string" then
449        specification = definers.analyze(specification,size)
450    end
451    local method = specification.method
452    if method and variants[method] then
453        specification = variants[method](specification)
454    end
455    specification = definers.resolve(specification)
456--     local hash    = constructors.hashinstance(specification,false,true)
457    local hash, extend, squeeze, slant, weight, auto = constructors.hashinstance(specification,false,compact)
458    local tfmdata = definers.registered(hash) -- id
459    local name    = specification.name
460    if tfmdata then
461        if trace_defining then
462            report_defining("already hashed: %s",hash)
463        end
464    else
465        tfmdata = definers.loadfont(specification) -- can be overloaded
466        if tfmdata then
467            tfmdata.original = specification.specification
468            if trace_defining then
469                report_defining("loaded and hashed: %s",hash)
470            end
471            tfmdata.properties.hash = hash
472            if id then
473                definers.register(tfmdata,id)
474            end
475        else
476            if trace_defining then
477                report_defining("not loaded and hashed: %s",hash)
478            end
479        end
480    end
481    if not tfmdata then -- or id?
482        if not reported[name] then
483            report_defining( "unknown font %a, loading aborted",name)
484        end
485    elseif trace_defining and type(tfmdata) == "table" then
486        local properties = tfmdata.properties or { }
487        local parameters = tfmdata.parameters or { }
488        report_defining("using %a font with id %a, name %a, size %a, fullname %a, filename %a",
489            properties.format or "unknown", id or "-", properties.name, parameters.size,
490            properties.fullname, basename(properties.filename))
491    end
492    statistics.stoptiming(fonts)
493    if compact and weight ~= 0 then
494        local parameters = (tfmtype == "table" and tfmdata or fontdata[tfmdata]).parameters
495        local amount = 65536 * weight
496        if auto then
497            if not squeeze or squeeze == 1 then
498                local xheight = parameters.xheight
499                squeeze = xheight / (xheight + amount)
500            end
501            if not extend or extend == 1 then
502                local emwidth = parameters.quad
503                extend = emwidth / (emwidth + 2 * amount)
504            end
505        end
506        parameters.hshift = amount -- * extend -- weight
507    end
508    return tfmdata, extend, squeeze, slant, weight
509end
510
511function font.getfont(id)
512    return fontdata[id] -- otherwise issues
513end
514