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