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

if not context then return end

local next, type, tonumber = next, type, tonumber

local fonts              = fonts
local utilities          = utilities

local handlers           = fonts.handlers
local otf                = handlers.otf
local afm                = handlers.afm
local registerotffeature = otf.features.register
local registerafmfeature = afm.features.register

local allocate           = utilities.storage.allocate
local getparameters      = utilities.parsers.getparameters

local implement          = interfaces and interfaces.implement

local trace_protrusion   = false  trackers.register("fonts.protrusion", function(v) trace_protrusion = v end)
local trace_expansion    = false  trackers.register("fonts.expansion",  function(v) trace_expansion  = v end)

local report_expansions  = logs.reporter("fonts","expansions")
local report_protrusions = logs.reporter("fonts","protrusions")

-- -- -- -- -- --
-- shared
-- -- -- -- -- --

local function get_class_and_vector(tfmdata,value,where) -- "expansions"
    local g_where = tfmdata.goodies and tfmdata.goodies[where]
    local f_where = fonts[where]
    local g_classes = g_where and g_where.classes
    local f_classes = f_where and f_where.classes
    local class = (g_classes and g_classes[value]) or (f_classes and f_classes[value])
    if class then
        local class_vector = class.vector
        local g_vectors = g_where and g_where.vectors
        local f_vectors = f_where and f_where.vectors
        local vector = (g_vectors and g_vectors[class_vector]) or (f_vectors and f_vectors[class_vector])
        return class, vector
    end
end

-- -- -- -- -- --
-- expansion (hz)
-- -- -- -- -- --

local expansions   = fonts.expansions or allocate()

fonts.expansions   = expansions

local classes      = expansions.classes or allocate()
local vectors      = expansions.vectors or allocate()

expansions.classes = classes
expansions.vectors = vectors

classes.preset = {
    stretch = 2,
    shrink  = 2,
    step    = .5,
    factor  = 1,
}

classes['quality'] = {
    stretch = 2,
    shrink  = 2,
    step    = .5,
    vector  = 'default',
    factor  = 1,
}

vectors['default'] = {
    [0x0041] = 0.5, -- A
    [0x0042] = 0.7, -- B
    [0x0043] = 0.7, -- C
    [0x0044] = 0.5, -- D
    [0x0045] = 0.7, -- E
    [0x0046] = 0.7, -- F
    [0x0047] = 0.5, -- G
    [0x0048] = 0.7, -- H
    [0x004B] = 0.7, -- K
    [0x004D] = 0.7, -- M
    [0x004E] = 0.7, -- N
    [0x004F] = 0.5, -- O
    [0x0050] = 0.7, -- P
    [0x0051] = 0.5, -- Q
    [0x0052] = 0.7, -- R
    [0x0053] = 0.7, -- S
    [0x0055] = 0.7, -- U
    [0x0057] = 0.7, -- W
    [0x005A] = 0.7, -- Z
    [0x0061] = 0.7, -- a
    [0x0062] = 0.7, -- b
    [0x0063] = 0.7, -- c
    [0x0064] = 0.7, -- d
    [0x0065] = 0.7, -- e
    [0x0067] = 0.7, -- g
    [0x0068] = 0.7, -- h
    [0x006B] = 0.7, -- k
    [0x006D] = 0.7, -- m
    [0x006E] = 0.7, -- n
    [0x006F] = 0.7, -- o
    [0x0070] = 0.7, -- p
    [0x0071] = 0.7, -- q
    [0x0073] = 0.7, -- s
    [0x0075] = 0.7, -- u
    [0x0077] = 0.7, -- w
    [0x007A] = 0.7, -- z
    [0x0032] = 0.7, -- 2
    [0x0033] = 0.7, -- 3
    [0x0036] = 0.7, -- 6
    [0x0038] = 0.7, -- 8
    [0x0039] = 0.7, -- 9
}

vectors['quality'] = vectors['default'] -- metatable ?

local function initialize(tfmdata,value)
    if value then
        local class, vector = get_class_and_vector(tfmdata,value,"expansions")
        if class then
            if vector then
                local stretch = class.stretch or 0
                local shrink  = class.shrink  or 0
                local step    = class.step    or 0
                local factor  = class.factor  or 1
                if trace_expansion then
                    report_expansions("setting class %a, vector %a, factor %a, stretch %a, shrink %a, step %a",
                        value,class.vector,factor,stretch,shrink,step)
                end
                tfmdata.parameters.expansion = {
                    stretch = 10 * stretch,
                    shrink  = 10 * shrink,
                    step    = 10 * step,
                    factor  = factor,
                }
                local data = characters and characters.data
                for i, chr in next, tfmdata.characters do
                    local v = vector[i]
                    if data and not v then -- we could move the data test outside (needed for plain)
                        local d = data[i]
                        if d then
                            local s = d.shcode
                            if not s then
                                -- sorry
                            elseif type(s) == "table" then
                                v = ((vector[s[1]] or 0) + (vector[s[#s]] or 0)) / 2
                            else
                                v = vector[s] or 0
                            end
                        end
                    end
                    if v and v ~= 0 then
                        chr.expansion_factor = v*factor
                    else -- can be option
                        chr.expansion_factor = factor
                    end
                end
            elseif trace_expansion then
                report_expansions("unknown vector %a in class %a",class.vector,value)
            end
        elseif trace_expansion then
            report_expansions("unknown class %a",value)
        end
    end
end

local specification = {
    name        = "expansion",
    description = "apply hz optimization",
    initializers = {
        base = initialize,
        node = initialize,
    }
}

registerotffeature(specification)
registerafmfeature(specification)

fonts.goodies.register("expansions",  function(...) return fonts.goodies.report("expansions", trace_expansion, ...) end)

implement {
    name      = "setupfontexpansion",
    arguments = "2 strings",
    actions   = function(class,settings) getparameters(classes,class,'preset',settings) end
}

-- -- -- -- -- --
-- protrusion
-- -- -- -- -- --

fonts.protrusions   = allocate()
local protrusions   = fonts.protrusions

protrusions.classes = allocate()
protrusions.vectors = allocate()

local classes       = protrusions.classes
local vectors       = protrusions.vectors

-- the values need to be revisioned

classes.preset = {
    factor = 1,
    left   = 1,
    right  = 1,
}

classes['pure']        = { vector = 'pure',        factor = 1 }
classes['punctuation'] = { vector = 'punctuation', factor = 1 }
classes['alpha']       = { vector = 'alpha',       factor = 1 }
classes['quality']     = { vector = 'quality',     factor = 1 }

vectors['pure'] = {

    [0x002C] = { 0, 1    }, -- comma
    [0x002E] = { 0, 1    }, -- period
    [0x003A] = { 0, 1    }, -- colon
    [0x003B] = { 0, 1    }, -- semicolon
    [0x002D] = { 0, 1    }, -- hyphen
    [0x00AD] = { 0, 1    }, -- also hyphen
    [0x2013] = { 0, 0.50 }, -- endash
    [0x2014] = { 0, 0.33 }, -- emdash
    [0x3001] = { 0, 1    }, -- ideographic comma      、
    [0x3002] = { 0, 1    }, -- ideographic full stop  。
    [0x060C] = { 0, 1    }, -- arabic comma           ،
    [0x061B] = { 0, 1    }, -- arabic semicolon       ؛
    [0x06D4] = { 0, 1    }, -- arabic full stop       ۔

}

vectors['punctuation'] = {

    [0x003F] = { 0,    0.20 }, -- ?
    [0x00BF] = { 0.20, 0    }, -- ¿
    [0x0021] = { 0,    0.20 }, -- !
    [0x00A1] = { 0.20, 0,   }, -- ¡
    [0x0028] = { 0.05, 0    }, -- (
    [0x0029] = { 0,    0.05 }, -- )
    [0x005B] = { 0.05, 0    }, -- [
    [0x005D] = { 0,    0.05 }, -- ]
    [0x002C] = { 0,    0.70 }, -- comma
    [0x002E] = { 0,    0.70 }, -- period
    [0x003A] = { 0,    0.50 }, -- colon
    [0x003B] = { 0,    0.50 }, -- semicolon
    [0x002D] = { 0,    0.70 }, -- hyphen
    [0x00AD] = { 0,    0.70 }, -- also hyphen
    [0x2013] = { 0,    0.30 }, -- endash
    [0x2014] = { 0,    0.20 }, -- emdash
    [0x060C] = { 0,    0.70 }, -- arabic comma
    [0x061B] = { 0,    0.50 }, -- arabic semicolon
    [0x06D4] = { 0,    0.70 }, -- arabic full stop
    [0x061F] = { 0,    0.20 }, -- ؟

    -- todo: left and right quotes: .5 double, .7 single

    [0x2039] = { 0.70, 0.70 }, -- left single guillemet   ‹
    [0x203A] = { 0.70, 0.70 }, -- right single guillemet  ›
    [0x00AB] = { 0.50, 0.50 }, -- left guillemet          «
    [0x00BB] = { 0.50, 0.50 }, -- right guillemet         »

    [0x2018] = { 0.70, 0.70 }, -- left single quotation mark             ‘
    [0x2019] = { 0,    0.70 }, -- right single quotation mark            ’
    [0x201A] = { 0.70, 0    }, -- single low-9 quotation mark            ,
    [0x201B] = { 0.70, 0    }, -- single high-reversed-9 quotation mark  ‛
    [0x201C] = { 0.50, 0.50 }, -- left double quotation mark             “
    [0x201D] = { 0,    0.50 }, -- right double quotation mark            ”
    [0x201E] = { 0.50, 0    }, -- double low-9 quotation mark            „
    [0x201F] = { 0.50, 0    }, -- double high-reversed-9 quotation mark  ‟

}

vectors['alpha'] = {

    [0x0041] = { .05, .05 }, -- A
    [0x0046] = {   0, .05 }, -- F
    [0x004A] = { .05,   0 }, -- J
    [0x004B] = {   0, .05 }, -- K
    [0x004C] = {   0, .05 }, -- L
    [0x0054] = { .05, .05 }, -- T
    [0x0056] = { .05, .05 }, -- V
    [0x0057] = { .05, .05 }, -- W
    [0x0058] = { .05, .05 }, -- X
    [0x0059] = { .05, .05 }, -- Y

    [0x006B] = {   0, .05 }, -- k
    [0x0072] = {   0, .05 }, -- r
    [0x0074] = {   0, .05 }, -- t
    [0x0076] = { .05, .05 }, -- v
    [0x0077] = { .05, .05 }, -- w
    [0x0078] = { .05, .05 }, -- x
    [0x0079] = { .05, .05 }, -- y

}

vectors['quality'] = table.merged(
    vectors['punctuation'],
    vectors['alpha']
)

-- As this is experimental code, users should not depend on it. The implications are still
-- discussed on the ConTeXt Dev List and we're not sure yet what exactly the spec is (the
-- next code is tested with a gyre font patched by / fea file made by Khaled Hosny). The
-- double trick should not be needed it proper hanging punctuation is used in which case
-- values < 1 can be used.
--
-- preferred (in context, usine vectors):
--
-- \definefontfeature[whatever][default][mode=node,protrusion=quality]
--
-- using lfbd and rtbd, with possibibility to enable only one side :
--
-- \definefontfeature[whocares][default][mode=node,protrusion=yes,  opbd=yes,script=latn]
-- \definefontfeature[whocares][default][mode=node,protrusion=right,opbd=yes,script=latn]
--
-- idem, using multiplier
--
-- \definefontfeature[whocares][default][mode=node,protrusion=2,opbd=yes,script=latn]
-- \definefontfeature[whocares][default][mode=node,protrusion=double,opbd=yes,script=latn]
--
-- idem, using named feature file (less frozen):
--
-- \definefontfeature[whocares][default][mode=node,protrusion=2,opbd=yes,script=latn,featurefile=texgyrepagella-regularxx.fea]

classes['double'] = { -- for testing opbd
    factor = 2,
    left   = 1,
    right  = 1,
}

local function map_opbd_onto_protrusion(tfmdata,value,opbd)
    local characters   = tfmdata.characters
    local descriptions = tfmdata.descriptions
    local properties   = tfmdata.properties
    local parameters   = tfmdata.parameters
    local resources    = tfmdata.resources
    local rawdata      = tfmdata.shared.rawdata
    local lookuphash   = rawdata.lookuphash
    local lookuptags   = resources.lookuptags
    local script       = properties.script
    local language     = properties.language
    local units        = parameters.units
    local done, factor, left, right = false, 1, 1, 1
    local class = classes[value]
    if class then
        factor = class.factor or 1
        left   = class.left   or 1
        right  = class.right  or 1
    else
        factor = tonumber(value) or 1
    end
    local lfactor = left  * factor
    local rfactor = right * factor
    if trace_protrusion then
        report_protrusions("left factor %0.3F, right factor %0.3F",lfactor,rfactor)
    end
    tfmdata.parameters.protrusion = {
        factor = factor,
        left   = left,
        right  = right,
    }
    if opbd ~= "right" then
        local validlookups, lookuplist = otf.collectlookups(rawdata,"lfbd",script,language)
        if validlookups then
            for i=1,#lookuplist do
                local lookup = lookuplist[i]
                local steps  = lookup.steps
                if steps then
                    if trace_protrusion then
                        report_protrusions("setting left using lfbd")
                    end
                    for i=1,#steps do
                        local step     = steps[i]
                        local coverage = step.coverage
                        if coverage then
                            for k, v in next, coverage do
                                if v == true then
                                    -- zero
                                else
                                    local w = descriptions[k].width
                                    local d = - v[1]
                                    if w == 0 or d == 0 then
                                        -- ignored
                                    else
                                        local p = lfactor * d/units
                                        characters[k].left_protruding = p
                                        if trace_protrusion then
                                            report_protrusions("lfbd -> %0.3F %C",p,k)
                                        end
                                    end
                                end
                            end
                        end
                    end
                    done = true
                end
            end
        end
    end
    if opbd ~= "left" then
        local validlookups, lookuplist = otf.collectlookups(rawdata,"rtbd",script,language)
        if validlookups then
            for i=1,#lookuplist do
                local lookup = lookuplist[i]
                local steps  = lookup.steps
                if steps then
                    if trace_protrusion then
                        report_protrusions("setting right using rtbd")
                    end
                    for i=1,#steps do
                        local step     = steps[i]
                        local coverage = step.coverage
                        if coverage then
                            for k, v in next, coverage do
                                if v == true then
                                    -- zero
                                else
                                    local w = descriptions[k].width
                                    local d = - v[3]
                                    if w == 0 or d == 0 then
                                        -- ignored
                                    else
                                        local p = rfactor * d/units
                                        characters[k].right_protruding = p
                                        if trace_protrusion then
                                            report_protrusions("rtbd -> %0.3F %C",p,k)
                                        end
                                    end
                                end
                            end
                        end
                    end
                end
                done = true
            end
        end
    end
end

-- The opbd test is just there because it was discussed on the context development list. However,
-- the mentioned fxlbi.otf font only has some kerns for digits. So, consider this feature not supported
-- till we have a proper test font.

local function initialize(tfmdata,value)
    if value then
        local opbd = tfmdata.shared.features.opbd
        if opbd then
            -- possible values: left right both yes no (experimental)
            map_opbd_onto_protrusion(tfmdata,value,opbd)
        else
            local class, vector = get_class_and_vector(tfmdata,value,"protrusions")
            if class then
                if vector then
                    local factor = class.factor or 1
                    local left   = class.left   or 1
                    local right  = class.right  or 1
                    if trace_protrusion then
                        report_protrusions("setting class %a, vector %a, factor %a, left %a, right %a",
                            value,class.vector,factor,left,right)
                    end
                    local data    = characters.data
                    local lfactor = left  * factor
                    local rfactor = right * factor
                    if trace_protrusion then
                        report_protrusions("left factor %0.3F, right factor %0.3F",lfactor,rfactor)
                    end
                    tfmdata.parameters.protrusion = {
                        factor = factor,
                        left   = left,
                        right  = right,
                    }
                    for i, chr in next, tfmdata.characters do
                        local v  = vector[i]
                        local pl = nil
                        local pr = nil
                        if v then
                            pl = v[1]
                            pr = v[2]
                        else
                            local d = data[i]
                            if d then
                                local s = d.shcode
                                if not s then
                                    -- sorry
                                elseif type(s) == "table" then
                                    local vl = vector[s[1]]
                                    local vr = vector[s[#s]]
                                    if vl then pl = vl[1] end
                                    if vr then pr = vr[2] end
                                else
                                    v = vector[s]
                                    if v then
                                        pl = v[1]
                                        pr = v[2]
                                    end
                                end
                            end
                        end
                        if pl and pl ~= 0 then
                            local p = pl * lfactor
                            chr.left_protruding  = p
                            if trace_protrusion then
                                report_protrusions("left  -> %0.3F %C ",p,i)
                            end
                        end
                        if pr and pr ~= 0 then
                            local p = pr * rfactor
                            chr.right_protruding = p
                            if trace_protrusion then
                                report_protrusions("right -> %0.3F %C",p,i)
                            end
                        end
                    end
                elseif trace_protrusion then
                    report_protrusions("unknown vector %a in class %a",class.vector,value)
                end
            elseif trace_protrusion then
                report_protrusions("unknown class %a",value)
            end
        end
    end
end

local specification = {
    name         = "protrusion",
    description  = "l/r margin character protrusion",
    initializers = {
        base = initialize,
        node = initialize,
    }
}

registerotffeature(specification)
registerafmfeature(specification)

fonts.goodies.register("protrusions", function(...) return fonts.goodies.report("protrusions", trace_protrusion, ...) end)

implement {
    name      = "setupfontprotrusion",
    arguments = "2 strings",
    actions   = function(class,settings) getparameters(classes,class,'preset',settings) end
}

local function initialize(tfmdata,value)
    local properties = tfmdata.properties
    local parameters = tfmdata.parameters
    if properties then
        value = tonumber(value)
        if value then
            if value < 0 then
                value = 0
            elseif value > 10 then
                report_expansions("threshold for %a @ %p limited to 10 pct",properties.fontname,parameters.size)
                value = 10
            end
            if value > 5 then
                report_expansions("threshold for %a @ %p exceeds 5 pct",properties.fontname,parameters.size)
            end
        end
        properties.threshold = value or nil -- nil enforces default
    end
end

local specification = {
    name         = "threshold",
    description  = "threshold for quality features",
    initializers = {
        base = initialize,
        node = initialize,
    }
}

registerotffeature(specification)
registerafmfeature(specification)