font-one.lua / last modification: 2020-01-30 14:16
if not modules then modules = { } end modules ['font-one'] = {
    version   = 1.001,
    comment   = "companion to font-ini.mkiv",
    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
    copyright = "PRAGMA ADE / ConTeXt Development Team",
    license   = "see context related readme files"
}

--[[ldx--
<p>Some code may look a bit obscure but this has to do with the fact that we also use
this code for testing and much code evolved in the transition from <l n='tfm'/> to
<l n='afm'/> to <l n='otf'/>.</p>

<p>The following code still has traces of intermediate font support where we handles
font encodings. Eventually font encoding went away but we kept some code around in
other modules.</p>

<p>This version implements a node mode approach so that users can also more easily
add features.</p>
--ldx]]--

local fonts, logs, trackers, containers, resolvers = fonts, logs, trackers, containers, resolvers

local next, type, tonumber, rawget = next, type, tonumber, rawget
local match, gsub = string.match, string.gsub
local abs = math.abs
local P, S, R, Cmt, C, Ct, Cs, Carg = lpeg.P, lpeg.S, lpeg.R, lpeg.Cmt, lpeg.C, lpeg.Ct, lpeg.Cs, lpeg.Carg
local lpegmatch, patterns = lpeg.match, lpeg.patterns
local sortedhash = table.sortedhash

local trace_features      = false  trackers.register("afm.features",   function(v) trace_features = v end)
local trace_indexing      = false  trackers.register("afm.indexing",   function(v) trace_indexing = v end)
local trace_loading       = false  trackers.register("afm.loading",    function(v) trace_loading  = v end)
local trace_defining      = false  trackers.register("fonts.defining", function(v) trace_defining = v end)

local report_afm          = logs.reporter("fonts","afm loading")

local setmetatableindex   = table.setmetatableindex
local derivetable         = table.derive

local findbinfile         = resolvers.findbinfile

local privateoffset       = fonts.constructors and fonts.constructors.privateoffset or 0xF0000 -- 0x10FFFF

local definers            = fonts.definers
local readers             = fonts.readers
local constructors        = fonts.constructors

local afm                 = constructors.handlers.afm
local pfb                 = constructors.handlers.pfb
local otf                 = fonts.handlers.otf

local otfreaders          = otf.readers
local otfenhancers        = otf.enhancers

local afmfeatures         = constructors.features.afm
local registerafmfeature  = afmfeatures.register

local afmenhancers        = constructors.enhancers.afm
local registerafmenhancer = afmenhancers.register

afm.version               = 1.513 -- incrementing this number one up will force a re-cache
afm.cache                 = containers.define("fonts", "one", afm.version, true)
afm.autoprefixed          = true -- this will become false some day (catches texnansi-blabla.*)

afm.helpdata              = { }  -- set later on so no local for this
afm.syncspace             = true -- when true, nicer stretch values

local overloads           = fonts.mappings.overloads

local applyruntimefixes   = fonts.treatments and fonts.treatments.applyfixes

--[[ldx--
<p>We cache files. Caching is taken care of in the loader. We cheat a bit by adding
ligatures and kern information to the afm derived data. That way we can set them faster
when defining a font.</p>

<p>We still keep the loading two phased: first we load the data in a traditional
fashion and later we transform it to sequences. Then we apply some methods also
used in opentype fonts (like <t>tlig</t>).</p>
--ldx]]--

function afm.load(filename)
    filename = resolvers.findfile(filename,'afm') or ""
    if filename ~= "" and not fonts.names.ignoredfile(filename) then
        local name = file.removesuffix(file.basename(filename))
        local data = containers.read(afm.cache,name)
        local attr = lfs.attributes(filename)
        local size = attr and attr.size or 0
        local time = attr and attr.modification or 0
        --
        local pfbfile = file.replacesuffix(name,"pfb")
        local pfbname = resolvers.findfile(pfbfile,"pfb") or ""
        if pfbname == "" then
            pfbname = resolvers.findfile(file.basename(pfbfile),"pfb") or ""
        end
        local pfbsize = 0
        local pfbtime = 0
        if pfbname ~= "" then
            local attr = lfs.attributes(pfbname)
            pfbsize = attr.size or 0
            pfbtime = attr.modification or 0
        end
        if not data or data.size ~= size or data.time ~= time or data.pfbsize ~= pfbsize or data.pfbtime ~= pfbtime then
            report_afm("reading %a",filename)
            data = afm.readers.loadfont(filename,pfbname)
            if data then
                afmenhancers.apply(data,filename)
             -- otfreaders.addunicodetable(data) -- only when not done yet
                fonts.mappings.addtounicode(data,filename)
                otfreaders.stripredundant(data)
             -- otfreaders.extend(data)
                otfreaders.pack(data)
                data.size = size
                data.time = time
                data.pfbsize = pfbsize
                data.pfbtime = pfbtime
                report_afm("saving %a in cache",name)
             -- data.resources.unicodes = nil -- consistent with otf but here we save not much
                data = containers.write(afm.cache, name, data)
                data = containers.read(afm.cache,name)
            end
        end
        if data then
         -- constructors.addcoreunicodes(unicodes)
            otfreaders.unpack(data)
            otfreaders.expand(data) -- inline tables
            otfreaders.addunicodetable(data) -- only when not done yet
            otfenhancers.apply(data,filename,data)
            if applyruntimefixes then
                applyruntimefixes(filename,data)
            end
        end
        return data
    end
end

-- we run a more advanced analyzer later on anyway

local uparser = fonts.mappings.makenameparser() -- each time

local function enhance_unify_names(data, filename)
    local unicodevector = fonts.encodings.agl.unicodes -- loaded runtime in context
    local unicodes      = { }
    local names         = { }
    local private       = data.private or privateoffset
    local descriptions  = data.descriptions
    for name, blob in sortedhash(data.characters) do -- sorting is nicer for privates
        local code = unicodevector[name] -- or characters.name_to_unicode[name]
        if not code then
            code = lpegmatch(uparser,name)
            if type(code) ~= "number" then
                code = private
                private = private + 1
                report_afm("assigning private slot %U for unknown glyph name %a",code,name)
            end
        end
        local index = blob.index
        unicodes[name] = code
        names[name] = index
        blob.name = name
        descriptions[code] = {
            boundingbox = blob.boundingbox,
            width       = blob.width,
            kerns       = blob.kerns,
            index       = index,
            name        = name,
        }
    end
    for unicode, description in next, descriptions do
        local kerns = description.kerns
        if kerns then
            local krn = { }
            for name, kern in next, kerns do
                local unicode = unicodes[name]
                if unicode then
                    krn[unicode] = kern
                else
                 -- print(unicode,name)
                end
            end
            description.kerns = krn
        end
    end
    data.characters = nil
    data.private    = private
    local resources = data.resources
    local filename  = resources.filename or file.removesuffix(file.basename(filename))
    resources.filename = resolvers.unresolve(filename) -- no shortcut
    resources.unicodes = unicodes -- name to unicode
    resources.marks    = { } -- todo
 -- resources.names    = names -- name to index
end

local everywhere = { ["*"] = { ["*"] = true } } -- or: { ["*"] = { "*" } }
local noflags    = { false, false, false, false }

local function enhance_normalize_features(data)
    local ligatures  = setmetatableindex("table")
    local kerns      = setmetatableindex("table")
    local extrakerns = setmetatableindex("table")
    for u, c in next, data.descriptions do
        local l = c.ligatures
        local k = c.kerns
        local e = c.extrakerns
        if l then
            ligatures[u] = l
            for u, v in next, l do
                l[u] = { ligature = v }
            end
            c.ligatures = nil
        end
        if k then
            kerns[u] = k
            for u, v in next, k do
                k[u] = v -- { v, 0 }
            end
            c.kerns = nil
        end
        if e then
            extrakerns[u] = e
            for u, v in next, e do
                e[u] = v -- { v, 0 }
            end
            c.extrakerns = nil
        end
    end
    local features = {
        gpos = { },
        gsub = { },
    }
    local sequences = {
        -- only filled ones
    }
    if next(ligatures) then
        features.gsub.liga = everywhere
        data.properties.hasligatures = true
        sequences[#sequences+1] = {
            features = {
                liga = everywhere,
            },
            flags    = noflags,
            name     = "s_s_0",
            nofsteps = 1,
            order    = { "liga" },
            type     = "gsub_ligature",
            steps    = {
                {
                    coverage = ligatures,
                },
            },
        }
    end
    if next(kerns) then
        features.gpos.kern = everywhere
        data.properties.haskerns = true
        sequences[#sequences+1] = {
            features = {
                kern = everywhere,
            },
            flags    = noflags,
            name     = "p_s_0",
            nofsteps = 1,
            order    = { "kern" },
            type     = "gpos_pair",
            steps    = {
                {
                    format   = "kern",
                    coverage = kerns,
                },
            },
        }
    end
    if next(extrakerns) then
        features.gpos.extrakerns = everywhere
        data.properties.haskerns = true
        sequences[#sequences+1] = {
            features = {
                extrakerns = everywhere,
            },
            flags    = noflags,
            name     = "p_s_1",
            nofsteps = 1,
            order    = { "extrakerns" },
            type     = "gpos_pair",
            steps    = {
                {
                    format   = "kern",
                    coverage = extrakerns,
                },
            },
        }
    end
    -- todo: compress kerns
    data.resources.features  = features
    data.resources.sequences = sequences
end

local function enhance_fix_names(data)
    for k, v in next, data.descriptions do
        local n = v.name
        local r = overloads[n]
        if r then
            local name = r.name
            if trace_indexing then
                report_afm("renaming characters %a to %a",n,name)
            end
            v.name    = name
            v.unicode = r.unicode
        end
    end
end

--[[ldx--
<p>These helpers extend the basic table with extra ligatures, texligatures
and extra kerns. This saves quite some lookups later.</p>
--ldx]]--

local addthem = function(rawdata,ligatures)
    if ligatures then
        local descriptions = rawdata.descriptions
        local resources    = rawdata.resources
        local unicodes     = resources.unicodes
     -- local names        = resources.names
        for ligname, ligdata in next, ligatures do
            local one = descriptions[unicodes[ligname]]
            if one then
                for _, pair in next, ligdata do
                    local two   = unicodes[pair[1]]
                    local three = unicodes[pair[2]]
                    if two and three then
                        local ol = one.ligatures
                        if ol then
                            if not ol[two] then
                                ol[two] = three
                            end
                        else
                            one.ligatures = { [two] = three }
                        end
                    end
                end
            end
        end
    end
end

local function enhance_add_ligatures(rawdata)
    addthem(rawdata,afm.helpdata.ligatures)
end

--[[ldx--
<p>We keep the extra kerns in separate kerning tables so that we can use
them selectively.</p>
--ldx]]--

-- This is rather old code (from the beginning when we had only tfm). If
-- we unify the afm data (now we have names all over the place) then
-- we can use shcodes but there will be many more looping then. But we
-- could get rid of the tables in char-cmp then. Als, in the generic version
-- we don't use the character database. (Ok, we can have a context specific
-- variant).

local function enhance_add_extra_kerns(rawdata) -- using shcodes is not robust here
    local descriptions = rawdata.descriptions
    local resources    = rawdata.resources
    local unicodes     = resources.unicodes
    local function do_it_left(what)
        if what then
            for unicode, description in next, descriptions do
                local kerns = description.kerns
                if kerns then
                    local extrakerns
                    for complex, simple in next, what do
                        complex = unicodes[complex]
                        simple = unicodes[simple]
                        if complex and simple then
                            local ks = kerns[simple]
                            if ks and not kerns[complex] then
                                if extrakerns then
                                    extrakerns[complex] = ks
                                else
                                    extrakerns = { [complex] = ks }
                                end
                            end
                        end
                    end
                    if extrakerns then
                        description.extrakerns = extrakerns
                    end
                end
            end
        end
    end
    local function do_it_copy(what)
        if what then
            for complex, simple in next, what do
                complex = unicodes[complex]
                simple  = unicodes[simple]
                if complex and simple then
                    local complexdescription = descriptions[complex]
                    if complexdescription then -- optional
                        local simpledescription = descriptions[complex]
                        if simpledescription then
                            local extrakerns
                            local kerns = simpledescription.kerns
                            if kerns then
                                for unicode, kern in next, kerns do
                                    if extrakerns then
                                        extrakerns[unicode] = kern
                                    else
                                        extrakerns = { [unicode] = kern }
                                    end
                                end
                            end
                            local extrakerns = simpledescription.extrakerns
                            if extrakerns then
                                for unicode, kern in next, extrakerns do
                                    if extrakerns then
                                        extrakerns[unicode] = kern
                                    else
                                        extrakerns = { [unicode] = kern }
                                    end
                                end
                            end
                            if extrakerns then
                                complexdescription.extrakerns = extrakerns
                            end
                        end
                    end
                end
            end
        end
    end
    -- add complex with values of simplified when present
    do_it_left(afm.helpdata.leftkerned)
    do_it_left(afm.helpdata.bothkerned)
    -- copy kerns from simple char to complex char unless set
    do_it_copy(afm.helpdata.bothkerned)
    do_it_copy(afm.helpdata.rightkerned)
end

--[[ldx--
<p>The copying routine looks messy (and is indeed a bit messy).</p>
--ldx]]--

local function adddimensions(data) -- we need to normalize afm to otf i.e. indexed table instead of name
    if data then
        for unicode, description in next, data.descriptions do
            local bb = description.boundingbox
            if bb then
                local ht =  bb[4]
                local dp = -bb[2]
                if ht == 0 or ht < 0 then
                    -- no need to set it and no negative heights, nil == 0
                else
                    description.height = ht
                end
                if dp == 0 or dp < 0 then
                    -- no negative depths and no negative depths, nil == 0
                else
                    description.depth  = dp
                end
            end
        end
    end
end

local function copytotfm(data)
    if data and data.descriptions then
        local metadata     = data.metadata
        local resources    = data.resources
        local properties   = derivetable(data.properties)
        local descriptions = derivetable(data.descriptions)
        local goodies      = derivetable(data.goodies)
        local characters   = { }
        local parameters   = { }
        local unicodes     = resources.unicodes
        --
        for unicode, description in next, data.descriptions do -- use parent table
            characters[unicode] = { }
        end
        --
        local filename   = constructors.checkedfilename(resources)
        local fontname   = metadata.fontname or metadata.fullname
        local fullname   = metadata.fullname or metadata.fontname
        local endash     = 0x2013
        local emdash     = 0x2014
        local space      = 0x0020 -- space
        local spacer     = "space"
        local spaceunits = 500
        --
        local monospaced  = metadata.monospaced
        local charwidth   = metadata.charwidth
        local italicangle = metadata.italicangle
        local charxheight = metadata.xheight and metadata.xheight > 0 and metadata.xheight
        properties.monospaced  = monospaced
        parameters.italicangle = italicangle
        parameters.charwidth   = charwidth
        parameters.charxheight = charxheight
        -- nearly the same as otf, catches
        local d_endash = descriptions[endash]
        local d_emdash = descriptions[emdash]
        local d_space  = descriptions[space]
        if not d_space or d_space == 0 then
            d_space = d_endash
        end
        if d_space then
            spaceunits, spacer = d_space.width or 0, "space"
        end
        if properties.monospaced then
            if spaceunits == 0 and d_emdash then
                spaceunits, spacer = d_emdash.width or 0, "emdash"
            end
        else
            if spaceunits == 0 and d_endash then
                spaceunits, spacer = d_emdash.width or 0, "endash"
            end
        end
        if spaceunits == 0 and charwidth then
            spaceunits, spacer = charwidth or 0, "charwidth"
        end
        if spaceunits == 0 then
            spaceunits = tonumber(spaceunits) or 500
        end
        if spaceunits == 0 then
            spaceunits = 500
        end
        --
        parameters.slant         = 0
        parameters.space         = spaceunits
        parameters.space_stretch = 500
        parameters.space_shrink  = 333
        parameters.x_height      = 400
        parameters.quad          = 1000
        --
        if italicangle and italicangle ~= 0 then
            parameters.italicangle  = italicangle
            parameters.italicfactor = math.cos(math.rad(90+italicangle))
            parameters.slant        = - math.tan(italicangle*math.pi/180)
        end
        if monospaced then
            parameters.space_stretch = 0
            parameters.space_shrink  = 0
        elseif afm.syncspace then
            parameters.space_stretch = spaceunits/2
            parameters.space_shrink  = spaceunits/3
        end
        parameters.extra_space = parameters.space_shrink
        if charxheight then
            parameters.x_height = charxheight
        else
            -- same as otf
            local x = 0x0078 -- x
            if x then
                local x = descriptions[x]
                if x then
                    parameters.x_height = x.height
                end
            end
            --
        end
        --
        if metadata.sup then
            local dummy    = { 0, 0, 0 }
            parameters[ 1] = metadata.designsize        or 0
            parameters[ 2] = metadata.checksum          or 0
            parameters[ 3],
            parameters[ 4],
            parameters[ 5] = unpack(metadata.space      or dummy)
            parameters[ 6] =        metadata.quad       or 0
            parameters[ 7] =        metadata.extraspace or 0
            parameters[ 8],
            parameters[ 9],
            parameters[10] = unpack(metadata.num        or dummy)
            parameters[11],
            parameters[12] = unpack(metadata.denom      or dummy)
            parameters[13],
            parameters[14],
            parameters[15] = unpack(metadata.sup        or dummy)
            parameters[16],
            parameters[17] = unpack(metadata.sub        or dummy)
            parameters[18] =        metadata.supdrop    or 0
            parameters[19] =        metadata.subdrop    or 0
            parameters[20],
            parameters[21] = unpack(metadata.delim      or dummy)
            parameters[22] =        metadata.axisheight or 0
        end
        --
        parameters.designsize = (metadata.designsize or 10)*65536
        parameters.ascender   = abs(metadata.ascender  or 0)
        parameters.descender  = abs(metadata.descender or 0)
        parameters.units      = 1000
        --
        properties.spacer        = spacer
        properties.encodingbytes = 2
        properties.format        = fonts.formats[filename] or "type1"
        properties.filename      = filename
        properties.fontname      = fontname
        properties.fullname      = fullname
        properties.psname        = fullname
        properties.name          = filename or fullname or fontname
        properties.private       = properties.private or data.private or privateoffset
        --
        if next(characters) then
            return {
                characters   = characters,
                descriptions = descriptions,
                parameters   = parameters,
                resources    = resources,
                properties   = properties,
                goodies      = goodies,
            }
        end
    end
    return nil
end

--[[ldx--
<p>Originally we had features kind of hard coded for <l n='afm'/> files but since I
expect to support more font formats, I decided to treat this fontformat like any
other and handle features in a more configurable way.</p>
--ldx]]--

function afm.setfeatures(tfmdata,features)
    local okay = constructors.initializefeatures("afm",tfmdata,features,trace_features,report_afm)
    if okay then
        return constructors.collectprocessors("afm",tfmdata,features,trace_features,report_afm)
    else
        return { } -- will become false
    end
end

local function addtables(data)
    local resources  = data.resources
    local lookuptags = resources.lookuptags
    local unicodes   = resources.unicodes
    if not lookuptags then
        lookuptags = { }
        resources.lookuptags = lookuptags
    end
    setmetatableindex(lookuptags,function(t,k)
        local v = type(k) == "number" and ("lookup " .. k) or k
        t[k] = v
        return v
    end)
    if not unicodes then
        unicodes = { }
        resources.unicodes = unicodes
        setmetatableindex(unicodes,function(t,k)
            setmetatableindex(unicodes,nil)
            for u, d in next, data.descriptions do
                local n = d.name
                if n then
                    t[n] = u
                end
            end
            return rawget(t,k)
        end)
    end
    constructors.addcoreunicodes(unicodes) -- do we really need this?
end

local function afmtotfm(specification)
    local afmname = specification.filename or specification.name
    if specification.forced == "afm" or specification.format == "afm" then -- move this one up
        if trace_loading then
            report_afm("forcing afm format for %a",afmname)
        end
    else
        local tfmname = findbinfile(afmname,"ofm") or ""
        if tfmname ~= "" then
            if trace_loading then
                report_afm("fallback from afm to tfm for %a",afmname)
            end
            return -- just that
        end
    end
    if afmname ~= "" then
        -- weird, isn't this already done then?
        local features = constructors.checkedfeatures("afm",specification.features.normal)
        specification.features.normal = features
        constructors.hashinstance(specification,true) -- also weird here
        --
        specification = definers.resolve(specification) -- new, was forgotten
        local cache_id = specification.hash
        local tfmdata  = containers.read(constructors.cache, cache_id) -- cache with features applied
        if not tfmdata then
            local rawdata = afm.load(afmname)
            if rawdata and next(rawdata) then
                addtables(rawdata)
                adddimensions(rawdata)
                tfmdata = copytotfm(rawdata)
                if tfmdata and next(tfmdata) then
                    local shared = tfmdata.shared
                    if not shared then
                        shared         = { }
                        tfmdata.shared = shared
                    end
                    shared.rawdata   = rawdata
                    shared.dynamics  = { }
                    tfmdata.changed  = { }
                    shared.features  = features
                    shared.processes = afm.setfeatures(tfmdata,features)
                end
            elseif trace_loading then
                report_afm("no (valid) afm file found with name %a",afmname)
            end
            tfmdata = containers.write(constructors.cache,cache_id,tfmdata)
        end
        return tfmdata
    end
end

--[[ldx--
<p>As soon as we could intercept the <l n='tfm'/> reader, I implemented an
<l n='afm'/> reader. Since traditional <l n='pdftex'/> could use <l n='opentype'/>
fonts with <l n='afm'/> companions, the following method also could handle
those cases, but now that we can handle <l n='opentype'/> directly we no longer
need this features.</p>
--ldx]]--

local function read_from_afm(specification)
    local tfmdata = afmtotfm(specification)
    if tfmdata then
        tfmdata.properties.name = specification.name
        tfmdata = constructors.scale(tfmdata, specification)
        local allfeatures = tfmdata.shared.features or specification.features.normal
        constructors.applymanipulators("afm",tfmdata,allfeatures,trace_features,report_afm)
        fonts.loggers.register(tfmdata,'afm',specification)
    end
    return tfmdata
end

--[[ldx--
<p>We have the usual two modes and related features initializers and processors.</p>
--ldx]]--

registerafmfeature {
    name         = "mode",
    description  = "mode",
    initializers = {
        base = otf.modeinitializer,
        node = otf.modeinitializer,
    }
}

registerafmfeature {
    name         = "features",
    description  = "features",
    default      = true,
    initializers = {
        node     = otf.nodemodeinitializer,
        base     = otf.basemodeinitializer,
    },
    processors   = {
        node     = otf.featuresprocessor,
    }
}

-- readers

fonts.formats.afm = "type1"
fonts.formats.pfb = "type1"

local function check_afm(specification,fullname)
    local foundname = findbinfile(fullname, 'afm') or "" -- just to be sure
    if foundname == "" then
        foundname = fonts.names.getfilename(fullname,"afm") or ""
    end
    if fullname and foundname == "" and afm.autoprefixed then
        local encoding, shortname = match(fullname,"^(.-)%-(.*)$") -- context: encoding-name.*
        if encoding and shortname and fonts.encodings.known[encoding] then
            shortname = findbinfile(shortname,'afm') or "" -- just to be sure
            if shortname ~= "" then
                foundname = shortname
                if trace_defining then
                    report_afm("stripping encoding prefix from filename %a",afmname)
                end
            end
        end
    end
    if foundname ~= "" then
        specification.filename = foundname
        specification.format   = "afm"
        return read_from_afm(specification)
    end
end

function readers.afm(specification,method)
    local fullname = specification.filename or ""
    local tfmdata  = nil
    if fullname == "" then
        local forced = specification.forced or ""
        if forced ~= "" then
            tfmdata = check_afm(specification,specification.name .. "." .. forced)
        end
        if not tfmdata then
            local check_tfm = readers.check_tfm
            method = (check_tfm and (method or definers.method or "afm or tfm")) or "afm"
            if method == "tfm" then
                tfmdata = check_tfm(specification,specification.name)
            elseif method == "afm" then
                tfmdata = check_afm(specification,specification.name)
            elseif method == "tfm or afm" then
                tfmdata = check_tfm(specification,specification.name) or check_afm(specification,specification.name)
            else -- method == "afm or tfm" or method == "" then
                tfmdata = check_afm(specification,specification.name) or check_tfm(specification,specification.name)
            end
        end
    else
        tfmdata = check_afm(specification,fullname)
    end
    return tfmdata
end

function readers.pfb(specification,method) -- only called when forced
    local original = specification.specification
    if trace_defining then
        report_afm("using afm reader for %a",original)
    end
    specification.forced = "afm"
    local function swap(name)
        local value = specification[swap]
        if value then
            specification[swap] = gsub("%.pfb",".afm",1)
        end
    end
    swap("filename")
    swap("fullname")
    swap("forcedname")
    swap("specification")
    return readers.afm(specification,method)
end

-- now we register them

registerafmenhancer("unify names",          enhance_unify_names)
registerafmenhancer("add ligatures",        enhance_add_ligatures)
registerafmenhancer("add extra kerns",      enhance_add_extra_kerns)
registerafmenhancer("normalize features",   enhance_normalize_features)
registerafmenhancer("check extra features", otfenhancers.enhance)
registerafmenhancer("fix names",            enhance_fix_names)