font-onr.lua / last modification: 2020-01-30 14:16
if not modules then modules = { } end modules ['font-onr'] = {
    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, resolvers = fonts, logs, trackers, resolvers

local next, type, tonumber, rawset = next, type, tonumber, rawset
local match, lower, gsub, strip, find = string.match, string.lower, string.gsub, string.strip, string.find
local char, byte, sub = string.char, string.byte, string.sub
local abs = math.abs
local bxor, rshift = bit32.bxor, bit32.rshift
local P, S, R, V, Cmt, C, Ct, Cs, Carg, Cf, Cg, Cc = lpeg.P, lpeg.S, lpeg.R, lpeg.V, lpeg.Cmt, lpeg.C, lpeg.Ct, lpeg.Cs, lpeg.Carg, lpeg.Cf, lpeg.Cg, lpeg.Cc
local lpegmatch, patterns = lpeg.match, lpeg.patterns

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 report_afm         = logs.reporter("fonts","afm loading")
local report_pfb         = logs.reporter("fonts","pfb loading")

local handlers           = fonts.handlers
local afm                = handlers.afm or { }
handlers.afm             = afm
local readers            = afm.readers or { }
afm.readers              = readers

afm.version              = 1.513 -- incrementing this number one up will force a re-cache

--[[ldx--
<p>We start with the basic reader which we give a name similar to the built in <l n='tfm'/>
and <l n='otf'/> reader.</p>
<p>We use a new (unfinished) pfb loader but I see no differences between the old
and new vectors (we actually had one bad vector with the old loader).</p>
--ldx]]--

local get_indexes, get_shapes

do

    local decrypt

    do

        local r, c1, c2, n = 0, 0, 0, 0

        local function step(c)
            local cipher = byte(c)
            local plain  = bxor(cipher,rshift(r,8))
            r = ((cipher + r) * c1 + c2) % 65536
            return char(plain)
        end

        decrypt = function(binary,initial,seed)
            r, c1, c2, n = initial, 52845, 22719, seed
            binary       = gsub(binary,".",step)
            return sub(binary,n+1)
        end

     -- local pattern = Cs((P(1) / step)^1)
     --
     -- decrypt = function(binary,initial,seed)
     --     r, c1, c2, n = initial, 52845, 22719, seed
     --     binary = lpegmatch(pattern,binary)
     --     return sub(binary,n+1)
     -- end

    end

    local charstrings = P("/CharStrings")
    local subroutines = P("/Subrs")
    local encoding    = P("/Encoding")
    local dup         = P("dup")
    local put         = P("put")
    local array       = P("array")
    local name        = P("/") * C((R("az","AZ","09")+S("-_."))^1)
    local digits      = R("09")^1
    local cardinal    = digits / tonumber
    local spaces      = P(" ")^1
    local spacing     = patterns.whitespace^0

    local routines, vector, chars, n, m

    local initialize = function(str,position,size)
        n = 0
        m = size
        return position + 1
    end

    local setroutine = function(str,position,index,size,filename)
        if routines[index] then
            -- we have passed the end
            return false
        end
        local forward = position + size
        local stream  = decrypt(sub(str,position+1,forward),4330,4)
        routines[index] = { byte(stream,1,#stream) }
        n = n + 1
        if n >= m then
            -- m should be index now but can we assume ordering?
            return #str
        end
        return forward + 1
    end

    local setvector = function(str,position,name,size,filename)
        local forward = position + tonumber(size)
        if n >= m then
            return #str
        elseif forward < #str then
            if n == 0 and name ~= ".notdef" then
                report_pfb("reserving .notdef at index 0 in %a",filename) -- luatex needs that
                n = n + 1
            end
            vector[n] = name
            n = n + 1
            return forward
        else
            return #str
        end
    end

    local setshapes = function(str,position,name,size,filename)
        local forward = position + tonumber(size)
        local stream  = sub(str,position+1,forward)
        if n > m then
            return #str
        elseif forward < #str then
            if n == 0 and name ~= ".notdef" then
                report_pfb("reserving .notdef at index 0 in %a",filename) -- luatex needs that
                n = n + 1
            end
            vector[n] = name
            n = n + 1
            chars [n] = decrypt(stream,4330,4)
            return forward
        else
            return #str
        end
    end

    local p_rd = spacing * (P("RD") + P("-|"))
    local p_np = spacing * (P("NP") + P( "|"))
    local p_nd = spacing * (P("ND") + P( "|"))

    local p_filterroutines = -- dup <i> <n> RD or -| <n encrypted bytes> NP or |
        (1-subroutines)^0 * subroutines * spaces * Cmt(cardinal,initialize)
      * (Cmt(cardinal * spaces * cardinal * p_rd * Carg(1), setroutine) * p_np + (1-p_nd))^1

    local p_filtershapes = -- /foo <n> RD <n encrypted bytes> ND
        (1-charstrings)^0 * charstrings * spaces * Cmt(cardinal,initialize)
      * (Cmt(name * spaces * cardinal * p_rd * Carg(1) , setshapes) * p_nd + P(1))^1

    local p_filternames = Ct (
        (1-charstrings)^0 * charstrings * spaces * Cmt(cardinal,initialize)
        * (Cmt(name * spaces * cardinal * Carg(1), setvector) + P(1))^1
    )

    -- /Encoding 256 array
    -- 0 1 255 {1 index exch /.notdef put} for
    -- dup 0 /Foo put

    local p_filterencoding =
        (1-encoding)^0 * encoding * spaces * digits * spaces * array * (1-dup)^0
      * Cf(
            Ct("") * Cg(spacing * dup * spaces * cardinal * spaces * name * spaces * put)^1
        ,rawset)

    -- if one of first 4 not 0-9A-F then binary else hex

    local key = spacing * P("/") * R("az","AZ")
    local str = spacing * Cs { (P("(")/"") * ((1 - P("\\(") - P("\\)") - S("()")) + V(1))^0 * (P(")")/"") }
    local num = spacing * (R("09") + S("+-."))^1 / tonumber
    local arr = spacing * Ct (S("[{") * (num)^0 * spacing * S("]}"))
    local boo = spacing * (P("true") * Cc(true) + P("false") * Cc(false))
    local nam = spacing * P("/") * Cs(R("az","AZ")^1)

    local p_filtermetadata = (
        P("/") * Carg(1) * ( (
            C("version")            * str
          + C("Copyright")          * str
          + C("Notice")             * str
          + C("FullName")           * str
          + C("FamilyName")         * str
          + C("Weight")             * str
          + C("ItalicAngle")        * num
          + C("isFixedPitch")       * boo
          + C("UnderlinePosition")  * num
          + C("UnderlineThickness") * num
          + C("FontName")           * nam
          + C("FontMatrix")         * arr
          + C("FontBBox")           * arr
        ) ) / function(t,k,v) t[lower(k)] = v end
      + P(1)
    )^0 * Carg(1)

    local function loadpfbvector(filename,shapestoo,streams)
        -- for the moment limited to encoding only

        local data = io.loaddata(resolvers.findfile(filename))

        if not data then
            report_pfb("no data in %a",filename)
            return
        end

        if not (find(data,"!PS-AdobeFont-",1,true) or find(data,"%!FontType1",1,true)) then
            report_pfb("no font in %a",filename)
            return
        end

        local ascii, binary = match(data,"(.*)eexec%s+......(.*)")

        if not binary then
            report_pfb("no binary data in %a",filename)
            return
        end

        binary = decrypt(binary,55665,4)

        local names    = { }
        local encoding = lpegmatch(p_filterencoding,ascii)
        local metadata = lpegmatch(p_filtermetadata,ascii,1,{})
        local glyphs   = { }

        routines, vector, chars = { }, { }, { }
        if shapestoo or streams then
         -- io.savedata("foo.txt",binary)
            lpegmatch(p_filterroutines,binary,1,filename)
            lpegmatch(p_filtershapes,binary,1,filename)
            local data = {
                dictionaries = {
                    {
                        charstrings = chars,
                        charset     = vector,
                        subroutines = routines,
                    }
                },
            }
            -- only cff 1 in type 1 fonts
            fonts.handlers.otf.readers.parsecharstrings(false,data,glyphs,true,"cff",streams,true)
        else
            lpegmatch(p_filternames,binary,1,filename)
        end

        names = vector

        routines, vector, chars = nil, nil, nil

        return names, encoding, glyphs, metadata

    end

    local pfb      = handlers.pfb or { }
    handlers.pfb   = pfb
    pfb.loadvector = loadpfbvector

    get_indexes = function(data,pfbname)
        local vector = loadpfbvector(pfbname)
        if vector then
            local characters = data.characters
            if trace_loading then
                report_afm("getting index data from %a",pfbname)
            end
            for index=0,#vector do -- hm, zero, often space or notdef
                local name = vector[index]
                local char = characters[name]
                if char then
                    if trace_indexing then
                        report_afm("glyph %a has index %a",name,index)
                    end
                    char.index = index
                else
                    if trace_indexing then
                        report_afm("glyph %a has index %a but no data",name,index)
                    end
                end
            end
        end
    end

    get_shapes = function(pfbname)
        local vector, encoding, glyphs = loadpfbvector(pfbname,true)
        return glyphs
    end

end

--[[ldx--
<p>We start with the basic reader which we give a name similar to the built in <l n='tfm'/>
and <l n='otf'/> reader. We only need data that is relevant for our use. We don't support
more complex arrangements like multiple master (obsolete), direction specific kerning, etc.</p>
--ldx]]--

local spacer     = patterns.spacer
local whitespace = patterns.whitespace
local lineend    = patterns.newline
local spacing    = spacer^0
local number     = spacing * S("+-")^-1 * (R("09") + S("."))^1 / tonumber
local name       = spacing * C((1 - whitespace)^1)
local words      = spacing * ((1 - lineend)^1 / strip)
local rest       = (1 - lineend)^0
local fontdata   = Carg(1)
local semicolon  = spacing * P(";")
local plus       = spacing * P("plus") * number
local minus      = spacing * P("minus") * number

-- kern pairs

local function addkernpair(data,one,two,value)
    local chr = data.characters[one]
    if chr then
        local kerns = chr.kerns
        if kerns then
            kerns[two] = tonumber(value)
        else
            chr.kerns = { [two] = tonumber(value) }
        end
    end
end

local p_kernpair = (fontdata * P("KPX") * name * name * number) / addkernpair

-- char metrics

local chr = false
local ind = 0

local function start(data,version)
    data.metadata.afmversion = version
    ind = 0
    chr = { }
end

local function stop()
    ind = 0
    chr = false
end

local function setindex(i)
    if i < 0 then
        ind = ind + 1 -- ?
    else
        ind = i
    end
    chr = {
        index = ind
    }
end

local function setwidth(width)
    chr.width = width
end

local function setname(data,name)
    data.characters[name] = chr
end

local function setboundingbox(boundingbox)
    chr.boundingbox = boundingbox
end

local function setligature(plus,becomes)
    local ligatures = chr.ligatures
    if ligatures then
        ligatures[plus] = becomes
    else
        chr.ligatures = { [plus] = becomes }
    end
end

local p_charmetric = ( (
    P("C")  * number          / setindex
  + P("WX") * number          / setwidth
  + P("N")  * fontdata * name / setname
  + P("B")  * Ct((number)^4)  / setboundingbox
  + P("L")  * (name)^2        / setligature
  ) * semicolon )^1

local p_charmetrics = P("StartCharMetrics") * number * (p_charmetric + (1-P("EndCharMetrics")))^0 * P("EndCharMetrics")
local p_kernpairs   = P("StartKernPairs")   * number * (p_kernpair   + (1-P("EndKernPairs"  )))^0 * P("EndKernPairs"  )

local function set_1(data,key,a)     data.metadata[lower(key)] = a           end
local function set_2(data,key,a,b)   data.metadata[lower(key)] = { a, b }    end
local function set_3(data,key,a,b,c) data.metadata[lower(key)] = { a, b, c } end

-- Notice         string
-- EncodingScheme string
-- MappingScheme  integer
-- EscChar        integer
-- CharacterSet   string
-- Characters     integer
-- IsBaseFont     boolean
-- VVector        number number
-- IsFixedV       boolean

local p_parameters = P(false)
  + fontdata
  * ((P("FontName") + P("FullName") + P("FamilyName"))/lower)
  * words / function(data,key,value)
        data.metadata[key] = value
    end
  + fontdata
  * ((P("Weight") + P("Version"))/lower)
  * name / function(data,key,value)
        data.metadata[key] = value
    end
  + fontdata
  * P("IsFixedPitch")
  * name / function(data,pitch)
        data.metadata.monospaced = toboolean(pitch,true)
    end
  + fontdata
  * P("FontBBox")
  * Ct(number^4) / function(data,boundingbox)
        data.metadata.boundingbox = boundingbox
  end
  + fontdata
  * ((P("CharWidth") + P("CapHeight") + P("XHeight") + P("Descender") + P("Ascender") + P("ItalicAngle"))/lower)
  * number / function(data,key,value)
        data.metadata[key] = value
    end
  + P("Comment") * spacing * ( P(false)
      + (fontdata * C("DESIGNSIZE")     * number                   * rest) / set_1 -- 1
      + (fontdata * C("TFM designsize") * number                   * rest) / set_1
      + (fontdata * C("DesignSize")     * number                   * rest) / set_1
      + (fontdata * C("CODINGSCHEME")   * words                    * rest) / set_1 --
      + (fontdata * C("CHECKSUM")       * number * words           * rest) / set_1 -- 2
      + (fontdata * C("SPACE")          * number * plus * minus    * rest) / set_3 -- 3 4 5
      + (fontdata * C("QUAD")           * number                   * rest) / set_1 -- 6
      + (fontdata * C("EXTRASPACE")     * number                   * rest) / set_1 -- 7
      + (fontdata * C("NUM")            * number * number * number * rest) / set_3 -- 8 9 10
      + (fontdata * C("DENOM")          * number * number          * rest) / set_2 -- 11 12
      + (fontdata * C("SUP")            * number * number * number * rest) / set_3 -- 13 14 15
      + (fontdata * C("SUB")            * number * number          * rest) / set_2 -- 16 17
      + (fontdata * C("SUPDROP")        * number                   * rest) / set_1 -- 18
      + (fontdata * C("SUBDROP")        * number                   * rest) / set_1 -- 19
      + (fontdata * C("DELIM")          * number * number          * rest) / set_2 -- 20 21
      + (fontdata * C("AXISHEIGHT")     * number                   * rest) / set_1 -- 22
    )

local fullparser = ( P("StartFontMetrics") * fontdata * name / start )
                 * ( p_charmetrics + p_kernpairs + p_parameters + (1-P("EndFontMetrics")) )^0
                 * ( P("EndFontMetrics") / stop )

local infoparser = ( P("StartFontMetrics") * fontdata * name / start )
                 * ( p_parameters + (1-P("EndFontMetrics")) )^0
                 * ( P("EndFontMetrics") / stop )

--    infoparser = ( P("StartFontMetrics") * fontdata * name / start )
--               * ( p_parameters + (1-P("EndFontMetrics") - P("StartCharMetrics")) )^0
--               * ( (P("EndFontMetrics") + P("StartCharMetrics")) / stop )

local function read(filename,parser)
    local afmblob = io.loaddata(filename)
    if afmblob then
        local data = {
            resources = {
                filename = resolvers.unresolve(filename),
                version  = afm.version,
                creator  = "context mkiv",
            },
            properties = {
                hasitalics = false,
            },
            goodies = {
            },
            metadata   = {
                filename = file.removesuffix(file.basename(filename))
            },
            characters = {
                -- a temporary store
            },
            descriptions = {
                -- the final store
            },
        }
        if trace_loading then
            report_afm("parsing afm file %a",filename)
        end
        lpegmatch(parser,afmblob,1,data)
        return data
    else
        if trace_loading then
            report_afm("no valid afm file %a",filename)
        end
        return nil
    end
end

function readers.loadfont(afmname,pfbname)
    local data = read(resolvers.findfile(afmname),fullparser)
    if data then
        if not pfbname or pfbname == "" then
            pfbname = resolvers.findfile(file.replacesuffix(file.nameonly(afmname),"pfb"))
        end
        if pfbname and pfbname ~= "" then
            data.resources.filename = resolvers.unresolve(pfbname)
            get_indexes(data,pfbname)
            return data
        else -- if trace_loading then
            report_afm("no pfb file for %a",afmname)
            -- better than loading the afm file: data.resources.filename = rawname
            -- but that will still crash the backend so we just return nothing now
        end
    end
end

-- for now, todo: n and check with otf (no afm needed here)

function readers.loadshapes(filename)
    local fullname = resolvers.findfile(filename) or ""
    if fullname == "" then
        return {
            filename = "not found: " .. filename,
            glyphs   = { }
        }
    else
        return {
            filename = fullname,
            format   = "opentype",
            glyphs   = get_shapes(fullname) or { },
            units    = 1000,
        }
    end
end


function readers.getinfo(filename)
    local data = read(resolvers.findfile(filename),infoparser)
    if data then
        return data.metadata
    end
end