#!/usr/bin/env texlua ----------------------------------------------------------------------- -- FILE: luaotfload-auxiliary.lua -- DESCRIPTION: part of luaotfload -- REQUIREMENTS: luaotfload 2.7 -- AUTHOR: Khaled Hosny, Élie Roux, Philipp Gesang ----------------------------------------------------------------------- -- --- this file addresses issue #24 --- https://github.com/lualatex/luaotfload/issues/24# 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 utf8 = unicode.utf8 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 rewrite_fontname = function (tfmdata, specification) local format = tfmdata.format or tfmdata.properties.format if format ~= "type1" then if stringfind (specification, " ") then tfmdata.name = stringformat ("%q", specification) else --- other specs should parse just fine tfmdata.name = specification end end end local rewriting = false local start_rewrite_fontname = function () 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 aux.start_rewrite_fontname = start_rewrite_fontname local stop_rewrite_fontname = function () 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 aux.stop_rewrite_fontname = stop_rewrite_fontname --[[doc-- This sets two dimensions apparently relied upon by the unicode-math package. --doc]]-- local set_sscale_dimens = function (fontdata) local mathconstants = fontdata.MathConstants local parameters = fontdata.parameters if mathconstants then parameters[10] = mathconstants.ScriptPercentScaleDown or 70 parameters[11] = mathconstants.ScriptScriptPercentScaleDown or 50 end return fontdata end luaotfload_callbacks [#luaotfload_callbacks + 1] = { "patch_font", set_sscale_dimens, "set_sscale_dimens", } --- fontobj -> int local lookup_units = function (fontdata) local metadata = fontdata.shared and fontdata.shared.rawdata.metadata if metadata and metadata.units_per_em then return metadata.units_per_em elseif fontdata.parameters and fontdata.parameters.units then return fontdata.parameters.units elseif fontdata.units then --- v1.x return fontdata.units end return 1000 end --[[doc-- This callback corrects some values of the Cambria font. --doc]]-- --- fontobj -> unit local patch_cambria_domh = function (fontdata) local mathconstants = fontdata.MathConstants if mathconstants and fontdata.psname == "CambriaMath" then --- my test Cambria has 2048 local units_per_em = fontdata.units_per_em or lookup_units(fontdata) local sz = fontdata.parameters.size or fontdata.size local mh = 2800 / units_per_em * 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 fixup_fontdata = function (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 set_capheight = function (fontdata) local shared = fontdata.shared local parameters = fontdata.parameters local capheight if shared and shared.rawdata.metadata and shared.rawdata.metadata.pfminfo then local units_per_em = parameters.units local size = parameters.size local os2_capheight = shared.rawdata.metadata.pfminfo.os2_capheight if capheight and os2_capheight > 0 then capheight = os2_capheight / units_per_em * size else local X8_str = stringbyte"X" local X8_chr = fontdata.characters[X8_str] if X8_chr then capheight = X8_chr.height else capheight = parameters.ascender / units_per_em * size end end else local X8_str = stringbyte "X" local X8_chr = fontdata.characters[X8_str] if X8_chr then capheight = X8_chr.height end end if capheight then --- is this legit? afaics there’s nothing else on the --- array part of that table fontdata.parameters[8] = capheight end end luaotfload_callbacks [#luaotfload_callbacks + 1] = { "patch_font", set_capheight, "set_capheight", } ----------------------------------------------------------------------- --- glyphs and characters ----------------------------------------------------------------------- --- int -> int -> bool local font_has_glyph = function (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 aux.font_has_glyph = font_has_glyph --- undocumented local raw_slot_of_name = function (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) local slot_of_name = function (font_id, glyphname, unsafe) local fontdata = identifiers[font_id] if fontdata then local unicode = fontdata.resources.unicodes[glyphname] if unicode then if type(unicode) == "number" then return unicode else return unicode[1] --- for multiple components end -- else -- --- missing end elseif unsafe == true then -- for Robert return raw_slot_of_name(font_id, glyphname) end return false end aux.slot_of_name = slot_of_name --[[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) local name_of_slot = function (codepoint) if not indices then --- this will load the glyph list local unicodes = encodings.agl.unicodes indices = table.swapped(unicodes) end local glyphname = indices[codepoint] if glyphname then return glyphname end return false end aux.name_of_slot = name_of_slot ----------------------------------------------------------------------- --- features / scripts / languages ----------------------------------------------------------------------- --- lots of arrowcode ahead --[[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 local provides_script = function (font_id, asked_script) 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 shared = tfmdata.shared if not shared then return false end local fontdata = shared.rawdata if not fontdata then return false end local fontname = fontdata.metadata.fontname local features = fontdata.resources.features 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 aux.provides_script = provides_script --[[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 local provides_language = function (font_id, asked_script, asked_language) 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 shared = tfmdata.shared if not shared then return false end local fontdata = shared.rawdata if not fontdata then return false end local fontname = fontdata.metadata.fontname local features = fontdata.resources.features 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 aux.provides_language = provides_language --[[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 local provides_feature = function (font_id, asked_script, asked_language, asked_feature) asked_script = stringlower(asked_script) asked_language = stringlower(asked_language) asked_feature = lpegmatch(strip_garbage, asked_feature) if font_id and font_id > 0 then local tfmdata = identifiers[font_id] if not tfmdata then return false end local shared = tfmdata.shared if not shared then return false end local fontdata = shared.rawdata if not fontdata then return false end local features = fontdata.resources.features local fontname = fontdata.metadata.fontname 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 aux.provides_feature = provides_feature ----------------------------------------------------------------------- --- font dimensions ----------------------------------------------------------------------- --- int -> string -> int local get_math_dimension = function (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 local sprint_math_dimension = function (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 aux.sprint_math_dimension = sprint_math_dimension ----------------------------------------------------------------------- --- extra database functions ----------------------------------------------------------------------- local namesresolve = fontnames.resolve local namesscan_dir = fontnames.scan_dir --[====[-- 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 --]====]-- aux.scan_external_dir = function () print "ERROR: scan_external_dir() is not implemented" end --- db queries -------------------------------------------------------- --- https://github.com/lualatex/luaotfload/issues/74 --- string -> (string * int) local resolve_fontname = function (name) local foundname, subfont, success = namesresolve(nil, nil, { name = name, lookup = "name", optsize = 0, specification = "name:" .. name, }) if success then return foundname, subfont end return false, false end aux.resolve_fontname = resolve_fontname --- string list -> (string * int) local resolve_fontlist resolve_fontlist = function (names, n) if not n then return resolve_fontlist(names, 1) end local this = names[n] if this then local foundname, subfont = resolve_fontname(this) if foundname then return foundname, subfont end return resolve_fontlist(names, n+1) end return false, false end aux.resolve_fontlist = resolve_fontlist --- 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 local read_font_index = function () 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]]-- local font_index = function () return access_font_index () end aux.read_font_index = read_font_index aux.font_index = font_index --- loaded fonts ------------------------------------------------------ --- just a proof of concept --- fontobj -> string list -> (string list) list local get_font_data get_font_data = function (tfmdata, keys, acc, n) if not acc then return get_font_data(tfmdata, keys, {}, 1) end local key = keys[n] if key then local val = tfmdata[key] if val then acc[#acc+1] = val else acc[#acc+1] = false end return get_font_data(tfmdata, keys, acc, n+1) 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 local get_loaded_fonts get_loaded_fonts = function (keys, acc, lastid) if not acc then if not keys then keys = default_keys end return get_loaded_fonts(keys, {}, lastid) end local id, tfmdata = next(identifiers, lastid) if id then local data = get_font_data(tfmdata, keys) acc[#acc+1] = { id, data } return get_loaded_fonts (keys, acc, id) end return acc end aux.get_loaded_fonts = get_loaded_fonts --- Raw access to the font.* namespace is unsafe so no documentation on --- this one. local get_raw_fonts = function ( ) local res = { } for i, v in font.each() do if v.filename then res[#res+1] = { i, v } end end return res end aux.get_raw_fonts = get_raw_fonts ----------------------------------------------------------------------- --- 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) local get_quad = function (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 aux.get_quad = get_quad ----------------------------------------------------------------------- --- initialization ----------------------------------------------------------------------- local inject_callbacks = function (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 { init = function () local ret = true luaotfload.aux = aux ret = inject_callbacks (luaotfload_callbacks) return ret end } -- vim:tw=79:sw=2:ts=8:et