font-otl.lua /size: 34 Kb    last modification: 2025-02-21 11:03
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, remove = table.derive, table.sortedhash, table.remove
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.144 -- 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,wipemath)
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 know 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        --
339        for unicode in next, data.descriptions do -- use parent table
340            characters[unicode] = { }
341        end
342        --
343        -- we need a runtime lookup because of running from cdrom or zip, brrr (shouldn't
344        -- we use the basename then?)
345        --
346        local filename = constructors.checkedfilename(resources)
347        local fontname = metadata.fontname
348        local fullname = metadata.fullname or fontname
349        local psname   = fontname or fullname
350        local subfont  = metadata.subfontindex
351        local units    = metadata.units or 1000
352        --
353        if units == 0 then -- catch bugs in fonts
354            units = 1000 -- maybe 2000 when ttf
355            metadata.units = 1000
356            report_otf("changing %a units to %a",0,units)
357        end
358        --
359        if not mathspecs then
360            -- go on
361        elseif wipemath then
362            -- No way that we let a non (or partial) math font register itself as math and interfere
363            -- in bad ways (apart from unwanted overhead). I've seen some examples and it proofs how
364            -- instable and unpredictable fonts can become.
365            local wiped     = false
366            local features  = resources.features
367            local sequences = resources.sequences
368            if features then
369                local gsub = features.gsub
370                if gsub and gsub.ssty then
371                    gsub.ssty = nil
372                    wiped = true
373                end
374            end
375            if sequences then
376                for i=#sequences,1,-1 do
377                    local sequence = sequences[i]
378                    local features = sequence.features
379                    if features then
380                        if features.ssty then
381                            remove(sequences,i)
382                            wiped = true
383                        end
384                    end
385                end
386            end
387            if resources.mathconstants then
388                resources.mathconstants = nil
389                wiped = true
390            end
391            if resources.math then
392                metadata.math = nil
393                wiped = true
394            end
395            if wiped then
396                report_otf("math data wiped from %a",fullname)
397            end
398        else
399            for name, value in next, mathspecs do
400                mathparameters[name] = value
401            end
402            for unicode, character in next, characters do
403                local d = descriptions[unicode] -- we could use parent table here
404                local m = d.math
405                if m then
406                    -- watch out: luatex uses horiz_variants for the parts
407                    --
408                    local italic   = m.italic
409                    local vitalic  = m.vitalic
410                    --
411                    local variants = m.hvariants
412                    local parts    = m.hparts
413                    if variants then
414                        local c = character
415                        for i=1,#variants do
416                         -- local un = variants[i].glyph
417                            local un = variants[i]
418                            c.next = un
419                            c = characters[un]
420                        end -- c is now last in chain
421                        c.horiz_variants = parts
422                    elseif parts then
423                        character.horiz_variants = parts
424                        italic = m.hitalic
425                    end
426                    --
427                    local variants = m.vvariants
428                    local parts    = m.vparts
429                    if variants then
430                        local c = character
431                        for i=1,#variants do
432                         -- local un = variants[i].glyph
433                            local un = variants[i]
434                            c.next = un
435                            c = characters[un]
436                        end -- c is now last in chain
437                        c.vert_variants = parts
438                    elseif parts then
439                        character.vert_variants = parts
440                    end
441                    --
442                    if italic and italic ~= 0 then
443                        character.italic = italic
444                    end
445                    --
446                    if vitalic and vitalic ~= 0 then
447                        character.vert_italic = vitalic
448                    end
449                    --
450                    local accent = m.accent -- taccent?
451                    if accent then
452                        character.accent = accent
453                    end
454                    --
455                    local kerns = m.kerns
456                    if kerns then
457                        character.mathkerns = kerns
458                    end
459                end
460            end
461        end
462        --
463        local monospaced  = metadata.monospaced
464        local charwidth   = metadata.averagewidth -- or unset
465        local charxheight = metadata.xheight -- or unset
466        local italicangle = metadata.italicangle
467        local hasitalics  = metadata.hasitalics
468        properties.monospaced  = monospaced
469        properties.hasitalics  = hasitalics
470        parameters.italicangle = italicangle
471        parameters.charwidth   = charwidth
472        parameters.charxheight = charxheight
473        --
474        local space  = 0x0020
475        local emdash = 0x2014
476        if monospaced then
477            if descriptions[space] then
478                spaceunits, spacer = descriptions[space].width, "space"
479            end
480            if not spaceunits and descriptions[emdash] then
481                spaceunits, spacer = descriptions[emdash].width, "emdash"
482            end
483            if not spaceunits and charwidth then
484                spaceunits, spacer = charwidth, "charwidth"
485            end
486        else
487            if descriptions[space] then
488                spaceunits, spacer = descriptions[space].width, "space"
489            end
490            if not spaceunits and descriptions[emdash] then
491                spaceunits, spacer = descriptions[emdash].width/2, "emdash/2"
492            end
493            if not spaceunits and charwidth then
494                spaceunits, spacer = charwidth, "charwidth"
495            end
496        end
497        spaceunits = tonumber(spaceunits) or units/2
498        --
499        parameters.slant         = 0
500        parameters.space         = spaceunits            -- 3.333 (cmr10)
501        parameters.space_stretch = 1*units/2   --  500   -- 1.666 (cmr10)
502        parameters.space_shrink  = 1*units/3   --  333   -- 1.111 (cmr10)
503        parameters.x_height      = 2*units/5   --  400
504        parameters.quad          = units       -- 1000
505        if spaceunits < 2*units/5 then
506            -- todo: warning
507        end
508        if italicangle and italicangle ~= 0 then
509            parameters.italicangle  = italicangle
510            parameters.italicfactor = math.cos(math.rad(90+italicangle))
511            parameters.slant        = - math.tan(italicangle*math.pi/180)
512        end
513        if monospaced then
514            parameters.space_stretch = 0
515            parameters.space_shrink  = 0
516        elseif syncspace then --
517            parameters.space_stretch = spaceunits/2
518            parameters.space_shrink  = spaceunits/3
519        end
520        parameters.extra_space = parameters.space_shrink -- 1.111 (cmr10)
521        if charxheight then
522            parameters.x_height = charxheight
523        else
524            local x = 0x0078
525            if x then
526                local x = descriptions[x]
527                if x then
528                    parameters.x_height = x.height
529                end
530            end
531        end
532        --
533        parameters.designsize = (designsize/10)*65536
534        parameters.minsize    = (minsize   /10)*65536
535        parameters.maxsize    = (maxsize   /10)*65536
536        parameters.ascender   = abs(metadata.ascender  or 0)
537        parameters.descender  = abs(metadata.descender or 0)
538        parameters.units      = units
539        parameters.vheight    = metadata.defaultvheight
540        --
541        properties.space      = spacer
542        properties.format     = data.format or formats.otf
543        properties.filename   = filename
544        properties.fontname   = fontname
545        properties.fullname   = fullname
546        properties.psname     = psname
547        properties.name       = filename or fullname
548        properties.subfont    = subfont
549        --
550if not CONTEXTLMTXMODE or CONTEXTLMTXMODE == 0 then
551    --
552    properties.encodingbytes = 2
553elseif CONTEXTLMTXMODE then
554    local duplicates = resources and resources.duplicates
555    if duplicates then
556        local maxindex = data.nofglyphs or metadata.nofglyphs
557        if maxindex then
558            for u, d in sortedhash(duplicates) do
559                local du = descriptions[u]
560                if du then
561                    for uu in sortedhash(d) do
562                        maxindex = maxindex + 1
563                        descriptions[uu].dupindex = du.index
564                        descriptions[uu].index    = maxindex
565                    end
566                else
567                 -- report_otf("no %U in font %a, duplicates ignored",u,filename)
568                end
569            end
570        end
571    end
572    --
573end
574        --
575     -- properties.name          = specification.name
576     -- properties.sub           = specification.sub
577        --
578        properties.private       = properties.private or data.private or privateoffset
579        --
580        return {
581            characters     = characters,
582            descriptions   = descriptions,
583            parameters     = parameters,
584            mathparameters = mathparameters,
585            resources      = resources,
586            properties     = properties,
587            goodies        = goodies,
588        }
589    end
590end
591
592-- These woff files are a kind of joke in a tex environment because one can simply convert
593-- them to ttf/otf and use them as such (after all, we cache them too). The successor format
594-- woff2 is more complex so there we can as well call an external converter which in the end
595-- makes this code kind of obsolete before it's even used. Although ... it might become a
596-- more general conversion plug in.
597
598local converters = {
599    woff = {
600        cachename = "webfonts",
601        action    = otf.readers.woff2otf,
602    }
603}
604
605-- We can get differences between daylight saving etc ... but it makes no sense to
606-- mess with trickery .. so be it when you use a different binary.
607
608local function checkconversion(specification)
609    local filename  = specification.filename
610    local converter = converters[lower(file.suffix(filename))]
611    if converter then
612        local base = file.basename(filename)
613        local name = file.removesuffix(base)
614        local attr = lfs.attributes(filename)
615        local size = attr and attr.size or 0
616        local time = attr and attr.modification or 0
617        if size > 0 then
618            local cleanname = containers.cleanname(name)
619            local cachename = caches.setfirstwritablefile(cleanname,converter.cachename)
620            if not io.exists(cachename) or (time ~= lfs.attributes(cachename).modification) then
621                report_otf("caching font %a in %a",filename,cachename)
622                converter.action(filename,cachename) -- todo infoonly
623                lfs.touch(cachename,time,time)
624            end
625            specification.filename = cachename
626        end
627    end
628end
629
630local function otftotfm(specification)
631    local cache_id = specification.hash
632    local tfmdata  = containers.read(constructors.cache,cache_id)
633    if not tfmdata then
634        checkconversion(specification) -- for the moment here
635        local name     = specification.name
636        local sub      = specification.sub
637        local subindex = specification.subindex
638        local filename = specification.filename
639        local features = specification.features.normal
640        local instance = specification.instance or (features and features.axis)
641        local rawdata  = otf.load(filename,sub,instance)
642        if rawdata and next(rawdata) then
643            local descriptions = rawdata.descriptions
644            rawdata.lookuphash = { } -- to be done
645            tfmdata = copytotfm(rawdata,cache_id,features and features.wipemath)
646            if tfmdata and next(tfmdata) then
647                -- at this moment no characters are assigned yet, only empty slots
648                local features     = constructors.checkedfeatures("otf",features)
649                local shared       = tfmdata.shared
650                if not shared then
651                    shared         = { }
652                    tfmdata.shared = shared
653                end
654                shared.rawdata     = rawdata
655             -- shared.features    = features -- default
656                shared.dynamics    = { }
657             -- shared.processes   = { }
658                tfmdata.changed    = { }
659                shared.features    = features
660                shared.processes   = otf.setfeatures(tfmdata,features)
661            end
662        end
663        containers.write(constructors.cache,cache_id,tfmdata)
664    end
665    return tfmdata
666end
667
668local function read_from_otf(specification)
669    local tfmdata = otftotfm(specification)
670    if tfmdata then
671        -- this late ? .. needs checking
672        tfmdata.properties.name = specification.name
673        tfmdata.properties.sub  = specification.sub
674        tfmdata.properties.id   = specification.id
675        --
676        tfmdata = constructors.scale(tfmdata,specification)
677        local allfeatures = tfmdata.shared.features or specification.features.normal
678        constructors.applymanipulators("otf",tfmdata,allfeatures,trace_features,report_otf)
679        constructors.setname(tfmdata,specification) -- only otf?
680        fonts.loggers.register(tfmdata,file.suffix(specification.filename),specification)
681    end
682    return tfmdata
683end
684
685-- if context then
686--
687--     -- so the next will go to some generic module instead
688--
689-- else
690--
691--     local function checkmathsize(tfmdata,mathsize)
692--         local mathdata = tfmdata.shared.rawdata.metadata.math
693--         local mathsize = tonumber(mathsize)
694--         if mathdata then -- we cannot use mathparameters as luatex will complain
695--             local parameters = tfmdata.parameters
696--             parameters.scriptpercentage       = mathdata.ScriptPercentScaleDown
697--             parameters.scriptscriptpercentage = mathdata.ScriptScriptPercentScaleDown
698--             parameters.mathsize               = mathsize -- only when a number !
699--         end
700--     end
701--
702--     registerotffeature {
703--         name         = "mathsize",
704--         description  = "apply mathsize specified in the font",
705--         initializers = {
706--             base = checkmathsize,
707--             node = checkmathsize,
708--         }
709--     }
710--
711-- end
712
713-- readers
714
715function otf.collectlookups(rawdata,kind,script,language)
716    if not kind then
717        return
718    end
719    if not script then
720        script = default
721    end
722    if not language then
723        language = default
724    end
725    local lookupcache = rawdata.lookupcache
726    if not lookupcache then
727        lookupcache = { }
728        rawdata.lookupcache = lookupcache
729    end
730    local kindlookup = lookupcache[kind]
731    if not kindlookup then
732        kindlookup = { }
733        lookupcache[kind] = kindlookup
734    end
735    local scriptlookup = kindlookup[script]
736    if not scriptlookup then
737        scriptlookup = { }
738        kindlookup[script] = scriptlookup
739    end
740    local languagelookup = scriptlookup[language]
741    if not languagelookup then
742        local sequences   = rawdata.resources.sequences
743        local featuremap  = { }
744        local featurelist = { }
745        if sequences then
746            for s=1,#sequences do
747                local sequence = sequences[s]
748                local features = sequence.features
749                if features then
750                    features = features[kind]
751                    if features then
752                     -- features = features[script] or features[default] or features[wildcard]
753                        features = features[script] or features[wildcard]
754                        if features then
755                         -- features = features[language] or features[default] or features[wildcard]
756                            features = features[language] or features[wildcard]
757                            if features then
758                                if not featuremap[sequence] then
759                                    featuremap[sequence] = true
760                                    featurelist[#featurelist+1] = sequence
761                                end
762                            end
763                        end
764                    end
765                end
766            end
767            if #featurelist == 0 then
768                featuremap, featurelist = false, false
769            end
770        else
771            featuremap, featurelist = false, false
772        end
773        languagelookup = { featuremap, featurelist }
774        scriptlookup[language] = languagelookup
775    end
776    return unpack(languagelookup)
777end
778
779-- moved from font-oth.lua, todo: also afm
780
781local function getgsub(tfmdata,k,kind,value)
782    local shared  = tfmdata.shared
783    local rawdata = shared and shared.rawdata
784    if rawdata then
785        local sequences = rawdata.resources.sequences
786        if sequences then
787            local properties = tfmdata.properties
788            local validlookups, lookuplist = otf.collectlookups(rawdata,kind,properties.script,properties.language)
789            if validlookups then
790             -- local choice = tonumber(value) or 1 -- no random here (yet)
791                for i=1,#lookuplist do
792                    local lookup   = lookuplist[i]
793                    local steps    = lookup.steps
794                    local nofsteps = lookup.nofsteps
795                    for i=1,nofsteps do
796                        local coverage = steps[i].coverage
797                        if coverage then
798                            local found = coverage[k]
799                            if found then
800                                return found, lookup.type
801                            end
802                        end
803                    end
804                end
805            end
806        end
807    end
808end
809
810otf.getgsub = getgsub -- returns value, gsub_kind
811
812function otf.getsubstitution(tfmdata,k,kind,value)
813    local found, kind = getgsub(tfmdata,k,kind,value)
814    if not found then
815        --
816    elseif kind == "gsub_single" then
817        return found
818    elseif kind == "gsub_alternate" then
819        local choice = tonumber(value) or 1 -- no random here (yet)
820        return found[choice] or found[1] or k
821    end
822    return k
823end
824
825otf.getalternate = otf.getsubstitution
826
827function otf.getmultiple(tfmdata,k,kind)
828    local found, kind = getgsub(tfmdata,k,kind)
829    if found and kind == "gsub_multiple" then
830        return found
831    end
832    return { k }
833end
834
835function otf.getkern(tfmdata,left,right,kind)
836    local kerns = getgsub(tfmdata,left,kind or "kern",true) -- for now we use getsub
837    if kerns then
838        local found = kerns[right]
839        local kind  = type(found)
840        if kind == "table" then
841            found = found[1][3] -- can be more clever
842        elseif kind ~= "number" then
843            found = false
844        end
845        if found then
846            return found * tfmdata.parameters.factor
847        end
848    end
849    return 0
850end
851
852local function check_otf(forced,specification,suffix)
853    local name = specification.name
854    if forced then
855        name = specification.forcedname -- messy
856    end
857    local fullname = findbinfile(name,suffix) or ""
858    if fullname == "" then
859        fullname = fonts.names.getfilename(name,suffix) or ""
860    end
861    if fullname ~= "" and not fonts.names.ignoredfile(fullname) then
862        specification.filename = fullname
863        return read_from_otf(specification)
864    end
865end
866
867local function opentypereader(specification,suffix)
868    local forced = specification.forced or ""
869    if formats[forced] then
870        return check_otf(true,specification,forced)
871    else
872        return check_otf(false,specification,suffix)
873    end
874end
875
876readers.opentype = opentypereader -- kind of useless and obsolete
877
878function readers.otf(specification) return opentypereader(specification,"otf") end
879function readers.ttf(specification) return opentypereader(specification,"ttf") end
880function readers.ttc(specification) return opentypereader(specification,"ttf") end
881
882function readers.woff(specification)
883    checkconversion(specification)
884    opentypereader(specification,"")
885end
886
887-- this will be overloaded
888
889function otf.scriptandlanguage(tfmdata,attr)
890    local properties = tfmdata.properties
891    return properties.script or "dflt", properties.language or "dflt"
892end
893
894-- a little bit of abstraction
895
896local function justset(coverage,unicode,replacement)
897    coverage[unicode] = replacement
898end
899
900otf.coverup = {
901    stepkey = "steps",
902    actions = {
903        chainsubstitution = justset,
904        chainposition     = justset,
905        substitution      = justset,
906        alternate         = justset,
907        multiple          = justset,
908        kern              = justset,
909        pair              = justset,
910        single            = justset,
911        ligature          = function(coverage,unicode,ligature)
912            local first = ligature[1]
913            local tree  = coverage[first]
914            if not tree then
915                tree = { }
916                coverage[first] = tree
917            end
918            for i=2,#ligature do
919                local l = ligature[i]
920                local t = tree[l]
921                if not t then
922                    t = { }
923                    tree[l] = t
924                end
925                tree = t
926            end
927            tree.ligature = unicode
928        end,
929    },
930    register = function(coverage,featuretype,format)
931        return {
932            format   = format,
933            coverage = coverage,
934        }
935    end
936}
937