font-onr.lua /size: 18 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['font-onr'] = {
2    version   = 1.001,
3    optimize  = true,
4    comment   = "companion to font-ini.mkiv",
5    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
6    copyright = "PRAGMA ADE / ConTeXt Development Team",
7    license   = "see context related readme files"
8}
9
10-- Some code may look a bit obscure but this has to do with the fact that we also
11-- use this code for testing and much code evolved in the transition from TFM to AFM
12-- to OTF.
13--
14-- The following code still has traces of intermediate font support where we handles
15-- font encodings. Eventually font encoding went away but we kept some code around
16-- in other modules.
17--
18-- This version implements a node mode approach so that users can also more easily
19-- add features.
20
21local fonts, logs, trackers, resolvers = fonts, logs, trackers, resolvers
22
23local next, type, tonumber, rawset = next, type, tonumber, rawset
24local match, lower, gsub, strip, find = string.match, string.lower, string.gsub, string.strip, string.find
25local char, byte, sub = string.char, string.byte, string.sub
26local abs = math.abs
27local bxor, rshift = bit32.bxor, bit32.rshift
28local 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
29local lpegmatch, patterns = lpeg.match, lpeg.patterns
30
31local trace_indexing     = false  trackers.register("afm.indexing",   function(v) trace_indexing = v end)
32local trace_loading      = false  trackers.register("afm.loading",    function(v) trace_loading  = v end)
33
34local report_afm         = logs.reporter("fonts","afm loading")
35local report_pfb         = logs.reporter("fonts","pfb loading")
36
37local handlers           = fonts.handlers
38local afm                = handlers.afm or { }
39handlers.afm             = afm
40local readers            = afm.readers or { }
41afm.readers              = readers
42
43afm.version              = 1.513 -- incrementing this number one up will force a re-cache
44
45-- We start with the basic reader which we give a name similar to the built in TFM
46-- and OTF reader. We use a PFB loader but I see no differences between the old and
47-- new vectors (we actually had one bad vector with the old loader).
48
49local get_indexes, get_shapes
50
51do
52
53    local decrypt
54
55    do
56
57        local r, c1, c2, n = 0, 0, 0, 0
58
59        local function step(c)
60            local cipher = byte(c)
61            local plain  = bxor(cipher,rshift(r,8))
62            r = ((cipher + r) * c1 + c2) % 65536
63            return char(plain)
64        end
65
66        decrypt = function(binary,initial,seed)
67            r, c1, c2, n = initial, 52845, 22719, seed
68            binary       = gsub(binary,".",step)
69            return sub(binary,n+1)
70        end
71
72     -- local pattern = Cs((P(1) / step)^1)
73     --
74     -- decrypt = function(binary,initial,seed)
75     --     r, c1, c2, n = initial, 52845, 22719, seed
76     --     binary = lpegmatch(pattern,binary)
77     --     return sub(binary,n+1)
78     -- end
79
80    end
81
82    local charstrings = P("/CharStrings")
83    local subroutines = P("/Subrs")
84    local encoding    = P("/Encoding")
85    local dup         = P("dup")
86    local put         = P("put")
87    local array       = P("array")
88    local name        = P("/") * C((R("az","AZ","09")+S("-_."))^1)
89    local digits      = R("09")^1
90    local cardinal    = digits / tonumber
91    local spaces      = P(" ")^1
92    local spacing     = patterns.whitespace^0
93
94    local routines, vector, chars, n, m
95
96    local initialize = function(str,position,size)
97        n = 0
98        m = size
99        return position + 1
100    end
101
102    local setroutine = function(str,position,index,size,filename)
103        if routines[index] then
104            -- we have passed the end
105            return false
106        end
107        local forward = position + size
108        local stream  = decrypt(sub(str,position+1,forward),4330,4)
109        routines[index] = { byte(stream,1,#stream) }
110        n = n + 1
111        if n >= m then
112            -- m should be index now but can we assume ordering?
113            return #str
114        end
115        return forward + 1
116    end
117
118    local setvector = function(str,position,name,size,filename)
119        local forward = position + tonumber(size)
120        if n >= m then
121            return #str
122        elseif forward < #str then
123            if n == 0 and name ~= ".notdef" then
124                report_pfb("reserving .notdef at index 0 in %a",filename) -- luatex needs that
125                n = n + 1
126            end
127            vector[n] = name
128            n = n + 1
129            return forward
130        else
131            return #str
132        end
133    end
134
135    local setshapes = function(str,position,name,size,filename)
136        local forward = position + tonumber(size)
137        local stream  = sub(str,position+1,forward)
138        if n > m then
139            return #str
140        elseif forward < #str then
141            if n == 0 and name ~= ".notdef" then
142                report_pfb("reserving .notdef at index 0 in %a",filename) -- luatex needs that
143                n = n + 1
144            end
145            vector[n] = name
146            n = n + 1
147            chars [n] = decrypt(stream,4330,4)
148            return forward
149        else
150            return #str
151        end
152    end
153
154    local p_rd = spacing * (P("RD") + P("-|"))
155    local p_np = spacing * (P("NP") + P( "|"))
156    local p_nd = spacing * (P("ND") + P( "|"))
157
158    local p_filterroutines = -- dup <i> <n> RD or -| <n encrypted bytes> NP or |
159        (1-subroutines)^0 * subroutines * spaces * Cmt(cardinal,initialize)
160      * (Cmt(cardinal * spaces * cardinal * p_rd * Carg(1), setroutine) * p_np + (1-p_nd))^1
161
162    local p_filtershapes = -- /foo <n> RD <n encrypted bytes> ND
163        (1-charstrings)^0 * charstrings * spaces * Cmt(cardinal,initialize)
164      * (Cmt(name * spaces * cardinal * p_rd * Carg(1) , setshapes) * p_nd + P(1))^1
165
166    local p_filternames = Ct (
167        (1-charstrings)^0 * charstrings * spaces * Cmt(cardinal,initialize)
168        * (Cmt(name * spaces * cardinal * Carg(1), setvector) + P(1))^1
169    )
170
171    -- /Encoding 256 array
172    -- 0 1 255 {1 index exch /.notdef put} for
173    -- dup 0 /Foo put
174
175    local p_filterencoding =
176        (1-encoding)^0 * encoding * spaces * digits * spaces * array * (1-dup)^0
177      * Cf(
178            Ct("") * Cg(spacing * dup * spaces * cardinal * spaces * name * spaces * put)^1
179        ,rawset)
180
181    -- if one of first 4 not 0-9A-F then binary else hex
182
183    local key = spacing * P("/") * R("az","AZ")
184    local str = spacing * Cs { (P("(")/"") * ((1 - P("\\(") - P("\\)") - S("()")) + V(1))^0 * (P(")")/"") }
185    local num = spacing * (R("09") + S("+-."))^1 / tonumber
186    local arr = spacing * Ct (S("[{") * (num)^0 * spacing * S("]}"))
187    local boo = spacing * (P("true") * Cc(true) + P("false") * Cc(false))
188    local nam = spacing * P("/") * Cs(R("az","AZ")^1)
189
190    local p_filtermetadata = (
191        P("/") * Carg(1) * ( (
192            C("version")            * str
193          + C("Copyright")          * str
194          + C("Notice")             * str
195          + C("FullName")           * str
196          + C("FamilyName")         * str
197          + C("Weight")             * str
198          + C("ItalicAngle")        * num
199          + C("isFixedPitch")       * boo
200          + C("UnderlinePosition")  * num
201          + C("UnderlineThickness") * num
202          + C("FontName")           * nam
203          + C("FontMatrix")         * arr
204          + C("FontBBox")           * arr
205        ) ) / function(t,k,v) t[lower(k)] = v end
206      + P(1)
207    )^0 * Carg(1)
208
209    -- cache this?
210
211    local function loadpfbvector(filename,shapestoo,streams)
212        -- for the moment limited to encoding only
213
214        local data = io.loaddata(resolvers.findfile(filename))
215
216        if not data then
217            report_pfb("no data in %a",filename)
218            return
219        end
220
221        if not (find(data,"!PS-AdobeFont-",1,true) or find(data,"%!FontType1",1,true)) then
222            report_pfb("no font in %a",filename)
223            return
224        end
225
226        local ascii, binary = match(data,"(.*)eexec%s+......(.*)")
227
228        if not binary then
229            report_pfb("no binary data in %a",filename)
230            return
231        end
232
233        binary = decrypt(binary,55665,4)
234
235        local names    = { }
236        local encoding = lpegmatch(p_filterencoding,ascii)
237        local metadata = lpegmatch(p_filtermetadata,ascii,1,{})
238        local glyphs   = { }
239
240        routines, vector, chars = { }, { }, { }
241        if shapestoo or streams then
242         -- io.savedata("foo.txt",binary)
243            lpegmatch(p_filterroutines,binary,1,filename)
244            lpegmatch(p_filtershapes,binary,1,filename)
245            local data = {
246                dictionaries = {
247                    {
248                        charstrings = chars,
249                        charset     = vector,
250                        subroutines = routines,
251                    }
252                },
253            }
254            -- only cff 1 in type 1 fonts
255            fonts.handlers.otf.readers.parsecharstrings(false,data,glyphs,true,"cff",streams,true)
256        else
257            lpegmatch(p_filternames,binary,1,filename)
258        end
259
260        names = vector
261
262        routines, vector, chars = nil, nil, nil
263
264        return names, encoding, glyphs, metadata
265
266    end
267
268    local pfb      = handlers.pfb or { }
269    handlers.pfb   = pfb
270    pfb.loadvector = loadpfbvector
271
272    get_indexes = function(data,pfbname)
273        local vector = loadpfbvector(pfbname)
274        if vector then
275            local characters = data.characters
276            if trace_loading then
277                report_afm("getting index data from %a",pfbname)
278            end
279            for index=0,#vector do -- hm, zero, often space or notdef
280                local name = vector[index]
281                local char = characters[name]
282                if char then
283                    if trace_indexing then
284                        report_afm("glyph %a has index %a",name,index)
285                    end
286                    char.index = index
287                else
288                    if trace_indexing then
289                        report_afm("glyph %a has index %a but no data",name,index)
290                    end
291                end
292            end
293        end
294    end
295
296    get_shapes = function(pfbname)
297        local vector, encoding, glyphs = loadpfbvector(pfbname,true)
298        return glyphs
299    end
300
301end
302
303-- We start with the basic reader which we give a name similar to the built in TFM
304-- and OTF reader. We only need data that is relevant for our use. We don't support
305-- more complex arrangements like multiple master (obsolete), direction specific
306-- kerning, etc.
307
308local spacer     = patterns.spacer
309local whitespace = patterns.whitespace
310local lineend    = patterns.newline
311local spacing    = spacer^0
312local number     = spacing * S("+-")^-1 * (R("09") + S("."))^1 / tonumber
313local name       = spacing * C((1 - whitespace)^1)
314local words      = spacing * ((1 - lineend)^1 / strip)
315local rest       = (1 - lineend)^0
316local fontdata   = Carg(1)
317local semicolon  = spacing * P(";")
318local plus       = spacing * P("plus") * number
319local minus      = spacing * P("minus") * number
320
321-- kern pairs
322
323local function addkernpair(data,one,two,value)
324    local chr = data.characters[one]
325    if chr then
326        local kerns = chr.kerns
327        if kerns then
328            kerns[two] = tonumber(value)
329        else
330            chr.kerns = { [two] = tonumber(value) }
331        end
332    end
333end
334
335local p_kernpair = (fontdata * P("KPX") * name * name * number) / addkernpair
336
337-- char metrics
338
339local chr = false
340local ind = 0
341
342local function start(data,version)
343    data.metadata.afmversion = version
344    ind = 0
345    chr = { }
346end
347
348local function stop()
349    ind = 0
350    chr = false
351end
352
353local function setindex(i)
354    if i < 0 then
355        ind = ind + 1 -- ?
356    else
357        ind = i
358    end
359    chr = {
360        index = ind
361    }
362end
363
364local function setwidth(width)
365    chr.width = width
366end
367
368local function setname(data,name)
369    data.characters[name] = chr
370end
371
372local function setboundingbox(boundingbox)
373    chr.boundingbox = boundingbox
374end
375
376local function setligature(plus,becomes)
377    local ligatures = chr.ligatures
378    if ligatures then
379        ligatures[plus] = becomes
380    else
381        chr.ligatures = { [plus] = becomes }
382    end
383end
384
385local p_charmetric = ( (
386    P("C")  * number          / setindex
387  + P("WX") * number          / setwidth
388  + P("N")  * fontdata * name / setname
389  + P("B")  * Ct((number)^4)  / setboundingbox
390  + P("L")  * (name)^2        / setligature
391  ) * semicolon )^1
392
393local p_charmetrics = P("StartCharMetrics") * number * (p_charmetric + (1-P("EndCharMetrics")))^0 * P("EndCharMetrics")
394local p_kernpairs   = P("StartKernPairs")   * number * (p_kernpair   + (1-P("EndKernPairs"  )))^0 * P("EndKernPairs"  )
395
396local function set_1(data,key,a)     data.metadata[lower(key)] = a           end
397local function set_2(data,key,a,b)   data.metadata[lower(key)] = { a, b }    end
398local function set_3(data,key,a,b,c) data.metadata[lower(key)] = { a, b, c } end
399
400-- Notice         string
401-- EncodingScheme string
402-- MappingScheme  integer
403-- EscChar        integer
404-- CharacterSet   string
405-- Characters     integer
406-- IsBaseFont     boolean
407-- VVector        number number
408-- IsFixedV       boolean
409
410local p_parameters = P(false)
411  + fontdata
412  * ((P("FontName") + P("FullName") + P("FamilyName"))/lower)
413  * words / function(data,key,value)
414        data.metadata[key] = value
415    end
416  + fontdata
417  * ((P("Weight") + P("Version"))/lower)
418  * name / function(data,key,value)
419        data.metadata[key] = value
420    end
421  + fontdata
422  * P("IsFixedPitch")
423  * name / function(data,pitch)
424        data.metadata.monospaced = toboolean(pitch,true)
425    end
426  + fontdata
427  * P("FontBBox")
428  * Ct(number^4) / function(data,boundingbox)
429        data.metadata.boundingbox = boundingbox
430  end
431  + fontdata
432  * ((P("CharWidth") + P("CapHeight") + P("XHeight") + P("Descender") + P("Ascender") + P("ItalicAngle"))/lower)
433  * number / function(data,key,value)
434        data.metadata[key] = value
435    end
436  + P("Comment") * spacing * ( P(false)
437      + (fontdata * C("DESIGNSIZE")     * number                   * rest) / set_1 -- 1
438      + (fontdata * C("TFM designsize") * number                   * rest) / set_1
439      + (fontdata * C("DesignSize")     * number                   * rest) / set_1
440      + (fontdata * C("CODINGSCHEME")   * words                    * rest) / set_1 --
441      + (fontdata * C("CHECKSUM")       * number * words           * rest) / set_1 -- 2
442      + (fontdata * C("SPACE")          * number * plus * minus    * rest) / set_3 -- 3 4 5
443      + (fontdata * C("QUAD")           * number                   * rest) / set_1 -- 6
444      + (fontdata * C("EXTRASPACE")     * number                   * rest) / set_1 -- 7
445      + (fontdata * C("NUM")            * number * number * number * rest) / set_3 -- 8 9 10
446      + (fontdata * C("DENOM")          * number * number          * rest) / set_2 -- 11 12
447      + (fontdata * C("SUP")            * number * number * number * rest) / set_3 -- 13 14 15
448      + (fontdata * C("SUB")            * number * number          * rest) / set_2 -- 16 17
449      + (fontdata * C("SUPDROP")        * number                   * rest) / set_1 -- 18
450      + (fontdata * C("SUBDROP")        * number                   * rest) / set_1 -- 19
451      + (fontdata * C("DELIM")          * number * number          * rest) / set_2 -- 20 21
452      + (fontdata * C("AXISHEIGHT")     * number                   * rest) / set_1 -- 22
453    )
454
455local fullparser = ( P("StartFontMetrics") * fontdata * name / start )
456                 * ( p_charmetrics + p_kernpairs + p_parameters + (1-P("EndFontMetrics")) )^0
457                 * ( P("EndFontMetrics") / stop )
458
459local infoparser = ( P("StartFontMetrics") * fontdata * name / start )
460                 * ( p_parameters + (1-P("EndFontMetrics")) )^0
461                 * ( P("EndFontMetrics") / stop )
462
463--    infoparser = ( P("StartFontMetrics") * fontdata * name / start )
464--               * ( p_parameters + (1-P("EndFontMetrics") - P("StartCharMetrics")) )^0
465--               * ( (P("EndFontMetrics") + P("StartCharMetrics")) / stop )
466
467local function read(filename,parser)
468    local afmblob = io.loaddata(filename)
469    if afmblob then
470        local data = {
471            resources = {
472                filename = resolvers.unresolve(filename),
473                version  = afm.version,
474                creator  = "context mkiv",
475            },
476            properties = {
477                hasitalics = false,
478            },
479            goodies = {
480            },
481            metadata   = {
482                filename = file.removesuffix(file.basename(filename))
483            },
484            characters = {
485                -- a temporary store
486            },
487            descriptions = {
488                -- the final store
489            },
490        }
491        if trace_loading then
492            report_afm("parsing afm file %a",filename)
493        end
494        lpegmatch(parser,afmblob,1,data)
495        return data
496    else
497        if trace_loading then
498            report_afm("no valid afm file %a",filename)
499        end
500        return nil
501    end
502end
503
504function readers.loadfont(afmname,pfbname)
505    local data = read(resolvers.findfile(afmname),fullparser)
506    if data then
507        if not pfbname or pfbname == "" then
508            pfbname = resolvers.findfile(file.replacesuffix(file.nameonly(afmname),"pfb"))
509        end
510        if pfbname and pfbname ~= "" then
511            data.resources.filename = resolvers.unresolve(pfbname)
512            get_indexes(data,pfbname)
513            return data
514        else -- if trace_loading then
515            report_afm("no pfb file for %a",afmname)
516            -- better than loading the afm file: data.resources.filename = rawname
517            -- but that will still crash the backend so we just return nothing now
518        end
519    end
520end
521
522-- for now, todo: n and check with otf (no afm needed here)
523
524function readers.loadshapes(filename)
525    local fullname = resolvers.findfile(filename) or ""
526    if fullname == "" then
527        return {
528            filename = "not found: " .. filename,
529            glyphs   = { }
530        }
531    else
532        return {
533            filename = fullname,
534            format   = "opentype",
535            glyphs   = get_shapes(fullname) or { },
536            units    = 1000,
537        }
538    end
539end
540
541
542function readers.getinfo(filename)
543    local data = read(resolvers.findfile(filename),infoparser)
544    if data then
545        return data.metadata
546    end
547end
548