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
10
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
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
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
123
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
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
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
194
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
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
275 elseif not newchars[target] then
276
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
287 else
288 for unicode = start, stop do
289 local unic = unicode + offset - start
290 if isprivate(unic) or isprivate(unicode) then
291
292 elseif not newchars[target] then
293
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
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
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
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
348 if validvectors[current] then
349 checkenabled()
350 end
351end
352
353
354
355
356
357
358
359
360function collections.prepare(name)
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")
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()
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
418
419 local data = {
420
421 width = width,
422 height = character.height,
423 depth = character.depth,
424
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
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)
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
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)
528 if not char then
529 font, char = currentfont(), font
530 end
531 if chardata[font][char] then
532 return true
533 else
534 local v = vectors[font]
535 return v and v[char] and true or false
536 end
537end
538
539
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 |