----------------------------------------------------------------------- -- FILE: luaotfload-auxiliary.lua -- DESCRIPTION: part of luaotfload -- AUTHOR: Khaled Hosny, Élie Roux, Philipp Gesang ----------------------------------------------------------------------- local ProvidesLuaModule = { name = "luaotfload-auxiliary", version = "3.00", --TAGVERSION date = "2019-09-13", --TAGDATE description = "luaotfload submodule / auxiliary functions", license = "GPL v2.0" } if luatexbase and luatexbase.provides_module then luatexbase.provides_module (ProvidesLuaModule) end luaotfload = luaotfload or { } local log = luaotfload.log local logreport = log.report local fonthashes = fonts.hashes local encodings = fonts.encodings local identifiers = fonthashes.identifiers local fontnames = fonts.names local fontid = font.id local texsprint = tex.sprint local dofile = dofile local getmetatable = getmetatable local setmetatable = setmetatable local utfcodepoint = utf8.codepoint local stringlower = string.lower local stringformat = string.format local stringgsub = string.gsub local stringbyte = string.byte local stringfind = string.find local tablecopy = table.copy local aux = { } local luaotfload_callbacks = { } ----------------------------------------------------------------------- --- font patches ----------------------------------------------------------------------- --- https://github.com/khaledhosny/luaotfload/issues/54 local function rewrite_fontname(tfmdata, specification) local format = tfmdata.format or tfmdata.properties.format if stringfind (specification, " ") then tfmdata.name = stringformat ("%q", specification) else --- other specs should parse just fine tfmdata.name = specification end end local rewriting = false function aux.start_rewrite_fontname() if rewriting == false then luatexbase.add_to_callback ( "luaotfload.patch_font", rewrite_fontname, "luaotfload.rewrite_fontname") rewriting = true logreport ("log", 1, "aux", "start rewriting tfmdata.name field") end end function aux.stop_rewrite_fontname() if rewriting == true then luatexbase.remove_from_callback ("luaotfload.patch_font", "luaotfload.rewrite_fontname") rewriting = false logreport ("log", 1, "aux", "stop rewriting tfmdata.name field") end end --[[doc-- This sets two dimensions apparently relied upon by the unicode-math package. --doc]]-- local function set_sscale_dimens(fontdata) local resources = fontdata.resources if not resources then return end local mathconstants = resources.MathConstants if not mathconstants then return end local parameters = fontdata.parameters if not parameters then return end --- the default values below are complete crap parameters [10] = mathconstants.ScriptPercentScaleDown or 70 parameters [11] = mathconstants.ScriptScriptPercentScaleDown or 50 end luaotfload_callbacks [#luaotfload_callbacks + 1] = { "patch_font", set_sscale_dimens, "set_sscale_dimens", } local default_units = 1000 --- fontobj -> int local function lookup_units(fontdata) local units = fontdata.units if units and units > 0 then return units end local shared = fontdata.shared if not shared then return default_units end local rawdata = shared.rawdata if not rawdata then return default_units end local metadata = rawdata.metadata if not metadata then return default_units end local capheight = metadata.capheight if not capheight then return default_units end local units = metadata.units or fontdata.units if not units or units == 0 then return default_units end return units end --[[doc-- This callback corrects some values of the Cambria font. --doc]]-- --- fontobj -> unit local function patch_cambria_domh(fontdata) local mathconstants = fontdata.MathConstants if mathconstants and fontdata.psname == "CambriaMath" then --- my test Cambria has 2048 local units = fontdata.units or lookup_units(fontdata) local sz = fontdata.parameters.size or fontdata.size local mh = 2800 / units * sz if mathconstants.DisplayOperatorMinHeight < mh then mathconstants.DisplayOperatorMinHeight = mh end end end luaotfload_callbacks [#luaotfload_callbacks + 1] = { "patch_font", patch_cambria_domh, "patch_cambria_domh", } --[[doc-- Add missing field to fonts that lack it. Addresses issue https://github.com/lualatex/luaotfload/issues/253 This is considered a hack, especially since importing the unicode-math package fixes the problem quite nicely. --doc]]-- --- fontobj -> unit local function fixup_fontdata(data) local t = type (data) --- Some OT fonts like Libertine R lack the resources table, causing --- the fontloader to nil-index. if t == "table" then if data and not data.resources then data.resources = { } end end end luaotfload_callbacks [#luaotfload_callbacks + 1] = { "patch_font_unsafe", fixup_fontdata, "fixup_fontdata", } --[[doc-- Comment from fontspec: “Here we patch fonts tfm table to emulate \XeTeX's \cs{fontdimen8}, which stores the caps-height of the font. (Cf.\ \cs{fontdimen5} which stores the x-height.) Falls back to measuring the glyph if the font doesn't contain the necessary information. This needs to be extended for fonts that don't contain an `X'.” --doc]]-- local capheight_reference_chars = { "X", "M", "Ж", "ξ", } local capheight_reference_codepoints do capheight_reference_codepoints = { } for i = 1, #capheight_reference_chars do local chr = capheight_reference_chars [i] capheight_reference_codepoints [i] = utfcodepoint (chr) end end local function determine_capheight(fontdata) local parameters = fontdata.parameters if not parameters then return false end local characters = fontdata.characters if not characters then return false end --- Pretty simplistic but it does return *some* value for most fonts; --- we could also refine the approach to return some kind of average --- of all capital letters or a user-provided subset. for i = 1, #capheight_reference_codepoints do local refcp = capheight_reference_codepoints [i] local refchar = characters [refcp] if refchar then logreport ("both", 4, "aux", "picked height of character ‘%s’ (U+%d) as \\fontdimen8 \z candidate", capheight_reference_chars [i], refcp) return refchar.height end end return false end local function query_ascender(fontdata) local parameters = fontdata.parameters if not parameters then return false end local ascender = parameters.ascender if ascender then return ascender --- pre-scaled end local shared = fontdata.shared if not shared then return false end local rawdata = shared.rawdata if not rawdata then return false end local metadata = rawdata.metadata if not metadata then return false end ascender = metadata.ascender if not ascender then return false end local size = parameters.size if not size then return false end local units = lookup_units (fontdata) if not units or units == 0 then return false end return ascender * size / units --- scaled end local function query_capheight(fontdata) local parameters = fontdata.parameters if not parameters then return false end local shared = fontdata.shared if not shared then return false end local rawdata = shared.rawdata if not rawdata then return false end local metadata = rawdata.metadata if not metadata then return false end local capheight = metadata.capheight if not capheight then return false end local size = parameters.size if not size then return false end local units = lookup_units (fontdata) if not units or units == 0 then return false end return capheight * size / units end local function query_fontdimen8(fontdata) local parameters = fontdata.parameters if not parameters then return false end local fontdimen8 = parameters [8] if fontdimen8 then return fontdimen8 end return false end local function caphtfmt(ref, ht) if not ht then return "" end if not ref then return tostring (ht) end return stringformat ("%s(δ=%s)", ht, ht - ref) end local function set_capheight(fontdata) if not fontdata then logreport ("both", 0, "aux", "error: set_capheight() received garbage") return end local capheight_dimen8 = query_fontdimen8 (fontdata) local capheight_alleged = query_capheight (fontdata) local capheight_ascender = query_ascender (fontdata) local capheight_measured = determine_capheight (fontdata) logreport ("term", 4, "aux", "capht: param[8]=%s advertised=%s ascender=%s measured=%s", tostring (capheight_dimen8), caphtfmt (capheight_dimen8, capheight_alleged), caphtfmt (capheight_dimen8, capheight_ascender), caphtfmt (capheight_dimen8, capheight_measured)) if capheight_dimen8 then --- nothing to do return end local capheight = capheight_alleged or capheight_ascender or capheight_measured if capheight then fontdata.parameters [8] = capheight end end luaotfload_callbacks [#luaotfload_callbacks + 1] = { "patch_font", set_capheight, "set_capheight", } -- Of course there are also fonts with no sensible x-height, so let's add a -- fallback there: local function set_xheight(tfmdata) local parameters = tfmdata.parameters if not parameters then return end if not ((parameters.x_height or parameters[5] or 0) == 0) then return end if tfmdata.characters and tfmdata.characters[120] then parameters.x_height = tfmdata.characters[120].height else parameters.x_height = (parameters.ascender or 0)/2 end end luaotfload_callbacks [#luaotfload_callbacks + 1] = { "patch_font", set_xheight, "set_xheight", } ----------------------------------------------------------------------- --- glyphs and characters ----------------------------------------------------------------------- --- int -> int -> bool function aux.font_has_glyph(font_id, codepoint) local fontdata = fonts.hashes.identifiers[font_id] if fontdata then if fontdata.characters[codepoint] ~= nil then return true end end return false end --- undocumented local function raw_slot_of_name(font_id, glyphname) local fontdata = font.fonts[font_id] if fontdata.type == "virtual" then --- get base font for glyph idx local codepoint = encodings.agl.unicodes[glyphname] local glyph = fontdata.characters[codepoint] if fontdata.characters[codepoint] then return codepoint end end return false end --[[doc-- This one is approximately “name_to_slot” from the microtype package; note that it is all about Adobe Glyph names and glyph slots in the font. The names and values may diverge from actual Unicode. http://www.adobe.com/devnet/opentype/archives/glyph.html The “unsafe” switch triggers a fallback lookup in the raw fonts table. As some of the information is stored as references, this may have unpredictable side-effects. --doc]]-- --- int -> string -> bool -> (int | false) function aux.slot_of_name(font_id, glyphname, unsafe) if not font_id or type (font_id) ~= "number" or not glyphname or type (glyphname) ~= "string" then logreport ("both", 0, "aux", "invalid parameters to slot_of_name (%s, %s)", tostring (font_id), tostring (glyphname)) return false end local tfmdata = identifiers [font_id] if not tfmdata then return raw_slot_of_name (font_id, glyphname) end local resources = tfmdata.resources if not resources then return false end local unicodes = resources.unicodes if not unicodes then return false end local unicode = unicodes [glyphname] if unicode then if type (unicode) == "number" then return unicode else return unicode [1] --- for multiple components end end return false end --[[doc-- Inverse of above; not authoritative as to my knowledge the official inverse of the AGL is the AGLFN. Maybe this whole issue should be dealt with in a separate package that loads char-def.lua and thereby solves the problem for the next couple decades. http://partners.adobe.com/public/developer/en/opentype/aglfn13.txt --doc]]-- local indices --- int -> (string | false) function aux.name_of_slot(codepoint) if not codepoint or type (codepoint) ~= "number" then logreport ("both", 0, "aux", "invalid parameters to name_of_slot (%s)", tostring (codepoint)) return false end if not indices then --- this will load the glyph list local unicodes = encodings.agl.unicodes if not unicodes or not next (unicodes)then logreport ("both", 0, "aux", "name_of_slot: failed to load the AGL.") end indices = table.swapped (unicodes) end local glyphname = indices [codepoint] if glyphname then return glyphname end return false end ----------------------------------------------------------------------- --- features / scripts / languages ----------------------------------------------------------------------- --- lots of arrowcode ahead local function get_features(tfmdata) local resources = tfmdata.resources if not resources then return false end local features = resources.features if not features then return false end return features end --[[doc-- This function, modeled after “check_script()” from fontspec, returns true if in the given font, the script “asked_script” is accounted for in at least one feature. --doc]]-- --- int -> string -> bool function aux.provides_script(font_id, asked_script) if not font_id or type (font_id) ~= "number" or not asked_script or type (asked_script) ~= "string" then logreport ("both", 0, "aux", "invalid parameters to provides_script(%s, %s)", tostring (font_id), tostring (asked_script)) return false end asked_script = stringlower(asked_script) if font_id and font_id > 0 then local tfmdata = identifiers[font_id] if not tfmdata then return false end local features = get_features (tfmdata) if features == false then logreport ("log", 1, "aux", "font no %d lacks a features table", font_id) return false end for method, featuredata in next, features do --- where method: "gpos" | "gsub" for feature, data in next, featuredata do if data[asked_script] then logreport ("log", 1, "aux", "font no %d (%s) defines feature %s for script %s", font_id, fontname, feature, asked_script) return true end end end logreport ("log", 0, "aux", "font no %d (%s) defines no feature for script %s", font_id, fontname, asked_script) end logreport ("log", 0, "aux", "no font with id %d", font_id) return false end --[[doc-- This function, modeled after “check_language()” from fontspec, returns true if in the given font, the language with tage “asked_language” is accounted for in the script with tag “asked_script” in at least one feature. --doc]]-- --- int -> string -> string -> bool function aux.provides_language(font_id, asked_script, asked_language) if not font_id or type (font_id) ~= "number" or not asked_script or type (asked_script) ~= "string" or not asked_language or type (asked_language) ~= "string" then logreport ("both", 0, "aux", "invalid parameters to provides_language(%s, %s, %s)", tostring (font_id), tostring (asked_script), tostring (asked_language)) return false end asked_script = stringlower(asked_script) asked_language = stringlower(asked_language) if font_id and font_id > 0 then local tfmdata = identifiers[font_id] if not tfmdata then return false end local features = get_features (tfmdata) if features == false then logreport ("log", 1, "aux", "font no %d lacks a features table", font_id) return false end for method, featuredata in next, features do --- where method: "gpos" | "gsub" for feature, data in next, featuredata do local scriptdata = data[asked_script] if scriptdata and scriptdata[asked_language] then logreport ("log", 1, "aux", "font no %d (%s) defines feature %s " .. "for script %s with language %s", font_id, fontname, feature, asked_script, asked_language) return true end end end logreport ("log", 0, "aux", "font no %d (%s) defines no feature " .. "for script %s with language %s", font_id, fontname, asked_script, asked_language) end logreport ("log", 0, "aux", "no font with id %d", font_id) return false end --[[doc-- We strip the syntax elements from feature definitions (shouldn’t actually be there in the first place, but who cares ...) --doc]]-- local lpeg = require"lpeg" local C, P, S = lpeg.C, lpeg.P, lpeg.S local lpegmatch = lpeg.match local sign = S"+-" local rhs = P"=" * P(1)^0 * P(-1) local strip_garbage = sign^-1 * C((1 - rhs)^1) --s = "+foo" --> foo --ss = "-bar" --> bar --sss = "baz" --> baz --t = "foo=bar" --> foo --tt = "+bar=baz" --> bar --ttt = "-baz=true" --> baz -- --print(lpeg.match(strip_garbage, s)) --print(lpeg.match(strip_garbage, ss)) --print(lpeg.match(strip_garbage, sss)) --print(lpeg.match(strip_garbage, t)) --print(lpeg.match(strip_garbage, tt)) --print(lpeg.match(strip_garbage, ttt)) --[[doc-- This function, modeled after “check_feature()” from fontspec, returns true if in the given font, the language with tag “asked_language” is accounted for in the script with tag “asked_script” in feature “asked_feature”. --doc]]-- --- int -> string -> string -> string -> bool function aux.provides_feature(font_id, asked_script, asked_language, asked_feature) if not font_id or type (font_id) ~= "number" or not asked_script or type (asked_script) ~= "string" or not asked_language or type (asked_language) ~= "string" or not asked_feature or type (asked_feature) ~= "string" then logreport ("both", 0, "aux", "invalid parameters to provides_feature(%s, %s, %s, %s)", tostring (font_id), tostring (asked_script), tostring (asked_language), tostring (asked_feature)) return false end asked_script = stringlower(asked_script) asked_language = stringlower(asked_language) asked_feature = lpegmatch(strip_garbage, asked_feature) if font_id > 0 then local tfmdata = identifiers[font_id] if not tfmdata then return false end local features = get_features (tfmdata) if features == false then logreport ("log", 1, "aux", "font no %d lacks a features table", font_id) return false end for method, featuredata in next, features do --- where method: "gpos" | "gsub" local feature = featuredata[asked_feature] if feature then local scriptdata = feature[asked_script] if scriptdata and scriptdata[asked_language] then logreport ("log", 1, "aux", "font no %d (%s) defines feature %s " .. "for script %s with language %s", font_id, fontname, asked_feature, asked_script, asked_language) return true end end end logreport ("log", 0, "aux", "font no %d (%s) does not define feature %s for script %s with language %s", font_id, fontname, asked_feature, asked_script, asked_language) end logreport ("log", 0, "aux", "no font with id %d", font_id) return false end ----------------------------------------------------------------------- --- font dimensions ----------------------------------------------------------------------- --- int -> string -> int local function get_math_dimension(font_id, dimenname) if type(font_id) == "string" then font_id = fontid(font_id) --- safeguard end local fontdata = identifiers[font_id] local mathdata = fontdata.mathparameters if mathdata then return mathdata[dimenname] or 0 end return 0 end aux.get_math_dimension = get_math_dimension --- int -> string -> unit function aux.sprint_math_dimension(font_id, dimenname) if type(font_id) == "string" then font_id = fontid(font_id) end local dim = get_math_dimension(font_id, dimenname) texsprint(luatexbase.catcodetables["latex-package"], dim, "sp") end ----------------------------------------------------------------------- --- extra database functions ----------------------------------------------------------------------- --[====[-- TODO -> port this to new db model --- local directories ------------------------------------------------- --- migrated from luaotfload-database.lua --- https://github.com/lualatex/luaotfload/pull/61#issuecomment-17776975 --- string -> (int * int) local scan_external_dir = function (dir) local old_names, new_names = names.data() if not old_names then old_names = load_names() end new_names = tablecopy(old_names) local n_scanned, n_new = scan_dir(dir, old_names, new_names) --- FIXME --- This doesn’t seem right. If a db update is triggered after this --- point, then the added fonts will be saved along with it -- --- which is not as “temporarily” as it should be. (This should be --- addressed during a refactoring of names_resolve().) names.data = new_names return n_scanned, n_new end aux.scan_external_dir = scan_external_dir --]====]-- function aux.scan_external_dir() print "ERROR: scan_external_dir() is not implemented" end --- db queries -------------------------------------------------------- --- https://github.com/lualatex/luaotfload/issues/74 --- string -> (string * int) local function resolve_fontname(name) local foundname, subfont = luaotfload.resolvers.name { name = name, specification = "name:" .. name, } if foundname then return foundname, subfont end return false, false end aux.resolve_fontname = resolve_fontname --- string list -> (string * int) function aux.resolve_fontlist(names) for n = 1, #names do local foundname, subfont = resolve_fontname(this) if foundname then return foundname, subfont end end return false, false end --- index access ------------------------------------------------------ --- Based on a discussion on the Luatex mailing list: --- http://tug.org/pipermail/luatex/2014-June/004881.html --[[doc-- aux.read_font_index -- Read the names index from the canonical location and return its contents. This does not affect the behavior of Luaotfload: The returned table is independent of what the font resolvers use internally. Access is raw: each call to the function will result in the entire table being re-read from disk. --doc]]-- local load_names = fontnames.load local access_font_index = fontnames.access_font_index function aux.read_font_index() return load_names (true) or { } end --[[doc-- aux.font_index -- Access Luaotfload’s internal database. If the database hasn’t been loaded yet this will cause it to be loaded, with all the possible side-effects like for instance creating the index file if it doesn’t exist, reading all font files, &c. --doc]]-- function aux.font_index() return access_font_index () end --- loaded fonts ------------------------------------------------------ --- just a proof of concept --- fontobj -> string list -> (string list) list local function get_font_data (tfmdata, keys) local acc = {} for n = 1, #keys do acc[n] = tfmdata[keys[n]] or false end return acc end --[[doc-- The next one operates on the fonts.hashes.identifiers table. It returns a list containing tuples of font ids and the contents of the fields specified in the first argument. Font table entries that were created indirectly -- e.g. by \letterspacefont or during font expansion -- will not be listed. --doc]]-- local default_keys = { "fullname" } --- string list -> (int * string list) list function aux.get_loaded_fonts (keys) keys = keys or default_keys local acc = {} for id, tfmdata in pairs(identifiers) do local data = get_font_data(tfmdata, keys) acc[#acc+1] = { id, data } end return acc end --- Raw access to the font.* namespace is unsafe so no documentation on --- this one. function aux.get_raw_fonts () local res = { } for i, v in font.each() do if v.filename then res[#res+1] = { i, v } end end return res end ----------------------------------------------------------------------- --- font parameters ----------------------------------------------------------------------- --- analogy of font-hsh fonthashes.parameters = fonthashes.parameters or { } fonthashes.quads = fonthashes.quads or { } local parameters = fonthashes.parameters or { } local quads = fonthashes.quads or { } setmetatable(parameters, { __index = function (t, font_id) local tfmdata = identifiers[font_id] if not tfmdata then --- unsafe; avoid tfmdata = font.fonts[font_id] end if tfmdata and type(tfmdata) == "table" then local fontparameters = tfmdata.parameters t[font_id] = fontparameters return fontparameters end return nil end}) --[[doc-- Note that the reason as to why we prefer functions over table indices is that functions are much safer against unintended manipulation. This justifies the overhead they cost. --doc]]-- --- int -> (number | false) function aux.get_quad(font_id) local quad = quads[font_id] if quad then return quad end local fontparameters = parameters[font_id] if fontparameters then local quad = fontparameters.quad or 0 quads[font_id] = quad return quad end return false end ----------------------------------------------------------------------- --- initialization ----------------------------------------------------------------------- local function inject_callbacks (lst) if not lst and next (lst) then return false end local inject = function (def) local cb, fn, id = unpack (def) cb = tostring (cb) id = tostring (id) if not cb or not fn or not id or not type (fn) == "function" then logreport ("both", 0, "aux", "Invalid callback requested (%q, %s, %q).", cb, tostring (fn), id) return false end cb = stringformat ("luaotfload.%s", cb) id = stringformat ("luaotfload.aux.%s", id) logreport ("log", 5, "aux", "Installing callback %q->%q.", cb, id) luatexbase.add_to_callback (cb, fn, id) return true end local ret = true for i = 1, #lst do ret = inject (lst [i]) end return ret end return function () luaotfload.aux = aux return inject_callbacks (luaotfload_callbacks) end -- vim:tw=79:sw=2:ts=8:et