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