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