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