font-col.lmt /size: 18 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['font-col'] = {
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-- possible optimization: delayed initialization of vectors
10-- we should also share equal vectors (math)
11
12local context, commands, trackers, logs = context, commands, trackers, logs
13local node, nodes, fonts, characters = node, nodes, fonts, characters
14local file, lpeg, table, string = file, lpeg, table, string
15
16local type, next, tonumber, toboolean = type, next, tonumber, toboolean
17local gmatch = string.gmatch
18local fastcopy = table.fastcopy
19local formatters = string.formatters
20
21local nuts               = nodes.nuts
22
23local setfont            = nuts.setfont
24
25local nextchar           = nuts.traversers.char
26local getscales          = nuts.getscales
27local setscales          = nuts.setscales
28local setprop            = nuts.setprop
29local getcharspec        = nuts.getcharspec
30
31local settings_to_hash   = utilities.parsers.settings_to_hash
32
33local trace_collecting   = false  trackers.register("fonts.collecting", function(v) trace_collecting = v end)
34
35local report_fonts       = logs.reporter("fonts","collections")
36
37local texconditionals    = tex.conditionals
38
39local enableaction       = nodes.tasks.enableaction
40local disableaction      = nodes.tasks.disableaction
41
42local collections        = fonts.collections or { }
43fonts.collections        = collections
44
45local definitions        = collections.definitions or { }
46collections.definitions  = definitions
47
48-- why public ?
49
50local vectors            = collections.vectors or { }
51collections.vectors      = vectors
52local rscales            = collections.rscales or { }
53collections.rscales      = rscales
54
55local helpers            = fonts.helpers
56local charcommand        = helpers.commands.char
57local rightcommand       = helpers.commands.right
58local addprivate         = helpers.addprivate
59local hasprivate         = helpers.hasprivate
60local isprivate          = helpers.isprivate
61local fontpatternhassize = helpers.fontpatternhassize
62
63local hashes             = fonts.hashes
64local fontdata           = hashes.identifiers
65local fontquads          = hashes.quads
66local chardata           = hashes.characters
67local propdata           = hashes.properties
68local mathparameters     = hashes.mathparameters
69
70local currentfont        = font.current
71local addcharacters      = font.addcharacters
72
73local implement          = interfaces.implement
74
75local list               = { }
76local current            = 0
77local enabled            = false
78
79local validvectors       = table.setmetatableindex(function(t,k)
80    local v = false
81    if not mathparameters[k] then
82        v = vectors[k]
83    end
84    t[k] = v
85    return v
86end)
87
88table.setmetatableindex(rscales,"table")
89
90local function checkenabled()
91    -- a bit ugly but nicer than a fuzzy state while defining math
92    if next(vectors) then
93        if not enabled then
94            enableaction("processors","fonts.collections.process")
95            enabled = true
96        end
97    else
98        if enabled then
99            disableaction("processors","fonts.collections.process")
100            enabled = false
101        end
102    end
103end
104
105collections.checkenabled = checkenabled
106
107function collections.reset(name,font)
108    if font and font ~= "" then
109        local d = definitions[name]
110        if d then
111            d[font] = nil
112            if not next(d) then
113                definitions[name] = nil
114            end
115        end
116    else
117        definitions[name] = nil
118    end
119end
120
121function collections.define(name,font,ranges,details)
122    -- todo: details -> method=force|conditional rscale=
123    -- todo: remap=name
124    local d = definitions[name]
125    if not d then
126        d = { }
127        definitions[name] = d
128    end
129    if name and trace_collecting then
130        report_fonts("extending collection %a using %a",name,font)
131    end
132    details = settings_to_hash(details)
133    -- todo, combine per font start/stop as arrays
134    local offset = details.offset
135    if type(offset) == "string" then
136        offset = characters.getrange(offset,true) or false
137    else
138        offset = tonumber(offset) or false
139    end
140    local target = details.target
141    if type(target) == "string" then
142        target = characters.getrange(target,true) or false
143    else
144        target = tonumber(target) or false
145    end
146    local rscale   = tonumber (details.rscale) or 1
147    local force    = toboolean(details.force,true)
148    local check    = toboolean(details.check,true)
149    local factor   = tonumber(details.factor)
150    local features = details.features
151    for s in gmatch(ranges,"[^, ]+") do
152        local start, stop, description, gaps = characters.getrange(s,true)
153        if start and stop then
154            if trace_collecting then
155                if description then
156                    report_fonts("using range %a, slots %U - %U, description %a)",s,start,stop,description)
157                end
158                for i=1,#d do
159                    local di = d[i]
160                    if (start >= di.start and start <= di.stop) or (stop >= di.start and stop <= di.stop) then
161                        report_fonts("overlapping ranges %U - %U and %U - %U",start,stop,di.start,di.stop)
162                    end
163                end
164            end
165            d[#d+1] = {
166                font     = font,
167                start    = start,
168                stop     = stop,
169                gaps     = gaps,
170                offset   = offset,
171                target   = target,
172                rscale   = rscale,
173                force    = force,
174                check    = check,
175                method   = details.method,
176                factor   = factor,
177                features = features,
178            }
179        end
180    end
181end
182
183-- todo: provide a lua variant (like with definefont)
184
185function collections.registermain(name)
186    local last = currentfont()
187    if trace_collecting then
188        report_fonts("registering font %a with name %a",last,name)
189    end
190    list[#list+1] = last
191end
192
193-- check: when true, only set when present in font
194-- force: when false, then not set when already set
195
196local uccodes = characters.uccodes
197local lccodes = characters.lccodes
198
199local methods = {
200    lowercase = function(oldchars,newchars,vector,start,stop,cloneid)
201        for k, v in next, oldchars do
202            if k >= start and k <= stop then
203                local lccode = lccodes[k]
204                if k ~= lccode and newchars[lccode] then
205                    vector[k] = { cloneid, lccode }
206                end
207            end
208        end
209    end,
210    uppercase = function(oldchars,newchars,vector,start,stop,cloneid)
211        for k, v in next, oldchars do
212            if k >= start and k <= stop then
213                local uccode = uccodes[k]
214                if k ~= uccode and newchars[uccode] then
215                    vector[k] = { cloneid, uccode }
216                end
217            end
218        end
219    end,
220}
221
222function collections.clonevector(name)
223    statistics.starttiming(fonts)
224    if trace_collecting then
225        report_fonts("processing collection %a",name)
226    end
227    local definitions = definitions[name]
228    local vector      = { }
229    local scales      = { }
230    vectors[current]  = vector
231    rscales[current]  = scales
232    for i=1,#definitions do
233        local definition = definitions[i]
234        local name       = definition.font
235        local start      = definition.start
236        local stop       = definition.stop
237        local check      = definition.check
238        local force      = definition.force
239        local offset     = definition.offset or start
240        local remap      = definition.remap -- not used
241        local target     = definition.target
242        local method     = definition.method
243        local cloneid    = list[i]
244        local oldchars   = fontdata[current].characters
245        local newchars   = fontdata[cloneid].characters
246        local factor     = definition.factor
247        local rscale     = false
248        if factor then
249            vector.factor = factor
250        end
251        if texconditionals["c_font_compact"] then
252            local rs = definition.rscale
253            if rs and rs ~= 1 then
254                rscale = rs
255            end
256        end
257        if trace_collecting then
258            if target then
259                report_fonts("remapping font %a to %a for range %U - %U, offset %X, target %U",current,cloneid,start,stop,offset,target)
260            else
261                report_fonts("remapping font %a to %a for range %U - %U, offset %X",current,cloneid,start,stop,offset)
262            end
263        end
264        if method then
265            method = methods[method]
266        end
267        if method then
268            method(oldchars,newchars,vector,start,stop,cloneid)
269        elseif check then
270            if target then
271                for unicode = start, stop do
272                    local unic = unicode + offset - start
273                    if isprivate(unic) or isprivate(target) then
274                        -- ignore
275                    elseif not newchars[target] then
276                        -- not in font
277                    elseif force or (not vector[unic] and not oldchars[unic]) then
278                        vector[unic] = { cloneid, target }
279                        if rscale then
280                            scales[unic] = rscale
281                        end
282                    end
283                    target = target + 1
284                end
285            elseif remap then
286                -- not used
287            else
288                for unicode = start, stop do
289                    local unic = unicode + offset - start
290                    if isprivate(unic) or isprivate(unicode) then
291                        -- ignore
292                    elseif not newchars[target] then
293                        -- not in font
294                    elseif force or (not vector[unic] and not oldchars[unic]) then
295                        vector[unic] = cloneid
296                        if rscale then
297                            scales[unic] = rscale
298                        end
299                    end
300                end
301            end
302        else
303            if target then
304                for unicode = start, stop do
305                    local unic = unicode + offset - start
306                    if isprivate(unic) or isprivate(target) then
307                        -- ignore
308                    elseif force or (not vector[unic] and not oldchars[unic]) then
309                        vector[unic] = { cloneid, target }
310                        if rscale then
311                            scales[unic] = rscale
312                        end
313                    end
314                    target = target + 1
315                end
316            elseif remap then
317                for unicode = start, stop do
318                    local unic = unicode + offset - start
319                    if isprivate(unic) or isprivate(unicode) then
320                        -- ignore
321                    elseif force or (not vector[unic] and not oldchars[unic]) then
322                        vector[unic] = { cloneid, remap[unicode] }
323                        if rscale then
324                            scales[unic] = rscale
325                        end
326                    end
327                end
328            else
329                for unicode = start, stop do
330                    local unic = unicode + offset - start
331                    if isprivate(unic) then
332                        -- ignore
333                    elseif force or (not vector[unic] and not oldchars[unic]) then
334                        vector[unic] = cloneid
335                        if rscale then
336                             scales[unic] = rscale
337                        end
338                    end
339                end
340            end
341        end
342    end
343    if trace_collecting then
344        report_fonts("activating collection %a for font %a",name,current)
345    end
346    statistics.stoptiming(fonts)
347    -- for WS: needs checking
348    if validvectors[current] then
349        checkenabled()
350    end
351end
352
353-- we already have this parser
354--
355-- local spec = (P("sa") + P("at") + P("scaled") + P("at") + P("mo")) * P(" ")^1 * (1-P(" "))^1 * P(" ")^0 * -1
356-- local okay = ((1-spec)^1 * spec * Cc(true)) + Cc(false)
357--
358-- if lpegmatch(okay,name) then
359
360function collections.prepare(name) -- we can do this in lua now .. todo
361    current = currentfont()
362    if vectors[current] then
363        return
364    end
365    local properties = propdata[current]
366    local mathsize   = properties.mathsize
367    if mathsize == 1 or mathsize == 2 or mathsize == 3 or properties.math_is_scaled or properties.mathisscaled then
368        return
369    end
370    local d = definitions[name]
371    if d then
372        if trace_collecting then
373            local filename = file.basename(properties.filename or "?")
374            report_fonts("applying collection %a to %a, file %a",name,current,filename)
375        end
376        list = { }
377        context.pushcatcodes("prt") -- context.unprotect()
378        context.font_fallbacks_start_cloning()
379        for i=1,#d do
380            local f     = d[i]
381            local name  = f.font
382            local scale = f.rscale or 1
383            if texconditionals["c_font_compact"] then
384                scale = 1
385            end
386            if fontpatternhassize(name) then
387                context.font_fallbacks_clone_unique(name,scale)
388            else
389                context.font_fallbacks_clone_inherited(name,scale)
390            end
391            context.font_fallbacks_register_main(name)
392        end
393        context.font_fallbacks_prepare_clone_vectors(name)
394        context.font_fallbacks_stop_cloning()
395        context.popcatcodes() -- context.protect()
396    end
397end
398
399function collections.report(message)
400    if trace_collecting then
401        report_fonts("tex: %s",message)
402    end
403end
404
405local function monoslot(font,char,parent,factor)
406    local tfmdata     = fontdata[font]
407    local privatename = formatters["faked mono %s"](char)
408    local privateslot = hasprivate(tfmdata,privatename)
409    if privateslot then
410        return privateslot
411    else
412        local characters = tfmdata.characters
413        local properties = tfmdata.properties
414        local width      = factor * fontquads[parent]
415        local character  = characters[char]
416        if character then
417            -- runtime patching of the font (can only be new characters)
418            -- instead of messing with existing dimensions
419            local data = {
420                -- no features so a simple copy
421                width    = width,
422                height   = character.height,
423                depth    = character.depth,
424                -- { "offset", ... }
425                commands = {
426                    rightcommand[(width - character.width or 0)/2],
427                    charcommand[char],
428                }
429            }
430            local u = addprivate(tfmdata, privatename, data)
431            addcharacters(properties.id, { characters = { [u] = data } } )
432            return u
433        else
434            return char
435        end
436    end
437end
438
439function collections.register(font,char,handler)
440    if font and char and type(handler) == "function" then
441        local vector = vectors[font]
442        if not vector then
443            vector = { }
444            vectors[font] = vector
445        end
446        vector[char] = handler
447    end
448end
449
450-- todo: also general one for missing
451
452local function apply(n,char,font,vector,vect)
453    local kind   = type(vect)
454    local rscale = rscales[font][char] or 1
455    local newfont, newchar
456    if kind == "table" then
457        newfont = vect[1]
458        newchar = vect[2]
459        if trace_collecting then
460            report_fonts("remapping character %C in font %a to character %C in font %a%s, rscale %s",
461                char,font,newchar,newfont,not chardata[newfont][newchar] and " (missing)" or "",rscale
462            )
463        end
464    elseif kind == "function" then
465        newfont, newchar = vect(font,char,vector)
466        if not newfont then
467            newfont = font
468        end
469        if not newchar then
470            newchar = char
471        end
472        if trace_collecting then
473            report_fonts("remapping character %C in font %a to character %C in font %a%s, rscale %f",
474                char,font,newchar,newfont,not chardata[newfont][newchar] and " (missing)" or "",rscale
475            )
476        end
477        vector[char] = { newfont, newchar }
478    else
479        local fakemono = vector.factor
480        if trace_collecting then
481            report_fonts("remapping font %a to %a for character %C%s, rscale %s",
482                font,vect,char,not chardata[vect][char] and " (missing)" or "",rscale
483            )
484        end
485        newfont = vect
486        if fakemono then
487            newchar = monoslot(vect,char,font,fakemono)
488        else
489            newchar = char
490        end
491    end
492    if rscale and rscale ~= 1 then
493        local s, x, y = getscales(n)
494        setscales(n,s*rscale,x,y)
495    end
496    setfont(n,newfont,newchar)
497    setprop(n, "original", { font = font, char = char })
498end
499
500function collections.process(head) -- this way we keep feature processing
501    for n, char, font in nextchar, head do
502        local vector = validvectors[font]
503        if vector then
504            local vect = vector[char]
505            if vect then
506-- print(char,font,vect)
507                apply(n,char,font,vector,vect)
508            end
509        end
510    end
511    return head
512end
513
514function collections.direct(n)
515    local char, font = getcharspec(n)
516    if font and char then
517        local vector = validvectors[font]
518        if vector then
519            local vect = vector[char]
520            if vect then
521                apply(n,char,font,vector,vect)
522            end
523        end
524    end
525end
526
527function collections.found(font,char) -- this way we keep feature processing
528    if not char then
529        font, char = currentfont(), font
530    end
531    if chardata[font][char] then
532        return true -- in normal font
533    else
534        local v = vectors[font]
535        return v and v[char] and true or false
536    end
537end
538
539-- interface
540
541implement {
542    name      = "fontcollectiondefine",
543    actions   = collections.define,
544    arguments = "4 strings",
545}
546
547implement {
548    name      = "fontcollectionreset",
549    actions   = collections.reset,
550    arguments = "2 strings",
551}
552
553implement {
554    name      = "fontcollectionprepare",
555    actions   = collections.prepare,
556    arguments = "string"
557}
558
559implement {
560    name      = "fontcollectionreport",
561    actions   = collections.report,
562    arguments = "string"
563}
564
565implement {
566    name      = "fontcollectionregister",
567    actions   = collections.registermain,
568    arguments = "string"
569}
570
571implement {
572    name      = "fontcollectionclone",
573    actions   = collections.clonevector,
574    arguments = "string"
575}
576
577implement {
578    name      = "doifelsecharinfont",
579    actions   = { collections.found, commands.doifelse },
580    arguments = "integer"
581}
582