330 lines
10 KiB
C++
330 lines
10 KiB
C++
|
|
#include "font_resolve.h"
|
||
|
|
#include "file.h"
|
||
|
|
#include "util.h"
|
||
|
|
#include "error.h"
|
||
|
|
#include "log.h"
|
||
|
|
#include "show.h"
|
||
|
|
#include "kutil.h"
|
||
|
|
|
||
|
|
#include <algorithm>
|
||
|
|
#include <regex>
|
||
|
|
#include <sstream>
|
||
|
|
#include <filesystem>
|
||
|
|
|
||
|
|
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<Font_variant> 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());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|