181 lines
6.5 KiB
C++
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
|