#include "font_resolve.h" #include "file.h" #include "util.h" #include "error.h" #include "log.h" #include "show.h" #include "kutil.h" #include #include #include #include namespace fs = std::filesystem; std::string name_to_dirname(std::string name) { std::string result {}; for (char c : name) { if (c == ' ') result += '-'; else result += std::tolower(c); } return result; } std::string name_to_google_query(std::string name) { std::string result {}; for (char c : name) { if (c == ' ') result += '+'; else result += c; } return result; } 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; } } static Resolved_font resolve_bundled(std::string family_name, std::string dir_name) { std::string bundled_dir = klammertext_dir() + "/sks/font/fonts/" + dir_name; std::string bundled_css = bundled_dir + ".css"; if (fs::exists(bundled_dir) && fs::exists(bundled_css)) { Resolved_font font {}; font.family_name = family_name; font.dir_name = dir_name; font.font_dir = bundled_dir; font.css_file = bundled_css; font.from_cache = false; classify_from_css(font, bundled_css); return font; } return {}; } static Resolved_font resolve_cached(std::string family_name, std::string dir_name) { std::string cache_base = cache_directory("_fonts"); std::string cache_dir = cache_base + "/" + dir_name; std::string cache_css = cache_base + "/" + dir_name + ".css"; if (fs::exists(cache_dir) && fs::exists(cache_css)) { Resolved_font font {}; font.family_name = family_name; font.dir_name = dir_name; font.font_dir = cache_dir; font.css_file = cache_css; font.from_cache = true; classify_from_css(font, cache_css); return font; } return {}; } static Resolved_font fetch_google_font(std::string family_name, std::string dir_name) { (void)K::log(1, "Fetching font \"" + family_name + "\" from Google Fonts"); std::string query = name_to_google_query(family_name); std::string url = "https://fonts.googleapis.com/css2?family=" + query + ":ital,wght@0,400;0,700;1,400;1,700&display=swap"; std::string cmd = "curl -s -H 'User-Agent: Mozilla/4.0' '" + url + "'"; std::string css_response = exec(cmd.c_str()); if (css_response.empty() || css_response.find("@font-face") == std::string::npos) { return {}; } // Parse @font-face blocks to extract style, weight, and .ttf URL struct Font_variant { std::string style; // "normal" or "italic" std::string weight; // "400" or "700" std::string url; }; std::vector variants {}; std::regex face_rgx( R"(@font-face\s*\{[^}]*font-style:\s*(\w+);[^}]*font-weight:\s*(\d+);[^}]*src:\s*url\((https?://[^)]+\.ttf)\)[^}]*\})", std::regex::multiline); auto begin = std::sregex_iterator(css_response.begin(), css_response.end(), face_rgx); auto end = std::sregex_iterator(); for (auto it = begin; it != end; ++it) { Font_variant v {}; v.style = (*it)[1]; v.weight = (*it)[2]; v.url = (*it)[3]; variants.push_back(v); } if (variants.empty()) { return {}; } // Create cache directory std::string cache_base = cache_directory("_fonts"); if (!fs::exists(cache_base)) fs::create_directories(cache_base); std::string cache_dir = cache_base + "/" + dir_name; if (!fs::exists(cache_dir)) fs::create_directory(cache_dir); Resolved_font font {}; font.family_name = family_name; font.dir_name = dir_name; font.font_dir = cache_dir; font.from_cache = true; // Download each .ttf file with descriptive names for (auto& v : variants) { std::string local_name; if (v.style == "normal" && v.weight == "400") local_name = "Regular.ttf"; else if (v.style == "normal" && v.weight == "700") local_name = "Bold.ttf"; else if (v.style == "italic" && v.weight == "400") local_name = "Italic.ttf"; else if (v.style == "italic" && v.weight == "700") local_name = "BoldItalic.ttf"; else continue; std::string ttf_path = cache_dir + "/" + local_name; if (!fs::exists(ttf_path)) { std::string dl_cmd = "curl -s -o '" + ttf_path + "' '" + v.url + "'"; (void)exec(dl_cmd.c_str()); if (!fs::exists(ttf_path)) { (void)K::log(1, "Failed to download font file: " + v.url); continue; } } if (local_name == "Regular.ttf") font.regular = local_name; else if (local_name == "Bold.ttf") font.bold = local_name; else if (local_name == "Italic.ttf") font.italic = local_name; else if (local_name == "BoldItalic.ttf") font.bold_italic = local_name; } // Generate @font-face CSS file std::string css_file = cache_base + "/" + dir_name + ".css"; std::stringstream css {}; auto emit_face = [&](std::string style, std::string weight, std::string filename) { if (filename.empty()) return; css << "\n@font-face {\n" << " font-family: '" << family_name << "';\n" << " font-style: " << style << ";\n" << " font-weight: " << weight << ";\n" << " src: url('" << dir_name << "/" << filename << "') format('truetype');\n" << "}\n"; }; emit_face("normal", "400", font.regular); emit_face("normal", "700", font.bold); emit_face("italic", "400", font.italic); emit_face("italic", "700", font.bold_italic); string_to_file(css_file, css.str()); font.css_file = css_file; return font; } 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(std::string family_name) { if (family_name.empty()) return {}; std::string dir_name = name_to_dirname(family_name); // 1. Check bundled fonts Resolved_font font = resolve_bundled(family_name, dir_name); if (!font.family_name.empty()) { extract_font_metrics(font); return font; } // 2. Check font cache font = resolve_cached(family_name, dir_name); if (!font.family_name.empty()) { extract_font_metrics(font); return font; } // 3. Fetch from Google Fonts font = fetch_google_font(family_name, dir_name); if (!font.family_name.empty()) { extract_font_metrics(font); return font; } // 4. Error throw Argument_error( "Font \"" + family_name + "\" not found.\n" " Not bundled in sks/font/fonts/" + dir_name + "/,\n" " not cached, and not available from Google Fonts.\n" " Check the font name or install it locally."); } // 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()); } } }