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