font-otd.lmt /size: 12 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['font-otd'] = {
2    version   = 1.001,
3    optimize  = true,
4    comment   = "companion to font-ini.mkiv",
5    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
6    copyright = "PRAGMA ADE / ConTeXt Development Team",
7    license   = "see context related readme files"
8}
9
10local type = type
11local match = string.match
12local sequenced = table.sequenced
13
14local trace_dynamics     = false  trackers.register("otf.dynamics", function(v) trace_dynamics = v end)
15local trace_applied      = false  trackers.register("otf.applied",  function(v) trace_applied      = v end)
16
17local report_otf         = logs.reporter("fonts","otf loading")
18local report_process     = logs.reporter("fonts","otf process")
19
20local allocate           = utilities.storage.allocate
21
22local fonts              = fonts
23local otf                = fonts.handlers.otf
24local hashes             = fonts.hashes
25local definers           = fonts.definers
26local constructors       = fonts.constructors
27local specifiers         = fonts.specifiers
28
29local fontidentifiers    = hashes.identifiers
30local fontresources      = hashes.resources
31local fontproperties     = hashes.properties
32local fontdynamics       = hashes.dynamics
33
34local contextsetups      = specifiers.contextsetups
35local contextnumbers     = specifiers.contextnumbers
36local contextmerged      = specifiers.contextmerged
37
38local setmetatableindex  = table.setmetatableindex
39
40local a_to_script        = { }
41local a_to_language      = { }
42
43-- we can have a scripts hash in fonts.hashes
44
45function otf.setdynamics(font,attribute)
46 -- local features = contextsetups[contextnumbers[attribute]] -- can be moved to caller
47    local features = contextsetups[attribute]
48    if features then
49        local dynamics = fontdynamics[font]
50        dynamic = contextmerged[attribute] or 0
51        local script, language
52        if dynamic == 2 then -- merge
53            language  = features.language or fontproperties[font].language or "dflt"
54            script    = features.script   or fontproperties[font].script   or "dflt"
55        else -- if dynamic == 1 then -- replace
56            language  = features.language or "dflt"
57            script    = features.script   or "dflt"
58        end
59        if script == "auto" then
60            -- checkedscript and resources are defined later so we cannot shortcut them -- todo: make installer
61            script = definers.checkedscript(fontidentifiers[font],fontresources[font],features)
62        end
63        local ds = dynamics[script] -- can be metatable magic (less testing)
64-- or dynamics.dflt
65        if not ds then
66            ds = { }
67            dynamics[script] = ds
68        end
69        local dsl = ds[language]
70-- or ds.dflt
71        if not dsl then
72            dsl = { }
73            ds[language] = dsl
74        end
75        local dsla = dsl[attribute]
76        if not dsla then
77            local tfmdata = fontidentifiers[font]
78            a_to_script  [attribute] = script
79            a_to_language[attribute] = language
80            -- we need to save some values .. quite messy
81            local properties = tfmdata.properties
82            local shared     = tfmdata.shared
83            local s_script   = properties.script
84            local s_language = properties.language
85            local s_mode     = properties.mode
86            local s_features = shared.features
87            properties.mode     = "node"
88            properties.language = language
89            properties.script   = script
90            properties.dynamics = true -- handy for tracing
91            shared.features     = { }
92            -- end of save
93            local set = constructors.checkedfeatures("otf",features)
94            set.mode = "node" -- really needed
95            dsla = otf.setfeatures(tfmdata,set)
96            if trace_dynamics then
97                report_otf("setting dynamics %s: attribute %a, script %a, language %a, set %a",contextnumbers[attribute],attribute,script,language,set)
98            end
99            -- we need to restore some values
100            properties.script   = s_script
101            properties.language = s_language
102            properties.mode     = s_mode
103            shared.features     = s_features
104            -- end of restore
105            dynamics[script][language][attribute] = dsla -- cache
106        elseif trace_dynamics then
107         -- report_otf("using dynamics %s: attribute %a, script %a, language %a",contextnumbers[attribute],attribute,script,language)
108        end
109        return dsla
110    end
111end
112
113function otf.scriptandlanguage(tfmdata,attr)
114    local properties = tfmdata.properties
115    if attr and attr > 0 then
116        return a_to_script[attr] or properties.script or "dflt", a_to_language[attr] or properties.language or "dflt"
117    else
118        return properties.script or "dflt", properties.language or "dflt"
119    end
120end
121
122-- we reimplement the dataset resolver
123
124local autofeatures    = fonts.analyzers.features
125local featuretypes    = otf.tables.featuretypes
126local defaultscript   = otf.features.checkeddefaultscript
127local defaultlanguage = otf.features.checkeddefaultlanguage
128
129local resolved         = { } -- we only resolve a font,script,language,attribute pair once
130local wildcard <const> = "*"
131
132-- what about analyze in local and not in font
133
134-- needs checking: some added features can pass twice
135
136local P, C, Cc, lpegmatch = lpeg.P, lpeg.C, lpeg.Cc, lpeg.match
137
138local pattern = P("always") * (P(-1) * Cc(true) + P(":") * C((1-P(-1))^1))
139
140local scriptsets = fonts.specifiers.scriptsets
141
142-- also register scriptset
143
144local function initialize(sequence,script,language,s_enabled,a_enabled,font,attr,dynamic,ra,autoscript,autolanguage)
145    local features = sequence.features
146    if features then
147        local order = sequence.order
148        if order then
149            local featuretype = featuretypes[sequence.type or "unknown"]
150            local lookupdone  = false
151            for i=1,#order do --
152                local kind = order[i] --
153                local e_e
154                local a_e = a_enabled and a_enabled[kind] -- the value (location)
155                if a_e ~= nil then
156                    e_e = a_e
157                else
158                    e_e = s_enabled and s_enabled[kind] -- the value (font)
159                end
160                if e_e then
161                    local usedattribute, usedscript, usedlanguage, usedlookup
162                    local valid = type(e_e) == "string" and lpegmatch(pattern,e_e)
163                    if valid then
164                        -- we have hit always
165                        usedattribute = autofeatures[kind] or false
166                        usedlanguage  = "*"
167                        usedscript    = "*"
168                        usedlookup    = { valid, usedattribute, sequence, kind }
169                    else
170                        -- we already checked for e_e
171                        local scripts   = features[kind] --
172                        local languages = scripts[script] or scripts[wildcard]
173                        -- experiment
174                        if not languages then
175                            local ss = scriptsets[script]
176                            if ss then
177                                for i=1,#ss do
178                                    languages = scripts[ss[i]]
179                                    if languages then
180                                        break
181                                    end
182                                end
183                            end
184                        end
185                        -- till here
186                        if not languages and autoscript then
187                            languages = defaultscript(featuretype,autoscript,scripts)
188                        end
189                        if languages then
190                            -- we need detailed control over default because we want to trace
191                            -- only first attribute match check, so we assume simple fina's
192                         -- local valid = false
193                            if languages[language] then
194                                valid = e_e
195                            elseif languages[wildcard] then
196                                valid = e_e
197                            elseif autolanguage and defaultlanguage(featuretype,autolanguage,languages) then
198                                valid = e_e
199                            end
200                        end
201                        if valid then
202                            usedattribute = autofeatures[kind] or false
203                            usedlanguage  = language -- was reversed
204                            usedscript    = script   -- was reversed
205                            usedlookup    = { valid, usedattribute, sequence, kind }
206                        end
207                    end
208                    if not usedlookup then
209                        -- go on
210                    elseif lookupdone then
211                        if trace_applied then
212                            report_process(
213                                "font %s, dynamic %a (%a), feature %a, script %a, language %a, lookup %a, value %a, nofsteps %a, lookup already set by %a",
214                                    font,attr or 0,dynamic,kind,usedscript,usedlanguage,sequence.name,valid,sequence.nofsteps,ra[#ra][4])
215                        end
216                    else
217                        ra[#ra+1] = usedlookup
218                        if trace_applied then
219                            report_process(
220                                "font %s, dynamic %a (%a), feature %a, script %a, language %a, lookup %a, value %a, nofsteps %a",
221                                    font,attr or 0,dynamic,kind,usedscript,usedlanguage,sequence.name,valid,sequence.nofsteps)
222                        else
223                            return -- no need to look further
224                        end
225                        lookupdone = true
226                    end
227                end
228            end
229        end
230    end
231end
232
233-- there is some fuzzy language/script state stuff in properties (temporary)
234
235function otf.dataset(tfmdata,font,attr) -- attr only when explicit (as in special parbuilder)
236
237    local script, language, s_enabled, a_enabled, dynamic
238
239    if attr and attr ~= 0 then
240        dynamic = contextmerged[attr] or 0
241     -- local features = contextsetups[contextnumbers[attr]] -- could be a direct list
242        local features = contextsetups[attr]
243        a_enabled = features -- location based
244        if dynamic == 1 then -- or dynamic == -1 then
245            -- replace
246            language  = features.language or "dflt"
247            script    = features.script   or "dflt"
248        elseif dynamic == 2 then -- or dynamic == -2 then
249            -- merge
250            local properties = tfmdata.properties
251            s_enabled = tfmdata.shared.features -- font based
252            language  = features.language or properties.language or  "dflt"
253            script    = features.script   or properties.script   or  "dflt"
254        else
255            -- error
256            local properties = tfmdata.properties
257            language  = properties.language or "dflt"
258            script    = properties.script   or "dflt"
259        end
260    else
261        local properties = tfmdata.properties
262        language  = properties.language or "dflt"
263        script    = properties.script   or "dflt"
264        s_enabled = tfmdata.shared.features -- can be made local to the resolver
265        dynamic   = 0
266    end
267
268    local res = resolved[font]
269    if not res then
270        res = { }
271        resolved[font] = res
272    end
273    local rs = res[script]
274    if not rs then
275        rs = { }
276        res[script] = rs
277    end
278    local rl = rs[language]
279    if not rl then
280        rl = { }
281        rs[language] = rl
282    end
283    local ra = rl[attr]
284    if ra == nil then -- attr can be false
285        ra = {
286            -- indexed but we can also add specific data by key in:
287        }
288        rl[attr] = ra
289        local sequences = tfmdata.shared.reorderedsequences or tfmdata.resources.sequences
290        if sequences then
291            local autoscript   = (s_enabled and s_enabled.autoscript  ) or (a_enabled and a_enabled.autoscript  )
292            local autolanguage = (s_enabled and s_enabled.autolanguage) or (a_enabled and a_enabled.autolanguage)
293            for s=1,#sequences do
294                -- just return nil or ra step
295                initialize(sequences[s],script,language,s_enabled,a_enabled,font,attr,dynamic,ra,autoscript,autolanguage)
296            end
297        end
298    end
299    return ra
300end
301