font-otl.lua /size: 33 Kb    last modification: 2021-10-28 13:50
1if not modules then modules = { } end modules ['font-otl'] = {
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-- After some experimenting with an alternative loader (one that is needed for
10-- getting outlines in mp) I decided not to be compatible with the old (built-in)
11-- one. The approach used in font-otn is as follows: we load the font in a compact
12-- format but still very compatible with the ff data structures. From there we
13-- create hashes to access the data efficiently. The implementation of feature
14-- processing is mostly based on looking at the data as organized in the glyphs and
15-- lookups as well as the specification. Keeping the lookup data in the glyphs is
16-- very instructive and handy for tracing. On the other hand hashing is what brings
17-- speed. So, the in the new approach (the old one will stay around too) we no
18-- longer keep data in the glyphs which saves us a (what in retrospect looks a bit
19-- like) a reconstruction step. It also means that the data format of the cached
20-- files changes. What method is used depends on that format. There is no fundamental
21-- change in processing, and not even in data organation. Most has to do with
22-- loading and storage.
23
24-- todo: less tounicodes
25
26local lower = string.lower
27local type, next, tonumber, tostring, unpack = type, next, tonumber, tostring, unpack
28local abs = math.abs
29local derivetable, sortedhash = table.derive, table.sortedhash
30local formatters = string.formatters
31
32local setmetatableindex   = table.setmetatableindex
33local allocate            = utilities.storage.allocate
34local registertracker     = trackers.register
35local registerdirective   = directives.register
36local starttiming         = statistics.starttiming
37local stoptiming          = statistics.stoptiming
38local elapsedtime         = statistics.elapsedtime
39local findbinfile         = resolvers.findbinfile
40
41----- trace_private       = false  registertracker("otf.private",        function(v) trace_private   = v end)
42----- trace_subfonts      = false  registertracker("otf.subfonts",       function(v) trace_subfonts  = v end)
43local trace_loading       = false  registertracker("otf.loading",        function(v) trace_loading   = v end)
44local trace_features      = false  registertracker("otf.features",       function(v) trace_features  = v end)
45----- trace_dynamics      = false  registertracker("otf.dynamics",       function(v) trace_dynamics  = v end)
46----- trace_sequences     = false  registertracker("otf.sequences",      function(v) trace_sequences = v end)
47----- trace_markwidth     = false  registertracker("otf.markwidth",      function(v) trace_markwidth = v end)
48local trace_defining      = false  registertracker("fonts.defining",     function(v) trace_defining  = v end)
49
50local report_otf          = logs.reporter("fonts","otf loading")
51
52local fonts               = fonts
53local otf                 = fonts.handlers.otf
54
55otf.version               = 3.119 -- beware: also sync font-mis.lua and in mtx-fonts
56otf.cache                 = containers.define("fonts", "otl", otf.version, true)
57otf.svgcache              = containers.define("fonts", "svg", otf.version, true)
58otf.pngcache              = containers.define("fonts", "png", otf.version, true)
59otf.pdfcache              = containers.define("fonts", "pdf", otf.version, true)
60otf.mpscache              = containers.define("fonts", "mps", otf.version, true)
61
62otf.svgenabled            = false
63otf.pngenabled            = false
64
65local otfreaders          = otf.readers
66
67local hashes              = fonts.hashes
68local definers            = fonts.definers
69local readers             = fonts.readers
70local constructors        = fonts.constructors
71
72local otffeatures         = constructors.features.otf
73local registerotffeature  = otffeatures.register
74
75local otfenhancers        = constructors.enhancers.otf
76local registerotfenhancer = otfenhancers.register
77
78local forceload           = false
79local cleanup             = 0     -- mk: 0=885M 1=765M 2=735M (regular run 730M)
80local syncspace           = true
81local forcenotdef         = false
82
83local privateoffset       = fonts.constructors and fonts.constructors.privateoffset or 0xF0000 -- 0x10FFFF
84
85local applyruntimefixes   = fonts.treatments and fonts.treatments.applyfixes
86
87local wildcard            = "*"
88local default             = "dflt"
89
90local formats             = fonts.formats
91
92formats.otf               = "opentype"
93formats.ttf               = "truetype"
94formats.ttc               = "truetype"
95
96registerdirective("fonts.otf.loader.cleanup",       function(v) cleanup       = tonumber(v) or (v and 1) or 0 end)
97registerdirective("fonts.otf.loader.force",         function(v) forceload     = v end)
98registerdirective("fonts.otf.loader.syncspace",     function(v) syncspace     = v end)
99registerdirective("fonts.otf.loader.forcenotdef",   function(v) forcenotdef   = v end)
100
101-- otfenhancers.patch("before","migrate metadata","cambria",function() end)
102
103registerotfenhancer("check extra features", function() end) -- placeholder
104
105-- Kai has memory problems on osx so here is an experiment (I only tested on windows as
106-- my test mac is old and gets no updates and is therefore rather useless.):
107
108local checkmemory = utilities.lua and utilities.lua.checkmemory
109local threshold   = 100 -- MB
110local tracememory = false
111
112registertracker("fonts.otf.loader.memory",function(v) tracememory = v end)
113
114if not checkmemory then -- we need a generic plug (this code might move):
115
116    local collectgarbage = collectgarbage
117
118    checkmemory = function(previous,threshold) -- threshold in MB
119        local current = collectgarbage("count")
120        if previous then
121            local checked = (threshold or 64)*1024
122            if current - previous > checked then
123                collectgarbage("collect")
124                current = collectgarbage("count")
125            end
126        end
127        return current
128    end
129
130end
131
132function otf.load(filename,sub,instance)
133    local base = file.basename(file.removesuffix(filename))
134    local name = file.removesuffix(base) -- already no suffix
135    local attr = lfs.attributes(filename)
136    local size = attr and attr.size or 0
137    local time = attr and attr.modification or 0
138    -- sub can be number of string
139    if sub == "" then
140        sub = false
141    end
142    local hash = name
143    if sub then
144        hash = hash .. "-" .. sub
145    end
146    if instance then
147        hash = hash .. "-" .. instance
148    end
149    hash = containers.cleanname(hash)
150    local data = containers.read(otf.cache,hash)
151    local reload = not data or data.size ~= size or data.time ~= time or data.tableversion ~= otfreaders.tableversion
152    if forceload then
153        report_otf("forced reload of %a due to hard coded flag",filename)
154        reload = true
155    end
156    if reload then
157        report_otf("loading %a, hash %a",filename,hash)
158        --
159        starttiming(otfreaders,true)
160        data = otfreaders.loadfont(filename,sub or 1,instance) -- we can pass the number instead (if it comes from a name search)
161        if data then
162            -- todo: make this a plugin
163            local used      = checkmemory()
164            local resources = data.resources
165            local svgshapes = resources.svgshapes
166            local pngshapes = resources.pngshapes
167            if cleanup == 0 then
168                checkmemory(used,threshold,tracememory)
169            end
170            if svgshapes then
171                resources.svgshapes = nil
172                if otf.svgenabled then
173                    local timestamp = os.date()
174                    -- work in progress ... a bit boring to do
175                    containers.write(otf.svgcache,hash, {
176                        svgshapes = svgshapes,
177                        timestamp = timestamp,
178                    })
179                    data.properties.svg = {
180                        hash      = hash,
181                        timestamp = timestamp,
182                    }
183                end
184                if cleanup > 1 then
185                    collectgarbage("collect")
186                else
187                    checkmemory(used,threshold,tracememory)
188                end
189            end
190            if pngshapes then
191                resources.pngshapes = nil
192                if otf.pngenabled then
193                    local timestamp = os.date()
194                    -- work in progress ... a bit boring to do
195                    containers.write(otf.pngcache,hash, {
196                        pngshapes = pngshapes,
197                        timestamp = timestamp,
198                    })
199                    data.properties.png = {
200                        hash      = hash,
201                        timestamp = timestamp,
202                    }
203                end
204                if cleanup > 1 then
205                    collectgarbage("collect")
206                else
207                    checkmemory(used,threshold,tracememory)
208                end
209            end
210            --
211            otfreaders.compact(data)
212            if cleanup == 0 then
213                checkmemory(used,threshold,tracememory)
214            end
215            otfreaders.rehash(data,"unicodes")
216            otfreaders.addunicodetable(data)
217            otfreaders.extend(data)
218            if cleanup == 0 then
219                checkmemory(used,threshold,tracememory)
220            end
221            if context then
222                otfreaders.condense(data)
223            end
224            otfreaders.pack(data)
225            report_otf("loading done")
226            report_otf("saving %a in cache",filename)
227            data = containers.write(otf.cache, hash, data)
228            if cleanup > 1 then
229                collectgarbage("collect")
230            else
231                checkmemory(used,threshold,tracememory)
232            end
233            stoptiming(otfreaders)
234            if elapsedtime then
235                report_otf("loading, optimizing, packing and caching time %s", elapsedtime(otfreaders))
236            end
237            if cleanup > 3 then
238                collectgarbage("collect")
239            else
240                checkmemory(used,threshold,tracememory)
241            end
242            data = containers.read(otf.cache,hash) -- this frees the old table and load the sparse one
243            if cleanup > 2 then
244                collectgarbage("collect")
245            else
246                checkmemory(used,threshold,tracememory)
247            end
248        else
249            stoptiming(otfreaders)
250            data = nil
251            report_otf("loading failed due to read error")
252        end
253    end
254    if data then
255        if trace_defining then
256            report_otf("loading from cache using hash %a",hash)
257        end
258        --
259        otfreaders.unpack(data)
260        otfreaders.expand(data) -- inline tables
261        otfreaders.addunicodetable(data) -- only when not done yet
262        --
263        otfenhancers.apply(data,filename,data) -- in context one can also use treatments
264        --
265     -- constructors.addcoreunicodes(data.resources.unicodes) -- still needed ?
266        --
267        if applyruntimefixes then
268            applyruntimefixes(filename,data) -- e.g. see treatments.lfg
269        end
270        --
271        data.metadata.math = data.resources.mathconstants
272        --
273        -- delayed tables (experiment)
274        --
275        local classes = data.resources.classes
276        if not classes then
277            local descriptions = data.descriptions
278            classes = setmetatableindex(function(t,k)
279                local d = descriptions[k]
280                local v = (d and d.class or "base") or false
281                t[k] = v
282                return v
283            end)
284            data.resources.classes = classes
285        end
286        --
287    end
288
289    return data
290end
291
292-- modes: node, base, none
293
294function otf.setfeatures(tfmdata,features)
295    local okay = constructors.initializefeatures("otf",tfmdata,features,trace_features,report_otf)
296    if okay then
297        return constructors.collectprocessors("otf",tfmdata,features,trace_features,report_otf)
298    else
299        return { } -- will become false
300    end
301end
302
303-- the first version made a top/mid/not extensible table, now we just
304-- pass on the variants data and deal with it in the tfm scaler (there
305-- is no longer an extensible table anyway)
306--
307-- we cannot share descriptions as virtual fonts might extend them (ok,
308-- we could use a cache with a hash
309--
310-- we already assign an empty table to characters as we can add for
311-- instance protruding info and loop over characters; one is not supposed
312-- to change descriptions and if one does so one should make a copy!
313
314local function copytotfm(data,cache_id)
315    if data then
316        local metadata       = data.metadata
317        local properties     = derivetable(data.properties)
318        local descriptions   = derivetable(data.descriptions)
319        local goodies        = derivetable(data.goodies)
320        local characters     = { } -- newtable if we knwo how many
321        local parameters     = { }
322        local mathparameters = { }
323        --
324        local resources      = data.resources
325        local unicodes       = resources.unicodes
326        local spaceunits     = 500
327        local spacer         = "space"
328        local designsize     = metadata.designsize or 100
329        local minsize        = metadata.minsize or designsize
330        local maxsize        = metadata.maxsize or designsize
331        local mathspecs      = metadata.math
332        --
333        if designsize == 0 then
334            designsize = 100
335            minsize    = 100
336            maxsize    = 100
337        end
338        if mathspecs then
339            for name, value in next, mathspecs do
340                mathparameters[name] = value
341            end
342        end
343        for unicode in next, data.descriptions do -- use parent table
344            characters[unicode] = { }
345        end
346        if mathspecs then
347            for unicode, character in next, characters do
348                local d = descriptions[unicode] -- we could use parent table here
349                local m = d.math
350                if m then
351                    -- watch out: luatex uses horiz_variants for the parts
352                    --
353                    local italic   = m.italic
354                    local vitalic  = m.vitalic
355                    --
356                    local variants = m.hvariants
357                    local parts    = m.hparts
358                    if variants then
359                        local c = character
360                        for i=1,#variants do
361                         -- local un = variants[i].glyph
362                            local un = variants[i]
363                            c.next = un
364                            c = characters[un]
365                        end -- c is now last in chain
366                        c.horiz_variants = parts
367                    elseif parts then
368                        character.horiz_variants = parts
369                        italic = m.hitalic
370                    end
371                    --
372                    local variants = m.vvariants
373                    local parts    = m.vparts
374                    if variants then
375                        local c = character
376                        for i=1,#variants do
377                         -- local un = variants[i].glyph
378                            local un = variants[i]
379                            c.next = un
380                            c = characters[un]
381                        end -- c is now last in chain
382                        c.vert_variants = parts
383                    elseif parts then
384                        character.vert_variants = parts
385                    end
386                    --
387                    if italic and italic ~= 0 then
388                        character.italic = italic
389                    end
390                    --
391                    if vitalic and vitalic ~= 0 then
392                        character.vert_italic = vitalic
393                    end
394                    --
395                    local accent = m.accent -- taccent?
396                    if accent then
397                        character.accent = accent
398                    end
399                    --
400                    local kerns = m.kerns
401                    if kerns then
402                        character.mathkerns = kerns
403                    end
404                end
405            end
406        end
407        -- we need a runtime lookup because of running from cdrom or zip, brrr (shouldn't
408        -- we use the basename then?)
409        local filename = constructors.checkedfilename(resources)
410        local fontname = metadata.fontname
411        local fullname = metadata.fullname or fontname
412        local psname   = fontname or fullname
413        local subfont  = metadata.subfontindex
414        local units    = metadata.units or 1000
415        --
416        if units == 0 then -- catch bugs in fonts
417            units = 1000 -- maybe 2000 when ttf
418            metadata.units = 1000
419            report_otf("changing %a units to %a",0,units)
420        end
421        --
422        local monospaced  = metadata.monospaced
423        local charwidth   = metadata.averagewidth -- or unset
424        local charxheight = metadata.xheight -- or unset
425        local italicangle = metadata.italicangle
426        local hasitalics  = metadata.hasitalics
427        properties.monospaced  = monospaced
428        properties.hasitalics  = hasitalics
429        parameters.italicangle = italicangle
430        parameters.charwidth   = charwidth
431        parameters.charxheight = charxheight
432        --
433        local space  = 0x0020
434        local emdash = 0x2014
435        if monospaced then
436            if descriptions[space] then
437                spaceunits, spacer = descriptions[space].width, "space"
438            end
439            if not spaceunits and descriptions[emdash] then
440                spaceunits, spacer = descriptions[emdash].width, "emdash"
441            end
442            if not spaceunits and charwidth then
443                spaceunits, spacer = charwidth, "charwidth"
444            end
445        else
446            if descriptions[space] then
447                spaceunits, spacer = descriptions[space].width, "space"
448            end
449            if not spaceunits and descriptions[emdash] then
450                spaceunits, spacer = descriptions[emdash].width/2, "emdash/2"
451            end
452            if not spaceunits and charwidth then
453                spaceunits, spacer = charwidth, "charwidth"
454            end
455        end
456        spaceunits = tonumber(spaceunits) or units/2
457        --
458        parameters.slant         = 0
459        parameters.space         = spaceunits            -- 3.333 (cmr10)
460        parameters.space_stretch = 1*units/2   --  500   -- 1.666 (cmr10)
461        parameters.space_shrink  = 1*units/3   --  333   -- 1.111 (cmr10)
462        parameters.x_height      = 2*units/5   --  400
463        parameters.quad          = units       -- 1000
464        if spaceunits < 2*units/5 then
465            -- todo: warning
466        end
467        if italicangle and italicangle ~= 0 then
468            parameters.italicangle  = italicangle
469            parameters.italicfactor = math.cos(math.rad(90+italicangle))
470            parameters.slant        = - math.tan(italicangle*math.pi/180)
471        end
472        if monospaced then
473            parameters.space_stretch = 0
474            parameters.space_shrink  = 0
475        elseif syncspace then --
476            parameters.space_stretch = spaceunits/2
477            parameters.space_shrink  = spaceunits/3
478        end
479        parameters.extra_space = parameters.space_shrink -- 1.111 (cmr10)
480        if charxheight then
481            parameters.x_height = charxheight
482        else
483            local x = 0x0078
484            if x then
485                local x = descriptions[x]
486                if x then
487                    parameters.x_height = x.height
488                end
489            end
490        end
491        --
492        parameters.designsize = (designsize/10)*65536
493        parameters.minsize    = (minsize   /10)*65536
494        parameters.maxsize    = (maxsize   /10)*65536
495        parameters.ascender   = abs(metadata.ascender  or 0)
496        parameters.descender  = abs(metadata.descender or 0)
497        parameters.units      = units
498        parameters.vheight    = metadata.defaultvheight
499        --
500        properties.space      = spacer
501        properties.format     = data.format or formats.otf
502        properties.filename   = filename
503        properties.fontname   = fontname
504        properties.fullname   = fullname
505        properties.psname     = psname
506        properties.name       = filename or fullname
507        properties.subfont    = subfont
508        --
509if not CONTEXTLMTXMODE or CONTEXTLMTXMODE == 0 then
510    --
511    properties.encodingbytes = 2
512elseif CONTEXTLMTXMODE then
513    local duplicates = resources and resources.duplicates
514    if duplicates then
515        local maxindex = data.nofglyphs or metadata.nofglyphs
516        if maxindex then
517            for u, d in sortedhash(duplicates) do
518                local du = descriptions[u]
519                if du then
520                    for uu in sortedhash(d) do
521                        maxindex = maxindex + 1
522                        descriptions[uu].dupindex = du.index
523                        descriptions[uu].index    = maxindex
524                    end
525                else
526                 -- report_otf("no %U in font %a, duplicates ignored",u,filename)
527                end
528            end
529        end
530    end
531    --
532end
533        --
534     -- properties.name          = specification.name
535     -- properties.sub           = specification.sub
536        --
537        properties.private       = properties.private or data.private or privateoffset
538        --
539        return {
540            characters     = characters,
541            descriptions   = descriptions,
542            parameters     = parameters,
543            mathparameters = mathparameters,
544            resources      = resources,
545            properties     = properties,
546            goodies        = goodies,
547        }
548    end
549end
550
551-- These woff files are a kind of joke in a tex environment because one can simply convert
552-- them to ttf/otf and use them as such (after all, we cache them too). The successor format
553-- woff2 is more complex so there we can as well call an external converter which in the end
554-- makes this code kind of obsolete before it's even used. Although ... it might become a
555-- more general conversion plug in.
556
557local converters = {
558    woff = {
559        cachename = "webfonts",
560        action    = otf.readers.woff2otf,
561    }
562}
563
564-- We can get differences between daylight saving etc ... but it makes no sense to
565-- mess with trickery .. so be it when you use a different binary.
566
567local function checkconversion(specification)
568    local filename  = specification.filename
569    local converter = converters[lower(file.suffix(filename))]
570    if converter then
571        local base = file.basename(filename)
572        local name = file.removesuffix(base)
573        local attr = lfs.attributes(filename)
574        local size = attr and attr.size or 0
575        local time = attr and attr.modification or 0
576        if size > 0 then
577            local cleanname = containers.cleanname(name)
578            local cachename = caches.setfirstwritablefile(cleanname,converter.cachename)
579            if not io.exists(cachename) or (time ~= lfs.attributes(cachename).modification) then
580                report_otf("caching font %a in %a",filename,cachename)
581                converter.action(filename,cachename) -- todo infoonly
582                lfs.touch(cachename,time,time)
583            end
584            specification.filename = cachename
585        end
586    end
587end
588
589local function otftotfm(specification)
590    local cache_id = specification.hash
591    local tfmdata  = containers.read(constructors.cache,cache_id)
592    if not tfmdata then
593
594        checkconversion(specification) -- for the moment here
595
596        local name     = specification.name
597        local sub      = specification.sub
598        local subindex = specification.subindex
599        local filename = specification.filename
600        local features = specification.features.normal
601        local instance = specification.instance or (features and features.axis)
602        local rawdata  = otf.load(filename,sub,instance)
603        if rawdata and next(rawdata) then
604            local descriptions = rawdata.descriptions
605            rawdata.lookuphash = { } -- to be done
606            tfmdata = copytotfm(rawdata,cache_id)
607            if tfmdata and next(tfmdata) then
608                -- at this moment no characters are assigned yet, only empty slots
609                local features     = constructors.checkedfeatures("otf",features)
610                local shared       = tfmdata.shared
611                if not shared then
612                    shared         = { }
613                    tfmdata.shared = shared
614                end
615                shared.rawdata     = rawdata
616             -- shared.features    = features -- default
617                shared.dynamics    = { }
618             -- shared.processes   = { }
619                tfmdata.changed    = { }
620                shared.features    = features
621                shared.processes   = otf.setfeatures(tfmdata,features)
622            end
623        end
624        containers.write(constructors.cache,cache_id,tfmdata)
625    end
626    return tfmdata
627end
628
629local function read_from_otf(specification)
630    local tfmdata = otftotfm(specification)
631    if tfmdata then
632        -- this late ? .. needs checking
633        tfmdata.properties.name = specification.name
634        tfmdata.properties.sub  = specification.sub
635        tfmdata.properties.id   = specification.id
636        --
637        tfmdata = constructors.scale(tfmdata,specification)
638        local allfeatures = tfmdata.shared.features or specification.features.normal
639        constructors.applymanipulators("otf",tfmdata,allfeatures,trace_features,report_otf)
640        constructors.setname(tfmdata,specification) -- only otf?
641        fonts.loggers.register(tfmdata,file.suffix(specification.filename),specification)
642    end
643    return tfmdata
644end
645
646local function checkmathsize(tfmdata,mathsize)
647    local mathdata = tfmdata.shared.rawdata.metadata.math
648    local mathsize = tonumber(mathsize)
649    if mathdata then -- we cannot use mathparameters as luatex will complain
650        local parameters = tfmdata.parameters
651        parameters.scriptpercentage       = mathdata.ScriptPercentScaleDown
652        parameters.scriptscriptpercentage = mathdata.ScriptScriptPercentScaleDown
653        parameters.mathsize               = mathsize -- only when a number !
654    end
655end
656
657registerotffeature {
658    name         = "mathsize",
659    description  = "apply mathsize specified in the font",
660    initializers = {
661        base = checkmathsize,
662        node = checkmathsize,
663    }
664}
665
666-- readers
667
668function otf.collectlookups(rawdata,kind,script,language)
669    if not kind then
670        return
671    end
672    if not script then
673        script = default
674    end
675    if not language then
676        language = default
677    end
678    local lookupcache = rawdata.lookupcache
679    if not lookupcache then
680        lookupcache = { }
681        rawdata.lookupcache = lookupcache
682    end
683    local kindlookup = lookupcache[kind]
684    if not kindlookup then
685        kindlookup = { }
686        lookupcache[kind] = kindlookup
687    end
688    local scriptlookup = kindlookup[script]
689    if not scriptlookup then
690        scriptlookup = { }
691        kindlookup[script] = scriptlookup
692    end
693    local languagelookup = scriptlookup[language]
694    if not languagelookup then
695        local sequences   = rawdata.resources.sequences
696        local featuremap  = { }
697        local featurelist = { }
698        if sequences then
699            for s=1,#sequences do
700                local sequence = sequences[s]
701                local features = sequence.features
702                if features then
703                    features = features[kind]
704                    if features then
705                     -- features = features[script] or features[default] or features[wildcard]
706                        features = features[script] or features[wildcard]
707                        if features then
708                         -- features = features[language] or features[default] or features[wildcard]
709                            features = features[language] or features[wildcard]
710                            if features then
711                                if not featuremap[sequence] then
712                                    featuremap[sequence] = true
713                                    featurelist[#featurelist+1] = sequence
714                                end
715                            end
716                        end
717                    end
718                end
719            end
720            if #featurelist == 0 then
721                featuremap, featurelist = false, false
722            end
723        else
724            featuremap, featurelist = false, false
725        end
726        languagelookup = { featuremap, featurelist }
727        scriptlookup[language] = languagelookup
728    end
729    return unpack(languagelookup)
730end
731
732-- moved from font-oth.lua, todo: also afm
733
734local function getgsub(tfmdata,k,kind,value)
735    local shared  = tfmdata.shared
736    local rawdata = shared and shared.rawdata
737    if rawdata then
738        local sequences = rawdata.resources.sequences
739        if sequences then
740            local properties = tfmdata.properties
741            local validlookups, lookuplist = otf.collectlookups(rawdata,kind,properties.script,properties.language)
742            if validlookups then
743             -- local choice = tonumber(value) or 1 -- no random here (yet)
744                for i=1,#lookuplist do
745                    local lookup   = lookuplist[i]
746                    local steps    = lookup.steps
747                    local nofsteps = lookup.nofsteps
748                    for i=1,nofsteps do
749                        local coverage = steps[i].coverage
750                        if coverage then
751                            local found = coverage[k]
752                            if found then
753                                return found, lookup.type
754                            end
755                        end
756                    end
757                end
758            end
759        end
760    end
761end
762
763otf.getgsub = getgsub -- returns value, gsub_kind
764
765function otf.getsubstitution(tfmdata,k,kind,value)
766    local found, kind = getgsub(tfmdata,k,kind,value)
767    if not found then
768        --
769    elseif kind == "gsub_single" then
770        return found
771    elseif kind == "gsub_alternate" then
772        local choice = tonumber(value) or 1 -- no random here (yet)
773        return found[choice] or found[1] or k
774    end
775    return k
776end
777
778otf.getalternate = otf.getsubstitution
779
780function otf.getmultiple(tfmdata,k,kind)
781    local found, kind = getgsub(tfmdata,k,kind)
782    if found and kind == "gsub_multiple" then
783        return found
784    end
785    return { k }
786end
787
788function otf.getkern(tfmdata,left,right,kind)
789    local kerns = getgsub(tfmdata,left,kind or "kern",true) -- for now we use getsub
790    if kerns then
791        local found = kerns[right]
792        local kind  = type(found)
793        if kind == "table" then
794            found = found[1][3] -- can be more clever
795        elseif kind ~= "number" then
796            found = false
797        end
798        if found then
799            return found * tfmdata.parameters.factor
800        end
801    end
802    return 0
803end
804
805local function check_otf(forced,specification,suffix)
806    local name = specification.name
807    if forced then
808        name = specification.forcedname -- messy
809    end
810    local fullname = findbinfile(name,suffix) or ""
811    if fullname == "" then
812        fullname = fonts.names.getfilename(name,suffix) or ""
813    end
814    if fullname ~= "" and not fonts.names.ignoredfile(fullname) then
815        specification.filename = fullname
816        return read_from_otf(specification)
817    end
818end
819
820local function opentypereader(specification,suffix)
821    local forced = specification.forced or ""
822    if formats[forced] then
823        return check_otf(true,specification,forced)
824    else
825        return check_otf(false,specification,suffix)
826    end
827end
828
829readers.opentype = opentypereader -- kind of useless and obsolete
830
831function readers.otf(specification) return opentypereader(specification,"otf") end
832function readers.ttf(specification) return opentypereader(specification,"ttf") end
833function readers.ttc(specification) return opentypereader(specification,"ttf") end
834
835function readers.woff(specification)
836    checkconversion(specification)
837    opentypereader(specification,"")
838end
839
840-- this will be overloaded
841
842function otf.scriptandlanguage(tfmdata,attr)
843    local properties = tfmdata.properties
844    return properties.script or "dflt", properties.language or "dflt"
845end
846
847-- a little bit of abstraction
848
849local function justset(coverage,unicode,replacement)
850    coverage[unicode] = replacement
851end
852
853otf.coverup = {
854    stepkey = "steps",
855    actions = {
856        chainsubstitution = justset,
857        chainposition     = justset,
858        substitution      = justset,
859        alternate         = justset,
860        multiple          = justset,
861        kern              = justset,
862        pair              = justset,
863        single            = justset,
864        ligature          = function(coverage,unicode,ligature)
865            local first = ligature[1]
866            local tree  = coverage[first]
867            if not tree then
868                tree = { }
869                coverage[first] = tree
870            end
871            for i=2,#ligature do
872                local l = ligature[i]
873                local t = tree[l]
874                if not t then
875                    t = { }
876                    tree[l] = t
877                end
878                tree = t
879            end
880            tree.ligature = unicode
881        end,
882    },
883    register = function(coverage,featuretype,format)
884        return {
885            format   = format,
886            coverage = coverage,
887        }
888    end
889}
890