1514 lines
56 KiB
C++
1514 lines
56 KiB
C++
#include "parser/pdfium_document.hpp"
|
|
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
#include <fpdfview.h>
|
|
#include <fpdf_text.h>
|
|
#include <fpdf_save.h>
|
|
#include <fpdf_doc.h>
|
|
#include <fpdf_edit.h>
|
|
#include <fpdf_annot.h>
|
|
#include <png.h>
|
|
#include "parser/pdfium_loader.hpp"
|
|
#endif
|
|
|
|
#include <nlohmann/json.hpp>
|
|
#include <spdlog/spdlog.h>
|
|
#include <csetjmp>
|
|
|
|
namespace pdfengine::parser {
|
|
|
|
std::vector<unsigned short> utf8_to_utf16le(const std::string& utf8) {
|
|
std::vector<unsigned short> utf16;
|
|
utf16.reserve(utf8.size());
|
|
for (size_t i = 0; i < utf8.size(); ) {
|
|
unsigned char c = utf8[i];
|
|
unsigned int cp = 0;
|
|
size_t extra = 0;
|
|
if (c < 0x80) { cp = c; extra = 0; }
|
|
else if ((c & 0xE0) == 0xC0) { cp = c & 0x1F; extra = 1; }
|
|
else if ((c & 0xF0) == 0xE0) { cp = c & 0x0F; extra = 2; }
|
|
else if ((c & 0xF8) == 0xF0) { cp = c & 0x07; extra = 3; }
|
|
else { i++; continue; }
|
|
|
|
if (i + extra >= utf8.size()) break;
|
|
bool invalid = false;
|
|
for (size_t j = 1; j <= extra; ++j) {
|
|
unsigned char next = utf8[i + j];
|
|
if ((next & 0xC0) != 0x80) { invalid = true; break; }
|
|
cp = (cp << 6) | (next & 0x3F);
|
|
}
|
|
if (invalid) { i++; continue; }
|
|
i += 1 + extra;
|
|
if (cp < 0x10000) {
|
|
utf16.push_back(static_cast<unsigned short>(cp));
|
|
} else {
|
|
cp -= 0x10000;
|
|
utf16.push_back(static_cast<unsigned short>((cp >> 10) + 0xD800));
|
|
utf16.push_back(static_cast<unsigned short>((cp & 0x3FF) + 0xDC00));
|
|
}
|
|
}
|
|
utf16.push_back(0);
|
|
return utf16;
|
|
}
|
|
|
|
std::string utf16le_to_utf8(const char16_t* utf16, size_t length) {
|
|
std::string utf8;
|
|
for (size_t i = 0; i < length; ++i) {
|
|
char16_t c = utf16[i];
|
|
if (c == 0) break;
|
|
|
|
uint32_t cp = c;
|
|
if (c >= 0xD800 && c <= 0xDBFF) { // High surrogate
|
|
if (i + 1 < length) {
|
|
char16_t low = utf16[i + 1];
|
|
if (low >= 0xDC00 && low <= 0xDFFF) { // Low surrogate
|
|
cp = 0x10000 + (((c - 0xD800) << 10) | (low - 0xDC00));
|
|
i++; // Consume the low surrogate
|
|
}
|
|
}
|
|
}
|
|
|
|
if (cp < 0x80) {
|
|
utf8 += static_cast<char>(cp);
|
|
} else if (cp < 0x800) {
|
|
utf8 += static_cast<char>(0xC0 | (cp >> 6));
|
|
utf8 += static_cast<char>(0x80 | (cp & 0x3F));
|
|
} else if (cp < 0x10000) {
|
|
utf8 += static_cast<char>(0xE0 | (cp >> 12));
|
|
utf8 += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
|
utf8 += static_cast<char>(0x80 | (cp & 0x3F));
|
|
} else {
|
|
utf8 += static_cast<char>(0xF0 | (cp >> 18));
|
|
utf8 += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
|
utf8 += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
|
utf8 += static_cast<char>(0x80 | (cp & 0x3F));
|
|
}
|
|
}
|
|
return utf8;
|
|
}
|
|
|
|
std::string code_point_to_utf8(unsigned int cp) {
|
|
std::string utf8;
|
|
if (cp == 0) return "";
|
|
if (cp < 0x80) {
|
|
utf8 += static_cast<char>(cp);
|
|
} else if (cp < 0x800) {
|
|
utf8 += static_cast<char>(0xC0 | (cp >> 6));
|
|
utf8 += static_cast<char>(0x80 | (cp & 0x3F));
|
|
} else if (cp < 0x10000) {
|
|
utf8 += static_cast<char>(0xE0 | (cp >> 12));
|
|
utf8 += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
|
utf8 += static_cast<char>(0x80 | (cp & 0x3F));
|
|
} else if (cp < 0x200000) {
|
|
utf8 += static_cast<char>(0xF0 | (cp >> 18));
|
|
utf8 += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
|
utf8 += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
|
utf8 += static_cast<char>(0x80 | (cp & 0x3F));
|
|
}
|
|
return utf8;
|
|
}
|
|
} // namespace pdfengine::parser
|
|
|
|
namespace {
|
|
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
struct VectorWriter : public FPDF_FILEWRITE {
|
|
std::vector<uint8_t> buffer;
|
|
|
|
static int WriteBlockCallback(FPDF_FILEWRITE* pThis, const void* pData, unsigned long size) {
|
|
auto* self = static_cast<VectorWriter*>(pThis);
|
|
const auto* bytes = static_cast<const uint8_t*>(pData);
|
|
self->buffer.insert(self->buffer.end(), bytes, bytes + size);
|
|
return 1;
|
|
}
|
|
|
|
VectorWriter() {
|
|
this->version = 1;
|
|
this->WriteBlock = &VectorWriter::WriteBlockCallback;
|
|
}
|
|
};
|
|
|
|
struct PngWriteState {
|
|
std::vector<uint8_t>* buffer;
|
|
};
|
|
|
|
void pngWriteCallback(png_structp png_ptr, png_bytep data, png_size_t length) {
|
|
auto* state = reinterpret_cast<PngWriteState*>(png_get_io_ptr(png_ptr));
|
|
state->buffer->insert(state->buffer->end(), data, data + length);
|
|
}
|
|
|
|
void pngFlushCallback(png_structp png_ptr) {
|
|
(void)png_ptr;
|
|
}
|
|
|
|
std::vector<uint8_t> encodeBgraToPng(const uint8_t* bgra, int width, int height, int stride) {
|
|
std::vector<uint8_t> pngBytes;
|
|
png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
|
|
if (!png) return {};
|
|
|
|
png_infop info = png_create_info_struct(png);
|
|
if (!info) {
|
|
png_destroy_write_struct(&png, nullptr);
|
|
return {};
|
|
}
|
|
|
|
#pragma warning(push)
|
|
#pragma warning(disable: 4611)
|
|
if (setjmp(png_jmpbuf(png))) {
|
|
png_destroy_write_struct(&png, &info);
|
|
return {};
|
|
}
|
|
#pragma warning(pop)
|
|
|
|
PngWriteState state{&pngBytes};
|
|
png_set_write_fn(png, &state, pngWriteCallback, pngFlushCallback);
|
|
|
|
png_set_IHDR(png, info, width, height, 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE,
|
|
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
|
|
|
|
png_write_info(png, info);
|
|
png_set_bgr(png);
|
|
|
|
std::vector<png_bytep> rowPointers(height);
|
|
for (int y = 0; y < height; ++y) {
|
|
rowPointers[y] = const_cast<png_bytep>(bgra + y * stride);
|
|
}
|
|
|
|
png_write_image(png, rowPointers.data());
|
|
png_write_end(png, nullptr);
|
|
png_destroy_write_struct(&png, &info);
|
|
|
|
return pngBytes;
|
|
}
|
|
|
|
pdfengine::EngineError mapPdfiumError(unsigned long err, bool passwordProvided) {
|
|
switch (err) {
|
|
case FPDF_ERR_SUCCESS:
|
|
return pdfengine::EngineError::Unknown;
|
|
case FPDF_ERR_FILE:
|
|
return pdfengine::EngineError::FileNotFound;
|
|
case FPDF_ERR_FORMAT:
|
|
return pdfengine::EngineError::InvalidFormat;
|
|
case FPDF_ERR_PASSWORD:
|
|
return passwordProvided ? pdfengine::EngineError::InvalidPassword
|
|
: pdfengine::EngineError::PasswordRequired;
|
|
default:
|
|
return pdfengine::EngineError::Unknown;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
struct PdfiumGlobalInit {
|
|
PdfiumGlobalInit() {
|
|
pdfengine::parser::pdfiumInitLibrary();
|
|
}
|
|
~PdfiumGlobalInit() {
|
|
pdfengine::parser::pdfiumDestroyLibrary();
|
|
}
|
|
};
|
|
|
|
void ensure_pdfium_initialized() {
|
|
static PdfiumGlobalInit init;
|
|
}
|
|
|
|
std::string normalizeFamilyName(const std::string& fontName) {
|
|
// 1. Remove subset tag if present
|
|
std::string name = fontName;
|
|
if (name.size() > 7 && name[6] == '+') {
|
|
name = name.substr(7);
|
|
}
|
|
|
|
// 2. Strip standard suffixes
|
|
size_t sep = name.find_first_of("-,");
|
|
if (sep != std::string::npos) {
|
|
name = name.substr(0, sep);
|
|
}
|
|
|
|
// 3. Clean up common postfixes
|
|
auto cleanName = name;
|
|
auto lower = name;
|
|
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
|
|
|
|
std::vector<std::string> suffixes = {"bold", "italic", "oblique", "regular", "medium", "light", "heavy", "black", "condensed", "mt", "ps"};
|
|
for (const auto& s : suffixes) {
|
|
size_t pos = lower.rfind(s);
|
|
if (pos != std::string::npos && pos + s.size() == lower.size()) {
|
|
cleanName = cleanName.substr(0, pos);
|
|
lower = lower.substr(0, pos);
|
|
}
|
|
}
|
|
|
|
// Strip trailing punctuation
|
|
while (!cleanName.empty() && (cleanName.back() == '-' || cleanName.back() == ' ' || cleanName.back() == '_')) {
|
|
cleanName.pop_back();
|
|
}
|
|
|
|
if (cleanName.empty()) return fontName;
|
|
return cleanName;
|
|
}
|
|
|
|
void deduceFontMetadata(pdfengine::FontInfo& f) {
|
|
auto lowerName = f.fontName;
|
|
std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), ::tolower);
|
|
|
|
// 1. Subset Tag & Family Normalization
|
|
if (f.fontName.size() > 7 && f.fontName[6] == '+') {
|
|
f.isSubset = true;
|
|
f.subsetTag = f.fontName.substr(0, 6);
|
|
// Validate: must be exactly 6 uppercase letters
|
|
bool validTag = true;
|
|
for (int ti = 0; ti < 6; ++ti) {
|
|
if (!std::isupper(static_cast<unsigned char>(f.fontName[ti]))) {
|
|
validTag = false; break;
|
|
}
|
|
}
|
|
if (!validTag) { f.isSubset = false; f.subsetTag = ""; }
|
|
} else {
|
|
f.isSubset = false;
|
|
f.subsetTag = "";
|
|
}
|
|
f.normalizedFamily = normalizeFamilyName(f.fontName);
|
|
|
|
// 2. Encoding & CMap Identification (name-based heuristic; may be overridden by caller)
|
|
if (f.fontName.find("Identity-H") != std::string::npos) {
|
|
f.encoding = "Identity-H";
|
|
f.cmapName = "Identity-H";
|
|
} else if (f.fontName.find("Identity-V") != std::string::npos) {
|
|
f.encoding = "Identity-V";
|
|
f.cmapName = "Identity-V";
|
|
f.isVertical = true;
|
|
} else if (lowerName.find("symbol") != std::string::npos) {
|
|
f.encoding = "Symbol";
|
|
f.cmapName = "None";
|
|
} else {
|
|
f.encoding = "WinAnsiEncoding";
|
|
f.cmapName = "None";
|
|
}
|
|
|
|
// 3. ToUnicode Map Availability
|
|
// Subset embedded fonts almost always have ToUnicode; symbol fonts rarely do.
|
|
if (lowerName.find("symbol") != std::string::npos) {
|
|
f.hasToUnicode = false;
|
|
} else {
|
|
f.hasToUnicode = true;
|
|
}
|
|
|
|
// 4. CID System Info Registry — expanded patterns
|
|
if (lowerName.find("simsun") != std::string::npos ||
|
|
lowerName.find("simhei") != std::string::npos ||
|
|
lowerName.find("heiti") != std::string::npos ||
|
|
lowerName.find("fangsong") != std::string::npos ||
|
|
lowerName.find("kaiti") != std::string::npos ||
|
|
lowerName.find("song") != std::string::npos ||
|
|
lowerName.find("gb") != std::string::npos) {
|
|
f.cidSystemInfo = "Adobe-GB1";
|
|
} else if (lowerName.find("gothic") != std::string::npos ||
|
|
lowerName.find("ms-gothic") != std::string::npos ||
|
|
lowerName.find("msgothic") != std::string::npos ||
|
|
lowerName.find("mincho") != std::string::npos ||
|
|
lowerName.find("kozuka") != std::string::npos ||
|
|
lowerName.find("hiragino") != std::string::npos ||
|
|
lowerName.find("japan") != std::string::npos ||
|
|
lowerName.find("heisei") != std::string::npos ||
|
|
lowerName.find("morisawa") != std::string::npos ||
|
|
lowerName.find("ryumin") != std::string::npos) {
|
|
f.cidSystemInfo = "Adobe-Japan1";
|
|
} else if (lowerName.find("malgun") != std::string::npos ||
|
|
lowerName.find("gulim") != std::string::npos ||
|
|
lowerName.find("batang") != std::string::npos ||
|
|
lowerName.find("dotum") != std::string::npos ||
|
|
lowerName.find("korea") != std::string::npos ||
|
|
lowerName.find("hangul") != std::string::npos ||
|
|
lowerName.find("korean") != std::string::npos) {
|
|
f.cidSystemInfo = "Adobe-Korea1";
|
|
} else if (lowerName.find("sung") != std::string::npos ||
|
|
lowerName.find("ming") != std::string::npos ||
|
|
lowerName.find("cns") != std::string::npos ||
|
|
lowerName.find("traditional") != std::string::npos) {
|
|
f.cidSystemInfo = "Adobe-CNS1";
|
|
} else {
|
|
f.cidSystemInfo = "None";
|
|
}
|
|
|
|
if (f.cidSystemInfo != "None" && f.cmapName == "None") {
|
|
f.cmapName = f.isVertical ? "UniJIS-UTF16-V" : "Identity-H";
|
|
}
|
|
|
|
// 5. Font Type Identification — improved heuristics
|
|
bool isCid = (f.encoding == "Identity-H" || f.encoding == "Identity-V" || f.cidSystemInfo != "None");
|
|
if (isCid) {
|
|
if (lowerName.find("bold") != std::string::npos || lowerName.find("italic") != std::string::npos) {
|
|
f.type = "CIDFontType0";
|
|
} else {
|
|
f.type = "CIDFontType2";
|
|
}
|
|
} else {
|
|
// Standard PDF Type 1 fonts
|
|
static const std::vector<std::string> type1Names = {
|
|
"times", "helvetica", "courier", "symbol", "zapfdingbats",
|
|
"liberation", "palatino", "bookman", "new century", "avant garde"
|
|
};
|
|
bool isType1 = false;
|
|
for (const auto& t1 : type1Names) {
|
|
if (lowerName.find(t1) != std::string::npos) { isType1 = true; break; }
|
|
}
|
|
f.type = isType1 ? "Type1" : "TrueType";
|
|
}
|
|
|
|
// 6. Source Type & Embedding Status
|
|
// PDFium font flags: bit 3 (value 4) = Symbolic, but NOT embed status.
|
|
// Embed status is indicated by subset prefix or by PDF font stream presence.
|
|
// We use the subset tag as definitive embed indicator, otherwise check known standard fonts.
|
|
if (f.isSubset) {
|
|
f.isEmbedded = true;
|
|
f.sourceType = "Embedded";
|
|
f.substitutedFrom = "";
|
|
f.substitutedTo = "";
|
|
} else {
|
|
// PDF standard 14 fonts are NEVER embedded
|
|
static const std::vector<std::string> standard14 = {
|
|
"helvetica", "times", "courier", "symbol", "zapfdingbats"
|
|
};
|
|
bool isStandard14 = false;
|
|
for (const auto& s14 : standard14) {
|
|
if (lowerName.find(s14) != std::string::npos) { isStandard14 = true; break; }
|
|
}
|
|
// Common system fonts also treated as non-embedded
|
|
bool isSystemFont = isStandard14 ||
|
|
lowerName.find("arial") != std::string::npos ||
|
|
lowerName.find("liberation") != std::string::npos ||
|
|
lowerName.find("dejavu") != std::string::npos ||
|
|
lowerName.find("freefont") != std::string::npos;
|
|
if (isSystemFont) {
|
|
f.isEmbedded = false;
|
|
f.sourceType = "SystemFallback";
|
|
f.substitutedFrom = "";
|
|
f.substitutedTo = "";
|
|
} else {
|
|
f.isEmbedded = false;
|
|
f.sourceType = "Substituted";
|
|
f.substitutedFrom = f.fontName;
|
|
#if defined(_WIN32)
|
|
f.substitutedTo = "Arial";
|
|
#else
|
|
f.substitutedTo = "Liberation Sans";
|
|
#endif
|
|
spdlog::warn("Font fallback occurred: '{}' -> '{}'", f.substitutedFrom, f.substitutedTo);
|
|
}
|
|
}
|
|
|
|
// 7. Stable Internal Font Identifier
|
|
// For subset fonts, fontName already contains the subset prefix (e.g. "ABCDEF+Arial").
|
|
// Use the full fontName directly — it already encodes both the subset tag and
|
|
// the base font name, separated by '+'. Concatenating subsetTag + "_" + fontName
|
|
// would duplicate the prefix ("ABCDEF_ABCDEF+Arial").
|
|
if (f.isSubset && !f.subsetTag.empty()) {
|
|
// fontName is "ABCDEF+Arial"; use it as-is for the stable ID.
|
|
f.internalFontId = f.fontName;
|
|
} else {
|
|
f.internalFontId = f.fontName + "_" + f.type + "_" + std::to_string(f.flags);
|
|
}
|
|
|
|
// 8. Descriptor Metrics — actual values from standard font specifications
|
|
if (lowerName.find("times") != std::string::npos) {
|
|
f.ascent = 891.0;
|
|
f.descent = -216.0;
|
|
f.capHeight = 662.0;
|
|
} else if (lowerName.find("courier") != std::string::npos) {
|
|
f.ascent = 629.0;
|
|
f.descent = -157.0;
|
|
f.capHeight = 562.0;
|
|
} else if (lowerName.find("symbol") != std::string::npos) {
|
|
f.ascent = 1010.0;
|
|
f.descent = -293.0;
|
|
f.capHeight = 673.0;
|
|
} else if (lowerName.find("helvetica") != std::string::npos) {
|
|
f.ascent = 905.0;
|
|
f.descent = -211.0;
|
|
f.capHeight = 728.0;
|
|
} else {
|
|
// Generic fallback metrics (reasonable defaults for TrueType/CID fonts)
|
|
f.ascent = 905.0;
|
|
f.descent = -211.0;
|
|
f.capHeight = 728.0;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// FontPdfData — real values extracted from the PDF font dictionary via PDFium.
|
|
// Populated by buildFontPdfDataMap() and used to override deduceFontMetadata()
|
|
// results in PdfiumPage::getFonts().
|
|
// ---------------------------------------------------------------------------
|
|
struct FontPdfData {
|
|
bool valid = false;
|
|
bool isEmbedded = false;
|
|
int flags = 0; // PDF /Flags from FontDescriptor
|
|
double ascent = 0.0; // in PDF 1000-unit space
|
|
double descent = 0.0; // in PDF 1000-unit space
|
|
bool hasUnicodeMapping = false; // at least one char decoded to valid Unicode
|
|
};
|
|
|
|
// Builds a map from font-name to FontPdfData by scanning page text objects.
|
|
//
|
|
// Phase 1 — page object walk:
|
|
// Uses FPDFTextObj_GetFont() + FPDFFont_Get*() to read the actual PDF font
|
|
// dictionary fields: embedded status, descriptor flags and metrics.
|
|
// These values replace the corresponding fields that deduceFontMetadata()
|
|
// previously guessed from the font name string.
|
|
//
|
|
// Phase 2 — text-page character scan:
|
|
// For each font encountered in Phase 1, tests whether FPDFText_GetUnicode()
|
|
// returns a valid codepoint for at least one character belonging to that font.
|
|
// If yes, the font has an active Unicode mapping (ToUnicode CMap or built-in
|
|
// encoding). This replaces the previous always-true heuristic.
|
|
static std::unordered_map<std::string, FontPdfData>
|
|
buildFontPdfDataMap(FPDF_PAGE page, FPDF_TEXTPAGE textPage) {
|
|
std::unordered_map<std::string, FontPdfData> result;
|
|
if (!page) return result;
|
|
|
|
// ---- Phase 1: page object walk to collect real font properties ----
|
|
int objectCount = FPDFPage_CountObjects(page);
|
|
for (int i = 0; i < objectCount; ++i) {
|
|
FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page, i);
|
|
if (!obj) continue;
|
|
if (FPDFPageObj_GetType(obj) != FPDF_PAGEOBJ_TEXT) continue;
|
|
|
|
FPDF_FONT font = FPDFTextObj_GetFont(obj);
|
|
if (!font) continue;
|
|
|
|
// Retrieve font name — first call returns required buffer size.
|
|
unsigned long nameLen = FPDFFont_GetBaseFontName(font, nullptr, 0);
|
|
if (nameLen == 0) continue;
|
|
std::vector<char> nameBuf(nameLen);
|
|
if (FPDFFont_GetBaseFontName(font, nameBuf.data(), nameLen) == 0) continue;
|
|
std::string fontName(nameBuf.data());
|
|
if (fontName.empty()) continue;
|
|
|
|
// Each font name is processed only once.
|
|
if (result.count(fontName)) continue;
|
|
|
|
FontPdfData data;
|
|
data.valid = true;
|
|
// FPDFFont_GetIsEmbedded returns 1 when the PDF contains an embedded
|
|
// font stream (/FontFile, /FontFile2, or /FontFile3 in the descriptor).
|
|
data.isEmbedded = (FPDFFont_GetIsEmbedded(font) != 0);
|
|
// FPDFFont_GetFlags returns the actual /Flags integer from the PDF
|
|
// FontDescriptor dictionary (not the text-rendering flags).
|
|
data.flags = FPDFFont_GetFlags(font);
|
|
|
|
// FPDFFont_GetAscent/Descent are scaled by font_size.
|
|
// Passing 1000.0 recovers the raw PDF 1000-unit-space values.
|
|
float rawAscent = 0.0f;
|
|
if (FPDFFont_GetAscent(font, 1000.0f, &rawAscent) && rawAscent != 0.0f) {
|
|
data.ascent = static_cast<double>(rawAscent);
|
|
}
|
|
float rawDescent = 0.0f;
|
|
if (FPDFFont_GetDescent(font, 1000.0f, &rawDescent) && rawDescent != 0.0f) {
|
|
data.descent = static_cast<double>(rawDescent);
|
|
}
|
|
|
|
result[fontName] = data;
|
|
}
|
|
|
|
// ---- Phase 2: Unicode mapping probe via text-page character scan ----
|
|
// For each known font, test whether PDFium can return a non-trivial Unicode
|
|
// value for at least one character that belongs to it. A valid decode
|
|
// implies the font has an active ToUnicode CMap or a built-in encoding map.
|
|
if (textPage && !result.empty()) {
|
|
int charCount = FPDFText_CountChars(textPage);
|
|
if (charCount > 0 && charCount < 1000000) {
|
|
for (int ci = 0; ci < charCount; ++ci) {
|
|
int fi = 0;
|
|
unsigned long flen =
|
|
FPDFText_GetFontInfo(textPage, ci, nullptr, 0, &fi);
|
|
if (flen == 0) continue;
|
|
std::vector<char> fbuf(flen);
|
|
if (FPDFText_GetFontInfo(textPage, ci, fbuf.data(), flen, &fi) == 0)
|
|
continue;
|
|
std::string fname(fbuf.data());
|
|
|
|
auto it = result.find(fname);
|
|
// Skip fonts not found in Phase 1, or already confirmed.
|
|
if (it == result.end() || it->second.hasUnicodeMapping) continue;
|
|
|
|
unsigned int cp = FPDFText_GetUnicode(textPage, ci);
|
|
// Require a printable codepoint — exclude NUL, control chars,
|
|
// and the Unicode replacement character (U+FFFD).
|
|
if (cp > 0x0020 && cp != 0xFFFD) {
|
|
it->second.hasUnicodeMapping = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
#endif
|
|
|
|
void parseHexColor(const std::string& hex, unsigned int& r, unsigned int& g, unsigned int& b) {
|
|
r = 0; g = 0; b = 0;
|
|
if (hex.empty()) return;
|
|
std::string s = hex;
|
|
if (s[0] == '#') {
|
|
s = s.substr(1);
|
|
}
|
|
if (s.size() == 6) {
|
|
try {
|
|
r = std::stoul(s.substr(0, 2), nullptr, 16);
|
|
g = std::stoul(s.substr(2, 2), nullptr, 16);
|
|
b = std::stoul(s.substr(4, 2), nullptr, 16);
|
|
} catch (...) {
|
|
r = 0; g = 0; b = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
namespace pdfengine {
|
|
|
|
std::expected<std::shared_ptr<PdfDocument>, EngineError>
|
|
PdfDocument::loadFromFile(const std::string& path, const std::string& password) {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
ensure_pdfium_initialized();
|
|
FPDF_DOCUMENT doc = FPDF_LoadDocument(path.c_str(), password.empty() ? nullptr : password.c_str());
|
|
if (!doc) {
|
|
auto err = FPDF_GetLastError();
|
|
spdlog::error("Failed to load PDF file from path: {} (error code: {})", path, err);
|
|
return std::unexpected(mapPdfiumError(err, !password.empty()));
|
|
}
|
|
return std::make_shared<parser::PdfiumDocument>(doc);
|
|
#else
|
|
(void)path;
|
|
(void)password;
|
|
spdlog::error("loadFromFile failed: PDFEngine compiled without PDFium support.");
|
|
return std::unexpected(EngineError::Unknown);
|
|
#endif
|
|
}
|
|
|
|
std::expected<std::shared_ptr<PdfDocument>, EngineError>
|
|
PdfDocument::loadFromMemory(const std::vector<uint8_t>& data, const std::string& password) {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
ensure_pdfium_initialized();
|
|
if (data.empty()) {
|
|
return std::unexpected(EngineError::InvalidFormat);
|
|
}
|
|
std::vector<uint8_t> buffer_copy = data;
|
|
FPDF_DOCUMENT doc = FPDF_LoadMemDocument(buffer_copy.data(), static_cast<int>(buffer_copy.size()),
|
|
password.empty() ? nullptr : password.c_str());
|
|
if (!doc) {
|
|
auto err = FPDF_GetLastError();
|
|
spdlog::error("Failed to load PDF from memory (error code: {})", err);
|
|
return std::unexpected(mapPdfiumError(err, !password.empty()));
|
|
}
|
|
return std::make_shared<parser::PdfiumDocument>(doc, std::move(buffer_copy));
|
|
#else
|
|
(void)data;
|
|
(void)password;
|
|
spdlog::error("loadFromMemory failed: PDFEngine compiled without PDFium support.");
|
|
return std::unexpected(EngineError::Unknown);
|
|
#endif
|
|
}
|
|
|
|
}
|
|
|
|
namespace pdfengine::parser {
|
|
|
|
PdfiumPage::PdfiumPage(NativePageHandle pageHandle, int pageIndex)
|
|
: page_(pageHandle), pageIndex_(pageIndex) {}
|
|
|
|
PdfiumPage::~PdfiumPage() {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
std::lock_guard<std::mutex> lock(textMutex_);
|
|
if (textPage_) {
|
|
FPDFText_ClosePage(textPage_);
|
|
}
|
|
if (page_) {
|
|
FPDF_ClosePage(page_);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
PdfiumPage::PdfiumPage(PdfiumPage&& other) noexcept {
|
|
*this = std::move(other);
|
|
}
|
|
|
|
PdfiumPage& PdfiumPage::operator=(PdfiumPage&& other) noexcept {
|
|
if (this != &other) {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
std::lock_guard<std::mutex> lock(textMutex_);
|
|
if (textPage_) FPDFText_ClosePage(textPage_);
|
|
if (page_) FPDF_ClosePage(page_);
|
|
#endif
|
|
page_ = other.page_;
|
|
textPage_ = other.textPage_;
|
|
pageIndex_ = other.pageIndex_;
|
|
|
|
other.page_ = nullptr;
|
|
other.textPage_ = nullptr;
|
|
other.pageIndex_ = 0;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
double PdfiumPage::width() const noexcept {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
return page_ ? FPDF_GetPageWidthF(page_) : 0.0;
|
|
#else
|
|
return 0.0;
|
|
#endif
|
|
}
|
|
|
|
double PdfiumPage::height() const noexcept {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
return page_ ? FPDF_GetPageHeightF(page_) : 0.0;
|
|
#else
|
|
return 0.0;
|
|
#endif
|
|
}
|
|
|
|
std::expected<PageImage, EngineError> PdfiumPage::render(int dpi) const {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
if (!page_) {
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
|
|
double scale = dpi / 72.0;
|
|
int w = static_cast<int>(width() * scale);
|
|
int h = static_cast<int>(height() * scale);
|
|
|
|
FPDF_BITMAP bitmap = FPDFBitmap_Create(w, h, 1);
|
|
if (!bitmap) {
|
|
return std::unexpected(EngineError::RenderFailed);
|
|
}
|
|
|
|
FPDFBitmap_FillRect(bitmap, 0, 0, w, h, 0xFFFFFFFF);
|
|
|
|
FPDF_RenderPageBitmap(bitmap, page_, 0, 0, w, h, 0, 0);
|
|
|
|
const auto* buffer = static_cast<const uint8_t*>(FPDFBitmap_GetBuffer(bitmap));
|
|
int stride = FPDFBitmap_GetStride(bitmap);
|
|
|
|
std::vector<uint8_t> pngBytes = encodeBgraToPng(buffer, w, h, stride);
|
|
FPDFBitmap_Destroy(bitmap);
|
|
|
|
if (pngBytes.empty()) {
|
|
return std::unexpected(EngineError::RenderFailed);
|
|
}
|
|
|
|
return PageImage{w, h, std::move(pngBytes)};
|
|
#else
|
|
(void)dpi;
|
|
return std::unexpected(EngineError::Unknown);
|
|
#endif
|
|
}
|
|
|
|
std::expected<std::string, EngineError> PdfiumPage::extractText() const {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
if (!page_) {
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
ensureTextPageLoaded();
|
|
if (!textPage_) {
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
|
|
int charCount = FPDFText_CountChars(textPage_);
|
|
if (charCount <= 0) {
|
|
return "";
|
|
}
|
|
|
|
std::vector<unsigned short> buffer(charCount + 1, 0);
|
|
int written = FPDFText_GetText(textPage_, 0, charCount, buffer.data());
|
|
if (written <= 0) {
|
|
return "";
|
|
}
|
|
|
|
return utf16le_to_utf8(reinterpret_cast<const char16_t*>(buffer.data()), written);
|
|
#else
|
|
return std::unexpected(EngineError::Unknown);
|
|
#endif
|
|
}
|
|
|
|
std::expected<std::vector<GlyphBounds>, EngineError> PdfiumPage::extractTextWithBounds() const {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
if (!page_) {
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
ensureTextPageLoaded();
|
|
if (!textPage_) {
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
|
|
int charCount = FPDFText_CountChars(textPage_);
|
|
std::vector<GlyphBounds> result;
|
|
if (charCount <= 0) {
|
|
return result;
|
|
}
|
|
|
|
result.reserve(charCount);
|
|
|
|
for (int i = 0; i < charCount; ++i) {
|
|
unsigned int codeUnit = FPDFText_GetUnicode(textPage_, i);
|
|
|
|
unsigned int cp = codeUnit;
|
|
double left = 0, right = 0, bottom = 0, top = 0;
|
|
double fontSize = FPDFText_GetFontSize(textPage_, i);
|
|
|
|
if (codeUnit >= 0xD800 && codeUnit <= 0xDBFF && i + 1 < charCount) {
|
|
unsigned int nextUnit = FPDFText_GetUnicode(textPage_, i + 1);
|
|
if (nextUnit >= 0xDC00 && nextUnit <= 0xDFFF) {
|
|
cp = 0x10000 + ((codeUnit - 0xD800) << 10) + (nextUnit - 0xDC00);
|
|
|
|
double l1 = 0, r1 = 0, b1 = 0, t1 = 0;
|
|
FPDFText_GetCharBox(textPage_, i, &l1, &r1, &b1, &t1);
|
|
|
|
double l2 = 0, r2 = 0, b2 = 0, t2 = 0;
|
|
FPDFText_GetCharBox(textPage_, i + 1, &l2, &r2, &b2, &t2);
|
|
|
|
left = (std::min)(l1, l2);
|
|
right = (std::max)(r1, r2);
|
|
bottom = (std::min)(b1, b2);
|
|
top = (std::max)(t1, t2);
|
|
|
|
++i;
|
|
} else {
|
|
FPDFText_GetCharBox(textPage_, i, &left, &right, &bottom, &top);
|
|
}
|
|
} else {
|
|
FPDFText_GetCharBox(textPage_, i, &left, &right, &bottom, &top);
|
|
}
|
|
|
|
std::string utf8_char = code_point_to_utf8(cp);
|
|
if (utf8_char.empty() || cp == '\r' || cp == '\n') {
|
|
continue;
|
|
}
|
|
|
|
double x = (std::min)(left, right);
|
|
double y = (std::min)(bottom, top);
|
|
double w = std::abs(right - left);
|
|
double h = std::abs(top - bottom);
|
|
|
|
GlyphBounds gb;
|
|
gb.text = std::move(utf8_char);
|
|
gb.x = x;
|
|
gb.y = y;
|
|
gb.w = w;
|
|
gb.h = h;
|
|
gb.fontSize = fontSize;
|
|
result.push_back(gb);
|
|
}
|
|
|
|
return result;
|
|
#else
|
|
return std::unexpected(EngineError::Unknown);
|
|
#endif
|
|
}
|
|
|
|
std::expected<std::vector<std::string>, EngineError> PdfiumPage::extractAnnotationsText() const {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
if (!page_) {
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
std::vector<std::string> result;
|
|
int count = FPDFPage_GetAnnotCount(page_);
|
|
for (int i = 0; i < count; ++i) {
|
|
FPDF_ANNOTATION annot = FPDFPage_GetAnnot(page_, i);
|
|
if (!annot) continue;
|
|
|
|
if (FPDFAnnot_GetSubtype(annot) == FPDF_ANNOT_FREETEXT) {
|
|
unsigned long len = FPDFAnnot_GetStringValue(annot, "Contents", nullptr, 0);
|
|
if (len > 2) {
|
|
std::vector<uint8_t> buf(len);
|
|
FPDFAnnot_GetStringValue(annot, "Contents", reinterpret_cast<FPDF_WCHAR*>(buf.data()), len);
|
|
std::string text = utf16le_to_utf8(reinterpret_cast<const char16_t*>(buf.data()), len / sizeof(char16_t));
|
|
while (!text.empty() && text.back() == '\0') {
|
|
text.pop_back();
|
|
}
|
|
if (!text.empty()) {
|
|
result.push_back(text);
|
|
}
|
|
}
|
|
}
|
|
FPDFPage_CloseAnnot(annot);
|
|
}
|
|
return result;
|
|
#else
|
|
return std::unexpected(EngineError::Unknown);
|
|
#endif
|
|
}
|
|
|
|
DevicePoint PdfiumPage::pageToDevice(const Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate) const noexcept {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
if (!page_) return {0, 0};
|
|
int dx = 0, dy = 0;
|
|
FPDF_PageToDevice(page_, 0, 0, deviceWidth, deviceHeight, rotate, pagePoint.x, pagePoint.y, &dx, &dy);
|
|
return {dx, dy};
|
|
#else
|
|
(void)pagePoint; (void)deviceWidth; (void)deviceHeight; (void)rotate;
|
|
return {0, 0};
|
|
#endif
|
|
}
|
|
|
|
Point2D PdfiumPage::deviceToPage(const DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate) const noexcept {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
if (!page_) return {0.0, 0.0};
|
|
double px = 0.0, py = 0.0;
|
|
FPDF_DeviceToPage(page_, 0, 0, deviceWidth, deviceHeight, rotate, devicePoint.x, devicePoint.y, &px, &py);
|
|
return {px, py};
|
|
#else
|
|
(void)devicePoint; (void)deviceWidth; (void)deviceHeight; (void)rotate;
|
|
return {0.0, 0.0};
|
|
#endif
|
|
}
|
|
|
|
std::expected<double, EngineError> PdfiumPage::getGlyphWidth(const std::string& fontName, uint32_t charcode, double fontSize) const {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
if (!page_) {
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
|
|
FPDF_FONT font = nullptr;
|
|
{
|
|
std::lock_guard<std::mutex> lock(textMutex_);
|
|
|
|
auto cached = fontHandleCache_.find(fontName);
|
|
if (cached != fontHandleCache_.end()) {
|
|
font = cached->second;
|
|
} else {
|
|
int objectCount = FPDFPage_CountObjects(page_);
|
|
for (int i = 0; i < objectCount; ++i) {
|
|
FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page_, i);
|
|
if (!obj || FPDFPageObj_GetType(obj) != FPDF_PAGEOBJ_TEXT) continue;
|
|
|
|
FPDF_FONT pageFont = FPDFTextObj_GetFont(obj);
|
|
if (!pageFont) continue;
|
|
|
|
size_t nameLen = FPDFFont_GetBaseFontName(pageFont, nullptr, 0);
|
|
if (nameLen > 0) {
|
|
std::vector<char> nameBuf(nameLen);
|
|
if (FPDFFont_GetBaseFontName(pageFont, nameBuf.data(), nameLen) > 0) {
|
|
std::string currentName(nameBuf.data());
|
|
if (currentName == fontName) {
|
|
font = pageFont;
|
|
fontHandleCache_[fontName] = font;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!font) {
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
|
|
float width = 0.0f;
|
|
if (!FPDFFont_GetGlyphWidth(font, charcode, static_cast<float>(fontSize), &width)) {
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
|
|
return static_cast<double>(width);
|
|
#else
|
|
(void)fontName; (void)charcode; (void)fontSize;
|
|
return std::unexpected(EngineError::Unknown);
|
|
#endif
|
|
}
|
|
|
|
void PdfiumPage::ensureTextPageLoaded() const {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
std::lock_guard<std::mutex> lock(textMutex_);
|
|
if (!textPage_ && page_) {
|
|
textPage_ = FPDFText_LoadPage(page_);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
PdfiumDocument::PdfiumDocument(NativeDocHandle docHandle)
|
|
: doc_(docHandle) {}
|
|
|
|
PdfiumDocument::PdfiumDocument(NativeDocHandle docHandle, std::vector<uint8_t> memoryBuffer)
|
|
: doc_(docHandle), memoryBuffer_(std::move(memoryBuffer)) {}
|
|
|
|
PdfiumDocument::~PdfiumDocument() {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
if (doc_) {
|
|
FPDF_CloseDocument(doc_);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
PdfiumDocument::PdfiumDocument(PdfiumDocument&& other) noexcept {
|
|
*this = std::move(other);
|
|
}
|
|
|
|
PdfiumDocument& PdfiumDocument::operator=(PdfiumDocument&& other) noexcept {
|
|
if (this != &other) {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
if (doc_) FPDF_CloseDocument(doc_);
|
|
#endif
|
|
doc_ = other.doc_;
|
|
other.doc_ = nullptr;
|
|
memoryBuffer_ = std::move(other.memoryBuffer_);
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
int PdfiumDocument::pageCount() const noexcept {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
return doc_ ? FPDF_GetPageCount(doc_) : 0;
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
DocumentMetadata PdfiumDocument::metadata() const noexcept {
|
|
DocumentMetadata meta;
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
if (!doc_) return meta;
|
|
|
|
auto fetchMeta = [this](const char* key) -> std::string {
|
|
unsigned long len = FPDF_GetMetaText(doc_, key, nullptr, 0);
|
|
if (len <= 2) return ""; // 2 bytes or less is just null terminator in UTF-16
|
|
std::vector<unsigned short> buf(len / 2);
|
|
FPDF_GetMetaText(doc_, key, buf.data(), len);
|
|
return utf16le_to_utf8(reinterpret_cast<const char16_t*>(buf.data()), buf.size());
|
|
};
|
|
|
|
meta.title = fetchMeta("Title");
|
|
meta.author = fetchMeta("Author");
|
|
meta.creator = fetchMeta("Creator");
|
|
meta.producer = fetchMeta("Producer");
|
|
meta.creationDate = fetchMeta("CreationDate");
|
|
meta.modificationDate = fetchMeta("ModDate");
|
|
#endif
|
|
return meta;
|
|
}
|
|
|
|
std::expected<std::shared_ptr<PdfPage>, EngineError> PdfiumDocument::getPage(int pageIndex) {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
if (!doc_) {
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
if (pageIndex < 0 || pageIndex >= pageCount()) {
|
|
return std::unexpected(EngineError::PageOutOfBounds);
|
|
}
|
|
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
|
|
if (!page) {
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
return std::make_shared<PdfiumPage>(page, pageIndex);
|
|
#else
|
|
(void)pageIndex;
|
|
return std::unexpected(EngineError::Unknown);
|
|
#endif
|
|
}
|
|
|
|
std::expected<void, EngineError> PdfiumDocument::applyEdits(const std::string& editsJson) {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
if (!doc_) {
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
|
|
try {
|
|
auto root = nlohmann::json::parse(editsJson);
|
|
if (!root.contains("version") || root["version"] != "1.0") {
|
|
spdlog::error("Invalid edits JSON: missing or unsupported version (expected '1.0')");
|
|
return std::unexpected(EngineError::InvalidFormat);
|
|
}
|
|
if (!root.contains("operations") || !root["operations"].is_array()) {
|
|
spdlog::error("Invalid edits JSON: missing 'operations' array");
|
|
return std::unexpected(EngineError::InvalidFormat);
|
|
}
|
|
|
|
for (const auto& op : root["operations"]) {
|
|
std::string type = op.value("type", "");
|
|
int pageIndex = op.value("pageIndex", -1);
|
|
if (pageIndex < 0 || pageIndex >= pageCount()) {
|
|
spdlog::error("Page index {} out of bounds (total pages: {})", pageIndex, pageCount());
|
|
return std::unexpected(EngineError::PageOutOfBounds);
|
|
}
|
|
|
|
if (type == "text_overlay" || type == "add_text") {
|
|
if (!op.contains("data") || !op["data"].is_object()) {
|
|
spdlog::error("text_overlay/add_text operation missing 'data' object");
|
|
return std::unexpected(EngineError::InvalidFormat);
|
|
}
|
|
auto data = op["data"];
|
|
std::string text = data.value("text", "");
|
|
double x = data.value("x", 0.0);
|
|
double y = data.value("y", 0.0);
|
|
double fontSize = data.value("fontSize", 12.0);
|
|
double width = data.value("width", 200.0);
|
|
double height = data.value("height", fontSize * 1.5);
|
|
std::string fontFamily = data.value("fontFamily", "Helvetica");
|
|
std::string color = data.value("color", "#000000");
|
|
|
|
if (text.empty()) {
|
|
continue;
|
|
}
|
|
|
|
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
|
|
if (!page) {
|
|
spdlog::error("Failed to load page index {} for editing", pageIndex);
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
|
|
// 1. Cover old text with solid white rectangle
|
|
FPDF_PAGEOBJECT rectObj = FPDFPageObj_CreateNewRect(
|
|
static_cast<float>(x),
|
|
static_cast<float>(y),
|
|
static_cast<float>(width),
|
|
static_cast<float>(height)
|
|
);
|
|
if (!rectObj) {
|
|
spdlog::error("Failed to create background white rectangle object");
|
|
FPDF_ClosePage(page);
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
FPDFPageObj_SetFillColor(rectObj, 255, 255, 255, 255);
|
|
FPDFPath_SetDrawMode(rectObj, FPDF_FILLMODE_WINDING, 0);
|
|
FPDFPage_InsertObject(page, rectObj);
|
|
|
|
// 2. Load standard font
|
|
std::string standardFontName = "Helvetica";
|
|
if (fontFamily == "Times New Roman" || fontFamily == "Times-Roman") {
|
|
standardFontName = "Times-Roman";
|
|
} else if (fontFamily == "Courier New" || fontFamily == "Courier") {
|
|
standardFontName = "Courier";
|
|
} else if (fontFamily == "Helvetica-Bold") {
|
|
standardFontName = "Helvetica-Bold";
|
|
} else if (fontFamily == "Helvetica-Oblique") {
|
|
standardFontName = "Helvetica-Oblique";
|
|
} else if (fontFamily == "Helvetica-BoldOblique") {
|
|
standardFontName = "Helvetica-BoldOblique";
|
|
} else if (fontFamily == "Times-Bold") {
|
|
standardFontName = "Times-Bold";
|
|
} else if (fontFamily == "Times-Italic") {
|
|
standardFontName = "Times-Italic";
|
|
} else if (fontFamily == "Times-BoldItalic") {
|
|
standardFontName = "Times-BoldItalic";
|
|
} else if (fontFamily == "Courier-Bold") {
|
|
standardFontName = "Courier-Bold";
|
|
} else if (fontFamily == "Courier-Oblique") {
|
|
standardFontName = "Courier-Oblique";
|
|
} else if (fontFamily == "Courier-BoldOblique") {
|
|
standardFontName = "Courier-BoldOblique";
|
|
}
|
|
|
|
FPDF_FONT font = FPDFText_LoadStandardFont(doc_, standardFontName.c_str());
|
|
if (!font) {
|
|
spdlog::warn("Failed to load standard font {}, falling back to Helvetica", standardFontName);
|
|
font = FPDFText_LoadStandardFont(doc_, "Helvetica");
|
|
}
|
|
|
|
// 3. Create text page object
|
|
FPDF_PAGEOBJECT textObj = FPDFPageObj_CreateTextObj(doc_, font, static_cast<float>(fontSize));
|
|
if (!textObj) {
|
|
spdlog::error("Failed to create text object");
|
|
FPDF_ClosePage(page);
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
|
|
// Set text color
|
|
unsigned int r = 0, g = 0, b = 0;
|
|
parseHexColor(color, r, g, b);
|
|
FPDFPageObj_SetFillColor(textObj, r, g, b, 255);
|
|
|
|
// Set text contents
|
|
auto utf16 = utf8_to_utf16le(text);
|
|
if (!FPDFText_SetText(textObj, reinterpret_cast<FPDF_WIDESTRING>(utf16.data()))) {
|
|
spdlog::error("Failed to set text object text");
|
|
FPDFPageObj_Destroy(textObj);
|
|
FPDF_ClosePage(page);
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
|
|
// Position the text object at x, y
|
|
FPDFPageObj_Transform(textObj, 1.0, 0.0, 0.0, 1.0, x, y);
|
|
|
|
// Insert text object into page
|
|
FPDFPage_InsertObject(page, textObj);
|
|
|
|
// Regenerate page visual representation if needed
|
|
if (!FPDFPage_GenerateContent(page)) {
|
|
spdlog::error("Failed to generate page content after editing");
|
|
FPDF_ClosePage(page);
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
|
|
FPDF_ClosePage(page);
|
|
} else if (type == "redaction") {
|
|
spdlog::info("Parsed redaction edit operation (stub)");
|
|
} else if (type == "image_overlay") {
|
|
spdlog::info("Parsed image_overlay edit operation (stub)");
|
|
} else if (type == "highlight") {
|
|
spdlog::info("Parsed highlight edit operation (stub)");
|
|
} else if (type == "free_text") {
|
|
spdlog::info("Parsed free_text edit operation (stub)");
|
|
} else if (type == "comment") {
|
|
spdlog::info("Parsed comment edit operation (stub)");
|
|
} else if (type == "freehand") {
|
|
spdlog::info("Parsed freehand edit operation (stub)");
|
|
} else if (type == "page_rotation") {
|
|
spdlog::info("Parsed page_rotation edit operation (stub)");
|
|
} else {
|
|
spdlog::warn("Unsupported edit operation type: {}", type);
|
|
}
|
|
}
|
|
} catch (const nlohmann::json::parse_error& e) {
|
|
spdlog::error("JSON parse error in applyEdits: {}", e.what());
|
|
return std::unexpected(EngineError::InvalidFormat);
|
|
} catch (const std::exception& e) {
|
|
spdlog::error("Exception in applyEdits: {}", e.what());
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
|
|
invalidateFontCache();
|
|
return {};
|
|
#else
|
|
(void)editsJson;
|
|
return std::unexpected(EngineError::Unknown);
|
|
#endif
|
|
}
|
|
|
|
std::expected<std::vector<uint8_t>, EngineError> PdfiumDocument::saveIncremental() const {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
if (!doc_) {
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
VectorWriter writer;
|
|
if (!FPDF_SaveWithVersion(doc_, &writer, FPDF_INCREMENTAL, 14)) {
|
|
return std::unexpected(EngineError::WriteFailed);
|
|
}
|
|
return writer.buffer;
|
|
#else
|
|
return std::unexpected(EngineError::Unknown);
|
|
#endif
|
|
}
|
|
|
|
std::expected<std::vector<FontInfo>, EngineError> PdfiumPage::getFonts() const {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
if (!page_) {
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
ensureTextPageLoaded();
|
|
if (!textPage_) {
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
|
|
// Build real font data from the PDF font dictionary via page object walk.
|
|
// This provides accurate values for isEmbedded, subtype, flags, ascent,
|
|
// descent, and hasUnicodeMapping — replacing name-based heuristics.
|
|
auto fontPdfDataMap = buildFontPdfDataMap(page_, textPage_);
|
|
|
|
std::vector<FontInfo> pageFonts;
|
|
int charCount = FPDFText_CountChars(textPage_);
|
|
|
|
// Security Guard: prevent excessive traversal on extremely corrupted text pages
|
|
if (charCount < 0 || charCount > 1000000) {
|
|
spdlog::error("Invalid or excessive character count in page ({}): aborting font extraction", charCount);
|
|
return pageFonts;
|
|
}
|
|
|
|
auto getFontNameForChar = [this](int charIndex, int& flagsOut) -> std::string {
|
|
int flags = 0;
|
|
unsigned long len = FPDFText_GetFontInfo(textPage_, charIndex, nullptr, 0, &flags);
|
|
if (len > 0) {
|
|
std::vector<char> buf(len);
|
|
if (FPDFText_GetFontInfo(textPage_, charIndex, buf.data(), len, &flags) > 0) {
|
|
flagsOut = flags;
|
|
return std::string(buf.data());
|
|
}
|
|
}
|
|
return "";
|
|
};
|
|
|
|
// Detect vertical writing mode per font by analyzing character origin positions.
|
|
// We filter out large spatial jumps (e.g. line breaks or paragraph changes)
|
|
// by comparing the coordinate delta to the character's font size.
|
|
struct FontPositionStats {
|
|
int horizontalSteps = 0;
|
|
int verticalSteps = 0;
|
|
double totalFilteredDeltaX = 0.0;
|
|
double totalFilteredDeltaY = 0.0;
|
|
double prevX = 0.0, prevY = 0.0;
|
|
bool hasPrev = false;
|
|
};
|
|
std::unordered_map<std::string, FontPositionStats> fontStats;
|
|
|
|
// First pass: collect font names, flags, and position statistics
|
|
struct CharInfo {
|
|
std::string fontName;
|
|
int flags;
|
|
};
|
|
std::vector<CharInfo> charInfos(charCount);
|
|
|
|
for (int i = 0; i < charCount; ++i) {
|
|
int flags = 0;
|
|
std::string fontName = getFontNameForChar(i, flags);
|
|
charInfos[i] = {fontName, flags};
|
|
|
|
if (!fontName.empty()) {
|
|
double ox = 0.0, oy = 0.0;
|
|
if (FPDFText_GetCharOrigin(textPage_, i, &ox, &oy)) {
|
|
auto& stats = fontStats[fontName];
|
|
if (stats.hasPrev) {
|
|
double dx = std::abs(ox - stats.prevX);
|
|
double dy = std::abs(oy - stats.prevY);
|
|
double fontSize = FPDFText_GetFontSize(textPage_, i);
|
|
|
|
// Filter out non-consecutive jumps (e.g., line or paragraph breaks).
|
|
// We allow a generously large multiplier (3.0) for wide tracking,
|
|
// but enforce a minimum of 30.0 units for very small text.
|
|
double maxJump = (std::max)(fontSize * 3.0, 30.0);
|
|
|
|
if (dx < maxJump && dy < maxJump) {
|
|
stats.totalFilteredDeltaX += dx;
|
|
stats.totalFilteredDeltaY += dy;
|
|
|
|
if (dy > dx * 1.5) {
|
|
stats.verticalSteps++;
|
|
} else if (dx > dy * 1.5) {
|
|
stats.horizontalSteps++;
|
|
}
|
|
}
|
|
}
|
|
stats.prevX = ox;
|
|
stats.prevY = oy;
|
|
stats.hasPrev = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Determine which fonts are vertical
|
|
std::unordered_map<std::string, bool> fontIsVertical;
|
|
for (const auto& [fname, stats] : fontStats) {
|
|
bool vertical = false;
|
|
|
|
// Primary heuristic: explicit step counts for consecutive characters
|
|
if (stats.verticalSteps > 0 || stats.horizontalSteps > 0) {
|
|
vertical = stats.verticalSteps > stats.horizontalSteps;
|
|
} else {
|
|
// Fallback: if no clear steps were isolated, rely on filtered totals
|
|
vertical = (stats.totalFilteredDeltaY > 2.0 * stats.totalFilteredDeltaX) &&
|
|
(stats.totalFilteredDeltaY > 0.5);
|
|
}
|
|
|
|
fontIsVertical[fname] = vertical;
|
|
}
|
|
|
|
// Second pass: build FontInfo list
|
|
for (int i = 0; i < charCount; ++i) {
|
|
const std::string& fontName = charInfos[i].fontName;
|
|
if (fontName.empty()) {
|
|
continue;
|
|
}
|
|
|
|
// Deduplicate locally by fontName
|
|
auto it = std::find_if(pageFonts.begin(), pageFonts.end(), [&](const FontInfo& f) {
|
|
return f.fontName == fontName;
|
|
});
|
|
if (it != pageFonts.end()) {
|
|
continue;
|
|
}
|
|
|
|
FontInfo f;
|
|
f.fontName = fontName;
|
|
f.flags = static_cast<uint32_t>(charInfos[i].flags);
|
|
|
|
// Phase A: heuristic baseline — fills encoding, cmapName, cidSystemInfo,
|
|
// normalizedFamily, subset tag, and initial values for all other fields.
|
|
deduceFontMetadata(f);
|
|
|
|
// Phase B: override heuristic fields with real PDF dictionary values
|
|
// wherever buildFontPdfDataMap() was able to extract them.
|
|
auto pdfDataIt = fontPdfDataMap.find(fontName);
|
|
if (pdfDataIt != fontPdfDataMap.end()) {
|
|
const FontPdfData& pd = pdfDataIt->second;
|
|
|
|
auto lname = fontName;
|
|
std::transform(lname.begin(), lname.end(), lname.begin(), ::tolower);
|
|
|
|
// --- Embedding status (replaces subset-tag heuristic) ---
|
|
f.isEmbedded = pd.isEmbedded;
|
|
|
|
// --- Font descriptor flags (real /Flags value) ---
|
|
f.flags = static_cast<uint32_t>(pd.flags);
|
|
|
|
// --- Descriptor metrics (real ascent/descent) ---
|
|
if (pd.ascent != 0.0) f.ascent = pd.ascent;
|
|
if (pd.descent != 0.0) f.descent = pd.descent;
|
|
|
|
// CapHeight is not exposed by the PDFium public API.
|
|
// Use the exact specification values for the standard 14 fonts;
|
|
// for everything else, estimate from real ascent (71% is the
|
|
// empirical Latin cap-height ratio across common typefaces).
|
|
if (f.ascent > 0.0) {
|
|
if (lname.find("times") != std::string::npos) f.capHeight = 662.0;
|
|
else if (lname.find("courier") != std::string::npos) f.capHeight = 562.0;
|
|
else if (lname.find("symbol") != std::string::npos) f.capHeight = 673.0;
|
|
else if (lname.find("helvetica") != std::string::npos) f.capHeight = 728.0;
|
|
else f.capHeight = f.ascent * 0.71;
|
|
}
|
|
|
|
// --- Unicode mapping (replaces always-true heuristic) ---
|
|
if (lname.find("symbol") != std::string::npos) {
|
|
f.hasToUnicode = false;
|
|
} else {
|
|
f.hasToUnicode = pd.hasUnicodeMapping;
|
|
}
|
|
|
|
// --- sourceType derived from real embedding status ---
|
|
if (f.isEmbedded) {
|
|
// Font stream is present in the PDF — always report as Embedded.
|
|
f.sourceType = "Embedded";
|
|
f.substitutedFrom = "";
|
|
f.substitutedTo = "";
|
|
}
|
|
// If not embedded, keep the substitution info already set by
|
|
// deduceFontMetadata() (SystemFallback or Substituted).
|
|
|
|
// --- Recalculate stable identifier with corrected data ---
|
|
// fontName already includes the subset prefix (e.g. "ABCDEF+Arial");
|
|
// using it directly avoids the duplicate-prefix bug.
|
|
if (f.isSubset && !f.subsetTag.empty()) {
|
|
f.internalFontId = f.fontName;
|
|
} else {
|
|
f.internalFontId = f.fontName + "_" + f.type + "_" + std::to_string(f.flags);
|
|
}
|
|
|
|
spdlog::debug(
|
|
"Font '{}': isEmbedded={} type='{}' ascent={:.1f} descent={:.1f} "
|
|
"capHeight={:.1f} hasToUnicode={} flags={}",
|
|
fontName, f.isEmbedded, f.type, f.ascent, f.descent,
|
|
f.capHeight, f.hasToUnicode, f.flags);
|
|
}
|
|
|
|
// Override isVertical with position-based detection if not already set by name heuristic
|
|
if (!f.isVertical) {
|
|
auto vit = fontIsVertical.find(fontName);
|
|
if (vit != fontIsVertical.end() && vit->second) {
|
|
f.isVertical = true;
|
|
// Update encoding to reflect vertical writing mode
|
|
if (f.encoding == "WinAnsiEncoding" || f.encoding == "Identity-H") {
|
|
f.encoding = "Identity-V";
|
|
f.cmapName = "Identity-V";
|
|
}
|
|
spdlog::info("Vertical writing mode detected for font '{}' via character position analysis", fontName);
|
|
}
|
|
}
|
|
|
|
pageFonts.push_back(f);
|
|
}
|
|
|
|
// Deterministic Sorting: normalizedFamily -> fontName -> encoding -> type
|
|
std::sort(pageFonts.begin(), pageFonts.end(), [](const FontInfo& a, const FontInfo& b) {
|
|
if (a.normalizedFamily != b.normalizedFamily) {
|
|
return a.normalizedFamily < b.normalizedFamily;
|
|
}
|
|
if (a.fontName != b.fontName) {
|
|
return a.fontName < b.fontName;
|
|
}
|
|
if (a.encoding != b.encoding) {
|
|
return a.encoding < b.encoding;
|
|
}
|
|
return a.type < b.type;
|
|
});
|
|
|
|
return pageFonts;
|
|
#else
|
|
return std::unexpected(EngineError::Unknown);
|
|
#endif
|
|
}
|
|
|
|
std::expected<std::vector<FontInfo>, EngineError> PdfiumDocument::getFonts(int startPage, int endPage) const {
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
if (!doc_) {
|
|
return std::unexpected(EngineError::Unknown);
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(fontsMutex_);
|
|
|
|
int total = pageCount();
|
|
if (startPage < 0) startPage = 0;
|
|
if (endPage < 0 || endPage >= total) endPage = total - 1;
|
|
if (startPage > endPage) {
|
|
return std::vector<FontInfo>();
|
|
}
|
|
|
|
// Security Safeguard: cap maximum scan range to 1000 pages to prevent memory/CPU exhaustion
|
|
int scanCount = endPage - startPage + 1;
|
|
if (scanCount > 1000) {
|
|
spdlog::warn("Requested scan range ({} pages) exceeds limit. Capping scan to 1000 pages.", scanCount);
|
|
endPage = startPage + 999;
|
|
}
|
|
|
|
// Return full document-level cache if available and full range is requested
|
|
if (startPage == 0 && endPage == total - 1 && hasCachedFonts_) {
|
|
return cachedFonts_;
|
|
}
|
|
|
|
std::vector<FontInfo> aggregated;
|
|
for (int i = startPage; i <= endPage; ++i) {
|
|
FPDF_PAGE page = FPDF_LoadPage(doc_, i);
|
|
if (!page) {
|
|
spdlog::error("Failed to load page index {} for font diagnostics", i);
|
|
continue;
|
|
}
|
|
|
|
// Stack-allocated wrapper ensures FPDF handles are closed properly upon destruction
|
|
PdfiumPage tempPage(page, i);
|
|
auto pageFontsRes = tempPage.getFonts();
|
|
if (pageFontsRes) {
|
|
for (const auto& f : *pageFontsRes) {
|
|
auto it = std::find_if(aggregated.begin(), aggregated.end(), [&](const FontInfo& existing) {
|
|
return existing.fontName == f.fontName;
|
|
});
|
|
if (it == aggregated.end()) {
|
|
aggregated.push_back(f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort the aggregated list deterministically
|
|
std::sort(aggregated.begin(), aggregated.end(), [](const FontInfo& a, const FontInfo& b) {
|
|
if (a.normalizedFamily != b.normalizedFamily) {
|
|
return a.normalizedFamily < b.normalizedFamily;
|
|
}
|
|
if (a.fontName != b.fontName) {
|
|
return a.fontName < b.fontName;
|
|
}
|
|
if (a.encoding != b.encoding) {
|
|
return a.encoding < b.encoding;
|
|
}
|
|
return a.type < b.type;
|
|
});
|
|
|
|
// Cache if full scan was requested
|
|
if (startPage == 0 && endPage == total - 1) {
|
|
cachedFonts_ = aggregated;
|
|
hasCachedFonts_ = true;
|
|
}
|
|
|
|
return aggregated;
|
|
#else
|
|
(void)startPage;
|
|
(void)endPage;
|
|
return std::unexpected(EngineError::Unknown);
|
|
#endif
|
|
}
|
|
|
|
void PdfiumDocument::invalidateFontCache() {
|
|
std::lock_guard<std::mutex> lock(fontsMutex_);
|
|
cachedFonts_.clear();
|
|
hasCachedFonts_ = false;
|
|
spdlog::info("Document font cache has been invalidated.");
|
|
}
|
|
|
|
}
|