A snapshot of the development tree. The substantial changes since the last one:
COMMAND OUTPUT POLICY. The three commands display text in exactly three cases,
and each owns a stream: LOGGING under "-v" greater than 0 and an ERROR before
termination go to STDERR; OUTPUT THE USER ASKED FOR goes to STDOUT. For ktext
that output is a document, so "ktext doc.kt -d | ..." is now safe -- logging
used to share the stream and land inside the document. A bare command prints
its usage and succeeds rather than failing. Colour is emitted only to a
terminal, per stream, and NO_COLOR is honoured.
"-v 1" reports every decision whose outcome you could not have read off your own
input: the klammerset that was loaded and from which file, a font's directory, a
":files" name's file, how "-o" was expanded. Higher levels are the trace.
The commands no longer warn and continue: an anomaly is an error, described with
its location. Two exceptions remain, each for a stated reason -- a condition
that is expected and temporary by design, and a judgment that is a heuristic
rather than exact.
@cond IS NOW A TRUE SPECIAL FORM, resolved at APPLICATION time rather than when
the file is read. Two consequences for a writer:
* a state variable reaches the predicate. "@@@state Flag :value true @@@
@cond *Flag* | T | F @" renders "T"; it used to see the literal "*Flag*" and
silently take the false branch. The document now behaves like a klammer
body, whose arguments are bound before its conditionals are decided.
* nothing in a discarded branch happens -- it is not read, not evaluated, not
expanded. An @eval in the branch not taken used to run anyway.
Its predicate relation is total and strict: true, True, 1; false, False, 0, and
empty; anything else is an error at the @cond rather than silently false.
@eval REACHING OUTSIDE. ":shell" and ":haskell" now keep the command's standard
error out of the document (it appears under "-v 1") and treat a nonzero exit as
an error naming what the command reported. A command that exits nonzero on
purpose -- "grep" finding no match -- says so with "|| true".
KLAMMER SETS. Several combine: "--klammersets a b c" loads all three in the
order given, sharing one namespace, with the definition modes deciding
collisions. "none" means none and may not be combined with other symbols. A
klammerset with symbol X is declared in a file X/X.k, which is what lets two
sets require the same third set without loading it twice.
TESTS. Four new suites: the kdiag command's interface, the @eval primitive's
contract with the outside world, and verbosity at both tiers. Three suites
that could not run on macOS at all now do.
Assembled from dev commit 6c8ee6c22fca.
623 lines
23 KiB
C++
623 lines
23 KiB
C++
// The Klammertext font store: INFRASTRUCTURE, not part of the
|
|
// Klammermachine (the Machine class never references it) and not part of
|
|
// any klammer set. Klammer sets (the SKS's @document, a future music
|
|
// set) consume the store; the kdesc command lists, installs into, and
|
|
// samples it without loading any klammer set. The store's search runs
|
|
// over the KLAMMERTEXT_FONTS directories and ends at the distribution's
|
|
// default font set in $KLAMMERTEXT_HOME/fnt.
|
|
|
|
#include "font_store.h"
|
|
#include "file.h"
|
|
#include "locator.h"
|
|
#include "util.h"
|
|
#include "error.h"
|
|
#include "log.h"
|
|
#include "show.h"
|
|
|
|
#include <algorithm>
|
|
#include <fstream>
|
|
#include <regex>
|
|
#include <set>
|
|
#include <sstream>
|
|
#include <filesystem>
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
// A filesystem extension lowercased for case-insensitive matching, so a
|
|
// font named Foo.TTF or Foo.OTF is recognized like Foo.ttf / Foo.otf.
|
|
static std::string lower_extension(const fs::path& path)
|
|
{
|
|
std::string ext = path.extension();
|
|
for (char& c : ext) {
|
|
c = std::tolower(static_cast<unsigned char>(c));
|
|
}
|
|
return ext;
|
|
}
|
|
|
|
|
|
std::string name_to_dirname(const std::string& name)
|
|
{
|
|
std::string result {};
|
|
for (char c : name) {
|
|
if (c == ' ')
|
|
result += '-';
|
|
else
|
|
result += std::tolower(c);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
// The directories searched for installed fonts: the colon-separated
|
|
// KLAMMERTEXT_FONTS environment variable, or ~/.klammertext/fonts when it
|
|
// is not set. Directories are searched in listed order, and the default
|
|
// fonts in $KLAMMERTEXT_HOME/fnt are searched last, so an installed font
|
|
// can deliberately shadow a default one.
|
|
strings_t installed_font_dirs()
|
|
{
|
|
strings_t result {};
|
|
std::string paths {};
|
|
const char* env = std::getenv("KLAMMERTEXT_FONTS");
|
|
if (env && *env) {
|
|
paths = env;
|
|
} else if (const char* home = std::getenv("HOME"); home && *home) {
|
|
paths = std::string(home) + "/.klammertext/fonts";
|
|
}
|
|
std::stringstream ss(paths);
|
|
std::string dir;
|
|
while (std::getline(ss, dir, ':')) {
|
|
if (!dir.empty()) {
|
|
result.push_back(dir);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::string default_font_dir()
|
|
{
|
|
const char* home = std::getenv(klammertext_home_var.c_str());
|
|
if (home == nullptr || *home == '\0') {
|
|
throw Argument_error(
|
|
"The environment variable " + klammertext_home_var + " is not defined");
|
|
}
|
|
return std::string(home) + "/fnt";
|
|
}
|
|
|
|
|
|
static void classify_from_css(Resolved_font& font, std::string css_path)
|
|
{
|
|
// Parse the @font-face blocks in the CSS to determine variant → filename mapping
|
|
std::string css = string_from_file(css_path);
|
|
std::regex face_rgx(
|
|
R"(@font-face\s*\{[^}]*font-style:\s*(\w+);[^}]*font-weight:\s*(\w+);[^}]*url\('([^']+\.ttf)'\)[^}]*\})",
|
|
std::regex::multiline);
|
|
auto begin = std::sregex_iterator(css.begin(), css.end(), face_rgx);
|
|
auto end = std::sregex_iterator();
|
|
for (auto it = begin; it != end; ++it) {
|
|
std::string style = (*it)[1];
|
|
std::string weight = (*it)[2];
|
|
std::string url_path = (*it)[3];
|
|
// URL is like 'dir-name/Filename.ttf' — extract just the filename
|
|
std::string filename = url_path.substr(url_path.rfind('/') + 1);
|
|
bool is_bold = (weight == "700" || weight == "bold");
|
|
bool is_italic = (style == "italic" || style == "oblique");
|
|
if (is_bold && is_italic)
|
|
font.bold_italic = filename;
|
|
else if (is_bold)
|
|
font.bold = filename;
|
|
else if (is_italic)
|
|
font.italic = filename;
|
|
else
|
|
font.regular = filename;
|
|
}
|
|
}
|
|
|
|
// Resolve a font within one base directory: <base>/<dir-name>/ holding the
|
|
// .ttf files and <base>/<dir-name>.css declaring the @font-face variants.
|
|
static Resolved_font resolve_in_directory(
|
|
const std::string& family_name, const std::string& dir_name, const std::string& base_dir)
|
|
{
|
|
std::string font_dir = base_dir + "/" + dir_name;
|
|
std::string css_file = font_dir + ".css";
|
|
if (fs::exists(font_dir) && fs::exists(css_file)) {
|
|
Resolved_font font {};
|
|
font.family_name = family_name;
|
|
font.dir_name = dir_name;
|
|
font.font_dir = font_dir;
|
|
font.css_file = css_file;
|
|
classify_from_css(font, css_file);
|
|
return font;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
// Every font available for the three @document role parameters: each
|
|
// <name>.css with a matching <name>/ directory, across the installed
|
|
// directories and the default font set. The reported name is the family name
|
|
// from the CSS (what the writer types), falling back to the directory name.
|
|
strings_t available_font_families()
|
|
{
|
|
strings_t result {};
|
|
static const std::regex family_rgx(R"(font-family:\s*'([^']+)')");
|
|
auto scan = [&](const std::string& base) {
|
|
if (!fs::exists(base)) {
|
|
return;
|
|
}
|
|
for (auto& entry : fs::directory_iterator(base)) {
|
|
if (entry.path().extension() == ".css" &&
|
|
fs::is_directory(base + "/" + entry.path().stem().string())) {
|
|
std::string css = string_from_file(entry.path());
|
|
std::smatch match {};
|
|
if (std::regex_search(css, match, family_rgx)) {
|
|
result.push_back(match[1]);
|
|
} else {
|
|
result.push_back(entry.path().stem());
|
|
}
|
|
}
|
|
}
|
|
};
|
|
for (const std::string& dir : installed_font_dirs()) {
|
|
scan(dir);
|
|
}
|
|
scan(default_font_dir());
|
|
std::sort(result.begin(), result.end());
|
|
result.erase(std::unique(result.begin(), result.end()), result.end());
|
|
return result;
|
|
}
|
|
|
|
|
|
static void extract_font_metrics(Resolved_font& font)
|
|
{
|
|
if (font.regular.empty() || font.font_dir.empty())
|
|
return;
|
|
std::string ttf_path = font.font_dir + "/" + font.regular;
|
|
if (!fs::exists(ttf_path))
|
|
return;
|
|
// Extract both x-height and cap-height ratios from OS/2 table
|
|
std::string script =
|
|
"python3 -c \""
|
|
"import struct; "
|
|
"f = open('" + ttf_path + "', 'rb'); "
|
|
"_, n = struct.unpack('>IH', f.read(6)); "
|
|
"f.read(6); "
|
|
"t = {};\n"
|
|
"for _ in range(n):\n"
|
|
" tag = f.read(4).decode('latin-1').strip('\\\\x00'); "
|
|
" _, o, l = struct.unpack('>III', f.read(12)); "
|
|
" t[tag] = o\n"
|
|
"f.seek(t['head'] + 18); "
|
|
"upm = struct.unpack('>H', f.read(2))[0]; "
|
|
"f.seek(t['OS/2']); "
|
|
"ver = struct.unpack('>H', f.read(2))[0]; "
|
|
"f.seek(t['OS/2'] + 86); "
|
|
"xh, ch = struct.unpack('>hh', f.read(4)); "
|
|
"print(f'{xh/upm:.4f} {ch/upm:.4f}') if ver >= 2 else None; "
|
|
"f.close()\"";
|
|
std::string result = trim(exec(script.c_str()));
|
|
if (!result.empty()) {
|
|
try {
|
|
auto pos = result.find(' ');
|
|
if (pos != std::string::npos) {
|
|
font.xheight_ratio = std::stof(result.substr(0, pos));
|
|
font.capheight_ratio = std::stof(result.substr(pos + 1));
|
|
}
|
|
} catch (...) {}
|
|
}
|
|
}
|
|
|
|
Resolved_font resolve_font(const std::string& family_name)
|
|
{
|
|
if (family_name.empty())
|
|
return {};
|
|
|
|
std::string dir_name = name_to_dirname(family_name);
|
|
|
|
// Installed directories in listed order, then the default font set, so an
|
|
// installed font can shadow a default one.
|
|
strings_t bases = installed_font_dirs();
|
|
bases.push_back(default_font_dir());
|
|
for (const std::string& base : bases) {
|
|
Resolved_font font = resolve_in_directory(family_name, dir_name, base);
|
|
if (!font.family_name.empty()) {
|
|
extract_font_metrics(font);
|
|
// A DERIVED value: an installed font may shadow a default one, and
|
|
// which directory won is not readable from ":serif_font Whatever".
|
|
// The commonest font question -- "why does it not look like the
|
|
// one I installed?" -- is answered by this line.
|
|
(void)K::log(1, "Font \"" + family_name + "\": " + base + "/" + dir_name);
|
|
return font;
|
|
}
|
|
}
|
|
|
|
std::stringstream ss {};
|
|
ss << "The font \"" << family_name << "\" is not installed.\n\n"
|
|
<< "Available fonts:\n";
|
|
for (const std::string& name : available_font_families()) {
|
|
ss << " " << name << "\n";
|
|
}
|
|
ss << "\nFonts are searched in the directories of the KLAMMERTEXT_FONTS\n"
|
|
<< "environment variable (colon-separated; default $HOME/.klammertext/fonts)\n"
|
|
<< "and then in the default font set. To install a font,\n"
|
|
<< "place its files as <fonts-dir>/" << dir_name << "/*.ttf with a\n"
|
|
<< "<fonts-dir>/" << dir_name << ".css declaring its @font-face variants.";
|
|
throw Argument_error(ss.str());
|
|
}
|
|
|
|
|
|
// Font file classification: read the family name, weight, and style from
|
|
// the font's internal tables (sfnt 'name', 'OS/2', 'fvar') rather than
|
|
// from filenames, which vary by source (Google zips, foundries, ...).
|
|
|
|
namespace {
|
|
|
|
uint16_t be16(const std::string& d, size_t off)
|
|
{
|
|
return (uint8_t(d[off]) << 8) | uint8_t(d[off + 1]);
|
|
}
|
|
|
|
uint32_t be32(const std::string& d, size_t off)
|
|
{
|
|
return (uint32_t(be16(d, off)) << 16) | be16(d, off + 2);
|
|
}
|
|
|
|
std::string read_binary_file(const std::string& path)
|
|
{
|
|
std::ifstream in(path, std::ios::binary);
|
|
std::stringstream ss {};
|
|
ss << in.rdbuf();
|
|
return ss.str();
|
|
}
|
|
|
|
// Decode a name-table string: UTF-16BE for Windows records (keep the BMP
|
|
// low bytes; family names are almost always Latin), bytes as-is otherwise.
|
|
std::string decode_name(const std::string& raw, bool utf16be)
|
|
{
|
|
std::string result {};
|
|
if (utf16be) {
|
|
for (size_t i = 0; i + 1 < raw.size(); i += 2) {
|
|
if (raw[i] == 0) {
|
|
result += raw[i + 1];
|
|
}
|
|
}
|
|
} else {
|
|
result = raw;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Font_file classify_font_file(const std::string& path)
|
|
{
|
|
Font_file file {};
|
|
file.path = path;
|
|
// Normalize to lowercase so the installed filename, its @font-face url,
|
|
// and the truetype/opentype format detection are uniform regardless of
|
|
// how the source file's extension was capitalized.
|
|
file.extension = lower_extension(path);
|
|
|
|
std::string d = read_binary_file(path);
|
|
if (d.size() < 12) {
|
|
file.note = "not a font file (too short)";
|
|
return file;
|
|
}
|
|
uint32_t tag = be32(d, 0);
|
|
if (tag == 0x74746366) { // 'ttcf'
|
|
file.note = "font collections (.ttc) are not supported; "
|
|
"use the individual font files";
|
|
return file;
|
|
}
|
|
if (tag != 0x00010000 && tag != 0x4F54544F) { // sfnt or 'OTTO'
|
|
file.note = "not a TrueType or OpenType font";
|
|
return file;
|
|
}
|
|
|
|
uint16_t num_tables = be16(d, 4);
|
|
std::map<std::string, std::pair<uint32_t, uint32_t>> tables {}; // tag -> offset,length
|
|
for (uint16_t i = 0; i < num_tables; i++) {
|
|
size_t rec = 12 + i * 16;
|
|
if (rec + 16 > d.size()) {
|
|
break;
|
|
}
|
|
tables[d.substr(rec, 4)] = { be32(d, rec + 8), be32(d, rec + 12) };
|
|
}
|
|
file.variable = tables.contains("fvar");
|
|
|
|
// Family name from the 'name' table: typographic family (16) wins
|
|
// over family (1); Windows records (platform 3) win over Macintosh.
|
|
if (auto it = tables.find("name"); it != tables.end()) {
|
|
size_t base = it->second.first;
|
|
uint16_t count = be16(d, base + 2);
|
|
uint16_t string_offset = be16(d, base + 4);
|
|
int best_rank = -1;
|
|
for (uint16_t i = 0; i < count; i++) {
|
|
size_t rec = base + 6 + i * 12;
|
|
if (rec + 12 > d.size()) {
|
|
break;
|
|
}
|
|
uint16_t platform = be16(d, rec);
|
|
uint16_t name_id = be16(d, rec + 6);
|
|
uint16_t length = be16(d, rec + 8);
|
|
uint16_t offset = be16(d, rec + 10);
|
|
if (name_id != 1 && name_id != 16) {
|
|
continue;
|
|
}
|
|
int rank = (name_id == 16 ? 2 : 0) + (platform == 3 ? 1 : 0);
|
|
size_t at = base + string_offset + offset;
|
|
if (rank > best_rank && at + length <= d.size()) {
|
|
file.family = decode_name(d.substr(at, length), platform == 3);
|
|
best_rank = rank;
|
|
}
|
|
}
|
|
}
|
|
if (file.family.empty()) {
|
|
file.note = "no family name found in the font's name table";
|
|
return file;
|
|
}
|
|
|
|
bool italic = false;
|
|
if (auto it = tables.find("OS/2"); it != tables.end()) {
|
|
size_t base = it->second.first;
|
|
file.weight = be16(d, base + 4);
|
|
italic = be16(d, base + 62) & 0x0001; // fsSelection italic bit
|
|
} else if (auto ht = tables.find("head"); ht != tables.end()) {
|
|
uint16_t mac_style = be16(d, ht->second.first + 44);
|
|
file.weight = (mac_style & 0x0001) ? 700 : 400;
|
|
italic = mac_style & 0x0002;
|
|
}
|
|
|
|
if (file.variable) {
|
|
// A variable font covers the weight axis; use it as the regular
|
|
// (or italic) face and let renderers derive weights.
|
|
file.variant = italic ? "Italic" : "Regular";
|
|
} else if (file.weight >= 380 && file.weight <= 450) {
|
|
file.variant = italic ? "Italic" : "Regular";
|
|
} else if (file.weight >= 650 && file.weight <= 760) {
|
|
file.variant = italic ? "BoldItalic" : "Bold";
|
|
} else {
|
|
std::stringstream note {};
|
|
note << "weight " << file.weight
|
|
<< " not installed (only regular 400 and bold 700 are used)";
|
|
file.note = note.str();
|
|
}
|
|
return file;
|
|
}
|
|
|
|
std::vector<Font_file> classify_font_files(const std::string& directory)
|
|
{
|
|
std::vector<Font_file> result {};
|
|
if (!fs::exists(directory)) {
|
|
throw Argument_error(
|
|
"The font directory \"" + directory + "\" does not exist");
|
|
}
|
|
for (auto& entry : fs::recursive_directory_iterator(directory)) {
|
|
std::string ext = lower_extension(entry.path());
|
|
if (entry.is_regular_file() && (ext == ".ttf" || ext == ".otf")) {
|
|
result.push_back(classify_font_file(entry.path()));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// The css is always generated, never copied, so its urls are relative and
|
|
// the installed pair stays relocatable.
|
|
static std::string font_face_css(
|
|
const std::string& family, const std::string& dir_name,
|
|
const std::map<std::string, const Font_file*>& slots)
|
|
{
|
|
std::stringstream css {};
|
|
auto emit = [&](const std::string& variant,
|
|
const std::string& style, const std::string& weight) {
|
|
auto it = slots.find(variant);
|
|
if (it == slots.end()) {
|
|
return;
|
|
}
|
|
std::string format =
|
|
it->second->extension == ".otf" ? "opentype" : "truetype";
|
|
css << "\n@font-face {\n"
|
|
<< " font-family: '" << family << "';\n"
|
|
<< " font-style: " << style << ";\n"
|
|
<< " font-weight: " << weight << ";\n"
|
|
<< " src: url('" << dir_name << "/" << variant
|
|
<< it->second->extension << "') format('" << format << "');\n"
|
|
<< "}\n";
|
|
};
|
|
emit("Regular", "normal", "400");
|
|
emit("Bold", "normal", "700");
|
|
emit("Italic", "italic", "400");
|
|
emit("BoldItalic", "italic", "700");
|
|
return css.str();
|
|
}
|
|
|
|
std::string install_fonts(const std::string& source_dir, std::string dest_dir)
|
|
{
|
|
if (dest_dir.empty()) {
|
|
strings_t dirs = installed_font_dirs();
|
|
if (dirs.empty()) {
|
|
throw Argument_error(
|
|
"No installation directory: KLAMMERTEXT_FONTS is empty and "
|
|
"HOME is not set");
|
|
}
|
|
dest_dir = dirs[0];
|
|
}
|
|
std::vector<Font_file> files = classify_font_files(source_dir);
|
|
if (files.empty()) {
|
|
throw Argument_error(
|
|
"No font files (.ttf or .otf) found under \"" + source_dir + "\"");
|
|
}
|
|
|
|
// Choose one file per (family, variant) slot; a static face wins over
|
|
// a variable font's derived face.
|
|
std::map<std::string, std::map<std::string, const Font_file*>> families {};
|
|
std::stringstream report {};
|
|
for (const Font_file& file : files) {
|
|
if (file.variant.empty()) {
|
|
report << " skipped " << fs::path(file.path).filename().string()
|
|
<< ": " << file.note << "\n";
|
|
continue;
|
|
}
|
|
auto& slots = families[file.family];
|
|
auto it = slots.find(file.variant);
|
|
if (it == slots.end() || (it->second->variable && !file.variable)) {
|
|
slots[file.variant] = &file;
|
|
}
|
|
}
|
|
|
|
for (auto& [family, slots] : families) {
|
|
std::string dir_name = name_to_dirname(family);
|
|
std::string family_dir = dest_dir + "/" + dir_name;
|
|
fs::create_directories(family_dir);
|
|
strings_t variants {};
|
|
for (auto& [variant, file] : slots) {
|
|
copy_file_stream(file->path,
|
|
family_dir + "/" + variant + file->extension);
|
|
variants.push_back(variant + (file->variable ? " (variable)" : ""));
|
|
}
|
|
string_to_file(dest_dir + "/" + dir_name + ".css",
|
|
font_face_css(family, dir_name, slots));
|
|
report << " installed \"" << family << "\" (" << join(variants, ", ")
|
|
<< ") in " << family_dir << "\n";
|
|
}
|
|
return report.str();
|
|
}
|
|
|
|
std::string describe_fonts()
|
|
{
|
|
std::stringstream ss {};
|
|
std::set<std::string> seen {};
|
|
strings_t bases = installed_font_dirs();
|
|
bases.push_back(default_font_dir());
|
|
for (size_t i = 0; i < bases.size(); i++) {
|
|
const std::string& base = bases[i];
|
|
bool is_default = (i == bases.size() - 1);
|
|
ss << base << (is_default ? " (default font set)" : "") << ":\n";
|
|
if (!fs::exists(base)) {
|
|
ss << " [directory does not exist]\n";
|
|
continue;
|
|
}
|
|
strings_t names {};
|
|
for (auto& entry : fs::directory_iterator(base)) {
|
|
if (entry.path().extension() == ".css" &&
|
|
fs::is_directory(base + "/" + entry.path().stem().string())) {
|
|
names.push_back(entry.path().stem());
|
|
}
|
|
}
|
|
std::sort(names.begin(), names.end());
|
|
if (names.empty()) {
|
|
ss << " [no fonts]\n";
|
|
}
|
|
for (const std::string& dir_name : names) {
|
|
Resolved_font font = resolve_in_directory("?", dir_name, base);
|
|
std::string css = string_from_file(font.css_file);
|
|
std::smatch match {};
|
|
std::string family = dir_name;
|
|
if (std::regex_search(css, match,
|
|
std::regex(R"(font-family:\s*'([^']+)')"))) {
|
|
family = match[1];
|
|
}
|
|
strings_t variants {};
|
|
if (!font.regular.empty()) variants.push_back("Regular");
|
|
if (!font.bold.empty()) variants.push_back("Bold");
|
|
if (!font.italic.empty()) variants.push_back("Italic");
|
|
if (!font.bold_italic.empty()) variants.push_back("BoldItalic");
|
|
ss << " " << family << " (" << join(variants, ", ") << ")";
|
|
if (seen.contains(dir_name)) {
|
|
ss << " [shadowed by an earlier directory]";
|
|
}
|
|
seen.insert(dir_name);
|
|
ss << "\n";
|
|
}
|
|
}
|
|
return ss.str();
|
|
}
|
|
|
|
std::string write_font_samples(const std::string& output_dir, const std::string& source_dir)
|
|
{
|
|
fs::create_directories(output_dir);
|
|
strings_t families {};
|
|
if (source_dir.empty()) {
|
|
for (const std::string& family : available_font_families()) {
|
|
install_resolved_font(resolve_font(family), output_dir);
|
|
families.push_back(family);
|
|
}
|
|
} else {
|
|
// Uninstalled preview: install the classified fonts directly into
|
|
// the sample page's own fonts directory.
|
|
install_fonts(source_dir, output_dir + "/fonts");
|
|
for (auto& entry : fs::directory_iterator(output_dir + "/fonts")) {
|
|
if (entry.path().extension() == ".css") {
|
|
std::string css = string_from_file(entry.path());
|
|
std::smatch match {};
|
|
if (std::regex_search(css, match,
|
|
std::regex(R"(font-family:\s*'([^']+)')"))) {
|
|
families.push_back(match[1]);
|
|
}
|
|
}
|
|
}
|
|
std::sort(families.begin(), families.end());
|
|
}
|
|
|
|
std::stringstream html {};
|
|
html << "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n"
|
|
<< "<meta charset=\"UTF-8\">\n<title>Klammertext font samples</title>\n";
|
|
for (const std::string& family : families) {
|
|
html << "<link href=\"fonts/" << name_to_dirname(family)
|
|
<< ".css\" rel=\"stylesheet\">\n";
|
|
}
|
|
html << "<style>\n"
|
|
<< "body { margin: 2rem auto; max-width: 46rem; font-family: sans-serif; }\n"
|
|
<< "h2 { border-bottom: 1px solid #999; margin-top: 2.5rem; }\n"
|
|
<< ".sample { font-size: 1.3rem; margin: .3rem 0; }\n"
|
|
<< ".alphabet { font-size: 1.0rem; color: #333; margin: .3rem 0; }\n"
|
|
<< "</style>\n</head>\n<body>\n<h1>Klammertext font samples</h1>\n";
|
|
for (const std::string& family : families) {
|
|
html << "<h2>" << family << "</h2>\n"
|
|
<< "<div style=\"font-family: '" << family << "'\">\n"
|
|
<< "<p class=\"sample\">The quick brown fox jumps over the lazy dog.</p>\n"
|
|
<< "<p class=\"sample\" style=\"font-style: italic\">"
|
|
<< "The quick brown fox jumps over the lazy dog.</p>\n"
|
|
<< "<p class=\"sample\" style=\"font-weight: bold\">"
|
|
<< "The quick brown fox jumps over the lazy dog.</p>\n"
|
|
<< "<p class=\"sample\" style=\"font-weight: bold; font-style: italic\">"
|
|
<< "The quick brown fox jumps over the lazy dog.</p>\n"
|
|
<< "<p class=\"alphabet\">ABCDEFGHIJKLMNOPQRSTUVWXYZ "
|
|
<< "abcdefghijklmnopqrstuvwxyz 0123456789 "
|
|
<< "äöüß “quoted” 3.14159</p>\n"
|
|
<< "</div>\n";
|
|
}
|
|
html << "</body>\n</html>\n";
|
|
std::string index = output_dir + "/index.html";
|
|
string_to_file(index, html.str());
|
|
return index;
|
|
}
|
|
|
|
// Font assets are copied with copy_file_stream() (mac/file.h) rather than
|
|
// std::filesystem::copy_file, which fails on Apple `container` virtiofs mounts
|
|
// — see the note on copy_file_stream() in file.cpp for the full rationale.
|
|
|
|
|
|
void install_resolved_font(const Resolved_font& font, std::string output_dir)
|
|
{
|
|
if (font.family_name.empty())
|
|
return;
|
|
|
|
std::string output_font_dir = output_dir + "/fonts";
|
|
if (!fs::exists(output_font_dir))
|
|
fs::create_directory(output_font_dir);
|
|
|
|
// Copy .css file and font directory to output
|
|
std::string dest_css = output_font_dir + "/" + font.dir_name + ".css";
|
|
std::string dest_dir = output_font_dir + "/" + font.dir_name;
|
|
|
|
copy_file_stream(font.css_file, dest_css);
|
|
|
|
if (!fs::exists(dest_dir)) {
|
|
fs::create_directory(dest_dir);
|
|
for (auto& entry : fs::directory_iterator(font.font_dir)) {
|
|
copy_file_stream(entry.path(),
|
|
dest_dir + "/" + entry.path().filename().string());
|
|
}
|
|
}
|
|
}
|