/************************************************************************* ** FontCache.cpp ** ** ** ** This file is part of dvisvgm -- a fast DVI to SVG converter ** ** Copyright (C) 2005-2024 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 "FileSystem.hpp" #include "FontCache.hpp" #include "Pair.hpp" #include "StreamReader.hpp" #include "StreamWriter.hpp" #include "XXHashFunction.hpp" using namespace std; const uint8_t FontCache::FORMAT_VERSION = 5; static Pair32 read_pair (int bytes, StreamReader &sr) { int32_t x = sr.readSigned(bytes); int32_t y = sr.readSigned(bytes); return Pair32(x, y); } /** Removes all data from the cache. This does not affect the cache files. */ void FontCache::clear () { _glyphs.clear(); _fontname.clear(); } /** Assigns glyph data to a character and adds it to the cache. * @param[in] c character code * @param[in] glyph font glyph data */ void FontCache::setGlyph (int c, const Glyph &glyph) { _glyphs[c] = glyph; _changed = true; } /** Returns the corresponding glyph data of a given character of the current font. * @param[in] c character code * @return font glyph data (0 if no matching data was found) */ const Glyph* FontCache::getGlyph (int c) const { auto it = _glyphs.find(c); return (it != _glyphs.end()) ? &it->second : nullptr; } /** Writes the current cache data to a file (only if anything changed after * the last call of read()). * @param[in] fontname name of current font * @param[in] dir directory where the cache file should go * @return true if writing was successful */ bool FontCache::write (const string &fontname, const string &dir) const { if (!_changed) return true; if (!fontname.empty()) { string pathstr = dir.empty() ? FileSystem::getcwd() : dir; pathstr += "/" + fontname + ".fgd"; ofstream ofs(pathstr, ios::binary); return write(fontname, ofs); } return false; } bool FontCache::write (const string &dir) const { return _fontname.empty() ? false : write(_fontname, dir); } /** Returns the minimal number of bytes needed to store the given value. */ static int max_number_of_bytes (int32_t value) { int32_t limit = 0x7f; for (int i=1; i <= 4; i++) { if ((value < 0 && -value <= limit+1) || (value >= 0 && value <= limit)) return i; limit = (limit << 8) | 0xff; } return 4; } static int max_int_size () { return 0; } template static int max_int_size (const Glyph::Point &p1, const Args& ...args) { int max1 = max(max_number_of_bytes(p1.x()), max_number_of_bytes(p1.y())); return max(max1, max_int_size(args...)); } struct WriteActions : Glyph::IterationActions { WriteActions (StreamWriter &sw, HashFunction &hashfunc) : _sw(sw), _hashfunc(hashfunc) {} using Point = Glyph::Point; void moveto (const Point &p) override {write('M', p);} void lineto (const Point &p) override {write('L', p);} void quadto (const Point &p1, const Point &p2) override {write('Q', p1, p2);} void cubicto (const Point &p1, const Point &p2, const Point &p3) override {write('C', p1, p2, p3); } void closepath () override {write('Z');} template void write (char cmd, Args ...args) { int bytesPerValue = max_int_size(args...); int cmdchar = (bytesPerValue << 5) | (cmd - 'A'); _sw.writeUnsigned(cmdchar, 1, _hashfunc); writeParams(bytesPerValue, args...); } static void writeParams (int bytesPerValue) {} template void writeParams (int bytesPerValue, const Point &p, const Args& ...args) { _sw.writeSigned(p.x(), bytesPerValue, _hashfunc); _sw.writeSigned(p.y(), bytesPerValue, _hashfunc); writeParams(bytesPerValue, args...); } StreamWriter &_sw; HashFunction &_hashfunc; }; /** Writes the current cache data to a stream (only if anything changed after * the last call of read()). * @param[in] fontname name of current font * @param[in] os output stream * @return true if writing was successful */ bool FontCache::write (const string &fontname, ostream &os) const { if (!_changed) return true; if (!os) return false; StreamWriter sw(os); XXH32HashFunction hashfunc; sw.writeUnsigned(FORMAT_VERSION, 1, hashfunc); sw.writeBytes(hashfunc.digestBytes()); // space for checksum sw.writeString(fontname, hashfunc, true); sw.writeUnsigned(_glyphs.size(), 4, hashfunc); WriteActions actions(sw, hashfunc); for (const auto &charglyphpair : _glyphs) { const Glyph &glyph = charglyphpair.second; sw.writeUnsigned(charglyphpair.first, 4, hashfunc); sw.writeUnsigned(glyph.size(), 2, hashfunc); glyph.iterate(actions, false); } os.seekp(1); auto digest = hashfunc.digestBytes(); sw.writeBytes(digest); // insert checksum os.seekp(0, ios::end); return true; } /** Reads font glyph information from a file. * @param[in] fontname name of font data to read * @param[in] dir directory where the cache files are located * @return true if reading was successful */ bool FontCache::read (const string &fontname, const string &dir) { if (fontname.empty()) return false; if (_fontname == fontname) return true; clear(); string dirstr = dir.empty() ? FileSystem::getcwd() : dir; ostringstream oss; oss << dirstr << '/' << fontname << ".fgd"; ifstream ifs(oss.str(), ios::binary); return read(fontname, ifs); } /** Reads font glyph information from a stream. * @param[in] fontname name of font data to read * @param[in] is input stream to read the glyph data from * @return true if reading was successful */ bool FontCache::read (const string &fontname, istream &is) { if (_fontname == fontname) return true; clear(); _fontname = fontname; if (!is) return false; StreamReader sr(is); XXH32HashFunction hashfunc; if (sr.readUnsigned(1, hashfunc) != FORMAT_VERSION) return false; auto hashcmp = sr.readBytes(hashfunc.digestSize()); hashfunc.update(is); if (hashfunc.digestBytes() != hashcmp) return false; is.clear(); is.seekg(hashfunc.digestSize()+1); // continue reading after checksum string fname = sr.readString(); if (fname != fontname) return false; uint32_t num_glyphs = sr.readUnsigned(4); while (num_glyphs-- > 0) { uint32_t c = sr.readUnsigned(4); // character code uint16_t s = sr.readUnsigned(2); // number of path commands Glyph &glyph = _glyphs[c]; while (s-- > 0) { uint8_t cmdval = sr.readUnsigned(1); uint8_t cmdchar = (cmdval & 0x1f) + 'A'; int bytes = cmdval >> 5; switch (cmdchar) { case 'C': { Pair32 p1 = read_pair(bytes, sr); Pair32 p2 = read_pair(bytes, sr); Pair32 p3 = read_pair(bytes, sr); glyph.cubicto(p1, p2, p3); break; } case 'L': glyph.lineto(read_pair(bytes, sr)); break; case 'M': glyph.moveto(read_pair(bytes, sr)); break; case 'Q': { Pair32 p1 = read_pair(bytes, sr); Pair32 p2 = read_pair(bytes, sr); glyph.quadto(p1, p2); break; } case 'Z': glyph.closepath(); } } } _changed = false; return true; } /** Collects font cache information. * @param[in] dirname path to font cache directory * @param[out] infos the collected font information * @param[out] invalid names of outdated/corrupted cache files * @return true on success */ bool FontCache::fontinfo (const string &dirname, vector &infos, vector &invalid) { infos.clear(); invalid.clear(); if (!dirname.empty()) { vector fnames; FileSystem::collect(dirname, fnames); for (const string &fname : fnames) { if (fname[0] == 'f' && fname.length() > 5 && fname.substr(fname.length()-4) == ".fgd") { FontInfo info; string path = dirname+"/"+(fname.substr(1)); ifstream ifs(path, ios::binary); if (fontinfo(ifs, info)) infos.push_back(std::move(info)); else invalid.push_back(fname.substr(1)); } } } return !infos.empty(); } /** Collects font cache information of a single font. * @param[in] is input stream of the cache file * @param[out] info the collected data * @return true if data could be read, false if cache file is unavailable, outdated, or corrupted */ bool FontCache::fontinfo (std::istream &is, FontInfo &info) { info.name.clear(); info.numchars = info.numbytes = info.numcmds = 0; if (is) { is.clear(); is.seekg(0); try { StreamReader sr(is); XXH32HashFunction hashfunc; if ((info.version = sr.readUnsigned(1, hashfunc)) != FORMAT_VERSION) return false; info.checksum = sr.readBytes(hashfunc.digestSize()); hashfunc.update(is); if (hashfunc.digestBytes() != info.checksum) return false; is.clear(); is.seekg(hashfunc.digestSize()+1); // continue reading after checksum info.name = sr.readString(); info.numchars = sr.readUnsigned(4); for (uint32_t i=0; i < info.numchars; i++) { sr.readUnsigned(4); // character code uint16_t s = sr.readUnsigned(2); // number of path commands while (s-- > 0) { uint8_t cmdval = sr.readUnsigned(1); uint8_t cmdchar = (cmdval & 0x1f) + 'A'; int bytes = cmdval >> 5; int bc = 0; switch (cmdchar) { case 'C': bc = 6*bytes; break; case 'H': case 'L': case 'M': case 'T': case 'V': bc = 2*bytes; break; case 'Q': case 'S': bc = 4*bytes; break; case 'Z': break; default : return false; } info.numbytes += bc+1; // command length + command info.numcmds++; is.seekg(bc, ios::cur); } info.numbytes += 6; // number of path commands + char code } info.numbytes += 6+info.name.length(); // version + 0-byte + fontname + number of chars } catch (StreamReaderException &e) { return false; } } return true; } /** Collects font cache information and write it to a stream. * @param[in] dirname path to font cache directory * @param[in] os output is written to this stream * @param[in] purge if true, outdated and corrupted cache files are removed */ void FontCache::fontinfo (const string &dirname, ostream &os, bool purge) { if (!dirname.empty()) { ios::fmtflags osflags(os.flags()); vector infos; vector invalid_files; if (!fontinfo(dirname, infos, invalid_files)) os << "cache is empty\n"; else { os << "cache format version " << infos[0].version << endl; map sortmap; for (const FontInfo &info : infos) sortmap[info.name] = &info; for (const auto &strinfopair : sortmap) { os << dec << setfill(' ') << left << setw(10) << left << strinfopair.second->name << setw(5) << right << strinfopair.second->numchars << " glyph" << (strinfopair.second->numchars == 1 ? ' ':'s') << setw(10) << right << strinfopair.second->numcmds << " cmd" << (strinfopair.second->numcmds == 1 ? ' ':'s') << setw(12) << right << strinfopair.second->numbytes << " byte" << (strinfopair.second->numbytes == 1 ? ' ':'s') << " hash:" << hex; for (int byte : strinfopair.second->checksum) os << setw(2) << setfill('0') << byte; os << '\n'; } } if (purge) { for (const string &str : invalid_files) { string path=dirname+"/"+str; if (FileSystem::remove(path)) os << "invalid cache file " << str << " removed\n"; } } os.flags(osflags); // restore format flags } }