font-chk.lmt /size: 20 Kb    last modification: 2023-12-21 09:44
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-- This is kind of old but it makes no real sense to upgrade it to for instance
10-- using delayed type 3 fonts in order to be lean and mean and cut'n'paste
11-- compliant. When this kicks one needs to fix the choice of fonts anyway! So,
12-- instead we just keep the method we use but slightly adapted to the backend
13-- of lmtx.
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           = 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-- to tfmdata.properties ?
88
89local function onetimemessage(font,char,message) -- char == false returns table
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-- We provide access by (private) name for tracing purposes. We also need to make
158-- sure the dimensions are known at the lua and tex end. For previous variants see
159-- the mkiv files or older lmtx files. I decided to just drop the old stuff here.
160
161function checkers.placeholder(font,char,category)
162    local category = category or chardata[char].category or "lu" -- todo: unknown
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--     if slant then
171--         slant = 0.2
172--     end
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 -- so we can overload
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 -- with added
201                            local l = #s
202                            if l > 2 then
203                                -- check first
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                                    -- we work backward
213                                    local o = n -- we're not supposed to change n (lua 5.4+)
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                -- we have a special case
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-- for the moment here
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-- should not be needed as we add .notdef in the engine
396
397local dummyzero = {
398 -- width    = 0,
399 -- height   = 0,
400 -- depth    = 0,
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 -- if not characters[1] then
410 --     characters[1] = dummyzero -- test only
411 -- end
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     -- local h = tfmdata.parameters.xheight / 4 -- could be "visualspace=large" or so
435        local c = {
436            width    = w,
437            commands = { { "rule", h, w } },
438         -- commands = { { "line", w, 5*h, h } },
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-- local function adddots(tfmdata,unicode)
458--     if not tfmdata.characters[unicode] then
459--         local period   = tfmdata.characters[0x2E]
460--         local original = period.height
461--         local height   = tfmdata.parameters.xheight/2
462--         tfmdata.characters[unicode] = {
463--             width    = period.width,
464--             height   = height + original,
465--             unicode  = unicode,
466--             commands = { { "offset", 0, height, 0x2E } },
467--         }
468--    end
469-- end
470--
471-- local function addpunctuationdot(tfmdata)
472--     adddots(tfmdata,0x2027) -- hyphenation period : in lucida dejavusans xcharter
473--     adddots(tfmdata,0x2E31) -- word separator middle dot
474-- end
475--
476-- local punctuationdot_specification = {
477--     name        = "extradots",
478--     description = "extra dots",
479--     default     = true,
480--     manipulators = {
481--         base = addpunctuationdot,
482--         node = addpunctuationdot,
483--     }
484-- }
485--
486-- registerotffeature(punctuationdot_specification)
487-- registerafmfeature(punctuationdot_specification)
488
489-- local function fixdot(tfmdata)
490--     if tfmdata.characters[0xB7] then
491--         local period   = tfmdata.characters[0x2E]
492--         local self     = tfmdata.characters[0xB7]
493--         local original = period.height
494--         local height   = tfmdata.parameters.xheight/2
495--         self.advance  = self.width
496--         self.width    = period.width
497--         self.height   = height + original
498--         self.commands = { { "offset", 0, height, 0x2E } }
499--    end
500-- end
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 -- maybe also ht here
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 -- string.byte("X")
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    callbacks.register("missing_character", function(n,f,c)
565        local cached = cache[f]
566        local found  = cached[c]
567        if found == nil then
568            -- we can use fonts.helpers.name(f) but we need the monospace flag anyway so:
569            local metadata = fontdata[f].shared
570            if metadata then
571                metadata = metadata.rawdata
572                if metadata then
573                    metadata = metadata.metadata
574                    if metadata then
575                        if metadata.monospaced then
576                            found = locate(fallbacks.mono,n,f,c)
577                            if found then
578                                cached[c] = found
579                                goto done
580                            end
581                        end
582                        local fn = lower(metadata.fullname)
583                        for i=1,3 do
584                            local o = order[i]
585                            if find(fn,o) then
586                                found = locate(fallbacks[o],n,f,c)
587                                if found then
588                                    cached[c] = found
589                                    goto done
590                                end
591                            end
592                        end
593                    end
594                end
595            end
596            found = locate(fallbacks[mapping[getmacro("fontstyle")] or "mono"],n,f,c)
597            if found then
598                cached[c] = found
599                goto done
600            end
601        end
602      ::done::
603        if found then
604            n.font  = found[1]
605            n.scale = found[2]
606        end
607    end,"report details about a missing character")
608
609    function definers.registerfallbackfont(style,list)
610        local l = fallbacks[style]
611        if l then
612            for s in gmatch(list,"[^, ]+") do
613                if not contains(l,s) then
614                    l[#l+1] = s
615                end
616            end
617        end
618    end
619
620    implement {
621        name      = "registerfallbackfont",
622        public    = true,
623        protected = true,
624        arguments = { "optional", "optional" },
625        actions   = definers.registerfallbackfont,
626    }
627
628end
629