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