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