font-one.lua /size: 30 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['font-one'] = {
2    version   = 1.001,
3    optimize  = true,
4    comment   = "companion to font-ini.mkiv",
5    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
6    copyright = "PRAGMA ADE / ConTeXt Development Team",
7    license   = "see context related readme files"
8}
9
10-- Some code may look a bit obscure but this has to do with the fact that we also
11-- use this code for testing and much code evolved in the transition from TFM to AFM
12-- to OTF.
13--
14-- The following code still has traces of intermediate font support where we handles
15-- font encodings. Eventually font encoding went away but we kept some code around
16-- in other modules.
17--
18-- This version implements a node mode approach so that users can also more easily
19-- add features.
20
21local fonts, logs, trackers, containers, resolvers = fonts, logs, trackers, containers, resolvers
22
23local next, type, tonumber, rawget = next, type, tonumber, rawget
24local match, gsub = string.match, string.gsub
25local abs = math.abs
26local P, S, R, Cmt, C, Ct, Cs, Carg = lpeg.P, lpeg.S, lpeg.R, lpeg.Cmt, lpeg.C, lpeg.Ct, lpeg.Cs, lpeg.Carg
27local lpegmatch, patterns = lpeg.match, lpeg.patterns
28local sortedhash = table.sortedhash
29
30local trace_features      = false  trackers.register("afm.features",   function(v) trace_features = v end)
31local trace_indexing      = false  trackers.register("afm.indexing",   function(v) trace_indexing = v end)
32local trace_loading       = false  trackers.register("afm.loading",    function(v) trace_loading  = v end)
33local trace_defining      = false  trackers.register("fonts.defining", function(v) trace_defining = v end)
34
35local report_afm          = logs.reporter("fonts","afm loading")
36
37local setmetatableindex   = table.setmetatableindex
38local derivetable         = table.derive
39
40local findbinfile         = resolvers.findbinfile
41
42local privateoffset       = fonts.constructors and fonts.constructors.privateoffset or 0xF0000 -- 0x10FFFF
43
44local definers            = fonts.definers
45local readers             = fonts.readers
46local constructors        = fonts.constructors
47
48local afm                 = constructors.handlers.afm
49local pfb                 = constructors.handlers.pfb
50local otf                 = fonts.handlers.otf
51
52local otfreaders          = otf.readers
53local otfenhancers        = otf.enhancers
54
55local afmfeatures         = constructors.features.afm
56local registerafmfeature  = afmfeatures.register
57
58local afmenhancers        = constructors.enhancers.afm
59local registerafmenhancer = afmenhancers.register
60
61afm.version               = 1.513 -- incrementing this number one up will force a re-cache
62afm.cache                 = containers.define("fonts", "one", afm.version, true)
63afm.autoprefixed          = true -- this will become false some day (catches texnansi-blabla.*)
64
65afm.helpdata              = { }  -- set later on so no local for this
66afm.syncspace             = true -- when true, nicer stretch values
67
68local overloads           = fonts.mappings.overloads
69
70local applyruntimefixes   = fonts.treatments and fonts.treatments.applyfixes
71
72-- We cache files. Caching is taken care of in the loader. We cheat a bit by adding
73-- ligatures and kern information to the afm derived data. That way we can set them
74-- faster when defining a font.
75--
76-- We still keep the loading two phased: first we load the data in a traditional
77-- fashion and later we transform it to sequences. Then we apply some methods also
78-- used in opentype fonts (like tlig).
79
80function afm.load(filename)
81    filename = resolvers.findfile(filename,'afm') or ""
82    if filename ~= "" and not fonts.names.ignoredfile(filename) then
83        local name = file.removesuffix(file.basename(filename))
84        local data = containers.read(afm.cache,name)
85        local attr = lfs.attributes(filename)
86        local size = attr and attr.size or 0
87        local time = attr and attr.modification or 0
88        --
89        local pfbfile = file.replacesuffix(name,"pfb")
90        local pfbname = resolvers.findfile(pfbfile,"pfb") or ""
91        if pfbname == "" then
92            pfbname = resolvers.findfile(file.basename(pfbfile),"pfb") or ""
93        end
94        local pfbsize = 0
95        local pfbtime = 0
96        if pfbname ~= "" then
97            local attr = lfs.attributes(pfbname)
98            pfbsize = attr.size or 0
99            pfbtime = attr.modification or 0
100        end
101        if not data or data.size ~= size or data.time ~= time or data.pfbsize ~= pfbsize or data.pfbtime ~= pfbtime then
102            report_afm("reading %a",filename)
103            data = afm.readers.loadfont(filename,pfbname)
104            if data then
105                afmenhancers.apply(data,filename)
106             -- otfreaders.addunicodetable(data) -- only when not done yet
107                fonts.mappings.addtounicode(data,filename)
108                otfreaders.stripredundant(data)
109             -- otfreaders.extend(data)
110                otfreaders.pack(data)
111                data.size = size
112                data.time = time
113                data.pfbsize = pfbsize
114                data.pfbtime = pfbtime
115                report_afm("saving %a in cache",name)
116             -- data.resources.unicodes = nil -- consistent with otf but here we save not much
117                data = containers.write(afm.cache, name, data)
118                data = containers.read(afm.cache,name)
119            end
120        end
121        if data then
122         -- constructors.addcoreunicodes(unicodes)
123            otfreaders.unpack(data)
124            otfreaders.expand(data) -- inline tables
125            otfreaders.addunicodetable(data) -- only when not done yet
126            otfenhancers.apply(data,filename,data)
127            if applyruntimefixes then
128                applyruntimefixes(filename,data)
129            end
130        end
131        return data
132    end
133end
134
135-- we run a more advanced analyzer later on anyway
136
137local uparser = fonts.mappings.makenameparser() -- each time
138
139local function enhance_unify_names(data, filename)
140    local unicodevector = fonts.encodings.agl.unicodes -- loaded runtime in context
141    local unicodes      = { }
142    local names         = { }
143    local private       = data.private or privateoffset
144    local descriptions  = data.descriptions
145    for name, blob in sortedhash(data.characters) do -- sorting is nicer for privates
146        local code = unicodevector[name] -- or characters.name_to_unicode[name]
147        if not code then
148            code = lpegmatch(uparser,name)
149            if type(code) ~= "number" then
150                code = private
151                private = private + 1
152                report_afm("assigning private slot %U for unknown glyph name %a",code,name)
153            end
154        end
155        local index = blob.index
156        unicodes[name] = code
157        names[name] = index
158        blob.name = name
159        descriptions[code] = {
160            boundingbox = blob.boundingbox,
161            width       = blob.width,
162            kerns       = blob.kerns,
163            index       = index,
164            name        = name,
165        }
166    end
167    for unicode, description in next, descriptions do
168        local kerns = description.kerns
169        if kerns then
170            local krn = { }
171            for name, kern in next, kerns do
172                local unicode = unicodes[name]
173                if unicode then
174                    krn[unicode] = kern
175                else
176                 -- print(unicode,name)
177                end
178            end
179            description.kerns = krn
180        end
181    end
182    data.characters = nil
183    data.private    = private
184    local resources = data.resources
185    local filename  = resources.filename or file.removesuffix(file.basename(filename))
186    resources.filename = resolvers.unresolve(filename) -- no shortcut
187    resources.unicodes = unicodes -- name to unicode
188    resources.marks    = { } -- todo
189 -- resources.names    = names -- name to index
190end
191
192local everywhere = { ["*"] = { ["*"] = true } } -- or: { ["*"] = { "*" } }
193local noflags    = { false, false, false, false }
194
195local function enhance_normalize_features(data)
196    local ligatures  = setmetatableindex("table")
197    local kerns      = setmetatableindex("table")
198    local extrakerns = setmetatableindex("table")
199    for u, c in next, data.descriptions do
200        local l = c.ligatures
201        local k = c.kerns
202        local e = c.extrakerns
203        if l then
204            ligatures[u] = l
205            for u, v in next, l do
206                l[u] = { ligature = v }
207            end
208            c.ligatures = nil
209        end
210        if k then
211            kerns[u] = k
212            for u, v in next, k do
213                k[u] = v -- { v, 0 }
214            end
215            c.kerns = nil
216        end
217        if e then
218            extrakerns[u] = e
219            for u, v in next, e do
220                e[u] = v -- { v, 0 }
221            end
222            c.extrakerns = nil
223        end
224    end
225    local features = {
226        gpos = { },
227        gsub = { },
228    }
229    local sequences = {
230        -- only filled ones
231    }
232    if next(ligatures) then
233        features.gsub.liga = everywhere
234        data.properties.hasligatures = true
235        sequences[#sequences+1] = {
236            features = {
237                liga = everywhere,
238            },
239            flags    = noflags,
240            name     = "s_s_0",
241            nofsteps = 1,
242            order    = { "liga" },
243            type     = "gsub_ligature",
244            steps    = {
245                {
246                    coverage = ligatures,
247                },
248            },
249        }
250    end
251    if next(kerns) then
252        features.gpos.kern = everywhere
253        data.properties.haskerns = true
254        sequences[#sequences+1] = {
255            features = {
256                kern = everywhere,
257            },
258            flags    = noflags,
259            name     = "p_s_0",
260            nofsteps = 1,
261            order    = { "kern" },
262            type     = "gpos_pair",
263            steps    = {
264                {
265                    format   = "kern",
266                    coverage = kerns,
267                },
268            },
269        }
270    end
271    if next(extrakerns) then
272        features.gpos.extrakerns = everywhere
273        data.properties.haskerns = true
274        sequences[#sequences+1] = {
275            features = {
276                extrakerns = everywhere,
277            },
278            flags    = noflags,
279            name     = "p_s_1",
280            nofsteps = 1,
281            order    = { "extrakerns" },
282            type     = "gpos_pair",
283            steps    = {
284                {
285                    format   = "kern",
286                    coverage = extrakerns,
287                },
288            },
289        }
290    end
291    -- todo: compress kerns
292    data.resources.features  = features
293    data.resources.sequences = sequences
294end
295
296local function enhance_fix_names(data)
297    for k, v in next, data.descriptions do
298        local n = v.name
299        local r = overloads[n]
300        if r then
301            local name = r.name
302            if trace_indexing then
303                report_afm("renaming characters %a to %a",n,name)
304            end
305            v.name    = name
306            v.unicode = r.unicode
307        end
308    end
309end
310
311-- These helpers extend the basic table with extra ligatures, texligatures and extra
312-- kerns. This saves quite some lookups later.
313
314local addthem = function(rawdata,ligatures)
315    if ligatures then
316        local descriptions = rawdata.descriptions
317        local resources    = rawdata.resources
318        local unicodes     = resources.unicodes
319     -- local names        = resources.names
320        for ligname, ligdata in next, ligatures do
321            local one = descriptions[unicodes[ligname]]
322            if one then
323                for _, pair in next, ligdata do
324                    local two   = unicodes[pair[1]]
325                    local three = unicodes[pair[2]]
326                    if two and three then
327                        local ol = one.ligatures
328                        if ol then
329                            if not ol[two] then
330                                ol[two] = three
331                            end
332                        else
333                            one.ligatures = { [two] = three }
334                        end
335                    end
336                end
337            end
338        end
339    end
340end
341
342local function enhance_add_ligatures(rawdata)
343    addthem(rawdata,afm.helpdata.ligatures)
344end
345
346-- We keep the extra kerns in separate kerning tables so that we can use them
347-- selectively.
348--
349-- This is rather old code (from the beginning when we had only tfm). If we unify
350-- the afm data (now we have names all over the place) then we can use shcodes but
351-- there will be many more looping then. But we could get rid of the tables in
352-- char-cmp then. Als, in the generic version we don't use the character database.
353-- (Ok, we can have a context specific variant).
354
355local function enhance_add_extra_kerns(rawdata) -- using shcodes is not robust here
356    local descriptions = rawdata.descriptions
357    local resources    = rawdata.resources
358    local unicodes     = resources.unicodes
359    local function do_it_left(what)
360        if what then
361            for unicode, description in next, descriptions do
362                local kerns = description.kerns
363                if kerns then
364                    local extrakerns
365                    for complex, simple in next, what do
366                        complex = unicodes[complex]
367                        simple = unicodes[simple]
368                        if complex and simple then
369                            local ks = kerns[simple]
370                            if ks and not kerns[complex] then
371                                if extrakerns then
372                                    extrakerns[complex] = ks
373                                else
374                                    extrakerns = { [complex] = ks }
375                                end
376                            end
377                        end
378                    end
379                    if extrakerns then
380                        description.extrakerns = extrakerns
381                    end
382                end
383            end
384        end
385    end
386    local function do_it_copy(what)
387        if what then
388            for complex, simple in next, what do
389                complex = unicodes[complex]
390                simple  = unicodes[simple]
391                if complex and simple then
392                    local complexdescription = descriptions[complex]
393                    if complexdescription then -- optional
394                        local simpledescription = descriptions[complex]
395                        if simpledescription then
396                            local extrakerns
397                            local kerns = simpledescription.kerns
398                            if kerns then
399                                for unicode, kern in next, kerns do
400                                    if extrakerns then
401                                        extrakerns[unicode] = kern
402                                    else
403                                        extrakerns = { [unicode] = kern }
404                                    end
405                                end
406                            end
407                            local extrakerns = simpledescription.extrakerns
408                            if extrakerns then
409                                for unicode, kern in next, extrakerns do
410                                    if extrakerns then
411                                        extrakerns[unicode] = kern
412                                    else
413                                        extrakerns = { [unicode] = kern }
414                                    end
415                                end
416                            end
417                            if extrakerns then
418                                complexdescription.extrakerns = extrakerns
419                            end
420                        end
421                    end
422                end
423            end
424        end
425    end
426    -- add complex with values of simplified when present
427    do_it_left(afm.helpdata.leftkerned)
428    do_it_left(afm.helpdata.bothkerned)
429    -- copy kerns from simple char to complex char unless set
430    do_it_copy(afm.helpdata.bothkerned)
431    do_it_copy(afm.helpdata.rightkerned)
432end
433
434-- The copying routine looks messy (and is indeed a bit messy).
435
436local function adddimensions(data) -- we need to normalize afm to otf i.e. indexed table instead of name
437    if data then
438        for unicode, description in next, data.descriptions do
439            local bb = description.boundingbox
440            if bb then
441                local ht =  bb[4]
442                local dp = -bb[2]
443                if ht == 0 or ht < 0 then
444                    -- no need to set it and no negative heights, nil == 0
445                else
446                    description.height = ht
447                end
448                if dp == 0 or dp < 0 then
449                    -- no negative depths and no negative depths, nil == 0
450                else
451                    description.depth  = dp
452                end
453            end
454        end
455    end
456end
457
458local function copytotfm(data)
459    if data and data.descriptions then
460        local metadata     = data.metadata
461        local resources    = data.resources
462        local properties   = derivetable(data.properties)
463        local descriptions = derivetable(data.descriptions)
464        local goodies      = derivetable(data.goodies)
465        local characters   = { }
466        local parameters   = { }
467        local unicodes     = resources.unicodes
468        --
469        for unicode, description in next, data.descriptions do -- use parent table
470            characters[unicode] = { }
471        end
472        --
473        local filename   = constructors.checkedfilename(resources)
474        local fontname   = metadata.fontname or metadata.fullname
475        local fullname   = metadata.fullname or metadata.fontname
476        local endash     = 0x2013
477        local emdash     = 0x2014
478        local space      = 0x0020 -- space
479        local spacer     = "space"
480        local spaceunits = 500
481        --
482        local monospaced  = metadata.monospaced
483        local charwidth   = metadata.charwidth
484        local italicangle = metadata.italicangle
485        local charxheight = metadata.xheight and metadata.xheight > 0 and metadata.xheight
486        properties.monospaced  = monospaced
487        parameters.italicangle = italicangle
488        parameters.charwidth   = charwidth
489        parameters.charxheight = charxheight
490        -- nearly the same as otf, catches
491        local d_endash = descriptions[endash]
492        local d_emdash = descriptions[emdash]
493        local d_space  = descriptions[space]
494        if not d_space or d_space == 0 then
495            d_space = d_endash
496        end
497        if d_space then
498            spaceunits, spacer = d_space.width or 0, "space"
499        end
500        if properties.monospaced then
501            if spaceunits == 0 and d_emdash then
502                spaceunits, spacer = d_emdash.width or 0, "emdash"
503            end
504        else
505            if spaceunits == 0 and d_endash then
506                spaceunits, spacer = d_emdash.width or 0, "endash"
507            end
508        end
509        if spaceunits == 0 and charwidth then
510            spaceunits, spacer = charwidth or 0, "charwidth"
511        end
512        if spaceunits == 0 then
513            spaceunits = tonumber(spaceunits) or 500
514        end
515        if spaceunits == 0 then
516            spaceunits = 500
517        end
518        --
519        parameters.slant         = 0
520        parameters.space         = spaceunits
521        parameters.space_stretch = 500
522        parameters.space_shrink  = 333
523        parameters.x_height      = 400
524        parameters.quad          = 1000
525        --
526        if italicangle and italicangle ~= 0 then
527            parameters.italicangle  = italicangle
528            parameters.italicfactor = math.cos(math.rad(90+italicangle))
529            parameters.slant        = - math.tan(italicangle*math.pi/180)
530        end
531        if monospaced then
532            parameters.space_stretch = 0
533            parameters.space_shrink  = 0
534        elseif afm.syncspace then
535            parameters.space_stretch = spaceunits/2
536            parameters.space_shrink  = spaceunits/3
537        end
538        parameters.extra_space = parameters.space_shrink
539        if charxheight then
540            parameters.x_height = charxheight
541        else
542            -- same as otf
543            local x = 0x0078 -- x
544            if x then
545                local x = descriptions[x]
546                if x then
547                    parameters.x_height = x.height
548                end
549            end
550            --
551        end
552        --
553        if metadata.sup then
554            local dummy    = { 0, 0, 0 }
555            parameters[ 1] = metadata.designsize        or 0
556            parameters[ 2] = metadata.checksum          or 0
557            parameters[ 3],
558            parameters[ 4],
559            parameters[ 5] = unpack(metadata.space      or dummy)
560            parameters[ 6] =        metadata.quad       or 0
561            parameters[ 7] =        metadata.extraspace or 0
562            parameters[ 8],
563            parameters[ 9],
564            parameters[10] = unpack(metadata.num        or dummy)
565            parameters[11],
566            parameters[12] = unpack(metadata.denom      or dummy)
567            parameters[13],
568            parameters[14],
569            parameters[15] = unpack(metadata.sup        or dummy)
570            parameters[16],
571            parameters[17] = unpack(metadata.sub        or dummy)
572            parameters[18] =        metadata.supdrop    or 0
573            parameters[19] =        metadata.subdrop    or 0
574            parameters[20],
575            parameters[21] = unpack(metadata.delim      or dummy)
576            parameters[22] =        metadata.axisheight or 0
577        end
578        --
579        parameters.designsize = (metadata.designsize or 10)*65536
580        parameters.ascender   = abs(metadata.ascender  or 0)
581        parameters.descender  = abs(metadata.descender or 0)
582        parameters.units      = 1000
583        --
584        properties.spacer   = spacer
585        properties.format   = fonts.formats[filename] or "type1"
586        properties.filename = filename
587        properties.fontname = fontname
588        properties.fullname = fullname
589        properties.psname   = fullname
590        properties.name     = filename or fullname or fontname
591        properties.private  = properties.private or data.private or privateoffset
592        --
593if not CONTEXTLMTXMODE or CONTEXTLMTXMODE == 0 then
594        properties.encodingbytes = 2
595end
596        --
597        if next(characters) then
598            return {
599                characters   = characters,
600                descriptions = descriptions,
601                parameters   = parameters,
602                resources    = resources,
603                properties   = properties,
604                goodies      = goodies,
605            }
606        end
607    end
608    return nil
609end
610
611-- Originally we had features kind of hard coded for AFM files but since I expect to
612-- support more font formats, I decided to treat this fontformat like any other and
613-- handle features in a more configurable way.
614
615function afm.setfeatures(tfmdata,features)
616    local okay = constructors.initializefeatures("afm",tfmdata,features,trace_features,report_afm)
617    if okay then
618        return constructors.collectprocessors("afm",tfmdata,features,trace_features,report_afm)
619    else
620        return { } -- will become false
621    end
622end
623
624local function addtables(data)
625    local resources  = data.resources
626    local lookuptags = resources.lookuptags
627    local unicodes   = resources.unicodes
628    if not lookuptags then
629        lookuptags = { }
630        resources.lookuptags = lookuptags
631    end
632    setmetatableindex(lookuptags,function(t,k)
633        local v = type(k) == "number" and ("lookup " .. k) or k
634        t[k] = v
635        return v
636    end)
637    if not unicodes then
638        unicodes = { }
639        resources.unicodes = unicodes
640        setmetatableindex(unicodes,function(t,k)
641            setmetatableindex(unicodes,nil)
642            for u, d in next, data.descriptions do
643                local n = d.name
644                if n then
645                    t[n] = u
646                end
647            end
648            return rawget(t,k)
649        end)
650    end
651    constructors.addcoreunicodes(unicodes) -- do we really need this?
652end
653
654local function afmtotfm(specification)
655    local afmname = specification.filename or specification.name
656    if specification.forced == "afm" or specification.format == "afm" then -- move this one up
657        if trace_loading then
658            report_afm("forcing afm format for %a",afmname)
659        end
660    else
661        local tfmname = findbinfile(afmname,"ofm") or ""
662        if tfmname ~= "" then
663            if trace_loading then
664                report_afm("fallback from afm to tfm for %a",afmname)
665            end
666            return -- just that
667        end
668    end
669    if afmname ~= "" then
670        -- weird, isn't this already done then?
671        local features = constructors.checkedfeatures("afm",specification.features.normal)
672        specification.features.normal = features
673        constructors.hashinstance(specification,true) -- also weird here
674        --
675        specification = definers.resolve(specification) -- new, was forgotten
676        local cache_id = specification.hash
677        local tfmdata  = containers.read(constructors.cache, cache_id) -- cache with features applied
678        if not tfmdata then
679            local rawdata = afm.load(afmname)
680            if rawdata and next(rawdata) then
681                addtables(rawdata)
682                adddimensions(rawdata)
683                tfmdata = copytotfm(rawdata)
684                if tfmdata and next(tfmdata) then
685                    local shared = tfmdata.shared
686                    if not shared then
687                        shared         = { }
688                        tfmdata.shared = shared
689                    end
690                    shared.rawdata   = rawdata
691                    shared.dynamics  = { }
692                    tfmdata.changed  = { }
693                    shared.features  = features
694                    shared.processes = afm.setfeatures(tfmdata,features)
695                end
696            elseif trace_loading then
697                report_afm("no (valid) afm file found with name %a",afmname)
698            end
699            tfmdata = containers.write(constructors.cache,cache_id,tfmdata)
700        end
701        return tfmdata
702    end
703end
704
705-- As soon as we could intercept the TFM reader, I implemented an AFM reader. Since
706-- traditional pdfTeX could use OpenType fonts with AFM companions, the following
707-- method also could handle those cases, but now that we can handle OpenType
708-- directly we no longer need this features.
709
710local function read_from_afm(specification)
711    local tfmdata = afmtotfm(specification)
712    if tfmdata then
713        tfmdata.properties.name = specification.name
714        tfmdata.properties.id   = specification.id
715        tfmdata = constructors.scale(tfmdata, specification)
716        local allfeatures = tfmdata.shared.features or specification.features.normal
717        constructors.applymanipulators("afm",tfmdata,allfeatures,trace_features,report_afm)
718        fonts.loggers.register(tfmdata,'afm',specification)
719    end
720    return tfmdata
721end
722
723-- We have the usual two modes and related features initializers and processors.
724
725registerafmfeature {
726    name         = "mode",
727    description  = "mode",
728    initializers = {
729        base = otf.modeinitializer,
730        node = otf.modeinitializer,
731    }
732}
733
734registerafmfeature {
735    name         = "features",
736    description  = "features",
737    default      = true,
738    initializers = {
739        node     = otf.nodemodeinitializer,
740        base     = otf.basemodeinitializer,
741    },
742    processors   = {
743        node     = otf.featuresprocessor,
744    }
745}
746
747-- readers
748
749fonts.formats.afm = "type1"
750fonts.formats.pfb = "type1"
751
752local function check_afm(specification,fullname)
753    local foundname = findbinfile(fullname, 'afm') or "" -- just to be sure
754    if foundname == "" then
755        foundname = fonts.names.getfilename(fullname,"afm") or ""
756    end
757    if fullname and foundname == "" and afm.autoprefixed then
758        local encoding, shortname = match(fullname,"^(.-)%-(.*)$") -- context: encoding-name.*
759        if encoding and shortname and fonts.encodings.known[encoding] then
760            shortname = findbinfile(shortname,'afm') or "" -- just to be sure
761            if shortname ~= "" then
762                foundname = shortname
763                if trace_defining then
764                    report_afm("stripping encoding prefix from filename %a",afmname)
765                end
766            end
767        end
768    end
769    if foundname ~= "" then
770        specification.filename = foundname
771        specification.format   = "afm"
772        return read_from_afm(specification)
773    end
774end
775
776function readers.afm(specification,method)
777    local fullname = specification.filename or ""
778    local tfmdata  = nil
779    if fullname == "" then
780        local forced = specification.forced or ""
781        if forced ~= "" then
782            tfmdata = check_afm(specification,specification.name .. "." .. forced)
783        end
784        if not tfmdata then
785            local check_tfm = readers.check_tfm
786            method = (check_tfm and (method or definers.method or "afm or tfm")) or "afm"
787            if method == "tfm" then
788                tfmdata = check_tfm(specification,specification.name)
789            elseif method == "afm" then
790                tfmdata = check_afm(specification,specification.name)
791            elseif method == "tfm or afm" then
792                tfmdata = check_tfm(specification,specification.name) or check_afm(specification,specification.name)
793            else -- method == "afm or tfm" or method == "" then
794                tfmdata = check_afm(specification,specification.name) or check_tfm(specification,specification.name)
795            end
796        end
797    else
798        tfmdata = check_afm(specification,fullname)
799    end
800    return tfmdata
801end
802
803function readers.pfb(specification,method) -- only called when forced
804    local original = specification.specification
805    if trace_defining then
806        report_afm("using afm reader for %a",original)
807    end
808    specification.forced = "afm"
809    local function swap(name)
810        local value = specification[swap]
811        if value then
812            specification[swap] = gsub("%.pfb",".afm",1)
813        end
814    end
815    swap("filename")
816    swap("fullname")
817    swap("forcedname")
818    swap("specification")
819    return readers.afm(specification,method)
820end
821
822-- now we register them
823
824registerafmenhancer("unify names",          enhance_unify_names)
825registerafmenhancer("add ligatures",        enhance_add_ligatures)
826registerafmenhancer("add extra kerns",      enhance_add_extra_kerns)
827registerafmenhancer("normalize features",   enhance_normalize_features)
828registerafmenhancer("check extra features", otfenhancers.enhance)
829registerafmenhancer("fix names",            enhance_fix_names)
830