font-oto.lmt /size: 21 Kb    last modification: 2024-01-16 10:22
1if not modules then modules = { } end modules ['font-oto'] = { -- original tex
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
9local concat, unpack = table.concat, table.unpack
10local insert, remove = table.insert, table.remove
11local format, gmatch, gsub, find, match, lower, strip = string.format, string.gmatch, string.gsub, string.find, string.match, string.lower, string.strip
12local type, next, tonumber, tostring = type, next, tonumber, tostring
13
14local trace_baseinit         = false  trackers.register("otf.baseinit",     function(v) trace_baseinit     = v end)
15local trace_singles          = false  trackers.register("otf.singles",      function(v) trace_singles      = v end)
16local trace_multiples        = false  trackers.register("otf.multiples",    function(v) trace_multiples    = v end)
17local trace_alternatives     = false  trackers.register("otf.alternatives", function(v) trace_alternatives = v end)
18local trace_ligatures        = false  trackers.register("otf.ligatures",    function(v) trace_ligatures    = v end)
19local trace_kerns            = false  trackers.register("otf.kerns",        function(v) trace_kerns        = v end)
20local trace_preparing        = false  trackers.register("otf.preparing",    function(v) trace_preparing    = v end)
21
22local report_prepare         = logs.reporter("fonts","otf prepare")
23
24local fonts                  = fonts
25local otf                    = fonts.handlers.otf
26
27local otffeatures            = otf.features
28local registerotffeature     = otffeatures.register
29
30otf.defaultbasealternate     = "none" -- first last
31
32local getprivate             = fonts.constructors.getprivate
33
34local wildcard               = "*"
35local default                = "dflt"
36
37local formatters             = string.formatters
38local f_unicode              = formatters["%U"]
39local f_uniname              = formatters["%U (%s)"]
40local f_unilist              = formatters["% t (% t)"]
41
42local function gref(descriptions,n)
43    if type(n) == "number" then
44        local name = descriptions[n].name
45        if name then
46            return f_uniname(n,name)
47        else
48            return f_unicode(n)
49        end
50    elseif n then
51        local num = { }
52        local nam = { }
53        local j   = 0
54        for i=1,#n do
55            local ni = n[i]
56            if tonumber(ni) then -- first is likely a key
57                j = j + 1
58                local di = descriptions[ni]
59                num[j] = f_unicode(ni)
60                nam[j] = di and di.name or "-"
61            end
62        end
63        return f_unilist(num,nam)
64    else
65        return "<error in base mode tracing>"
66    end
67end
68
69local function cref(feature,sequence)
70    return formatters["feature %a, type %a, (chain) lookup %a"](feature,sequence.type,sequence.name)
71end
72
73local function report_substitution(feature,sequence,descriptions,unicode,substitution)
74    if unicode == substitution then
75        report_prepare("%s: base substitution %s maps onto itself",
76            cref(feature,sequence),
77            gref(descriptions,unicode))
78    else
79        report_prepare("%s: base substitution %s => %S",
80            cref(feature,sequence),
81            gref(descriptions,unicode),
82            gref(descriptions,substitution))
83    end
84end
85
86local function report_alternate(feature,sequence,descriptions,unicode,replacement,value,comment)
87    if unicode == replacement then
88        report_prepare("%s: base alternate %s maps onto itself",
89            cref(feature,sequence),
90            gref(descriptions,unicode))
91    else
92        report_prepare("%s: base alternate %s => %s (%S => %S)",
93            cref(feature,sequence),
94            gref(descriptions,unicode),
95            replacement and gref(descriptions,replacement),
96            value,
97            comment)
98    end
99end
100
101local function report_ligature(feature,sequence,descriptions,unicode,ligature)
102    report_prepare("%s: base ligature %s => %S",
103        cref(feature,sequence),
104        gref(descriptions,ligature),
105        gref(descriptions,unicode))
106end
107
108local function report_kern(feature,sequence,descriptions,unicode,otherunicode,value)
109    report_prepare("%s: base kern %s + %s => %S",
110        cref(feature,sequence),
111        gref(descriptions,unicode),
112        gref(descriptions,otherunicode),
113        value)
114end
115
116-- We need to make sure that luatex sees the difference between base fonts that have
117-- different glyphs in the same slots in fonts that have the same fullname (or filename).
118-- LuaTeX will merge fonts eventually (and subset later on). If needed we can use a more
119-- verbose name as long as we don't use <()<>[]{}/%> and the length is < 128.
120
121local basehash, basehashes, applied = { }, 1, { }
122
123local function registerbasehash(tfmdata)
124    local properties = tfmdata.properties
125    local hash       = concat(applied," ")
126    local base       = basehash[hash]
127    if not base then
128        basehashes     = basehashes + 1
129        base           = basehashes
130        basehash[hash] = base
131    end
132    properties.basehash = base
133    properties.fullname = (properties.fullname or properties.name) .. "-" .. base
134 -- report_prepare("fullname base hash '%a, featureset %a",tfmdata.properties.fullname,hash)
135    applied = { }
136end
137
138local function registerbasefeature(feature,value)
139    applied[#applied+1] = feature  .. "=" .. tostring(value)
140end
141
142-- The original basemode ligature builder used the names of components and did some expression
143-- juggling to get the chain right. The current variant starts with unicodes but still uses
144-- names to make the chain. This is needed because we have to create intermediates when needed
145-- but use predefined snippets when available. To some extend the current builder is more stupid
146-- but I don't worry that much about it as ligatures are rather predicatable.
147--
148-- Personally I think that an ff + i == ffi rule as used in for instance latin modern is pretty
149-- weird as no sane person will key that in and expect a glyph for that ligature plus the following
150-- character. Anyhow, as we need to deal with this, we do, but no guarantes are given.
151--
152--         latin modern       dejavu
153--
154-- f+f       102 102             102 102
155-- f+i       102 105             102 105
156-- f+l       102 108             102 108
157-- f+f+i                         102 102 105
158-- f+f+l     102 102 108         102 102 108
159-- ff+i    64256 105           64256 105
160-- ff+l                        64256 108
161--
162-- As you can see here, latin modern is less complete than dejavu but
163-- in practice one will not notice it.
164--
165-- The while loop is needed because we need to resolve for instance pseudo names like
166-- hyphen_hyphen to endash so in practice we end up with a bit too many definitions but the
167-- overhead is neglectable. We can have changed[first] or changed[second] but it quickly becomes
168-- messy if we need to take that into account.
169
170local function makefake(tfmdata,name,present)
171    local private   = getprivate(tfmdata)
172    local character = { intermediate = true, ligatures = { } }
173    tfmdata.resources.unicodes[name] = private
174    tfmdata.characters[private] = character
175    tfmdata.descriptions[private] = { name = name }
176    present[name] = private
177    return character
178end
179
180local function make_1(present,tree,name)
181    if tonumber(tree) then
182        present[name] = v
183    else
184        for k, v in next, tree do
185            if k == "ligature" then
186                present[name] = v
187            else
188                make_1(present,v,name .. "_" .. k)
189            end
190        end
191    end
192end
193
194local function make_3(present,tfmdata,characters,tree,name,preceding,unicode,done,v)
195    local character = characters[preceding]
196    if not character then
197        if trace_baseinit then
198            report_prepare("weird ligature in lookup %a, current %C, preceding %C",sequence.name,v,preceding)
199        end
200        character = makefake(tfmdata,name,present)
201    end
202    local ligatures = character.ligatures
203    if ligatures then
204        ligatures[unicode] = { char = v }
205    else
206        character.ligatures = { [unicode] = { char = v } }
207    end
208    if done then
209        local d = done[name]
210        if not d then
211            done[name] = { "dummy", v }
212        else
213            d[#d+1] = v
214        end
215    end
216end
217
218local function make_2(present,tfmdata,characters,tree,name,preceding,unicode,done)
219    if tonumber(tree) then
220        make_3(present,tfmdata,characters,tree,name,preceding,unicode,done,tree)
221    else
222        for k, v in next, tree do
223            if k == "ligature" then
224                make_3(present,tfmdata,characters,tree,name,preceding,unicode,done,v)
225            else
226                local code = present[name] or unicode
227                local name = name .. "_" .. k
228                make_2(present,tfmdata,characters,v,name,code,k,done)
229            end
230        end
231    end
232end
233
234local function preparesubstitutions(tfmdata,feature,value,validlookups,lookuplist)
235    local characters   = tfmdata.characters
236    local descriptions = tfmdata.descriptions
237    local resources    = tfmdata.resources
238    local changed      = tfmdata.changed
239
240    local ligatures    = { }
241    local alternate    = tonumber(value) or true and 1
242    local defaultalt   = otf.defaultbasealternate
243    local trace_singles      = trace_baseinit and trace_singles
244    local trace_alternatives = trace_baseinit and trace_alternatives
245    local trace_ligatures    = trace_baseinit and trace_ligatures
246
247    -- A chain of changes is handled in font-con which is cleaner because
248    -- we can have shared changes and such.
249
250    if not changed then
251        changed = { }
252        tfmdata.changed = changed
253    end
254
255    for i=1,#lookuplist do
256        local sequence = lookuplist[i]
257        local steps    = sequence.steps
258        local kind     = sequence.type
259        if kind == "gsub_single" then
260            for i=1,#steps do
261                for unicode, data in next, steps[i].coverage do
262                    if unicode ~= data and not changed[unicode] then
263                        changed[unicode] = data
264                    end
265                    if trace_singles then
266                        report_substitution(feature,sequence,descriptions,unicode,data)
267                    end
268                end
269            end
270        elseif kind == "gsub_alternate" then
271            for i=1,#steps do
272                for unicode, data in next, steps[i].coverage do
273                    local replacement = data[alternate]
274                    if replacement then
275                        if unicode ~= replacement and not changed[unicode] then
276                            changed[unicode] = replacement
277                        end
278                        if trace_alternatives then
279                            report_alternate(feature,sequence,descriptions,unicode,replacement,value,"normal")
280                        end
281                    elseif defaultalt == "first" then
282                        replacement = data[1]
283                        if unicode ~= replacement and not changed[unicode] then
284                            changed[unicode] = replacement
285                        end
286                        if trace_alternatives then
287                            report_alternate(feature,sequence,descriptions,unicode,replacement,value,defaultalt)
288                        end
289                    elseif defaultalt == "last" then
290                        replacement = data[#data]
291                        if unicode ~= replacement and not changed[unicode] then
292                            changed[unicode] = replacement
293                        end
294                        if trace_alternatives then
295                            report_alternate(feature,sequence,descriptions,unicode,replacement,value,defaultalt)
296                        end
297                    else
298                        if trace_alternatives then
299                            report_alternate(feature,sequence,descriptions,unicode,replacement,value,"unknown")
300                        end
301                    end
302                end
303            end
304        elseif kind == "gsub_ligature" then
305            for i=1,#steps do
306                for unicode, data in next, steps[i].coverage do
307                    ligatures[#ligatures+1] = { unicode, data, "" } -- lookupname }
308                    if trace_ligatures then
309                        report_ligature(feature,sequence,descriptions,unicode,data)
310                    end
311                end
312            end
313        end
314    end
315
316    local nofligatures = #ligatures
317
318    if nofligatures > 0 then
319        local characters = tfmdata.characters
320        local present    = { }
321        local done       = trace_baseinit and trace_ligatures and { }
322
323        for i=1,nofligatures do
324            local ligature = ligatures[i]
325            local unicode  = ligature[1]
326            local tree     = ligature[2]
327            make_1(present,tree,"ctx_"..unicode)
328        end
329
330        for i=1,nofligatures do
331            local ligature   = ligatures[i]
332            local unicode    = ligature[1]
333            local tree       = ligature[2]
334            local lookupname = ligature[3]
335            make_2(present,tfmdata,characters,tree,"ctx_"..unicode,unicode,unicode,done,sequence)
336        end
337
338    end
339
340end
341
342local function preparepositionings(tfmdata,feature,value,validlookups,lookuplist)
343    local characters   = tfmdata.characters
344    local descriptions = tfmdata.descriptions
345    local resources    = tfmdata.resources
346    local properties   = tfmdata.properties
347    local traceindeed  = trace_baseinit and trace_kerns
348    -- check out this sharedkerns trickery
349    for i=1,#lookuplist do
350        local sequence = lookuplist[i]
351        local steps    = sequence.steps
352        local kind     = sequence.type
353        local format   = sequence.format
354        if kind == "gpos_pair" then
355            for i=1,#steps do
356                local step   = steps[i]
357                local format = step.format
358                if format == "kern" or format == "move" then
359                    for unicode, data in next, steps[i].coverage do
360                        local character = characters[unicode]
361                        local kerns = character.kerns
362                        if not kerns then
363                            kerns = { }
364                            character.kerns = kerns
365                        end
366                        if traceindeed then
367                            for otherunicode, kern in next, data do
368                                if not kerns[otherunicode] and kern ~= 0 then
369                                    kerns[otherunicode] = kern
370                                    report_kern(feature,sequence,descriptions,unicode,otherunicode,kern)
371                                end
372                            end
373                        else
374                            for otherunicode, kern in next, data do
375                                if not kerns[otherunicode] and kern ~= 0 then
376                                    kerns[otherunicode] = kern
377                                end
378                            end
379                        end
380                    end
381                else
382                    for unicode, data in next, steps[i].coverage do
383                        local character = characters[unicode]
384                        local kerns     = character.kerns
385                        for otherunicode, kern in next, data do
386                            -- kern[2] is true (all zero) or a table
387                            local other = kern[2]
388                            if other == true or (not other and not (kerns and kerns[otherunicode])) then
389                                local kern = kern[1]
390                                if kern == true then
391                                    -- all zero
392                                elseif kern[1] ~= 0 or kern[2] ~= 0 or kern[4] ~= 0 then
393                                    -- a complex pair not suitable for basemode
394                                else
395                                    kern = kern[3]
396                                    if kern ~= 0 then
397                                        if kerns then
398                                            kerns[otherunicode] = kern
399                                        else
400                                            kerns = { [otherunicode] = kern }
401                                            character.kerns = kerns
402                                        end
403                                        if traceindeed then
404                                            report_kern(feature,sequence,descriptions,unicode,otherunicode,kern)
405                                        end
406                                    end
407                                end
408                            end
409                        end
410                    end
411                end
412            end
413        end
414    end
415
416end
417
418local function initializehashes(tfmdata)
419    -- already done
420end
421
422local function relocatessty(sequences,fullname)
423    local position     = false
424    local sstydata     = false
425    local nofsequences = #sequences
426    for s=1,#sequences do
427        local sequence = sequences[s]
428        local features = sequence.features
429        if features and features.ssty then
430            position = s
431            sstydata = sequence
432        end
433    end
434    if position and position ~= nofsequences then
435        table.remove(sequences,position)
436        sequences[nofsequences-1] = sstydata
437        if trace_preparing then
438            report_prepare("ssty feature relocated to the end in %a",fullname)
439        end
440    end
441end
442
443local function featuresinitializer(tfmdata,value)
444    if true then -- value then
445        local starttime = trace_preparing and os.clock()
446        local features  = tfmdata.shared.features
447        local fullname  = tfmdata.properties.fullname or "?"
448        if features then
449            initializehashes(tfmdata)
450            local collectlookups    = otf.collectlookups
451            local rawdata           = tfmdata.shared.rawdata
452            local properties        = tfmdata.properties
453            local script            = properties.script
454            local language          = properties.language
455            local rawresources      = rawdata.resources
456            local rawfeatures       = rawresources and rawresources.features
457            local basesubstitutions = rawfeatures and rawfeatures.gsub
458            local basepositionings  = rawfeatures and rawfeatures.gpos
459            local substitutionsdone = false
460            local positioningsdone  = false
461            --
462            if basesubstitutions or basepositionings then
463                local sequences = tfmdata.resources.sequences
464                if basesubstitutions.ssty then
465--                     relocatessty(sequences,fullname)
466                end
467                for s=1,#sequences do
468                    local sequence = sequences[s]
469                    local sfeatures = sequence.features
470                    if sfeatures then
471                        local order = sequence.order
472                        if order then
473                            for i=1,#order do --
474                                local feature = order[i]
475                                local value = features[feature]
476                                if value then
477                                    local validlookups, lookuplist = collectlookups(rawdata,feature,script,language)
478                                    if not validlookups then
479                                        -- skip
480                                    elseif basesubstitutions and basesubstitutions[feature] then
481                                        if trace_preparing then
482                                            report_prepare("filtering base %s feature %a for %a with value %a","sub",feature,fullname,value)
483                                        end
484                                        preparesubstitutions(tfmdata,feature,value,validlookups,lookuplist)
485                                        registerbasefeature(feature,value)
486                                        substitutionsdone = true
487                                    elseif basepositionings and basepositionings[feature] then
488                                        if trace_preparing then
489                                            report_prepare("filtering base %a feature %a for %a with value %a","pos",feature,fullname,value)
490                                        end
491                                        preparepositionings(tfmdata,feature,value,validlookups,lookuplist)
492                                        registerbasefeature(feature,value)
493                                        positioningsdone = true
494                                    end
495                                end
496                            end
497                        end
498                    end
499                end
500            end
501            registerbasehash(tfmdata)
502        end
503        if trace_preparing then
504            report_prepare("preparation time is %0.3f seconds for %a",os.clock()-starttime,fullname)
505        end
506    end
507end
508
509registerotffeature {
510    name         = "features",
511    description  = "features",
512    default      = true,
513    initializers = {
514     -- position = 1, -- after setscript (temp hack ... we need to force script / language to 1
515        base     = featuresinitializer,
516    }
517}
518
519otf.basemodeinitializer = featuresinitializer
520