font-otl.lua /size: 33 Kb    last modification: 2024-01-16 09:02
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
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 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        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
646-- if context then
647--
648--     -- so the next will go to some generic module instead
649--
650-- else
651--
652--     local function checkmathsize(tfmdata,mathsize)
653--         local mathdata = tfmdata.shared.rawdata.metadata.math
654--         local mathsize = tonumber(mathsize)
655--         if mathdata then -- we cannot use mathparameters as luatex will complain
656--             local parameters = tfmdata.parameters
657--             parameters.scriptpercentage       = mathdata.ScriptPercentScaleDown
658--             parameters.scriptscriptpercentage = mathdata.ScriptScriptPercentScaleDown
659--             parameters.mathsize               = mathsize -- only when a number !
660--         end
661--     end
662--
663--     registerotffeature {
664--         name         = "mathsize",
665--         description  = "apply mathsize specified in the font",
666--         initializers = {
667--             base = checkmathsize,
668--             node = checkmathsize,
669--         }
670--     }
671--
672-- end
673
674-- readers
675
676function otf.collectlookups(rawdata,kind,script,language)
677    if not kind then
678        return
679    end
680    if not script then
681        script = default
682    end
683    if not language then
684        language = default
685    end
686    local lookupcache = rawdata.lookupcache
687    if not lookupcache then
688        lookupcache = { }
689        rawdata.lookupcache = lookupcache
690    end
691    local kindlookup = lookupcache[kind]
692    if not kindlookup then
693        kindlookup = { }
694        lookupcache[kind] = kindlookup
695    end
696    local scriptlookup = kindlookup[script]
697    if not scriptlookup then
698        scriptlookup = { }
699        kindlookup[script] = scriptlookup
700    end
701    local languagelookup = scriptlookup[language]
702    if not languagelookup then
703        local sequences   = rawdata.resources.sequences
704        local featuremap  = { }
705        local featurelist = { }
706        if sequences then
707            for s=1,#sequences do
708                local sequence = sequences[s]
709                local features = sequence.features
710                if features then
711                    features = features[kind]
712                    if features then
713                     -- features = features[script] or features[default] or features[wildcard]
714                        features = features[script] or features[wildcard]
715                        if features then
716                         -- features = features[language] or features[default] or features[wildcard]
717                            features = features[language] or features[wildcard]
718                            if features then
719                                if not featuremap[sequence] then
720                                    featuremap[sequence] = true
721                                    featurelist[#featurelist+1] = sequence
722                                end
723                            end
724                        end
725                    end
726                end
727            end
728            if #featurelist == 0 then
729                featuremap, featurelist = false, false
730            end
731        else
732            featuremap, featurelist = false, false
733        end
734        languagelookup = { featuremap, featurelist }
735        scriptlookup[language] = languagelookup
736    end
737    return unpack(languagelookup)
738end
739
740-- moved from font-oth.lua, todo: also afm
741
742local function getgsub(tfmdata,k,kind,value)
743    local shared  = tfmdata.shared
744    local rawdata = shared and shared.rawdata
745    if rawdata then
746        local sequences = rawdata.resources.sequences
747        if sequences then
748            local properties = tfmdata.properties
749            local validlookups, lookuplist = otf.collectlookups(rawdata,kind,properties.script,properties.language)
750            if validlookups then
751             -- local choice = tonumber(value) or 1 -- no random here (yet)
752                for i=1,#lookuplist do
753                    local lookup   = lookuplist[i]
754                    local steps    = lookup.steps
755                    local nofsteps = lookup.nofsteps
756                    for i=1,nofsteps do
757                        local coverage = steps[i].coverage
758                        if coverage then
759                            local found = coverage[k]
760                            if found then
761                                return found, lookup.type
762                            end
763                        end
764                    end
765                end
766            end
767        end
768    end
769end
770
771otf.getgsub = getgsub -- returns value, gsub_kind
772
773function otf.getsubstitution(tfmdata,k,kind,value)
774    local found, kind = getgsub(tfmdata,k,kind,value)
775    if not found then
776        --
777    elseif kind == "gsub_single" then
778        return found
779    elseif kind == "gsub_alternate" then
780        local choice = tonumber(value) or 1 -- no random here (yet)
781        return found[choice] or found[1] or k
782    end
783    return k
784end
785
786otf.getalternate = otf.getsubstitution
787
788function otf.getmultiple(tfmdata,k,kind)
789    local found, kind = getgsub(tfmdata,k,kind)
790    if found and kind == "gsub_multiple" then
791        return found
792    end
793    return { k }
794end
795
796function otf.getkern(tfmdata,left,right,kind)
797    local kerns = getgsub(tfmdata,left,kind or "kern",true) -- for now we use getsub
798    if kerns then
799        local found = kerns[right]
800        local kind  = type(found)
801        if kind == "table" then
802            found = found[1][3] -- can be more clever
803        elseif kind ~= "number" then
804            found = false
805        end
806        if found then
807            return found * tfmdata.parameters.factor
808        end
809    end
810    return 0
811end
812
813local function check_otf(forced,specification,suffix)
814    local name = specification.name
815    if forced then
816        name = specification.forcedname -- messy
817    end
818    local fullname = findbinfile(name,suffix) or ""
819    if fullname == "" then
820        fullname = fonts.names.getfilename(name,suffix) or ""
821    end
822    if fullname ~= "" and not fonts.names.ignoredfile(fullname) then
823        specification.filename = fullname
824        return read_from_otf(specification)
825    end
826end
827
828local function opentypereader(specification,suffix)
829    local forced = specification.forced or ""
830    if formats[forced] then
831        return check_otf(true,specification,forced)
832    else
833        return check_otf(false,specification,suffix)
834    end
835end
836
837readers.opentype = opentypereader -- kind of useless and obsolete
838
839function readers.otf(specification) return opentypereader(specification,"otf") end
840function readers.ttf(specification) return opentypereader(specification,"ttf") end
841function readers.ttc(specification) return opentypereader(specification,"ttf") end
842
843function readers.woff(specification)
844    checkconversion(specification)
845    opentypereader(specification,"")
846end
847
848-- this will be overloaded
849
850function otf.scriptandlanguage(tfmdata,attr)
851    local properties = tfmdata.properties
852    return properties.script or "dflt", properties.language or "dflt"
853end
854
855-- a little bit of abstraction
856
857local function justset(coverage,unicode,replacement)
858    coverage[unicode] = replacement
859end
860
861otf.coverup = {
862    stepkey = "steps",
863    actions = {
864        chainsubstitution = justset,
865        chainposition     = justset,
866        substitution      = justset,
867        alternate         = justset,
868        multiple          = justset,
869        kern              = justset,
870        pair              = justset,
871        single            = justset,
872        ligature          = function(coverage,unicode,ligature)
873            local first = ligature[1]
874            local tree  = coverage[first]
875            if not tree then
876                tree = { }
877                coverage[first] = tree
878            end
879            for i=2,#ligature do
880                local l = ligature[i]
881                local t = tree[l]
882                if not t then
883                    t = { }
884                    tree[l] = t
885                end
886                tree = t
887            end
888            tree.ligature = unicode
889        end,
890    },
891    register = function(coverage,featuretype,format)
892        return {
893            format   = format,
894            coverage = coverage,
895        }
896    end
897}
898