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