1if not modules then modules = { } end modules ['font-chk'] = {
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
12
13
14
15local type, next = type, next
16local find, lower, gmatch = string.find, string.lower, string.gmatch
17local floor = math.floor
18
19local context = context
20
21local formatters = string.formatters
22local fastcopy = table.fastcopy
23local sortedkeys = table.sortedkeys
24local sortedhash = table.sortedhash
25local contains = table.contains
26
27local report = logs.reporter("fonts")
28local report_checking = logs.reporter("fonts","checking")
29
30local allocate = utilities.storage.allocate
31
32local getmacro = tokens.getters.macro
33
34local fonts = fonts
35
36fonts.checkers = fonts.checkers or { }
37local checkers = fonts.checkers
38
39local fonthashes = fonts.hashes
40local fontdata = fonthashes.identifiers
41local fontcharacters = fonthashes.characters
42
43local currentfont = font.current
44
45local definers = fonts.definers
46local helpers = fonts.helpers
47
48local addprivate = helpers.addprivate
49local hasprivate = helpers.hasprivate
50local getprivateslot = helpers.getprivateslot
51local getprivatecharornode = helpers.getprivatecharornode
52
53local otffeatures = fonts.constructors.features.otf
54local afmfeatures = fonts.constructors.features.afm
55
56local registerotffeature = otffeatures.register
57local registerafmfeature = afmfeatures.register
58
59local is_character = characters.is_character
60local chardata = characters.data
61
62local tasks = nodes.tasks
63local enableaction = tasks.enableaction
64local disableaction = tasks.disableaction
65
66local implement = interfaces.implement
67
68local glyph_code <const> = nodes.nodecodes.glyph
69
70local hpack_node = nodes.hpack
71
72local nuts = nodes.nuts
73local tonut = nuts.tonut
74
75local isglyph = nuts.isglyph
76local setchar = nuts.setchar
77
78local nextglyph = nuts.traversers.glyph
79
80local remove_node = nuts.remove
81local insertnodeafter = nuts.insertafter
82local insertnodebefore = nuts.insertbefore
83local copy_node = nuts.copy
84
85local actions = false
86
87
88
89local function onetimemessage(font,char,message)
90 local tfmdata = fontdata[font]
91 local shared = tfmdata.shared
92 if not shared then
93 shared = { }
94 tfmdata.shared = shared
95 end
96 local messages = shared.messages
97 if not messages then
98 messages = { }
99 shared.messages = messages
100 end
101 local category = messages[message]
102 if not category then
103 category = { }
104 messages[message] = category
105 end
106 if char == false then
107 return sortedkeys(category), category
108 end
109 local cc = category[char]
110 if not cc then
111 report_checking("char %C in font %a with id %a: %s",char,tfmdata.properties.fullname,font,message)
112 category[char] = 1
113 else
114 category[char] = cc + 1
115 end
116end
117
118fonts.loggers.onetimemessage = onetimemessage
119
120local fakes = {
121 MissingLowercase = { width = .45, height = .55, depth = .20 },
122 MissingUppercase = { width = .65, height = .70, depth = .25 },
123 MissingMark = { width = .15, height = .70, depth = -.50 },
124 MissingPunctuation = { width = .15, height = .55, depth = .20 },
125 MissingUnknown = { width = .45, height = .20, depth = 0 },
126}
127
128local mapping = allocate {
129 lu = { "MissingUppercase", "darkred" },
130 ll = { "MissingLowercase", "darkred" },
131 lt = { "MissingUppercase", "darkred" },
132 lm = { "MissingLowercase", "darkred" },
133 lo = { "MissingLowercase", "darkred" },
134 mn = { "MissingMark", "darkgreen" },
135 mc = { "MissingMark", "darkgreen" },
136 me = { "MissingMark", "darkgreen" },
137 nd = { "MissingLowercase", "darkblue" },
138 nl = { "MissingLowercase", "darkblue" },
139 no = { "MissingLowercase", "darkblue" },
140 pc = { "MissingPunctuation", "darkcyan" },
141 pd = { "MissingPunctuation", "darkcyan" },
142 ps = { "MissingPunctuation", "darkcyan" },
143 pe = { "MissingPunctuation", "darkcyan" },
144 pi = { "MissingPunctuation", "darkcyan" },
145 pf = { "MissingPunctuation", "darkcyan" },
146 po = { "MissingPunctuation", "darkcyan" },
147 sm = { "MissingLowercase", "darkmagenta" },
148 sc = { "MissingLowercase", "darkyellow" },
149 sk = { "MissingLowercase", "darkyellow" },
150 so = { "MissingLowercase", "darkyellow" },
151}
152
153table.setmetatableindex(mapping, { "MissingUnknown", "darkgray" })
154
155checkers.mapping = mapping
156
157
158
159
160
161function checkers.placeholder(font,char,category)
162 local category = category or chardata[char].category or "lu"
163 local fakedata = mapping[category] or mapping.lu
164 local tfmdata = fontdata[font]
165 local units = tfmdata.parameters.units or 1000
166 local slant = (tfmdata.parameters.slant or 0)/65536
167 local scale = units/1000
168 local rawdata = tfmdata.shared and tfmdata.shared.rawdata
169 local weight = (rawdata and rawdata.metadata and rawdata.metadata.pfmweight or 400)/400
170
171
172
173 local specification = {
174 code = "MissingGlyph",
175 scale = scale,
176 slant = slant,
177 weight = weight,
178 namespace = font,
179 shapes = { { shape = fakedata[1], color = fakedata[2] } },
180 }
181 fonts.helpers.setmetaglyphs("missing", font, char, specification)
182end
183
184function checkers.missing(head)
185 local lastfont = nil
186 local characters = nil
187 local found = nil
188 local addplaceholder = checkers.placeholder
189 if actions.replace or actions.decompose then
190 for n, char, font in nextglyph, head do
191 if font ~= lastfont then
192 lastfont = font
193 characters = fontcharacters[font]
194 end
195 if font > 0 and not characters[char] and is_character[chardata[char].category or "unknown"] then
196 if actions.decompose then
197 local c = chardata[char]
198 if c then
199 local s = c.specials
200 if s and (s[1] == "char" or s[1] == "with") then
201 local l = #s
202 if l > 2 then
203
204 local okay = true
205 for i=2,l do
206 if not characters[s[i]] then
207 okay = false
208 break
209 end
210 end
211 if okay then
212
213 local o = n
214 onetimemessage(font,char,"missing (decomposed)")
215 setchar(n,s[l])
216 for i=l-1,2,-1 do
217 head, o = insertnodebefore(head,o,copy_node(n))
218 setchar(o,s[i])
219 end
220 goto DONE
221 end
222 end
223 end
224 end
225 end
226 if actions.replace then
227 onetimemessage(font,char,"missing (replaced)")
228 local f, c = addplaceholder(font,char)
229 if f and c then
230 setchar(n, c, f)
231 end
232 goto DONE
233 end
234 if actions.remove then
235 onetimemessage(font,char,"missing (deleted)")
236 if not found then
237 found = { n }
238 else
239 found[#found+1] = n
240 end
241 goto DONE
242 end
243 onetimemessage(font,char,"missing (checked)")
244 ::DONE::
245 end
246 end
247 end
248 if found then
249 for i=1,#found do
250 head = remove_node(head,found[i],true)
251 end
252 end
253 return head
254end
255
256local relevant = {
257 "missing (decomposed)",
258 "missing (replaced)",
259 "missing (deleted)",
260 "missing (checked)",
261 "missing",
262}
263
264local function getmissing(id)
265 if id then
266 local list = getmissing(currentfont())
267 if list then
268 local _, list = next(getmissing(currentfont()))
269 return list
270 else
271 return { }
272 end
273 else
274 local t = { }
275 for id, d in next, fontdata do
276 local shared = d.shared
277 local messages = shared and shared.messages
278 if messages then
279 local filename = d.properties.filename
280 if not filename then
281 filename = tostring(d)
282 end
283 local tf = t[filename] or { }
284 for i=1,#relevant do
285 local tm = messages[relevant[i]]
286 if tm then
287 for k, v in next, tm do
288 tf[k] = (tf[k] or 0) + v
289 end
290 end
291 end
292 if next(tf) then
293 t[filename] = tf
294 end
295 end
296 end
297 local l = { }
298 for k, v in next, t do
299 l[k] = sortedkeys(v)
300 end
301 return l, t
302 end
303end
304
305checkers.getmissing = getmissing
306
307do
308
309 local reported = true
310
311 callback.register("glyph_not_found",function(font,char)
312 if font > 0 then
313 if char > 0 then
314 onetimemessage(font,char,"missing")
315 else
316
317 end
318 elseif not reported then
319 report("nullfont is used, maybe no bodyfont is defined")
320 reported = true
321 end
322 end)
323
324 local loaded = false
325
326 trackers.register("fonts.missing", function(v)
327 if v then
328 enableaction("processors","fonts.checkers.missing")
329 if v == true then
330 actions = { check = true }
331 else
332 actions = utilities.parsers.settings_to_set(v)
333 if not loaded and actions.replace then
334 metapost.simple("simplefun",'loadfile("mp-miss.mpxl");')
335 loaded = true
336 end
337 end
338 else
339 disableaction("processors","fonts.checkers.missing")
340 actions = false
341 end
342 end)
343
344 logs.registerfinalactions(function()
345 local collected, details = getmissing()
346 if next(collected) then
347 for filename, list in sortedhash(details) do
348 logs.startfilelogging(report,"missing characters",filename)
349 for u, v in sortedhash(list) do
350 report("%4i %U %c %s",v,u,u,chardata[u].description)
351 end
352 logs.stopfilelogging()
353 end
354 if logs.loggingerrors() then
355 for filename, list in sortedhash(details) do
356 logs.starterrorlogging(report,"missing characters",filename)
357 for u, v in sortedhash(list) do
358 report("%4i %U %c %s",v,u,u,chardata[u].description)
359 end
360 logs.stoperrorlogging()
361 end
362 end
363 end
364 end)
365
366end
367
368
369
370local function expandglyph(characters,index,done)
371 done = done or { }
372 if not done[index] then
373 local data = characters[index]
374 if data then
375 done[index] = true
376 local d = fastcopy(data)
377 local n = d.next
378 if n then
379 d.next = expandglyph(characters,n,done)
380 end
381 local p = d.parts
382 if p then
383 for i=1,#p do
384 local pi = p[i]
385 pi.glyph = expandglyph(characters,pi.glyph,done)
386 end
387 end
388 return d
389 end
390 end
391end
392
393helpers.expandglyph = expandglyph
394
395
396
397local dummyzero = {
398
399
400
401 commands = { { "special", "" } },
402}
403
404local function adddummysymbols(tfmdata)
405 local characters = tfmdata.characters
406 if not characters[0] then
407 characters[0] = dummyzero
408 end
409
410
411
412end
413
414local dummies_specification = {
415 name = "dummies",
416 description = "dummy symbols",
417 default = true,
418 manipulators = {
419 base = adddummysymbols,
420 node = adddummysymbols,
421 }
422}
423
424registerotffeature(dummies_specification)
425registerafmfeature(dummies_specification)
426
427
428
429local function addvisualspace(tfmdata)
430 local spacechar = tfmdata.characters[32]
431 if spacechar and not spacechar.commands then
432 local w = spacechar.width
433 local h = tfmdata.parameters.xheight
434
435 local c = {
436 width = w,
437 commands = { { "rule", h, w } },
438
439 }
440 local u = addprivate(tfmdata, "visualspace", c)
441 end
442end
443
444local visualspace_specification = {
445 name = "visualspace",
446 description = "visual space",
447 default = true,
448 manipulators = {
449 base = addvisualspace,
450 node = addvisualspace,
451 }
452}
453
454registerotffeature(visualspace_specification)
455registerafmfeature(visualspace_specification)
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502local function fixdot(tfmdata)
503 local self = tfmdata.characters[0xB7]
504 local period = tfmdata.characters[0x2E]
505 local hyphen = tfmdata.characters[0x2D]
506 if self and period and hyphen then
507 local swidth = self.width
508 local pwidth = period.width
509 if swidth > pwidth then
510 local sheight = self.height
511 local hheight = hyphen.height
512 self.advance = swidth
513 self.width = pwidth
514 self.commands = { { "offset", (pwidth-swidth)/2, (hheight-sheight)/2, 0xB7 } }
515 end
516 end
517end
518
519local dot_specification = {
520 name = "fixdot",
521 description = "fix dot",
522 default = true,
523 manipulators = {
524 base = fixdot,
525 node = fixdot,
526 }
527}
528
529registerotffeature(dot_specification)
530registerafmfeature(dot_specification)
531
532do
533
534
535 local reference = 88
536 local mapping = { ss = "sans", rm = "serif", tt = "mono" }
537 local order = { "sans", "serif", "mono" }
538 local fallbacks = { sans = { }, serif = { }, mono = { } }
539
540 local function locate(fallbacks,n,f,c)
541 for i=1,#fallbacks do
542 local id = fallbacks[i]
543 if type(id) == "string" then
544 local fid = definers.define { name = id }
545 report("using fallback font %!font:name! (id: %i)",fid,fid)
546 fallbacks[i] = fid
547 id = fid
548 end
549 if type(id) == "number" then
550 local cid = fontcharacters[id]
551 if cid[c] then
552 local fc = fontcharacters[f]
553 local sc = (fc[reference].height / cid[reference].height) * (n.scale or 1000)
554 report("character %C in font %!font:name! (id: %i) is taken from fallback font %!font:name! (id: %i)",c,f,f,id,id)
555 return { id, sc }
556 end
557 end
558 end
559 return false
560 end
561
562 local cache = table.setmetatableindex("table")
563
564
565
566
567
568 callbacks.register("missing_character", function(where,n,f,c)
569 local cached = cache[f]
570 local found = cached[c]
571 if found == nil then
572
573 local metadata = fontdata[f].shared
574 if metadata then
575 metadata = metadata.rawdata
576 if metadata then
577 metadata = metadata.metadata
578 if metadata then
579 if metadata.monospaced then
580 found = locate(fallbacks.mono,n,f,c)
581 if found then
582 cached[c] = found
583 goto done
584 end
585 end
586 local fn = lower(metadata.fullname or fonts.helpers.name(f))
587 for i=1,3 do
588 local o = order[i]
589 if find(fn,o) then
590 found = locate(fallbacks[o],n,f,c)
591 if found then
592 cached[c] = found
593 goto done
594 end
595 end
596 end
597 end
598 end
599 end
600 found = locate(fallbacks[mapping[getmacro("fontstyle")] or "mono"],n,f,c)
601 if found then
602 cached[c] = found
603 goto done
604 end
605 end
606 ::done::
607 if found then
608 n.font = found[1]
609 n.scale = found[2]
610 end
611 end,"report details about a missing character")
612
613 function definers.registerfallbackfont(style,list)
614 local l = fallbacks[style]
615 if l then
616 for s in gmatch(list,"[^, ]+") do
617 if not contains(l,s) then
618 l[#l+1] = s
619 end
620 end
621 end
622 end
623
624 implement {
625 name = "registerfallbackfont",
626 public = true,
627 protected = true,
628 arguments = { "optional", "optional" },
629 actions = definers.registerfallbackfont,
630 }
631
632end
633 |