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
88local function checkenabled()
89
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
121
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
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
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
192
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
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
273 elseif not newchars[target] then
274
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
285 else
286 for unicode = start, stop do
287 local unic = unicode + offset - start
288 if isprivate(unic) or isprivate(unicode) then
289
290 elseif not newchars[target] then
291
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
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
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
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
346 if validvectors[current] then
347 checkenabled()
348 end
349end
350
351
352
353
354
355
356
357
358function collections.prepare(name)
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")
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()
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
416
417 local data = {
418
419 width = width,
420 height = character.height,
421 depth = character.depth,
422
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
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)
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)
525 if not char then
526 font, char = currentfont(), font
527 end
528 if chardata[font][char] then
529 return true
530 else
531 local v = vectors[font]
532 return v and v[char] and true or false
533 end
534end
535
536
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 |