/************************************************************************* ** FontMap.cpp ** ** ** ** This file is part of dvisvgm -- a fast DVI to SVG converter ** ** Copyright (C) 2005-2025 Martin Gieseking ** ** ** ** This program is free software; you can redistribute it and/or ** ** modify it under the terms of the GNU General Public License as ** ** published by the Free Software Foundation; either version 3 of ** ** the License, or (at your option) any later version. ** ** ** ** This program is distributed in the hope that it will be useful, but ** ** WITHOUT ANY WARRANTY; without even the implied warranty of ** ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** ** GNU General Public License for more details. ** ** ** ** You should have received a copy of the GNU General Public License ** ** along with this program; if not, see . ** *************************************************************************/ #include #include #include #include #include #include "CMap.hpp" #include "Directory.hpp" #include "FileFinder.hpp" #include "FilePath.hpp" #include "FontManager.hpp" #include "FontMap.hpp" #include "MapLine.hpp" #include "Message.hpp" #include "Subfont.hpp" #include "utility.hpp" using namespace std; /** Returns the singleton instance. */ FontMap& FontMap::instance() { static FontMap fontmap; return fontmap; } /** Reads and evaluates a single font map file. * @param[in] fname path/name of map file to read (it's not looked up via FileFinder) * @param[in] mode selects how to integrate the map file entries into the global map tree * @param[in,out] includedFilesRef pointer to sequence of (nested) file paths currently being included * @return true if file could be opened */ bool FontMap::read (const string &fname, FontMap::Mode mode, vector *includedFilesRef) { ifstream ifs(fname); if (!ifs) return false; unique_ptr> includedFilesStore; int lineNumber = 1; while (ifs) { try { int c = ifs.peek(); if (c < 0 || strchr("\n&%;*", c)) // comment or empty line? ifs.ignore(numeric_limits::max(), '\n'); else if (c != '#') { MapLine mapline(ifs); apply(mapline, mode); } else { char line[256]; ifs.getline(line, 256); if (strncmp(line, "#include ", 9) == 0 || strncmp(line, "#includefirst ", 14) == 0) { FilePath path(fname); if (!includedFilesRef && !includedFilesStore) { // not yet inside an include chain? includedFilesStore = util::make_unique>(); includedFilesRef = includedFilesStore.get(); includedFilesRef->emplace_back(path.absolute()); } if (strncmp(line, "#include ", 9) == 0) include(line+9, path, *includedFilesRef); else includefirst(line+14, path, *includedFilesRef); } } lineNumber++; } catch (const MapLineException &e) { Message::wstream(true) << FilePath(fname).shorterAbsoluteOrRelative() << ", line " << lineNumber << ": " << e.what() << '\n'; } catch (const SubfontException &e) { Message::wstream(true) << FilePath(e.filename()).shorterAbsoluteOrRelative(); if (e.lineno() > 0) Message::wstream(false) << ", line " << e.lineno(); Message::wstream(false) << e.what() << '\n'; } } return true; } /** Reads and evaluates a single font map file. * @param[in] fname path/name of map file to read (it's not looked up via FileFinder) * @param[in] modechar character '+', '-', or '=' to determine how the map file entries are merged into the global map tree * @param[in,out] includedFilesRef pointer to sequence of (nested) file paths currently being included * @return true if file could be opened */ bool FontMap::read (const string &fname, char modechar, vector *includedFilesRef) { Mode mode; switch (modechar) { case '=': mode = Mode::REPLACE; break; case '-': mode = Mode::REMOVE; break; default : mode = Mode::APPEND; } return read(fname, mode, includedFilesRef); } /** Evaluates an #include statement and processes the contents of the included map file. * @param[in] line parameters of #include statement (optional mode character and file name/path * @param[in] includingFile path of file containing the #include statement * @param[in,out] includedFiles sequence of (nested) file paths currently being included */ void FontMap::include (string line, const FilePath &includingFile, vector &includedFiles) { auto it = line.begin(); while (it != line.end() && isspace(*it)) ++it; char modechar = '+'; if (it != line.end() && strchr("+-=", *it)) modechar = *it++; string fname = util::trim(line.substr(it-line.begin())); if (fname.size() > 1 && fname[0] == '"' && fname.back() == '"') fname = fname.substr(1, fname.size()-2); // strip surrounding quotes if (fname.empty()) throw FontMapException("file name missing after #include"); const char *path; auto pos = fname.find('/'); if (pos == 0) // absolute path given? path = fname.c_str(); else if (pos == string::npos) { // filename only given? path = FileFinder::instance().lookup(fname); if (!path) throw FontMapException("include file '"+fname+"' not found", FontMapException::Cause::FILE_ACCESS_ERROR); } else { fname = FilePath(fname, FilePath::PT_FILE, includingFile.absolute(false)).absolute(); path = fname.c_str(); } if (find(includedFiles.begin(), includedFiles.end(), path) != includedFiles.end()) throw FontMapException("circular inclusion of file '"+string(FilePath(path).shorterAbsoluteOrRelative())+"'"); includedFiles.emplace_back(path); if (!read(path, modechar, &includedFiles)) throw FontMapException("include file '"+fname+"' not found", FontMapException::Cause::FILE_ACCESS_ERROR); includedFiles.pop_back(); } void FontMap::includefirst (string line, const FilePath &includingFile, vector &includedFiles) { if (_firstincludeMode != FirstIncludeMode::OFF) return; try { _firstincludeMode = FirstIncludeMode::ACTIVE; include(std::move(line), includingFile, includedFiles); _firstincludeMode = FirstIncludeMode::DONE; } catch (FontMapException &e) { if (e.cause() != FontMapException::Cause::FILE_ACCESS_ERROR) throw; _firstincludeMode = FirstIncludeMode::OFF; } } /** Applies a mapline according to the given mode (append, remove, replace). * @param[in] mapline the mapline to be applied * @param[in] mode mode to use * @return true in case of success */ bool FontMap::apply (const MapLine& mapline, FontMap::Mode mode) { switch (mode) { case Mode::APPEND: return append(mapline); case Mode::REMOVE: return remove(mapline); default: return replace(mapline); } } /** Applies a mapline according to the given mode (append, remove, replace). * @param[in] mapline the mapline to be applied * @param[in] modechar character that denotes the mode (+, -, or =) * @return true in case of success */ bool FontMap::apply (const MapLine& mapline, char modechar) { Mode mode; switch (modechar) { case '=': mode = Mode::REPLACE; break; case '-': mode = Mode::REMOVE; break; default : mode = Mode::APPEND; } return apply(mapline, mode); } /** Reads and evaluates a sequence of map files. Each map file is looked up in the local * directory and the TeX file tree. * @param[in] fname_seq comma-separated list of map file names * @param[in] warn true: print warning if a file couldn't be read * @return true if at least one of the given map files was found */ bool FontMap::read (const string &fname_seq, bool warn) { bool found = false; string::size_type left=0; while (left < fname_seq.length()) { char modechar = fname_seq[left]; if (strchr("+-=", modechar)) left++; else modechar = '+'; string fname; auto right = fname_seq.find(',', left); if (right != string::npos) fname = fname_seq.substr(left, right-left); else { fname = fname_seq.substr(left); right = fname_seq.length(); } if (!fname.empty()) { if (!read(fname, modechar)) { if (const char *path = FileFinder::instance().lookup(fname, false)) found = found || read(path, modechar); else if (warn) Message::wstream(true) << "map file '" << fname << "' not found\n"; } } left = right+1; } return found; } /** Appends given map line data to the font map if there is no entry for the corresponding * font in the map yet. * @param[in] mapline parsed font data * @return true if data has been appended */ bool FontMap::append (const MapLine &mapline) { bool appended = false; if (!mapline.texname().empty()) { if (!mapline.fontfname().empty() || !mapline.encname().empty()) { vector subfonts; if (mapline.sfd()) subfonts = mapline.sfd()->subfonts(); else subfonts.push_back(nullptr); for (Subfont *subfont : subfonts) { string fontname = mapline.texname()+(subfont ? subfont->id() : ""); auto it = _entries.find(fontname); if (it == _entries.end()) { _entries.emplace(fontname, util::make_unique(mapline, subfont)); appended = true; } } } } return appended; } /** Replaces the map data of the given font. * If the font is locked (because it's already in use) nothing happens. * @param[in] mapline parsed font data * @return true if data has been replaced */ bool FontMap::replace (const MapLine &mapline) { if (mapline.texname().empty()) return false; if (mapline.fontfname().empty() && mapline.encname().empty()) return remove(mapline); vector subfonts; if (mapline.sfd()) subfonts = mapline.sfd()->subfonts(); else subfonts.push_back(nullptr); for (Subfont *subfont : subfonts) { string fontname = mapline.texname()+(subfont ? subfont->id() : ""); auto it = _entries.find(fontname); if (it == _entries.end()) _entries.emplace(fontname, util::make_unique(mapline, subfont)); else if (!it->second->locked) *it->second = Entry(mapline, subfont); } return true; } /** Removes the map entry of the given font. * If the font is locked (because it's already in use) nothing happens. * @param[in] mapline parsed font data * @return true if entry has been removed */ bool FontMap::remove (const MapLine &mapline) { bool removed = false; if (!mapline.texname().empty()) { vector subfonts; if (mapline.sfd()) subfonts = mapline.sfd()->subfonts(); else subfonts.push_back(nullptr); for (const Subfont *subfont : subfonts) { string fontname = mapline.texname()+(subfont ? subfont->id() : ""); auto it = _entries.find(fontname); if (it != _entries.end() && !it->second->locked) { _entries.erase(it); removed = true; } } } return removed; } ostream& FontMap::write (ostream &os) const { for (const auto &entry : _entries) os << entry.first << " -> " << entry.second->fontname << " [" << entry.second->encname << "]\n"; return os; } /** Reads and evaluates all map files in the given directory. * @param[in] dirname path to directory containing the map files to be read */ void FontMap::readdir (const string &dirname) { Directory dir(dirname); while (const char *fname = dir.read(Directory::ET_FILE)) { if (strlen(fname) >= 4 && strcmp(fname+strlen(fname)-4, ".map") == 0) { string path = dirname + "/" + fname; read(path); } } } /** Returns name of font that is mapped to a given font. * @param[in] fontname name of font whose mapped name is retrieved * @returns name of mapped font */ const FontMap::Entry* FontMap::lookup (const string &fontname) const { auto it = _entries.find(fontname); if (it == _entries.end()) return nullptr; return it->second.get(); } /** Sets the lock flag for the given font in order to avoid changing the map data of this font. * @param[in] fontname name of font to be locked */ void FontMap::lockFont (const string& fontname) { auto it = _entries.find(fontname); if (it != _entries.end()) it->second->locked = true; } /** Removes all (unlocked) entries from the font map. * @param[in] unlocked_only if true, only unlocked entries are removed */ void FontMap::clear (bool unlocked_only) { if (!unlocked_only) _entries.clear(); else { auto it=_entries.begin(); while (it != _entries.end()) { if (it->second->locked) ++it; else it = _entries.erase(it); } } } ///////////////////////////////////////////////// FontMap::Entry::Entry (const MapLine &mapline, Subfont *sf) : fontname(mapline.fontfname()), encname(mapline.encname()), subfont(sf), fontindex(mapline.fontindex()), locked(false), style(mapline.bold(), mapline.extend(), mapline.slant()) { }