freepdfeditor/spike/C_subset_growth/SubsetGrowth.cpp

181 lines
6.5 KiB
C++

// 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 <hb.h>
#include <hb-subset.h>
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_OUTLINE_H
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
namespace freepdfeditor::spike::c {
namespace {
// Read a whole file into a byte buffer.
bool read_file(const std::string& path, std::vector<unsigned char>& 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<unsigned char> subset_for(const std::vector<unsigned char>& font_bytes,
const std::vector<hb_codepoint_t>& chars)
{
hb_blob_t* blob = hb_blob_create(
reinterpret_cast<const char*>(font_bytes.data()),
static_cast<unsigned int>(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<unsigned char> 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<const unsigned char*>(data),
reinterpret_cast<const unsigned char*>(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<unsigned char>& 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<FT_Long>(font_bytes.size()), 0, &face) != 0) {
FT_Done_FreeType(lib);
return 0;
}
std::size_t n = static_cast<std::size_t>(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<unsigned char>& 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<FT_Long>(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<unsigned char> 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<hb_codepoint_t> 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<hb_codepoint_t> 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