if not modules then modules = { } end modules ['font-nms'] = { version = 1.002, comment = "companion to luaotfload.lua", author = "Khaled Hosny and Elie Roux", copyright = "Luaotfload Development Team", license = "GNU GPL v2" } fonts = fonts or { } fonts.names = fonts.names or { } local names = fonts.names local names_dir = "luatex-cache/generic/names" names.version = 2.010 -- not the same as in context names.data = nil names.path = { basename = "otfl-names.lua", dir = file.join(kpse.expand_var("$TEXMFVAR"), names_dir), } local success = pcall(require, "luatexbase.modutils") if success then success = pcall(luatexbase.require_module, "lualatex-platform", "2011/03/30") end local get_installed_fonts if success then get_installed_fonts = lualatex.platform.get_installed_fonts else function get_installed_fonts() end end local splitpath, collapsepath = file.splitpath, file.collapsepath local expandpath = kpse.expand_path local glob, basename = dir.glob, file.basename local extname = file.extname local upper, lower, format = string.upper, string.lower, string.format local gsub, match, rpadd = string.gsub, string.match, string.rpadd local gmatch, sub, find = string.gmatch, string.sub, string.find local utfgsub = unicode.utf8.gsub local report = logs.names_report local function sanitize(str) if str then return utfgsub(lower(str), "[^%a%d]", "") else return str -- nil end end local function fontnames_init() return { mappings = { }, status = { }, version = names.version, } end local function make_name(path) return file.replacesuffix(path, "lua"), file.replacesuffix(path, "luc") end local function load_names() local path = file.join(names.path.dir, names.path.basename) local luaname, lucname = make_name(path) local foundname local data if file.isreadable(lucname) then data = dofile(lucname) foundname = lucname elseif file.isreadable(luaname) then data = dofile(luaname) foundname = luaname end if data then report("info", 0, "Font names database loaded", "%s", foundname) else report("info", 0, [[Font names database not found, generating new one. This can take several minutes; please be patient.]]) data = names.update(fontnames_init()) names.save(data) end texio.write_nl("") return data end local synonyms = { regular = { "normal", "roman", "plain", "book", "medium" }, bold = { "boldregular", "demi", "demibold" }, italic = { "regularitalic", "normalitalic", "oblique", "slanted" }, bolditalic = { "boldoblique", "boldslanted", "demiitalic", "demioblique", "demislanted", "demibolditalic" }, } local loaded = false local reloaded = false function names.resolve(_,_,specification) -- the 1st two parameters are used by ConTeXt local name = sanitize(specification.name) local style = sanitize(specification.style) or "regular" local size if specification.optsize then size = tonumber(specification.optsize) elseif specification.size then size = specification.size / 65536 end if not loaded then names.data = names.load() loaded = true end local data = names.data if type(data) == "table" and data.version == names.version then if data.mappings then local found = { } for _,face in next, data.mappings do local family = sanitize(face.names and face.names.family) local subfamily = sanitize(face.names and face.names.subfamily) local fullname = sanitize(face.names and face.names.fullname) local psname = sanitize(face.names and face.names.psname) local fontname = sanitize(face.fontname) local pfullname = sanitize(face.fullname) local optsize, dsnsize, maxsize, minsize if #face.size > 0 then optsize = face.size dsnsize = optsize[1] and optsize[1] / 10 -- can be nil maxsize = optsize[2] and optsize[2] / 10 or dsnsize minsize = optsize[3] and optsize[3] / 10 or dsnsize end if name == family then if subfamily == style then if optsize then if dsnsize == size or (size > minsize and size <= maxsize) then found[1] = face break else found[#found+1] = face end else found[1] = face break end elseif synonyms[style] and table.contains(synonyms[style], subfamily) then if optsize then if dsnsize == size or (size > minsize and size <= maxsize) then found[1] = face break else found[#found+1] = face end else found[1] = face break end elseif subfamily == "regular" or table.contains(synonyms.regular, subfamily) then found.fallback = face end else if name == fullname or name == pfullname or name == fontname or name == psname then if optsize then if dsnsize == size or (size > minsize and size <= maxsize) then found[1] = face break else found[#found+1] = face end else found[1] = face break end end end end if #found == 1 then if kpse.lookup(found[1].filename[1]) then report("log", 0, "load font", "font family='%s', subfamily='%s' found: %s", name, style, found[1].filename[1] ) return found[1].filename[1], found[1].filename[2] end elseif #found > 1 then -- we found matching font(s) but not in the requested optical -- sizes, so we loop through the matches to find the one with -- least difference from the requested size. local closest local least = math.huge -- initial value is infinity for i,face in next, found do local dsnsize = face.size[1]/10 local difference = math.abs(dsnsize-size) if difference < least then closest = face least = difference end end if kpse.lookup(closest.filename[1]) then report("log", 0, "load font", "font family='%s', subfamily='%s' found: %s", name, style, closest.filename[1] ) return closest.filename[1], closest.filename[2] end elseif found.fallback then return found.fallback.filename[1], found.fallback.filename[2] end -- no font found so far if not reloaded then -- try reloading the database names.data = names.update(names.data) names.save(names.data) reloaded = true return names.resolve(_,_,specification) else -- else, fallback to filename -- XXX: specification.name is empty with absolute paths, looks -- like a bug in the specification parser return specification.name, false end end else if not reloaded then names.data = names.update() names.save(names.data) reloaded = true return names.resolve(_,_,specification) else return specification.name, false end end end names.resolvespec = names.resolve local function font_fullinfo(filename, subfont, texmf) local t = { } local f = fontloader.open(filename, subfont) if not f then report("log", 1, "error", "failed to open %s", filename) return end local m = fontloader.to_table(f) fontloader.close(f) collectgarbage('collect') -- see http://www.microsoft.com/typography/OTSPEC/features_pt.htm#size if m.fontstyle_name then for _,v in next, m.fontstyle_name do if v.lang == 1033 then t.fontstyle_name = v.name end end end if m.names then for _,v in next, m.names do if v.lang == "English (US)" then t.names = { -- see -- http://developer.apple.com/textfonts/ -- TTRefMan/RM06/Chap6name.html fullname = v.names.compatfull or v.names.fullname, family = v.names.preffamilyname or v.names.family, subfamily= t.fontstyle_name or v.names.prefmodifiers or v.names.subfamily, psname = v.names.postscriptname } end end else -- no names table, propably a broken font report("log", 1, "broken font rejected", "%s", basefile) return end t.fontname = m.fontname t.fullname = m.fullname t.familyname = m.familyname t.filename = { texmf and basename(filename) or filename, subfont } t.weight = m.pfminfo.weight t.width = m.pfminfo.width t.slant = m.italicangle -- don't waste the space with zero values t.size = { m.design_size ~= 0 and m.design_size or nil, m.design_range_top ~= 0 and m.design_range_top or nil, m.design_range_bottom ~= 0 and m.design_range_bottom or nil, } return t end local function load_font(filename, fontnames, newfontnames, texmf) local newmappings = newfontnames.mappings local newstatus = newfontnames.status local mappings = fontnames.mappings local status = fontnames.status local basefile = texmf and basename(filename) or filename if filename then if names.blacklist[filename] or names.blacklist[basename(filename)] then report("log", 2, "ignoring font", "%s", filename) return end local timestamp, db_timestamp db_timestamp = status[basefile] and status[basefile].timestamp timestamp = lfs.attributes(filename, "modification") local index_status = newstatus[basefile] or (not texmf and newstatus[basename(filename)]) if index_status and index_status.timestamp == timestamp then -- already indexed this run return end newstatus[basefile] = newstatus[basefile] or { } newstatus[basefile].timestamp = timestamp newstatus[basefile].index = newstatus[basefile].index or { } if db_timestamp == timestamp and not newstatus[basefile].index[1] then for _,v in next, status[basefile].index do local index = #newstatus[basefile].index newmappings[#newmappings+1] = mappings[v] newstatus[basefile].index[index+1] = #newmappings end report("log", 1, "font already indexed", "%s", basefile) return end local info = fontloader.info(filename) if info then if type(info) == "table" and #info > 1 then for i in next, info do local fullinfo = font_fullinfo(filename, i-1, texmf) if not fullinfo then return end local index = newstatus[basefile].index[i] if newstatus[basefile].index[i] then index = newstatus[basefile].index[i] else index = #newmappings+1 end newmappings[index] = fullinfo newstatus[basefile].index[i] = index end else local fullinfo = font_fullinfo(filename, false, texmf) if not fullinfo then return end local index if newstatus[basefile].index[1] then index = newstatus[basefile].index[1] else index = #newmappings+1 end newmappings[index] = fullinfo newstatus[basefile].index[1] = index end else report("log", 1, "failed to load", "%s", basefile) end end end local function path_normalize(path) --[[ path normalization: - a\b\c -> a/b/c - a/../b -> b - /cygdrive/a/b -> a:/b - reading symlinks under non-Win32 - using kpse.readable_file on Win32 ]] if os.type == "windows" or os.type == "msdos" or os.name == "cygwin" then path = path:gsub('\\', '/') path = path:lower() path = path:gsub('^/cygdrive/(%a)/', '%1:/') end if os.type ~= "windows" and os.type ~= "msdos" then local dest = lfs.readlink(path) if dest then if kpse.readable_file(dest) then path = dest elseif kpse.readable_file(file.join(file.dirname(path), dest)) then path = file.join(file.dirname(path), dest) else -- broken symlink? end end end path = collapsepath(path) return path end fonts.path_normalize = path_normalize names.blacklist = { } local function read_blacklist() local files = { kpse.lookup("otfl-blacklist.cnf", {all=true, format="tex"}) } local blacklist = names.blacklist local whitelist = { } if files and type(files) == "table" then for _,v in next, files do for line in io.lines(v) do line = line:strip() -- to get rid of lines like " % foo" if line:find("^%%") or line:is_empty() then -- comment or empty line else line = line:split("%")[1] line = line:strip() if string.sub(line,1,1) == "-" then whitelist[string.sub(line,2,-1)] = true else report("log", 2, "blacklisted file", "%s", line) blacklist[line] = true end end end end end for _,fontname in next, whitelist do blacklist[fontname] = nil end end local font_extensions = { "otf", "ttf", "ttc", "dfont" } local font_extensions_set = {} for key, value in next, font_extensions do font_extensions_set[value] = true end local installed_fonts_scanned = false local function scan_installed_fonts(fontnames, newfontnames) -- Try to query and add font list from operating system. -- This uses the lualatex-platform module. report("info", 0, "Scanning fonts known to operating system...") local fonts = get_installed_fonts() if fonts and #fonts > 0 then installed_fonts_scanned = true report("log", 2, "operating system fonts found", "%d", #fonts) for key, value in next, fonts do local file = value.path if file then local ext = extname(file) if ext and font_extensions_set[ext] then file = path_normalize(file) report("log", 1, "loading font", "%s", file) load_font(file, fontnames, newfontnames, false) end end end else report("log", 2, "Could not retrieve list of installed fonts") end end local function scan_dir(dirname, fontnames, newfontnames, texmf) --[[ This function scans a directory and populates the list of fonts with all the fonts it finds. - dirname is the name of the directory to scan - names is the font database to fill - texmf is a boolean saying if we are scanning a texmf directory ]] local list, found = { }, { } local nbfound = 0 report("log", 2, "scanning", "%s", dirname) for _,i in next, font_extensions do for _,ext in next, { i, upper(i) } do found = glob(format("%s/**.%s$", dirname, ext)) -- note that glob fails silently on broken symlinks, which happens -- sometimes in TeX Live. report("log", 2, "fonts found", "%s '%s' fonts found", #found, ext) nbfound = nbfound + #found table.append(list, found) end end report("log", 2, "fonts found", "%d fonts found in '%s'", nbfound, dirname) for _,file in next, list do file = path_normalize(file) report("log", 1, "loading font", "%s", file) load_font(file, fontnames, newfontnames, texmf) end end local function scan_texmf_fonts(fontnames, newfontnames) --[[ This function scans all fonts in the texmf tree, through kpathsea variables OPENTYPEFONTS and TTFONTS of texmf.cnf ]] if expandpath("$OSFONTDIR"):is_empty() then report("info", 0, "Scanning TEXMF fonts...") else report("info", 0, "Scanning TEXMF and OS fonts...") end local fontdirs = expandpath("$OPENTYPEFONTS"):gsub("^%.", "") fontdirs = fontdirs .. expandpath("$TTFONTS"):gsub("^%.", "") if not fontdirs:is_empty() then for _,d in next, splitpath(fontdirs) do scan_dir(d, fontnames, newfontnames, true) end end end --[[ For the OS fonts, there are several options: - if OSFONTDIR is set (which is the case under windows by default but not on the other OSs), it scans it at the same time as the texmf tree, in the scan_texmf_fonts. - if not: - under Windows and Mac OSX, we take a look at some hardcoded directories - under Unix, we read /etc/fonts/fonts.conf and read the directories in it This means that if you have fonts in fancy directories, you need to set them in OSFONTDIR. ]] local function read_fonts_conf(path, results) --[[ This function parses /etc/fonts/fonts.conf and returns all the dir it finds. The code is minimal, please report any error it may generate. ]] local f = io.open(path) if not f then report("log", 2, "cannot open file", "%s", path) return results end local incomments = false for line in f:lines() do while line and line ~= "" do -- spaghetti code... hmmm... if incomments then local tmp = find(line, '-->') if tmp then incomments = false line = sub(line, tmp+3) else line = nil end else local tmp = find(line, '