luatex-fonts-tfm.lua /size: 22 Kb    last modification: 2020-07-01 14:35
1if not modules then modules = { } end modules ['luatex-fonts-tfm'] = {
2    version   = 1.001,
3    comment   = "companion to font-ini.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- This is the generic tfm/vf loader. There are no fundamental differences with
10-- the one used in ConTeXt but we save some byte(codes) this way.
11
12local next, type = next, type
13local match, format = string.match, string.format
14local concat, sortedhash = table.concat, table.sortedhash
15local idiv = number.idiv
16
17local trace_defining           = false  trackers.register("fonts.defining", function(v) trace_defining = v end)
18local trace_features           = false  trackers.register("tfm.features",   function(v) trace_features = v end)
19
20local report_defining          = logs.reporter("fonts","defining")
21local report_tfm               = logs.reporter("fonts","tfm loading")
22
23local findbinfile              = resolvers.findbinfile
24local setmetatableindex        = table.setmetatableindex
25
26local fonts                    = fonts
27local handlers                 = fonts.handlers
28local helpers                  = fonts.helpers
29local readers                  = fonts.readers
30local constructors             = fonts.constructors
31local encodings                = fonts.encodings
32
33local tfm                      = constructors.handlers.tfm
34tfm.version                    = 1.000
35tfm.maxnestingdepth            = 5
36tfm.maxnestingsize             = 65536*1024
37
38local otf                      = fonts.handlers.otf
39local otfenhancers             = otf.enhancers
40
41local tfmfeatures              = constructors.features.tfm
42local registertfmfeature       = tfmfeatures.register
43
44local tfmenhancers             = constructors.enhancers.tfm
45local registertfmenhancer      = tfmenhancers.register
46
47local charcommand              = helpers.commands.char
48
49constructors.resolvevirtualtoo = false -- wil be set in font-ctx.lua
50
51fonts.formats.tfm              = "type1" -- we need to have at least a value here
52fonts.formats.ofm              = "type1" -- we need to have at least a value here
53
54function tfm.setfeatures(tfmdata,features)
55    local okay = constructors.initializefeatures("tfm",tfmdata,features,trace_features,report_tfm)
56    if okay then
57        return constructors.collectprocessors("tfm",tfmdata,features,trace_features,report_tfm)
58    else
59        return { } -- will become false
60    end
61end
62
63local depth = { } -- table.setmetatableindex("number")
64
65local loadtfm = font.read_tfm
66local loadvf  = font.read_vf
67
68local function read_from_tfm(specification)
69    local filename  = specification.filename
70    local size      = specification.size
71    depth[filename] = (depth[filename] or 0) + 1
72    if trace_defining then
73        report_defining("loading tfm file %a at size %s",filename,size)
74    end
75    local tfmdata = loadtfm(filename,size)
76    if tfmdata then
77
78        local features = specification.features and specification.features.normal or { }
79        local features = constructors.checkedfeatures("tfm",features)
80        specification.features.normal = features
81
82        -- If reencode returns a new table, we assume that we're doing something
83        -- special. An 'auto' reencode picks up its vector from the pfb file.
84
85        local newtfmdata = (depth[filename] == 1) and tfm.reencode(tfmdata,specification)
86        if newtfmdata then
87             tfmdata = newtfmdata
88        end
89
90        local resources  = tfmdata.resources  or { }
91        local properties = tfmdata.properties or { }
92        local parameters = tfmdata.parameters or { }
93        local shared     = tfmdata.shared     or { }
94        --
95        shared.features  = features
96        shared.resources = resources
97        --
98        properties.name       = tfmdata.name           -- todo: fallback
99        properties.fontname   = tfmdata.fontname       -- todo: fallback
100        properties.psname     = tfmdata.psname         -- todo: fallback
101        properties.fullname   = tfmdata.fullname       -- todo: fallback
102        properties.filename   = specification.filename -- todo: fallback
103        properties.format     = tfmdata.format or fonts.formats.tfm -- better than nothing
104        properties.usedbitmap = tfmdata.usedbitmap
105        --
106        tfmdata.properties = properties
107        tfmdata.resources  = resources
108        tfmdata.parameters = parameters
109        tfmdata.shared     = shared
110        --
111        shared.rawdata  = { resources = resources }
112        shared.features = features
113        --
114        -- The next branch is only entered when we have a proper encoded file i.e.
115        -- unicodes and such. It really nakes no sense to do feature juggling when
116        -- we have no names and unicodes.
117        --
118        if newtfmdata then
119            --
120            -- Some opentype processing assumes these to be present:
121            --
122            if not resources.marks then
123                resources.marks = { }
124            end
125            if not resources.sequences then
126                resources.sequences = { }
127            end
128            if not resources.features then
129                resources.features = {
130                    gsub = { },
131                    gpos = { },
132                }
133            end
134            if not tfmdata.changed then
135                tfmdata.changed = { }
136            end
137            if not tfmdata.descriptions then
138                tfmdata.descriptions = tfmdata.characters
139            end
140            --
141            -- It might be handy to have this:
142            --
143            otf.readers.addunicodetable(tfmdata)
144            --
145            -- We make a pseudo opentype font, e.g. kerns and ligatures etc:
146            --
147            tfmenhancers.apply(tfmdata,filename)
148            --
149            -- Now user stuff can kick in.
150            --
151            constructors.applymanipulators("tfm",tfmdata,features,trace_features,report_tfm)
152            --
153            -- As that can also mess with names and such, we are now ready for finalizing
154            -- the unicode information. This is a different order that for instance type one
155            -- (afm) files. First we try to deduce unicodes from already present information.
156            --
157            otf.readers.unifymissing(tfmdata)
158            --
159            -- Next we fill in the gaps, based on names from teh agl. Probably not much will
160            -- happen here.
161            --
162            fonts.mappings.addtounicode(tfmdata,filename)
163            --
164            -- The tounicode data is passed to the backend that constructs the vectors for us.
165            --
166            tfmdata.tounicode = 1
167            local tounicode   = fonts.mappings.tounicode
168            for unicode, v in next, tfmdata.characters do
169                local u = v.unicode
170                if u then
171                    v.tounicode = tounicode(u)
172                end
173            end
174            --
175            -- However, when we use a bitmap font those vectors can't be constructed because
176            -- that information is not carried with those fonts (there is no name info, nor
177            -- proper index info, nor unicodes at that end). So, we provide it ourselves.
178            --
179            if tfmdata.usedbitmap then
180                tfm.addtounicode(tfmdata)
181            end
182        end
183        --
184        shared.processes = next(features) and tfm.setfeatures(tfmdata,features) or nil
185        --
186        if size < 0 then
187            size = idiv(65536 * -size,100)
188        end
189        parameters.factor        = 1     -- already scaled
190        parameters.units         = 1000  -- just in case
191        parameters.size          = size
192        parameters.slant         = parameters.slant          or parameters[1] or 0
193        parameters.space         = parameters.space          or parameters[2] or 0
194        parameters.space_stretch = parameters.space_stretch  or parameters[3] or 0
195        parameters.space_shrink  = parameters.space_shrink   or parameters[4] or 0
196        parameters.x_height      = parameters.x_height       or parameters[5] or 0
197        parameters.quad          = parameters.quad           or parameters[6] or 0
198        parameters.extra_space   = parameters.extra_space    or parameters[7] or 0
199        --
200        constructors.enhanceparameters(parameters) -- official copies for us
201        --
202        properties.private       =  properties.private or tfmdata.private or privateoffset
203        --
204        if newtfmdata then
205            --
206            -- We do nothing as we assume flat tfm files. It would become real messy
207            -- otherwise and I don't have something for testing on my system anyway.
208            --
209        elseif constructors.resolvevirtualtoo then
210            fonts.loggers.register(tfmdata,file.suffix(filename),specification) -- strange, why here
211            local vfname = findbinfile(specification.name, 'ovf')
212            if vfname and vfname ~= "" then
213                local vfdata = loadvf(vfname,size)
214                if vfdata then
215                    local chars = tfmdata.characters
216                    for k,v in next, vfdata.characters do
217                        chars[k].commands = v.commands
218                    end
219                    properties.virtualized = true
220                    tfmdata.fonts = vfdata.fonts
221                    tfmdata.type = "virtual" -- else nested calls with cummulative scaling
222                    local fontlist = vfdata.fonts
223                    local name = file.nameonly(filename)
224                    for i=1,#fontlist do
225                        local n = fontlist[i].name
226                        local s = fontlist[i].size
227                        local d = depth[filename]
228                        s = constructors.scaled(s,vfdata.designsize)
229                        if d > tfm.maxnestingdepth then
230                            report_defining("too deeply nested virtual font %a with size %a, max nesting depth %s",n,s,tfm.maxnestingdepth)
231                            fontlist[i] = { id = 0 }
232                        elseif (d > 1) and (s > tfm.maxnestingsize) then
233                            report_defining("virtual font %a exceeds size %s",n,s)
234                            fontlist[i] = { id = 0 }
235                        else
236                            local t, id = constructors.readanddefine(n,s)
237                            fontlist[i] = { id = id }
238                        end
239                    end
240                end
241            end
242        end
243        --
244        properties.haskerns     = true
245        properties.hasligatures = true
246        properties.hasitalics   = true
247        resources.unicodes      = { }
248        resources.lookuptags    = { }
249        --
250        depth[filename] = depth[filename] - 1
251        --
252        return tfmdata
253    else
254        depth[filename] = depth[filename] - 1
255    end
256end
257
258local function check_tfm(specification,fullname) -- we could split up like afm/otf
259    local foundname = findbinfile(fullname, 'tfm') or ""
260    if foundname == "" then
261        foundname = findbinfile(fullname, 'ofm') or "" -- not needed in context
262    end
263    if foundname == "" then
264        foundname = fonts.names.getfilename(fullname,"tfm") or ""
265    end
266    if foundname ~= "" then
267        specification.filename = foundname
268        specification.format   = "ofm"
269        return read_from_tfm(specification)
270    elseif trace_defining then
271        report_defining("loading tfm with name %a fails",specification.name)
272    end
273end
274
275readers.check_tfm = check_tfm
276
277function readers.tfm(specification)
278    local fullname = specification.filename or ""
279    if fullname == "" then
280        local forced = specification.forced or ""
281        if forced ~= "" then
282            fullname = specification.name .. "." .. forced
283        else
284            fullname = specification.name
285        end
286    end
287    return check_tfm(specification,fullname)
288end
289
290readers.ofm = readers.tfm
291
292-- The reencoding acts upon the 'reencode' feature which can have values 'auto' or
293-- an enc file. You can also specify a 'pfbfile' feature (but it defaults to the
294-- tfm filename) and a 'bitmap' feature. When no enc file is givven (auto) we will
295-- get the vectors from the pfb file.
296
297do
298
299    local outfiles = { }
300
301    local tfmcache = table.setmetatableindex(function(t,tfmdata)
302        local id = font.define(tfmdata)
303        t[tfmdata] = id
304        return id
305    end)
306
307    local encdone  = table.setmetatableindex("table")
308
309    function tfm.reencode(tfmdata,specification)
310
311        local features = specification.features
312
313        if not features then
314            return
315        end
316
317        local features = features.normal
318
319        if not features then
320            return
321        end
322
323        local tfmfile = file.basename(tfmdata.name)
324        local encfile = features.reencode -- or features.enc
325        local pfbfile = features.pfbfile  -- or features.pfb
326        local bitmap  = features.bitmap   -- or features.pk
327
328        if not encfile then
329            return
330        end
331
332        local pfbfile = outfiles[tfmfile]
333
334        if pfbfile == nil then
335            if bitmap then
336                pfbfile = false
337            elseif type(pfbfile) ~= "string" then
338                pfbfile = tfmfile
339            end
340            if type(pfbfile) == "string" then
341                pfbfile = file.addsuffix(pfbfile,"pfb")
342             -- pdf.mapline(tfmfile .. "<" .. pfbfile)
343                report_tfm("using type1 shapes from %a for %a",pfbfile,tfmfile)
344            else
345                report_tfm("using bitmap shapes for %a",tfmfile)
346                pfbfile = false -- use bitmap
347            end
348            outfiles[tfmfile] = pfbfile
349        end
350
351        local encoding = false
352        local vector   = false
353
354        if type(pfbfile) == "string" then
355            local pfb = constructors.handlers.pfb
356            if pfb and pfb.loadvector then
357                local v, e = pfb.loadvector(pfbfile)
358                if v then
359                    vector = v
360                end
361                if e then
362                    encoding = e
363                end
364            end
365        end
366        if type(encfile) == "string" and encfile ~= "auto" then
367            encoding = fonts.encodings.load(file.addsuffix(encfile,"enc"))
368            if encoding then
369                encoding = encoding.vector
370            end
371        end
372        if not encoding then
373            report_tfm("bad encoding for %a, quitting",tfmfile)
374            return
375        end
376
377        local unicoding  = fonts.encodings.agl and fonts.encodings.agl.unicodes
378        local virtualid  = tfmcache[tfmdata]
379        local tfmdata    = table.copy(tfmdata) -- good enough for small fonts
380        local characters = { }
381        local originals  = tfmdata.characters
382        local indices    = { }
383        local parentfont = { "font", 1 }
384        local private    = tfmdata.privateoffset or constructors.privateoffset
385        local reported   = encdone[tfmfile][encfile]
386
387        -- create characters table
388
389        local backmap = vector and table.swapped(vector)
390        local done    = { } -- prevent duplicate
391
392        for index, name in sortedhash(encoding) do -- predictable order
393            local unicode  = unicoding[name]
394            local original = originals[index]
395            if original then
396                if unicode then
397                    original.unicode = unicode
398                else
399                    unicode = private
400                    private = private + 1
401                    if not reported then
402                        report_tfm("glyph %a in font %a with encoding %a gets unicode %U",name,tfmfile,encfile,unicode)
403                    end
404                end
405                characters[unicode] = original
406                indices[index]      = unicode
407                original.name       = name -- so one can lookup weird names
408                if backmap then
409                    original.index = backmap[name]
410                else -- probably bitmap
411                    original.commands = { parentfont, charcommand[index] } -- or "slot"
412                    original.oindex   = index
413                end
414                done[name] = true
415            elseif not done[name] then
416                report_tfm("bad index %a in font %a with name %a",index,tfmfile,name)
417            end
418        end
419
420        encdone[tfmfile][encfile] = true
421
422        -- redo kerns and ligatures
423
424        for k, v in next, characters do
425            local kerns = v.kerns
426            if kerns then
427                local t = { }
428                for k, v in next, kerns do
429                    local i = indices[k]
430                    if i then
431                        t[i] = v
432                    end
433                end
434                v.kerns = next(t) and t or nil
435            end
436            local ligatures = v.ligatures
437            if ligatures then
438                local t = { }
439                for k, v in next, ligatures do
440                    local i = indices[k]
441                    if i then
442                        t[i] = v
443                        v.char = indices[v.char]
444                    end
445                end
446                v.ligatures = next(t) and t or nil
447            end
448        end
449
450        -- wrap up
451
452        tfmdata.fonts         = { { id = virtualid } }
453        tfmdata.characters    = characters
454        tfmdata.fullname      = tfmdata.fullname or tfmdata.name
455        tfmdata.psname        = file.nameonly(pfbfile or tfmdata.name)
456        tfmdata.filename      = pfbfile
457        tfmdata.encodingbytes = 2
458     -- tfmdata.format        = bitmap and "type3" or "type1"
459        tfmdata.format        = "type1"
460        tfmdata.tounicode     = 1
461        tfmdata.embedding     = "subset"
462        tfmdata.usedbitmap    = bitmap and virtualid
463        tfmdata.private       = private
464
465        return tfmdata
466    end
467
468end
469
470-- This code adds a ToUnicode vector for bitmap fonts. We don't bother about
471-- ranges because we have small fonts. it works ok with acrobat but fails with
472-- the other viewers (they get confused by the bitmaps I guess).
473
474do
475
476    local template = [[
477/CIDInit /ProcSet findresource begin
478  12 dict begin
479  begincmap
480    /CIDSystemInfo << /Registry (TeX) /Ordering (bitmap-%s) /Supplement 0 >> def
481    /CMapName /TeX-bitmap-%s def
482    /CMapType 2 def
483    1 begincodespacerange
484      <00> <FF>
485    endcodespacerange
486    %s beginbfchar
487%s
488    endbfchar
489  endcmap
490CMapName currentdict /CMap defineresource pop end
491end
492end
493]]
494
495    local flushstreamobject = lpdf and lpdf.flushstreamobject -- context
496    local setfontattributes = lpdf and lpdf.setfontattributes -- context
497
498    if not flushstreamobject then
499        flushstreamobject = function(data)
500            return pdf.obj { immediate = true, type = "stream", string = data } -- generic
501        end
502    end
503
504    if not setfontattributes then
505        setfontattributes = function(id,data)
506            return pdf.setfontattributes(id,data) -- generic
507        end
508    end
509
510    function tfm.addtounicode(tfmdata)
511        local id   = tfmdata.usedbitmap
512        local map  = { }
513        local char = { } -- no need for range, hardly used
514        for k, v in next, tfmdata.characters do
515            local index     = v.oindex
516            local tounicode = v.tounicode
517            if index and tounicode then
518                map[index] = tounicode
519            end
520        end
521        for k, v in sortedhash(map) do
522            char[#char+1] = format("<%02X> <%s>",k,v)
523        end
524        char  = concat(char,"\n")
525        local stream    = format(template,id,id,#char,char)
526        local reference = flushstreamobject(stream,nil,true)
527        setfontattributes(id,format("/ToUnicode %i 0 R",reference))
528    end
529
530end
531
532-- Now we implement the regular features handlers. We need to convert the
533-- tfm specific structures to opentype structures. In basemode they are
534-- converted back so that is a bit of a waste but it's fast enough.
535
536do
537
538    local everywhere = { ["*"] = { ["*"] = true } } -- or: { ["*"] = { "*" } }
539    local noflags    = { false, false, false, false }
540
541    local function enhance_normalize_features(data)
542        local ligatures  = setmetatableindex("table")
543        local kerns      = setmetatableindex("table")
544        local characters = data.characters
545        for u, c in next, characters do
546            local l = c.ligatures
547            local k = c.kerns
548            if l then
549                ligatures[u] = l
550                for u, v in next, l do
551                    l[u] = { ligature = v.char }
552                end
553                c.ligatures = nil
554            end
555            if k then
556                kerns[u] = k
557                for u, v in next, k do
558                    k[u] = v -- { v, 0 }
559                end
560                c.kerns = nil
561            end
562        end
563
564        for u, l in next, ligatures do
565            for k, v in next, l do
566                local vl = v.ligature
567                local dl = ligatures[vl]
568                if dl then
569                    for kk, vv in next, dl do
570                        v[kk] = vv -- table.copy(vv)
571                    end
572                end
573            end
574        end
575
576        local features = {
577            gpos = { },
578            gsub = { },
579        }
580        local sequences = {
581            -- only filled ones
582        }
583        if next(ligatures) then
584            features.gsub.liga = everywhere
585            data.properties.hasligatures = true
586            sequences[#sequences+1] = {
587                features = {
588                    liga = everywhere,
589                },
590                flags    = noflags,
591                name     = "s_s_0",
592                nofsteps = 1,
593                order    = { "liga" },
594                type     = "gsub_ligature",
595                steps    = {
596                    {
597                        coverage = ligatures,
598                    },
599                },
600            }
601        end
602        if next(kerns) then
603            features.gpos.kern = everywhere
604            data.properties.haskerns = true
605            sequences[#sequences+1] = {
606                features = {
607                    kern = everywhere,
608                },
609                flags    = noflags,
610                name     = "p_s_0",
611                nofsteps = 1,
612                order    = { "kern" },
613                type     = "gpos_pair",
614                steps    = {
615                    {
616                        format   = "kern",
617                        coverage = kerns,
618                    },
619                },
620            }
621        end
622        data.resources.features  = features
623        data.resources.sequences = sequences
624        data.shared.resources = data.shared.resources or resources
625    end
626
627    registertfmenhancer("normalize features",   enhance_normalize_features)
628    registertfmenhancer("check extra features", otfenhancers.enhance)
629
630end
631
632-- As with type one (afm) loading, we just use the opentype ones:
633
634registertfmfeature {
635    name         = "mode",
636    description  = "mode",
637    initializers = {
638        base = otf.modeinitializer,
639        node = otf.modeinitializer,
640    }
641}
642
643registertfmfeature {
644    name         = "features",
645    description  = "features",
646    default      = true,
647    initializers = {
648        base     = otf.basemodeinitializer,
649        node     = otf.nodemodeinitializer,
650    },
651    processors   = {
652        node     = otf.featuresprocessor,
653    }
654}
655