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