if not modules then modules = { } end modules ['font-chk'] = { version = 1.001, comment = "companion to font-ini.mkiv", author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", copyright = "PRAGMA ADE / ConTeXt Development Team", license = "see context related readme files" } -- This is kind of old but it makes no real sense to upgrade it to for instance -- using delayed type 3 fonts in order to be lean and mean and cut'n'paste -- compliant. When this kicks one needs to fix the choice of fonts anyway! So, -- instead we just keep the method we use but slightly adapted to the backend -- of lmtx. local type, next = type, next local find, lower, gmatch = string.find, string.lower, string.gmatch local floor = math.floor local context = context local formatters = string.formatters local fastcopy = table.fastcopy local sortedkeys = table.sortedkeys local sortedhash = table.sortedhash local contains = table.contains local report = logs.reporter("fonts") local report_checking = logs.reporter("fonts","checking") local allocate = utilities.storage.allocate local getmacro = tokens.getters.macro local fonts = fonts fonts.checkers = fonts.checkers or { } local checkers = fonts.checkers local fonthashes = fonts.hashes local fontdata = fonthashes.identifiers local fontcharacters = fonthashes.characters local currentfont = font.current local definers = fonts.definers local helpers = fonts.helpers local addprivate = helpers.addprivate local hasprivate = helpers.hasprivate local getprivateslot = helpers.getprivateslot local getprivatecharornode = helpers.getprivatecharornode local otffeatures = fonts.constructors.features.otf local afmfeatures = fonts.constructors.features.afm local registerotffeature = otffeatures.register local registerafmfeature = afmfeatures.register local is_character = characters.is_character local chardata = characters.data local tasks = nodes.tasks local enableaction = tasks.enableaction local disableaction = tasks.disableaction local implement = interfaces.implement local glyph_code = nodes.nodecodes.glyph local hpack_node = nodes.hpack local nuts = nodes.nuts local tonut = nuts.tonut local isglyph = nuts.isglyph local setchar = nuts.setchar local nextglyph = nuts.traversers.glyph local remove_node = nuts.remove local insertnodeafter = nuts.insertafter local insertnodebefore = nuts.insertbefore local copy_node = nuts.copy local actions = false -- to tfmdata.properties ? local function onetimemessage(font,char,message) -- char == false returns table local tfmdata = fontdata[font] local shared = tfmdata.shared if not shared then shared = { } tfmdata.shared = shared end local messages = shared.messages if not messages then messages = { } shared.messages = messages end local category = messages[message] if not category then category = { } messages[message] = category end if char == false then return sortedkeys(category), category end local cc = category[char] if not cc then report_checking("char %C in font %a with id %a: %s",char,tfmdata.properties.fullname,font,message) category[char] = 1 else category[char] = cc + 1 end end fonts.loggers.onetimemessage = onetimemessage local fakes = { MissingLowercase = { width = .45, height = .55, depth = .20 }, MissingUppercase = { width = .65, height = .70, depth = .25 }, MissingMark = { width = .15, height = .70, depth = -.50 }, MissingPunctuation = { width = .15, height = .55, depth = .20 }, MissingUnknown = { width = .45, height = .20, depth = 0 }, } local mapping = allocate { lu = { "MissingUppercase", "darkred" }, ll = { "MissingLowercase", "darkred" }, lt = { "MissingUppercase", "darkred" }, lm = { "MissingLowercase", "darkred" }, lo = { "MissingLowercase", "darkred" }, mn = { "MissingMark", "darkgreen" }, mc = { "MissingMark", "darkgreen" }, me = { "MissingMark", "darkgreen" }, nd = { "MissingLowercase", "darkblue" }, nl = { "MissingLowercase", "darkblue" }, no = { "MissingLowercase", "darkblue" }, pc = { "MissingPunctuation", "darkcyan" }, pd = { "MissingPunctuation", "darkcyan" }, ps = { "MissingPunctuation", "darkcyan" }, pe = { "MissingPunctuation", "darkcyan" }, pi = { "MissingPunctuation", "darkcyan" }, pf = { "MissingPunctuation", "darkcyan" }, po = { "MissingPunctuation", "darkcyan" }, sm = { "MissingLowercase", "darkmagenta" }, sc = { "MissingLowercase", "darkyellow" }, sk = { "MissingLowercase", "darkyellow" }, so = { "MissingLowercase", "darkyellow" }, } table.setmetatableindex(mapping, { "MissingUnknown", "darkgray" }) checkers.mapping = mapping -- We provide access by (private) name for tracing purposes. We also need to make -- sure the dimensions are known at the lua and tex end. For previous variants see -- the mkiv files or older lmtx files. I decided to just drop the old stuff here. function checkers.placeholder(font,char,category) local category = category or chardata[char].category or "lu" -- todo: unknown local fakedata = mapping[category] or mapping.lu local tfmdata = fontdata[font] local units = tfmdata.parameters.units or 1000 local slant = (tfmdata.parameters.slant or 0)/65536 local scale = units/1000 local rawdata = tfmdata.shared and tfmdata.shared.rawdata local weight = (rawdata and rawdata.metadata and rawdata.metadata.pfmweight or 400)/400 -- if slant then -- slant = 0.2 -- end local specification = { code = "MissingGlyph", scale = scale, slant = slant, weight = weight, namespace = font, shapes = { { shape = fakedata[1], color = fakedata[2] } }, } fonts.helpers.setmetaglyphs("missing", font, char, specification) end function checkers.missing(head) local lastfont = nil local characters = nil local found = nil local addplaceholder = checkers.placeholder -- so we can overload if actions.replace or actions.decompose then for n, char, font in nextglyph, head do if font ~= lastfont then lastfont = font characters = fontcharacters[font] end if font > 0 and not characters[char] and is_character[chardata[char].category or "unknown"] then if actions.decompose then local c = chardata[char] if c then local s = c.specials if s and (s[1] == "char" or s[1] == "with") then -- with added local l = #s if l > 2 then -- check first local okay = true for i=2,l do if not characters[s[i]] then okay = false break end end if okay then -- we work backward local o = n -- we're not supposed to change n (lua 5.4+) onetimemessage(font,char,"missing (decomposed)") setchar(n,s[l]) for i=l-1,2,-1 do head, o = insertnodebefore(head,o,copy_node(n)) setchar(o,s[i]) end goto DONE end end end end end if actions.replace then onetimemessage(font,char,"missing (replaced)") local f, c = addplaceholder(font,char) if f and c then setchar(n, c, f) end goto DONE end if actions.remove then onetimemessage(font,char,"missing (deleted)") if not found then found = { n } else found[#found+1] = n end goto DONE end onetimemessage(font,char,"missing (checked)") ::DONE:: end end end if found then for i=1,#found do head = remove_node(head,found[i],true) end end return head end local relevant = { "missing (decomposed)", "missing (replaced)", "missing (deleted)", "missing (checked)", "missing", } local function getmissing(id) if id then local list = getmissing(currentfont()) if list then local _, list = next(getmissing(currentfont())) return list else return { } end else local t = { } for id, d in next, fontdata do local shared = d.shared local messages = shared and shared.messages if messages then local filename = d.properties.filename if not filename then filename = tostring(d) end local tf = t[filename] or { } for i=1,#relevant do local tm = messages[relevant[i]] if tm then for k, v in next, tm do tf[k] = (tf[k] or 0) + v end end end if next(tf) then t[filename] = tf end end end local l = { } for k, v in next, t do l[k] = sortedkeys(v) end return l, t end end checkers.getmissing = getmissing do local reported = true callback.register("glyph_not_found",function(font,char) if font > 0 then if char > 0 then onetimemessage(font,char,"missing") else -- we have a special case end elseif not reported then report("nullfont is used, maybe no bodyfont is defined") reported = true end end) local loaded = false trackers.register("fonts.missing", function(v) if v then enableaction("processors","fonts.checkers.missing") if v == true then actions = { check = true } else actions = utilities.parsers.settings_to_set(v) if not loaded and actions.replace then metapost.simple("simplefun",'loadfile("mp-miss.mpxl");') loaded = true end end else disableaction("processors","fonts.checkers.missing") actions = false end end) logs.registerfinalactions(function() local collected, details = getmissing() if next(collected) then for filename, list in sortedhash(details) do logs.startfilelogging(report,"missing characters",filename) for u, v in sortedhash(list) do report("%4i %U %c %s",v,u,u,chardata[u].description) end logs.stopfilelogging() end if logs.loggingerrors() then for filename, list in sortedhash(details) do logs.starterrorlogging(report,"missing characters",filename) for u, v in sortedhash(list) do report("%4i %U %c %s",v,u,u,chardata[u].description) end logs.stoperrorlogging() end end end end) end -- for the moment here local function expandglyph(characters,index,done) done = done or { } if not done[index] then local data = characters[index] if data then done[index] = true local d = fastcopy(data) local n = d.next if n then d.next = expandglyph(characters,n,done) end local p = d.parts if p then for i=1,#p do local pi = p[i] pi.glyph = expandglyph(characters,pi.glyph,done) end end return d end end end helpers.expandglyph = expandglyph -- should not be needed as we add .notdef in the engine local dummyzero = { -- width = 0, -- height = 0, -- depth = 0, commands = { { "special", "" } }, } local function adddummysymbols(tfmdata) local characters = tfmdata.characters if not characters[0] then characters[0] = dummyzero end -- if not characters[1] then -- characters[1] = dummyzero -- test only -- end end local dummies_specification = { name = "dummies", description = "dummy symbols", default = true, manipulators = { base = adddummysymbols, node = adddummysymbols, } } registerotffeature(dummies_specification) registerafmfeature(dummies_specification) -- local function addvisualspace(tfmdata) local spacechar = tfmdata.characters[32] if spacechar and not spacechar.commands then local w = spacechar.width local h = tfmdata.parameters.xheight -- local h = tfmdata.parameters.xheight / 4 -- could be "visualspace=large" or so local c = { width = w, commands = { { "rule", h, w } }, -- commands = { { "line", w, 5*h, h } }, } local u = addprivate(tfmdata, "visualspace", c) end end local visualspace_specification = { name = "visualspace", description = "visual space", default = true, manipulators = { base = addvisualspace, node = addvisualspace, } } registerotffeature(visualspace_specification) registerafmfeature(visualspace_specification) -- local function adddots(tfmdata,unicode) -- if not tfmdata.characters[unicode] then -- local period = tfmdata.characters[0x2E] -- local original = period.height -- local height = tfmdata.parameters.xheight/2 -- tfmdata.characters[unicode] = { -- width = period.width, -- height = height + original, -- unicode = unicode, -- commands = { { "offset", 0, height, 0x2E } }, -- } -- end -- end -- -- local function addpunctuationdot(tfmdata) -- adddots(tfmdata,0x2027) -- hyphenation period : in lucida dejavusans xcharter -- adddots(tfmdata,0x2E31) -- word separator middle dot -- end -- -- local punctuationdot_specification = { -- name = "extradots", -- description = "extra dots", -- default = true, -- manipulators = { -- base = addpunctuationdot, -- node = addpunctuationdot, -- } -- } -- -- registerotffeature(punctuationdot_specification) -- registerafmfeature(punctuationdot_specification) -- local function fixdot(tfmdata) -- if tfmdata.characters[0xB7] then -- local period = tfmdata.characters[0x2E] -- local self = tfmdata.characters[0xB7] -- local original = period.height -- local height = tfmdata.parameters.xheight/2 -- self.advance = self.width -- self.width = period.width -- self.height = height + original -- self.commands = { { "offset", 0, height, 0x2E } } -- end -- end local function fixdot(tfmdata) local self = tfmdata.characters[0xB7] local period = tfmdata.characters[0x2E] local hyphen = tfmdata.characters[0x2D] if self and period and hyphen then local swidth = self.width local pwidth = period.width if swidth > pwidth then -- maybe also ht here local sheight = self.height local hheight = hyphen.height self.advance = swidth self.width = pwidth self.commands = { { "offset", (pwidth-swidth)/2, (hheight-sheight)/2, 0xB7 } } end end end local dot_specification = { name = "fixdot", description = "fix dot", default = true, manipulators = { base = fixdot, node = fixdot, } } registerotffeature(dot_specification) registerafmfeature(dot_specification) do local reference = 88 -- string.byte("X") local mapping = { ss = "sans", rm = "serif", tt = "mono" } local order = { "sans", "serif", "mono" } local fallbacks = { sans = { }, serif = { }, mono = { } } local function locate(fallbacks,n,f,c) for i=1,#fallbacks do local id = fallbacks[i] if type(id) == "string" then local fid = definers.define { name = id } report("using fallback font %!font:name! (id: %i)",fid,fid) fallbacks[i] = fid id = fid end if type(id) == "number" then local cid = fontcharacters[id] if cid[c] then local fc = fontcharacters[f] local sc = (fc[reference].height / cid[reference].height) * (n.scale or 1000) report("character %C in font %!font:name! (id: %i) is taken from fallback font %!font:name! (id: %i)",c,f,f,id,id) return { id, sc } end end end return false end local cache = table.setmetatableindex("table") callbacks.register("missing_character", function(n,f,c) local cached = cache[f] local found = cached[c] if found == nil then -- we can use fonts.helpers.name(f) but we need the monospace flag anyway so: local metadata = fontdata[f].shared if metadata then metadata = metadata.rawdata if metadata then metadata = metadata.metadata if metadata then if metadata.monospaced then found = locate(fallbacks.mono,n,f,c) if found then cached[c] = found goto done end end local fn = lower(metadata.fullname) for i=1,3 do local o = order[i] if find(fn,o) then found = locate(fallbacks[o],n,f,c) if found then cached[c] = found goto done end end end end end end found = locate(fallbacks[mapping[getmacro("fontstyle")] or "mono"],n,f,c) if found then cached[c] = found goto done end end ::done:: if found then n.font = found[1] n.scale = found[2] end end,"report details about a missing character") function definers.registerfallbackfont(style,list) local l = fallbacks[style] if l then for s in gmatch(list,"[^, ]+") do if not contains(l,s) then l[#l+1] = s end end end end implement { name = "registerfallbackfont", public = true, protected = true, arguments = { "optional", "optional" }, actions = definers.registerfallbackfont, } end