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