font-oto.lua / last modification: 2020-01-30 14:16
if not modules then modules = { } end modules ['font-oto'] = { -- original tex
    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"
}

local concat, unpack = table.concat, table.unpack
local insert, remove = table.insert, table.remove
local format, gmatch, gsub, find, match, lower, strip = string.format, string.gmatch, string.gsub, string.find, string.match, string.lower, string.strip
local type, next, tonumber, tostring = type, next, tonumber, tostring

local trace_baseinit         = false  trackers.register("otf.baseinit",     function(v) trace_baseinit     = v end)
local trace_singles          = false  trackers.register("otf.singles",      function(v) trace_singles      = v end)
local trace_multiples        = false  trackers.register("otf.multiples",    function(v) trace_multiples    = v end)
local trace_alternatives     = false  trackers.register("otf.alternatives", function(v) trace_alternatives = v end)
local trace_ligatures        = false  trackers.register("otf.ligatures",    function(v) trace_ligatures    = v end)
local trace_kerns            = false  trackers.register("otf.kerns",        function(v) trace_kerns        = v end)
local trace_preparing        = false  trackers.register("otf.preparing",    function(v) trace_preparing    = v end)

local report_prepare         = logs.reporter("fonts","otf prepare")

local fonts                  = fonts
local otf                    = fonts.handlers.otf

local otffeatures            = otf.features
local registerotffeature     = otffeatures.register

otf.defaultbasealternate     = "none" -- first last

local getprivate             = fonts.constructors.getprivate

local wildcard               = "*"
local default                = "dflt"

local formatters             = string.formatters
local f_unicode              = formatters["%U"]
local f_uniname              = formatters["%U (%s)"]
local f_unilist              = formatters["% t (% t)"]

local function gref(descriptions,n)
    if type(n) == "number" then
        local name = descriptions[n].name
        if name then
            return f_uniname(n,name)
        else
            return f_unicode(n)
        end
    elseif n then
        local num = { }
        local nam = { }
        local j   = 0
        for i=1,#n do
            local ni = n[i]
            if tonumber(ni) then -- first is likely a key
                j = j + 1
                local di = descriptions[ni]
                num[j] = f_unicode(ni)
                nam[j] = di and di.name or "-"
            end
        end
        return f_unilist(num,nam)
    else
        return "<error in base mode tracing>"
    end
end

local function cref(feature,sequence)
    return formatters["feature %a, type %a, chain lookup %a"](feature,sequence.type,sequence.name)
end

local function report_substitution(feature,sequence,descriptions,unicode,substitution)
    if unicode == substitution then
        report_prepare("%s: base substitution %s maps onto itself",
            cref(feature,sequence),
            gref(descriptions,unicode))
    else
        report_prepare("%s: base substitution %s => %S",
            cref(feature,sequence),
            gref(descriptions,unicode),
            gref(descriptions,substitution))
    end
end

local function report_alternate(feature,sequence,descriptions,unicode,replacement,value,comment)
    if unicode == replacement then
        report_prepare("%s: base alternate %s maps onto itself",
            cref(feature,sequence),
            gref(descriptions,unicode))
    else
        report_prepare("%s: base alternate %s => %s (%S => %S)",
            cref(feature,sequence),
            gref(descriptions,unicode),
            replacement and gref(descriptions,replacement),
            value,
            comment)
    end
end

local function report_ligature(feature,sequence,descriptions,unicode,ligature)
    report_prepare("%s: base ligature %s => %S",
        cref(feature,sequence),
        gref(descriptions,ligature),
        gref(descriptions,unicode))
end

local function report_kern(feature,sequence,descriptions,unicode,otherunicode,value)
    report_prepare("%s: base kern %s + %s => %S",
        cref(feature,sequence),
        gref(descriptions,unicode),
        gref(descriptions,otherunicode),
        value)
end

-- We need to make sure that luatex sees the difference between base fonts that have
-- different glyphs in the same slots in fonts that have the same fullname (or filename).
-- LuaTeX will merge fonts eventually (and subset later on). If needed we can use a more
-- verbose name as long as we don't use <()<>[]{}/%> and the length is < 128.

local basehash, basehashes, applied = { }, 1, { }

local function registerbasehash(tfmdata)
    local properties = tfmdata.properties
    local hash       = concat(applied," ")
    local base       = basehash[hash]
    if not base then
        basehashes     = basehashes + 1
        base           = basehashes
        basehash[hash] = base
    end
    properties.basehash = base
    properties.fullname = (properties.fullname or properties.name) .. "-" .. base
 -- report_prepare("fullname base hash '%a, featureset %a",tfmdata.properties.fullname,hash)
    applied = { }
end

local function registerbasefeature(feature,value)
    applied[#applied+1] = feature  .. "=" .. tostring(value)
end

-- The original basemode ligature builder used the names of components and did some expression
-- juggling to get the chain right. The current variant starts with unicodes but still uses
-- names to make the chain. This is needed because we have to create intermediates when needed
-- but use predefined snippets when available. To some extend the current builder is more stupid
-- but I don't worry that much about it as ligatures are rather predicatable.
--
-- Personally I think that an ff + i == ffi rule as used in for instance latin modern is pretty
-- weird as no sane person will key that in and expect a glyph for that ligature plus the following
-- character. Anyhow, as we need to deal with this, we do, but no guarantes are given.
--
--         latin modern       dejavu
--
-- f+f       102 102             102 102
-- f+i       102 105             102 105
-- f+l       102 108             102 108
-- f+f+i                         102 102 105
-- f+f+l     102 102 108         102 102 108
-- ff+i    64256 105           64256 105
-- ff+l                        64256 108
--
-- As you can see here, latin modern is less complete than dejavu but
-- in practice one will not notice it.
--
-- The while loop is needed because we need to resolve for instance pseudo names like
-- hyphen_hyphen to endash so in practice we end up with a bit too many definitions but the
-- overhead is neglectable. We can have changed[first] or changed[second] but it quickly becomes
-- messy if we need to take that into account.

local function makefake(tfmdata,name,present)
    local private   = getprivate(tfmdata)
    local character = { intermediate = true, ligatures = { } }
    resources.unicodes[name] = private
    tfmdata.characters[private] = character
    tfmdata.descriptions[private] = { name = name }
    present[name] = private
    return character
end

local function make_1(present,tree,name)
    for k, v in next, tree do
        if k == "ligature" then
            present[name] = v
        else
            make_1(present,v,name .. "_" .. k)
        end
    end
end

local function make_2(present,tfmdata,characters,tree,name,preceding,unicode,done)
    for k, v in next, tree do
        if k == "ligature" then
            local character = characters[preceding]
            if not character then
                if trace_baseinit then
                    report_prepare("weird ligature in lookup %a, current %C, preceding %C",sequence.name,v,preceding)
                end
                character = makefake(tfmdata,name,present)
            end
            local ligatures = character.ligatures
            if ligatures then
                ligatures[unicode] = { char = v }
            else
                character.ligatures = { [unicode] = { char = v } }
            end
            if done then
                local d = done[name]
                if not d then
                    done[name] = { "dummy", v }
                else
                    d[#d+1] = v
                end
            end
        else
            local code = present[name] or unicode
            local name = name .. "_" .. k
            make_2(present,tfmdata,characters,v,name,code,k,done)
        end
    end
end

local function preparesubstitutions(tfmdata,feature,value,validlookups,lookuplist)
    local characters   = tfmdata.characters
    local descriptions = tfmdata.descriptions
    local resources    = tfmdata.resources
    local changed      = tfmdata.changed

    local ligatures    = { }
    local alternate    = tonumber(value) or true and 1
    local defaultalt   = otf.defaultbasealternate

    local trace_singles      = trace_baseinit and trace_singles
    local trace_alternatives = trace_baseinit and trace_alternatives
    local trace_ligatures    = trace_baseinit and trace_ligatures

    -- A chain of changes is handled in font-con which is clesner because
    -- we can have shared changes and such.

    if not changed then
        changed = { }
        tfmdata.changed = changed
    end

    for i=1,#lookuplist do
        local sequence = lookuplist[i]
        local steps    = sequence.steps
        local kind     = sequence.type
        if kind == "gsub_single" then
            for i=1,#steps do
                for unicode, data in next, steps[i].coverage do
                    if unicode ~= data then
                        changed[unicode] = data
                    end
                    if trace_singles then
                        report_substitution(feature,sequence,descriptions,unicode,data)
                    end
                end
            end
        elseif kind == "gsub_alternate" then
            for i=1,#steps do
                for unicode, data in next, steps[i].coverage do
                    local replacement = data[alternate]
                    if replacement then
                        if unicode ~= replacement then
                            changed[unicode] = replacement
                        end
                        if trace_alternatives then
                            report_alternate(feature,sequence,descriptions,unicode,replacement,value,"normal")
                        end
                    elseif defaultalt == "first" then
                        replacement = data[1]
                        if unicode ~= replacement then
                            changed[unicode] = replacement
                        end
                        if trace_alternatives then
                            report_alternate(feature,sequence,descriptions,unicode,replacement,value,defaultalt)
                        end
                    elseif defaultalt == "last" then
                        replacement = data[#data]
                        if unicode ~= replacement then
                            changed[unicode] = replacement
                        end
                        if trace_alternatives then
                            report_alternate(feature,sequence,descriptions,unicode,replacement,value,defaultalt)
                        end
                    else
                        if trace_alternatives then
                            report_alternate(feature,sequence,descriptions,unicode,replacement,value,"unknown")
                        end
                    end
                end
            end
        elseif kind == "gsub_ligature" then
            for i=1,#steps do
                for unicode, data in next, steps[i].coverage do
                    ligatures[#ligatures+1] = { unicode, data, "" } -- lookupname }
                    if trace_ligatures then
                        report_ligature(feature,sequence,descriptions,unicode,data)
                    end
                end
            end
        end
    end

    local nofligatures = #ligatures

    if nofligatures > 0 then
        local characters = tfmdata.characters
        local present    = { }
        local done       = trace_baseinit and trace_ligatures and { }

        for i=1,nofligatures do
            local ligature = ligatures[i]
            local unicode  = ligature[1]
            local tree     = ligature[2]
            make_1(present,tree,"ctx_"..unicode)
        end

        for i=1,nofligatures do
            local ligature   = ligatures[i]
            local unicode    = ligature[1]
            local tree       = ligature[2]
            local lookupname = ligature[3]
            make_2(present,tfmdata,characters,tree,"ctx_"..unicode,unicode,unicode,done,sequence)
        end

    end

end

local function preparepositionings(tfmdata,feature,value,validlookups,lookuplist)
    local characters   = tfmdata.characters
    local descriptions = tfmdata.descriptions
    local resources    = tfmdata.resources
    local properties   = tfmdata.properties
    local traceindeed  = trace_baseinit and trace_kerns
    -- check out this sharedkerns trickery
    for i=1,#lookuplist do
        local sequence = lookuplist[i]
        local steps    = sequence.steps
        local kind     = sequence.type
        local format   = sequence.format
        if kind == "gpos_pair" then
            for i=1,#steps do
                local step   = steps[i]
                local format = step.format
                if format == "kern" or format == "move" then
                    for unicode, data in next, steps[i].coverage do
                        local character = characters[unicode]
                        local kerns = character.kerns
                        if not kerns then
                            kerns = { }
                            character.kerns = kerns
                        end
                        if traceindeed then
                            for otherunicode, kern in next, data do
                                if not kerns[otherunicode] and kern ~= 0 then
                                    kerns[otherunicode] = kern
                                    report_kern(feature,sequence,descriptions,unicode,otherunicode,kern)
                                end
                            end
                        else
                            for otherunicode, kern in next, data do
                                if not kerns[otherunicode] and kern ~= 0 then
                                    kerns[otherunicode] = kern
                                end
                            end
                        end
                    end
                else
                    for unicode, data in next, steps[i].coverage do
                        local character = characters[unicode]
                        local kerns     = character.kerns
                        for otherunicode, kern in next, data do
                            -- kern[2] is true (all zero) or a table
                            local other = kern[2]
                            if other == true or (not other and not (kerns and kerns[otherunicode])) then
                                local kern = kern[1]
                                if kern == true then
                                    -- all zero
                                elseif kern[1] ~= 0 or kern[2] ~= 0 or kern[4] ~= 0 then
                                    -- a complex pair not suitable for basemode
                                else
                                    kern = kern[3]
                                    if kern ~= 0 then
                                        if kerns then
                                            kerns[otherunicode] = kern
                                        else
                                            kerns = { [otherunicode] = kern }
                                            character.kerns = kerns
                                        end
                                        if traceindeed then
                                            report_kern(feature,sequence,descriptions,unicode,otherunicode,kern)
                                        end
                                    end
                                end
                            end
                        end
                    end
                end
            end
        end
    end

end

local function initializehashes(tfmdata)
    -- already done
end

local function checkmathreplacements(tfmdata,fullname,fixitalics)
    if tfmdata.mathparameters then
        local characters = tfmdata.characters
        local changed    = tfmdata.changed
        if next(changed) then
            if trace_preparing or trace_baseinit then
                report_prepare("checking math replacements for %a",fullname)
            end
            for unicode, replacement in next, changed do
                local u = characters[unicode]
                local r = characters[replacement]
                if u and r then
                    local n = u.next
                    local v = u.vert_variants
                    local h = u.horiz_variants
                    if fixitalics then
                        -- quite some warnings on stix ...
                        local ui = u.italic
                        if ui and not r.italic then
                            if trace_preparing then
                                report_prepare("using %i units of italic correction from %C for %U",ui,unicode,replacement)
                            end
                            r.italic = ui -- print(ui,ri)
                        end
                    end
                    if n and not r.next then
                        if trace_preparing then
                            report_prepare("forcing %s for %C substituted by %U","incremental step",unicode,replacement)
                        end
                        r.next = n
                    end
                    if v and not r.vert_variants then
                        if trace_preparing then
                            report_prepare("forcing %s for %C substituted by %U","vertical variants",unicode,replacement)
                        end
                        r.vert_variants = v
                    end
                    if h and not r.horiz_variants then
                        if trace_preparing then
                            report_prepare("forcing %s for %C substituted by %U","horizontal variants",unicode,replacement)
                        end
                        r.horiz_variants = h
                    end
                else
                    if trace_preparing then
                        report_prepare("error replacing %C by %U",unicode,replacement)
                    end
                end
            end
        end
    end
end

local function featuresinitializer(tfmdata,value)
    if true then -- value then
        local starttime = trace_preparing and os.clock()
        local features  = tfmdata.shared.features
        local fullname  = tfmdata.properties.fullname or "?"
        if features then
            initializehashes(tfmdata)
            local collectlookups    = otf.collectlookups
            local rawdata           = tfmdata.shared.rawdata
            local properties        = tfmdata.properties
            local script            = properties.script
            local language          = properties.language
            local rawresources      = rawdata.resources
            local rawfeatures       = rawresources and rawresources.features
            local basesubstitutions = rawfeatures and rawfeatures.gsub
            local basepositionings  = rawfeatures and rawfeatures.gpos
            local substitutionsdone = false
            local positioningsdone  = false
            --
            if basesubstitutions or basepositionings then
                local sequences = tfmdata.resources.sequences
                for s=1,#sequences do
                    local sequence = sequences[s]
                    local sfeatures = sequence.features
                    if sfeatures then
                        local order = sequence.order
                        if order then
                            for i=1,#order do --
                                local feature = order[i]
                                local value = features[feature]
                                if value then
                                    local validlookups, lookuplist = collectlookups(rawdata,feature,script,language)
                                    if not validlookups then
                                        -- skip
                                    elseif basesubstitutions and basesubstitutions[feature] then
                                        if trace_preparing then
                                            report_prepare("filtering base %s feature %a for %a with value %a","sub",feature,fullname,value)
                                        end
                                        preparesubstitutions(tfmdata,feature,value,validlookups,lookuplist)
                                        registerbasefeature(feature,value)
                                        substitutionsdone = true
                                    elseif basepositionings and basepositionings[feature] then
                                        if trace_preparing then
                                            report_prepare("filtering base %a feature %a for %a with value %a","pos",feature,fullname,value)
                                        end
                                        preparepositionings(tfmdata,feature,value,validlookups,lookuplist)
                                        registerbasefeature(feature,value)
                                        positioningsdone = true
                                    end
                                end
                            end
                        end
                    end
                end
            end
            --
            if substitutionsdone then
                checkmathreplacements(tfmdata,fullname,features.fixitalics)
            end
            --
            registerbasehash(tfmdata)
        end
        if trace_preparing then
            report_prepare("preparation time is %0.3f seconds for %a",os.clock()-starttime,fullname)
        end
    end
end

registerotffeature {
    name         = "features",
    description  = "features",
    default      = true,
    initializers = {
     -- position = 1, -- after setscript (temp hack ... we need to force script / language to 1
        base     = featuresinitializer,
    }
}

otf.basemodeinitializer = featuresinitializer