255 lines
9.6 KiB
C++
255 lines
9.6 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
|
//
|
|
// GlyphUnicode.cpp — Spike B step 1 implementation. Walks a PDF's fonts via
|
|
// QPDF, finds one with a ToUnicode CMap and an embedded font stream, parses
|
|
// the ToUnicode, and falls back to the embedded font's cmap via FreeType.
|
|
//
|
|
// The ToUnicode CMap format (PDF spec §7.9.2) is a small declarative language:
|
|
// /CIDInit /ProcSet findresource begin
|
|
// 12 dict begin begincmap
|
|
// /CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def
|
|
// /CMapName /Adobe-Identity-UCS def
|
|
// /CMapType 2 def
|
|
// 1 begincodespacerange <00> <FF>
|
|
// 1 beginbfchar <41> <0041>
|
|
// endbfchar
|
|
// endcmap CMapName currentdict /CMap defineresource pop end end
|
|
// We parse just the beginbfchar/begincodespacerange sections to build a
|
|
// code→Unicode map. The production parser (M4) handles the full grammar
|
|
// (bfrange, CID-keyed fonts, surrogate pairs); this spike covers the common
|
|
// bfchar case which is what most simple-font ToUnicode CMaps use.
|
|
|
|
#include "GlyphUnicode.h"
|
|
|
|
#include <qpdf/QPDF.hh>
|
|
#include <qpdf/QPDFObjectHandle.hh>
|
|
#include <qpdf/QPDFPageDocumentHelper.hh>
|
|
|
|
#include <ft2build.h>
|
|
#include FT_FREETYPE_H
|
|
#include FT_FONT_FORMATS_H
|
|
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
namespace freepdfeditor::spike::b1 {
|
|
|
|
namespace {
|
|
|
|
// Parse a hex string like "41" or "0041" into a uint32. Returns false on
|
|
// malformed input.
|
|
bool parse_hex(const std::string& s, std::uint32_t& out)
|
|
{
|
|
out = 0;
|
|
if (s.empty()) return false;
|
|
for (char c : s) {
|
|
out <<= 4;
|
|
if (c >= '0' && c <= '9') out |= std::uint32_t(c - '0');
|
|
else if (c >= 'a' && c <= 'f') out |= std::uint32_t(c - 'a' + 10);
|
|
else if (c >= 'A' && c <= 'F') out |= std::uint32_t(c - 'A' + 10);
|
|
else return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Extract the raw bytes of a PDF stream object (the decoded stream data).
|
|
std::string stream_bytes(QPDFObjectHandle stream)
|
|
{
|
|
if (!stream.isStream()) return {};
|
|
auto buf = stream.getStreamData();
|
|
if (!buf) return {};
|
|
return std::string(reinterpret_cast<const char*>(buf->getBuffer()),
|
|
buf->getSize());
|
|
}
|
|
|
|
// Parse the bfchar/bfrange sections of a ToUnicode CMap into a code→Unicode
|
|
// map. Returns the number of mappings found.
|
|
std::size_t parse_tounicode(const std::string& cmap_data,
|
|
std::unordered_map<std::uint32_t, std::uint32_t>& out)
|
|
{
|
|
std::istringstream in(cmap_data);
|
|
std::string tok;
|
|
std::size_t found = 0;
|
|
while (in >> tok) {
|
|
if (tok == "beginbfchar") {
|
|
// Read count then count pairs of <code> <unicode>.
|
|
// We already consumed the count token before beginbfchar in real
|
|
// PDFs, but the count comes BEFORE beginbfchar. Re-read: the
|
|
// grammar is "N beginbfchar ... endbfchar" where N is the count.
|
|
// We hit beginbfchar without the count, so the count was the
|
|
// previous token. Simplest: read pairs until endbfchar.
|
|
while (in >> tok && tok != "endbfchar") {
|
|
// tok is <code>; next is <unicode>.
|
|
if (tok.empty() || tok[0] != '<') { continue; }
|
|
std::string code_hex = tok.substr(1, tok.find('>') - 1);
|
|
std::string uni_hex;
|
|
if (!(in >> uni_hex)) break;
|
|
if (uni_hex.empty() || uni_hex[0] != '<') continue;
|
|
uni_hex = uni_hex.substr(1, uni_hex.find('>') - 1);
|
|
std::uint32_t code = 0, uni = 0;
|
|
if (parse_hex(code_hex, code) && parse_hex(uni_hex, uni)) {
|
|
out[code] = uni;
|
|
++found;
|
|
}
|
|
}
|
|
}
|
|
// bfrange parsing is omitted (M4 work); bfchar covers the common case.
|
|
}
|
|
return found;
|
|
}
|
|
|
|
// Read the embedded font stream (FontFile2 for TrueType) into bytes.
|
|
std::string embedded_font_bytes(QPDFObjectHandle font_obj)
|
|
{
|
|
QPDFObjectHandle desc = font_obj.getKey("/FontDescriptor");
|
|
if (!desc.isDictionary()) return {};
|
|
for (const char* key : {"/FontFile2", "/FontFile3", "/FontFile"}) {
|
|
QPDFObjectHandle ff = desc.getKey(key);
|
|
if (ff.isStream()) return stream_bytes(ff);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
MappingResult map_glyphs(const std::string& pdf_path)
|
|
{
|
|
MappingResult r{};
|
|
|
|
QPDF q;
|
|
try {
|
|
const std::string path = pdf_path;
|
|
q.processFile(path.c_str());
|
|
} catch (const std::exception& e) {
|
|
r.error = std::string("QPDF open failed: ") + e.what();
|
|
return r;
|
|
}
|
|
|
|
QPDFPageDocumentHelper helper(q);
|
|
auto pages = helper.getAllPages();
|
|
|
|
// Walk all pages' font resources to find a font with both a ToUnicode CMap
|
|
// and an embedded font stream — the case that exercises both ladder steps.
|
|
QPDFObjectHandle target_font;
|
|
QPDFObjectHandle target_tounicode;
|
|
std::string embedded_font;
|
|
for (auto& page : pages) {
|
|
QPDFObjectHandle resources = page.getAttribute("/Resources", true);
|
|
if (!resources.isDictionary()) continue;
|
|
QPDFObjectHandle fonts = resources.getKey("/Font");
|
|
if (!fonts.isDictionary()) continue;
|
|
for (auto& kv : fonts.getDictAsMap()) {
|
|
QPDFObjectHandle font = kv.second;
|
|
if (!font.isDictionary()) continue;
|
|
QPDFObjectHandle tn = font.getKey("/ToUnicode");
|
|
std::string font_bytes = embedded_font_bytes(font);
|
|
if (tn.isStream() && !font_bytes.empty()) {
|
|
target_font = font;
|
|
target_tounicode = tn;
|
|
embedded_font = std::move(font_bytes);
|
|
break;
|
|
}
|
|
}
|
|
if (target_font.isDictionary()) break;
|
|
}
|
|
|
|
if (!target_font.isDictionary()) {
|
|
r.error = "no font with both ToUnicode and embedded stream found";
|
|
return r;
|
|
}
|
|
|
|
// Step 1: parse the ToUnicode CMap.
|
|
std::string tounicode_data = stream_bytes(target_tounicode);
|
|
std::unordered_map<std::uint32_t, std::uint32_t> code_to_uni;
|
|
std::size_t tn_count = parse_tounicode(tounicode_data, code_to_uni);
|
|
r.mapped_via_tounicode = tn_count;
|
|
|
|
// Step 4: for glyphs the ToUnicode didn't cover, fall back to the embedded
|
|
// font's cmap via FreeType. Build a gid→unicode map from FreeType's cmap.
|
|
std::unordered_map<std::uint32_t, std::uint32_t> cmap_gid_to_uni;
|
|
if (!embedded_font.empty()) {
|
|
FT_Library lib = nullptr;
|
|
if (FT_Init_FreeType(&lib) == 0) {
|
|
FT_Face face = nullptr;
|
|
if (FT_New_Memory_Face(lib,
|
|
reinterpret_cast<const FT_Byte*>(embedded_font.data()),
|
|
static_cast<FT_Long>(embedded_font.size()), 0, &face) == 0) {
|
|
// Walk the font's cmap: for each codepoint FreeType can map to
|
|
// a glyph, record gid→codepoint (reverse of the usual lookup,
|
|
// which is what the §4.1 step 4 "reverse lookup" describes).
|
|
// FT_Get_First_Char returns the charcode and writes the gid.
|
|
FT_UInt gid = 0;
|
|
FT_ULong charcode = FT_Get_First_Char(face, &gid);
|
|
while (gid != 0 && charcode != 0) {
|
|
cmap_gid_to_uni[gid] = static_cast<std::uint32_t>(charcode);
|
|
charcode = FT_Get_Next_Char(face, charcode, &gid);
|
|
}
|
|
FT_Done_Face(face);
|
|
}
|
|
FT_Done_FreeType(lib);
|
|
}
|
|
}
|
|
|
|
// Assemble the per-glyph mapping. The glyph count comes from FreeType;
|
|
// the ToUnicode map is keyed by character code, which for simple fonts is
|
|
// often the gid (Identity encoding) — we record both and prefer ToUnicode.
|
|
std::size_t total_glyphs = 0;
|
|
if (!embedded_font.empty()) {
|
|
FT_Library lib = nullptr;
|
|
if (FT_Init_FreeType(&lib) == 0) {
|
|
FT_Face face = nullptr;
|
|
if (FT_New_Memory_Face(lib,
|
|
reinterpret_cast<const FT_Byte*>(embedded_font.data()),
|
|
static_cast<FT_Long>(embedded_font.size()), 0, &face) == 0) {
|
|
total_glyphs = static_cast<std::size_t>(face->num_glyphs);
|
|
FT_Done_Face(face);
|
|
}
|
|
FT_Done_FreeType(lib);
|
|
}
|
|
}
|
|
|
|
std::size_t via_cmap = 0, unmapped = 0;
|
|
double conf_sum = 0.0;
|
|
for (std::uint32_t gid = 0; gid < total_glyphs; ++gid) {
|
|
GlyphMap m;
|
|
m.gid = gid;
|
|
// Step 1: ToUnicode (keyed by code; for simple fonts code == gid here).
|
|
auto it = code_to_uni.find(gid);
|
|
if (it != code_to_uni.end()) {
|
|
m.unicode = it->second;
|
|
m.confidence = 1.0f;
|
|
m.source = GlyphMap::Source::ToUnicode;
|
|
} else {
|
|
// Step 4: reverse lookup through the cmap.
|
|
auto cit = cmap_gid_to_uni.find(gid);
|
|
if (cit != cmap_gid_to_uni.end()) {
|
|
m.unicode = cit->second;
|
|
m.confidence = 0.7f;
|
|
m.source = GlyphMap::Source::Cmap;
|
|
++via_cmap;
|
|
} else {
|
|
m.unicode = 0;
|
|
m.confidence = 0.0f;
|
|
m.source = GlyphMap::Source::Unknown;
|
|
++unmapped;
|
|
}
|
|
}
|
|
if (m.unicode) conf_sum += m.confidence;
|
|
r.mappings.push_back(m);
|
|
}
|
|
r.mapped_via_cmap = via_cmap;
|
|
r.unmapped = unmapped;
|
|
r.coverage = total_glyphs ? double(total_glyphs - unmapped) / double(total_glyphs) : 0.0;
|
|
r.avg_confidence = (total_glyphs - unmapped) ? conf_sum / double(total_glyphs - unmapped) : 0.0;
|
|
r.ok = total_glyphs > 0;
|
|
return r;
|
|
}
|
|
|
|
} // namespace freepdfeditor::spike::b1
|