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