// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors // // SubsetGrowth.cpp — Spike C implementation using the HarfBuzz subset C API // and FreeType to verify the grown subset. #include "SubsetGrowth.h" #include #include #include #include FT_FREETYPE_H #include FT_OUTLINE_H #include #include #include #include #include #include #include namespace freepdfeditor::spike::c { namespace { // Read a whole file into a byte buffer. bool read_file(const std::string& path, std::vector& out) { std::ifstream in(path, std::ios::binary); if (!in) return false; std::ostringstream ss; ss << in.rdbuf(); std::string s = ss.str(); out.assign(s.begin(), s.end()); return true; } // Run hb-subset over `font_bytes` keeping only the codepoints in `chars`. // Returns the subsetted font bytes, or empty on failure. Per §4.2, growing a // subset produces a *new* font object with a new subset tag — we never mutate // the original. std::vector subset_for(const std::vector& font_bytes, const std::vector& chars) { hb_blob_t* blob = hb_blob_create( reinterpret_cast(font_bytes.data()), static_cast(font_bytes.size()), HB_MEMORY_MODE_READONLY, nullptr, nullptr); hb_face_t* face = hb_face_create(blob, 0); hb_subset_input_t* input = hb_subset_input_create_or_fail(); if (!input) { hb_face_destroy(face); hb_blob_destroy(blob); return {}; } // The default "retain all glyphs" set is too broad; clear it and add only // the codepoints we want, plus .notdef (gid 0) which every subset needs. // (HarfBuzz 10.x retains .notdef by default via the input's no-subset flags.) hb_set_t* unicode = hb_subset_input_unicode_set(input); hb_set_clear(unicode); for (hb_codepoint_t cp : chars) hb_set_add(unicode, cp); hb_face_t* subset = hb_subset_or_fail(face, input); std::vector out; if (subset) { hb_blob_t* result_blob = hb_face_reference_blob(subset); unsigned int length = 0; const char* data = hb_blob_get_data(result_blob, &length); if (data && length > 0) { out.assign(reinterpret_cast(data), reinterpret_cast(data) + length); } hb_blob_destroy(result_blob); hb_face_destroy(subset); } hb_subset_input_destroy(input); hb_face_destroy(face); hb_blob_destroy(blob); return out; } // Count glyphs in a font via FreeType (the number of entries in the cmap is a // proxy for the subset's coverage; the glyph count is the max gid + 1). std::size_t count_glyphs(const std::vector& font_bytes) { FT_Library lib = nullptr; if (FT_Init_FreeType(&lib) != 0) return 0; FT_Face face = nullptr; if (FT_New_Memory_Face(lib, font_bytes.data(), static_cast(font_bytes.size()), 0, &face) != 0) { FT_Done_FreeType(lib); return 0; } std::size_t n = static_cast(face->num_glyphs); FT_Done_Face(face); FT_Done_FreeType(lib); return n; } // Look up a codepoint → glyph id via the cmap, then try to load that glyph's // outline (proving it renders, not just that the cmap maps it). bool glyph_renders(const std::vector& font_bytes, hb_codepoint_t cp) { FT_Library lib = nullptr; if (FT_Init_FreeType(&lib) != 0) return false; FT_Face face = nullptr; bool ok = false; if (FT_New_Memory_Face(lib, font_bytes.data(), static_cast(font_bytes.size()), 0, &face) == 0) { FT_UInt gid = FT_Get_Char_Index(face, cp); if (gid != 0) { // Load the glyph outline (no scaling — we only care that it exists). if (FT_Load_Glyph(face, gid, FT_LOAD_NO_SCALE | FT_LOAD_NO_BITMAP) == 0) { ok = face->glyph->format == FT_GLYPH_FORMAT_OUTLINE || face->glyph->format == FT_GLYPH_FORMAT_BITMAP; } } FT_Done_Face(face); } FT_Done_FreeType(lib); return ok; } } // namespace GrowthResult grow_subset(const std::string& font_path) { GrowthResult r{}; std::vector full_font; if (!read_file(font_path, full_font)) { r.error = "could not read full font: " + font_path; return r; } // Step 1: the "originally embedded" subset — glyphs for "Hello". // Use the actual ASCII codepoints (H=0x48, e=0x65, l=0x6C, o=0x6F). std::vector initial_chars = { 0x48, 0x65, 0x6C, 0x6F // H, e, l, o (deduplicated by hb-set) }; auto initial_subset = subset_for(full_font, initial_chars); if (initial_subset.empty()) { r.error = "initial subset failed"; return r; } r.original_glyph_count = count_glyphs(initial_subset); r.original_font_bytes = initial_subset.size(); // Step 2: grow by adding U+03A9 (GREEK CAPITAL LETTER OMEGA) — the // character the user typed that wasn't in the original subset. Per §4.2 // this produces a *new* font object; the original subset is untouched. std::vector grown_chars = initial_chars; grown_chars.push_back(0x03A9); // Ω auto grown_subset = subset_for(full_font, grown_chars); if (grown_subset.empty()) { r.error = "grown subset failed"; return r; } r.grown_glyph_count = count_glyphs(grown_subset); r.grown_font_bytes = grown_subset.size(); // Step 3: verify. The grown subset must contain the new glyph and it must // render (FreeType can load its outline). r.new_glyph_present = glyph_renders(grown_subset, 0x03A9); r.new_glyph_renders = r.new_glyph_present; // glyph_renders checks outline // And the original subset must NOT contain it (we didn't mutate it). bool original_has_omega = glyph_renders(initial_subset, 0x03A9); r.ok = r.new_glyph_present && r.new_glyph_renders && !original_has_omega; if (!r.ok) { if (original_has_omega) { r.error = "original subset already had the glyph — growth not needed?"; } else if (!r.new_glyph_present) { r.error = "grown subset does not contain U+03A9"; } else if (!r.new_glyph_renders) { r.error = "new glyph present but does not render (no outline)"; } } return r; } } // namespace freepdfeditor::spike::c