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