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