Merge pull request 'furqan' (#70) from furqan into dev

Reviewed-on: https://gitea.maskantech.in/gitea_admin/pdf/pulls/70
This commit is contained in:
furqan
2026-06-29 05:23:56 +00:00
55 changed files with 8961 additions and 9059 deletions
+11
View File
@@ -13,6 +13,17 @@ add_library(pdfengine STATIC
src/core/skia_renderer.cpp
src/parser/pdfium_loader.cpp
src/parser/pdfium_document.cpp
src/parser/pdfium_internal.cpp
src/parser/pdfium_reflow.cpp
src/parser/pdfium_page.cpp
src/parser/pdfium_page_model.cpp
src/parser/pdfium_fonts.cpp
src/parser/pdfium_edit.cpp
src/parser/pdfium_edit_replace.cpp
src/parser/pdfium_edit_reflow.cpp
src/parser/pdfium_edit_annotations.cpp
src/parser/pdfium_edit_pages.cpp
src/parser/pdfium_edit_images.cpp
src/parser/content_stream_parser.cpp
src/parser/decoration_builder.cpp
src/text/selection.cpp
File diff suppressed because it is too large Load Diff
+18
View File
@@ -13,6 +13,7 @@
#include <mutex>
#include <optional>
#include <unordered_map>
#include <nlohmann/json_fwd.hpp>
#include "fonts/pdf_fonts/embedded_font_reconstructor.hpp"
namespace pdfengine::fonts::loader { class FontResolver; }
namespace pdfengine::fonts { class FontFace; }
@@ -156,6 +157,23 @@ private:
#ifdef PDFENGINE_WITH_QPDF
const fonts::pdf_fonts::ReconstructedFont* getReconstructedEmbeddedFont(const std::string& internalFontId);
#endif
std::expected<void, EngineError> applyOp_replaceText(const nlohmann::json& op, int pageIndex);
std::expected<void, EngineError> applyOp_reflow(const nlohmann::json& op, int pageIndex);
std::expected<void, EngineError> applyOp_textOverlay(const nlohmann::json& op, int pageIndex);
std::expected<void, EngineError> applyOp_decoration(const nlohmann::json& op, int pageIndex);
std::expected<void, EngineError> applyOp_redaction(const nlohmann::json& op, int pageIndex);
std::expected<void, EngineError> applyOp_updateField(const nlohmann::json& op, int pageIndex);
std::expected<void, EngineError> applyOp_imageOverlay(const nlohmann::json& op, int pageIndex);
std::expected<void, EngineError> applyOp_highlight(const nlohmann::json& op, int pageIndex);
std::expected<void, EngineError> applyOp_freeText(const nlohmann::json& op, int pageIndex);
std::expected<void, EngineError> applyOp_comment(const nlohmann::json& op, int pageIndex);
std::expected<void, EngineError> applyOp_freehand(const nlohmann::json& op, int pageIndex);
std::expected<void, EngineError> applyOp_pageRotation(const nlohmann::json& op, int pageIndex);
std::expected<void, EngineError> applyOp_pageDeletion(const nlohmann::json& op, int pageIndex);
std::expected<void, EngineError> applyOp_pageReorder(const nlohmann::json& op, int pageIndex);
std::expected<void, EngineError> applyOp_deleteAnnotation(const nlohmann::json& op, int pageIndex);
std::expected<void, EngineError> applyOp_updateAnnotation(const nlohmann::json& op, int pageIndex);
#endif
};
+89
View File
@@ -0,0 +1,89 @@
#include "parser/pdfium_internal.hpp"
namespace pdfengine::parser {
std::expected<void, EngineError> PdfiumDocument::applyEdits(const std::string& editsJson) {
#ifdef PDFENGINE_WITH_PDFIUM
if (!doc_) {
return std::unexpected(EngineError::Unknown);
}
lastReflowLayout_.clear();
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);
}
std::expected<void, EngineError> r{};
if (type == "replace_text") {
r = applyOp_replaceText(op, pageIndex);
} else if (type == "reflow_paragraph") {
r = applyOp_reflow(op, pageIndex);
} else if (type == "text_overlay" || type == "add_text") {
r = applyOp_textOverlay(op, pageIndex);
} else if (type == "underline" || type == "strikeout" || type == "squiggly") {
r = applyOp_decoration(op, pageIndex);
} else if (type == "redaction") {
r = applyOp_redaction(op, pageIndex);
} else if (type == "update_field") {
r = applyOp_updateField(op, pageIndex);
} else if (type == "image_overlay") {
r = applyOp_imageOverlay(op, pageIndex);
} else if (type == "highlight") {
r = applyOp_highlight(op, pageIndex);
} else if (type == "free_text") {
r = applyOp_freeText(op, pageIndex);
} else if (type == "comment") {
r = applyOp_comment(op, pageIndex);
} else if (type == "freehand") {
r = applyOp_freehand(op, pageIndex);
} else if (type == "page_rotation") {
r = applyOp_pageRotation(op, pageIndex);
} else if (type == "page_deletion") {
r = applyOp_pageDeletion(op, pageIndex);
} else if (type == "page_reorder") {
r = applyOp_pageReorder(op, pageIndex);
} else if (type == "delete_annotation") {
r = applyOp_deleteAnnotation(op, pageIndex);
} else if (type == "update_annotation") {
r = applyOp_updateAnnotation(op, pageIndex);
} else {
spdlog::warn("Unsupported edit operation type: {}", type);
}
if (!r) {
return std::unexpected(r.error());
}
}
} 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);
}
invalidateCaches();
return {};
#else
(void)editsJson;
return std::unexpected(EngineError::Unknown);
#endif
}
}
@@ -0,0 +1,572 @@
#include "parser/pdfium_internal.hpp"
namespace pdfengine::parser {
std::expected<void, EngineError> PdfiumDocument::applyOp_textOverlay(const nlohmann::json& op, int pageIndex) {
#ifdef PDFENGINE_WITH_PDFIUM
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()) {
return {};
}
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
spdlog::error("Failed to load page index {} for editing", pageIndex);
return std::unexpected(EngineError::Unknown);
}
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);
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");
}
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);
}
unsigned int r = 0, g = 0, b = 0;
parseHexColor(color, r, g, b);
FPDFPageObj_SetFillColor(textObj, r, g, b, 255);
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);
}
FPDFPageObj_Transform(textObj, 1.0, 0.0, 0.0, 1.0, x, y);
FPDFPage_InsertObject(page, textObj);
if (!FPDFPage_GenerateContent(page)) {
spdlog::error("Failed to generate page content after editing");
FPDF_ClosePage(page);
return std::unexpected(EngineError::Unknown);
}
FPDF_ClosePage(page);
return {};
#else
(void)op; (void)pageIndex;
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<void, EngineError> PdfiumDocument::applyOp_decoration(const nlohmann::json& op, int pageIndex) {
#ifdef PDFENGINE_WITH_PDFIUM
std::string type = op.value("type", "");
if (!op.contains("data") || !op["data"].is_object()) {
spdlog::error("{} operation missing 'data' object", type);
return std::unexpected(EngineError::InvalidFormat);
}
auto data = op["data"];
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
spdlog::error("Failed to load page index {} for decoration", pageIndex);
return std::unexpected(EngineError::Unknown);
}
int subtype = FPDF_ANNOT_UNDERLINE;
if (type == "strikeout") subtype = FPDF_ANNOT_STRIKEOUT;
else if (type == "squiggly") subtype = FPDF_ANNOT_SQUIGGLY;
FPDF_ANNOTATION annot = FPDFPage_CreateAnnot(page, subtype);
if (!annot) {
spdlog::error("Failed to create {} annotation", type);
FPDF_ClosePage(page);
return std::unexpected(EngineError::Unknown);
}
double pageHeight = FPDF_GetPageHeightF(page);
unsigned int r = 0, g = 0, b = 0;
parseHexColor(data.value("color", "#000000"), r, g, b);
FPDFAnnot_SetColor(annot, FPDFANNOT_COLORTYPE_Color, r, g, b, 255);
if (data.contains("quadPoints") && data["quadPoints"].is_array()) {
FS_RECTF boundingBox;
boundingBox.left = 99999.0f;
boundingBox.right = -99999.0f;
boundingBox.top = -99999.0f;
boundingBox.bottom = 99999.0f;
for (const auto& quad : data["quadPoints"]) {
FS_QUADPOINTSF points;
points.x1 = static_cast<float>(quad.value("x1", 0.0));
points.y1 = static_cast<float>(pageHeight - quad.value("y1", 0.0));
points.x2 = static_cast<float>(quad.value("x2", 0.0));
points.y2 = static_cast<float>(pageHeight - quad.value("y2", 0.0));
points.x3 = static_cast<float>(quad.value("x3", 0.0));
points.y3 = static_cast<float>(pageHeight - quad.value("y3", 0.0));
points.x4 = static_cast<float>(quad.value("x4", 0.0));
points.y4 = static_cast<float>(pageHeight - quad.value("y4", 0.0));
FPDFAnnot_AppendAttachmentPoints(annot, &points);
float minX = (std::min)({points.x1, points.x2, points.x3, points.x4});
float maxX = (std::max)({points.x1, points.x2, points.x3, points.x4});
float minY = (std::min)({points.y1, points.y2, points.y3, points.y4});
float maxY = (std::max)({points.y1, points.y2, points.y3, points.y4});
if (minX < boundingBox.left) boundingBox.left = minX;
if (maxX > boundingBox.right) boundingBox.right = maxX;
if (minY < boundingBox.bottom) boundingBox.bottom = minY;
if (maxY > boundingBox.top) boundingBox.top = maxY;
}
if (boundingBox.left <= boundingBox.right) {
FPDFAnnot_SetRect(annot, &boundingBox);
}
}
std::string author = data.value("author", "");
if (!author.empty()) {
auto utf16 = utf8_to_utf16le(author);
FPDFAnnot_SetStringValue(annot, "T", reinterpret_cast<FPDF_WIDESTRING>(utf16.data()));
}
FPDFPage_CloseAnnot(annot);
FPDF_ClosePage(page);
return {};
#else
(void)op; (void)pageIndex;
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<void, EngineError> PdfiumDocument::applyOp_highlight(const nlohmann::json& op, int pageIndex) {
#ifdef PDFENGINE_WITH_PDFIUM
if (!op.contains("data") || !op["data"].is_object()) {
spdlog::error("highlight operation missing 'data' object");
return std::unexpected(EngineError::InvalidFormat);
}
auto data = op["data"];
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
spdlog::error("Failed to load page index {} for highlight", pageIndex);
return std::unexpected(EngineError::Unknown);
}
FPDF_ANNOTATION annot = FPDFPage_CreateAnnot(page, FPDF_ANNOT_HIGHLIGHT);
if (!annot) {
spdlog::error("Failed to create highlight annotation");
FPDF_ClosePage(page);
return std::unexpected(EngineError::Unknown);
}
double pageHeight = FPDF_GetPageHeightF(page);
std::string colorStr = data.value("color", "#ffff00");
unsigned int r = 255, g = 255, b = 0;
parseHexColor(colorStr, r, g, b);
FPDFAnnot_SetColor(annot, FPDFANNOT_COLORTYPE_Color, r, g, b, 255);
if (data.contains("quadPoints") && data["quadPoints"].is_array()) {
FS_RECTF boundingBox;
boundingBox.left = 99999.0f;
boundingBox.right = -99999.0f;
boundingBox.top = -99999.0f;
boundingBox.bottom = 99999.0f;
for (const auto& quad : data["quadPoints"]) {
FS_QUADPOINTSF points;
points.x1 = static_cast<float>(quad.value("x1", 0.0));
points.y1 = static_cast<float>(pageHeight - quad.value("y1", 0.0));
points.x2 = static_cast<float>(quad.value("x2", 0.0));
points.y2 = static_cast<float>(pageHeight - quad.value("y2", 0.0));
points.x3 = static_cast<float>(quad.value("x3", 0.0));
points.y3 = static_cast<float>(pageHeight - quad.value("y3", 0.0));
points.x4 = static_cast<float>(quad.value("x4", 0.0));
points.y4 = static_cast<float>(pageHeight - quad.value("y4", 0.0));
FPDFAnnot_AppendAttachmentPoints(annot, &points);
float minX = (std::min)({points.x1, points.x2, points.x3, points.x4});
float maxX = (std::max)({points.x1, points.x2, points.x3, points.x4});
float minY = (std::min)({points.y1, points.y2, points.y3, points.y4});
float maxY = (std::max)({points.y1, points.y2, points.y3, points.y4});
if (minX < boundingBox.left) boundingBox.left = minX;
if (maxX > boundingBox.right) boundingBox.right = maxX;
if (minY < boundingBox.bottom) boundingBox.bottom = minY;
if (maxY > boundingBox.top) boundingBox.top = maxY;
}
if (boundingBox.left <= boundingBox.right) {
FPDFAnnot_SetRect(annot, &boundingBox);
}
}
std::string author = data.value("author", "");
if (!author.empty()) {
auto utf16 = utf8_to_utf16le(author);
FPDFAnnot_SetStringValue(annot, "T", reinterpret_cast<FPDF_WIDESTRING>(utf16.data()));
}
std::string content = data.value("content", "");
if (!content.empty()) {
auto utf16 = utf8_to_utf16le(content);
FPDFAnnot_SetStringValue(annot, "Contents", reinterpret_cast<FPDF_WIDESTRING>(utf16.data()));
}
FPDFPage_CloseAnnot(annot);
FPDF_ClosePage(page);
return {};
#else
(void)op; (void)pageIndex;
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<void, EngineError> PdfiumDocument::applyOp_freeText(const nlohmann::json& op, int pageIndex) {
#ifdef PDFENGINE_WITH_PDFIUM
spdlog::info("Parsed free_text edit operation (stub)");
return {};
#else
(void)op; (void)pageIndex;
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<void, EngineError> PdfiumDocument::applyOp_comment(const nlohmann::json& op, int pageIndex) {
#ifdef PDFENGINE_WITH_PDFIUM
if (!op.contains("data") || !op["data"].is_object()) {
spdlog::error("comment operation missing 'data' object");
return std::unexpected(EngineError::InvalidFormat);
}
auto data = op["data"];
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
spdlog::error("Failed to load page index {} for comment", pageIndex);
return std::unexpected(EngineError::Unknown);
}
FPDF_ANNOTATION annot = FPDFPage_CreateAnnot(page, FPDF_ANNOT_TEXT);
if (!annot) {
spdlog::error("Failed to create comment annotation");
FPDF_ClosePage(page);
return std::unexpected(EngineError::Unknown);
}
double pageHeight = FPDF_GetPageHeightF(page);
double x = data.value("x", 0.0);
double y = data.value("y", 0.0);
FS_RECTF rect;
rect.left = static_cast<float>(x);
rect.right = static_cast<float>(x + 24.0);
rect.top = static_cast<float>(pageHeight - y);
rect.bottom = static_cast<float>(pageHeight - y - 24.0);
FPDFAnnot_SetRect(annot, &rect);
std::string colorStr = data.value("color", "#ffeb3b");
unsigned int r = 255, g = 235, b = 59;
parseHexColor(colorStr, r, g, b);
FPDFAnnot_SetColor(annot, FPDFANNOT_COLORTYPE_Color, r, g, b, 255);
std::string author = data.value("author", "");
if (!author.empty()) {
auto utf16 = utf8_to_utf16le(author);
FPDFAnnot_SetStringValue(annot, "T", reinterpret_cast<FPDF_WIDESTRING>(utf16.data()));
}
std::string content = data.value("content", "");
if (!content.empty()) {
auto utf16 = utf8_to_utf16le(content);
FPDFAnnot_SetStringValue(annot, "Contents", reinterpret_cast<FPDF_WIDESTRING>(utf16.data()));
}
std::string timestamp = data.value("timestamp", "");
if (!timestamp.empty()) {
auto utf16 = utf8_to_utf16le(timestamp);
FPDFAnnot_SetStringValue(annot, "M", reinterpret_cast<FPDF_WIDESTRING>(utf16.data()));
}
FPDFPage_CloseAnnot(annot);
FPDF_ClosePage(page);
return {};
#else
(void)op; (void)pageIndex;
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<void, EngineError> PdfiumDocument::applyOp_freehand(const nlohmann::json& op, int pageIndex) {
#ifdef PDFENGINE_WITH_PDFIUM
if (!op.contains("data") || !op["data"].is_object()) {
spdlog::error("freehand operation missing 'data' object");
return std::unexpected(EngineError::InvalidFormat);
}
auto data = op["data"];
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
spdlog::error("Failed to load page index {} for freehand", pageIndex);
return std::unexpected(EngineError::Unknown);
}
FPDF_ANNOTATION annot = FPDFPage_CreateAnnot(page, FPDF_ANNOT_INK);
if (!annot) {
spdlog::error("Failed to create ink annotation");
FPDF_ClosePage(page);
return std::unexpected(EngineError::Unknown);
}
double pageHeight = FPDF_GetPageHeightF(page);
std::string colorStr = data.value("color", "#000000");
unsigned int r = 0, g = 0, b = 0;
parseHexColor(colorStr, r, g, b);
FPDFAnnot_SetColor(annot, FPDFANNOT_COLORTYPE_Color, r, g, b, 255);
float thickness = static_cast<float>(data.value("thickness", 2.0));
FPDFAnnot_SetBorder(annot, 0.0f, 0.0f, thickness);
float minX = 1e9f, minY = 1e9f, maxX = -1e9f, maxY = -1e9f;
bool anyPoints = false;
if (data.contains("paths") && data["paths"].is_array()) {
for (const auto& path : data["paths"]) {
if (!path.is_array() || path.size() < 2) continue;
std::vector<FS_POINTF> pts;
pts.reserve(path.size());
for (const auto& pt : path) {
float px = static_cast<float>(pt.value("x", 0.0));
float py = static_cast<float>(pageHeight - pt.value("y", 0.0));
pts.push_back(FS_POINTF{px, py});
anyPoints = true;
minX = (std::min)(minX, px); maxX = (std::max)(maxX, px);
minY = (std::min)(minY, py); maxY = (std::max)(maxY, py);
}
if (pts.size() >= 2) {
FPDFAnnot_AddInkStroke(annot, pts.data(), pts.size());
}
}
}
if (anyPoints) {
float pad = thickness + 1.0f;
FS_RECTF rect;
rect.left = minX - pad;
rect.bottom = minY - pad;
rect.right = maxX + pad;
rect.top = maxY + pad;
FPDFAnnot_SetRect(annot, &rect);
}
FPDFPage_CloseAnnot(annot);
FPDF_ClosePage(page);
return {};
#else
(void)op; (void)pageIndex;
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<void, EngineError> PdfiumDocument::applyOp_deleteAnnotation(const nlohmann::json& op, int pageIndex) {
#ifdef PDFENGINE_WITH_PDFIUM
if (!op.contains("data") || !op["data"].is_object()) {
spdlog::error("delete_annotation operation missing 'data' object");
return std::unexpected(EngineError::InvalidFormat);
}
std::string targetId = op["data"].value("annotationId", "");
if (targetId.empty()) {
spdlog::error("delete_annotation missing 'annotationId'");
return std::unexpected(EngineError::InvalidFormat);
}
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
spdlog::error("Failed to load page index {} for delete_annotation", pageIndex);
return std::unexpected(EngineError::Unknown);
}
int count = FPDFPage_GetAnnotCount(page);
int foundIndex = -1;
for (int i = 0; i < count; ++i) {
FPDF_ANNOTATION annot = FPDFPage_GetAnnot(page, i);
if (!annot) continue;
std::string id;
unsigned long len = FPDFAnnot_GetStringValue(annot, "NM", nullptr, 0);
if (len > 2) {
std::vector<uint8_t> buf(len);
FPDFAnnot_GetStringValue(annot, "NM", reinterpret_cast<FPDF_WCHAR*>(buf.data()), len);
id = utf16le_to_utf8(reinterpret_cast<const char16_t*>(buf.data()), len / sizeof(char16_t));
while (!id.empty() && id.back() == '\0') id.pop_back();
}
if (id.empty()) {
id = "anno_" + std::to_string(pageIndex) + "_" + std::to_string(i);
}
FPDFPage_CloseAnnot(annot);
if (id == targetId) { foundIndex = i; break; }
}
if (foundIndex >= 0) {
if (!FPDFPage_RemoveAnnot(page, foundIndex)) {
spdlog::error("FPDFPage_RemoveAnnot failed for index {}", foundIndex);
}
} else {
spdlog::warn("delete_annotation: annotation '{}' not found on page {}", targetId, pageIndex);
}
FPDF_ClosePage(page);
return {};
#else
(void)op; (void)pageIndex;
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<void, EngineError> PdfiumDocument::applyOp_updateAnnotation(const nlohmann::json& op, int pageIndex) {
#ifdef PDFENGINE_WITH_PDFIUM
if (!op.contains("data") || !op["data"].is_object()) {
spdlog::error("update_annotation operation missing 'data' object");
return std::unexpected(EngineError::InvalidFormat);
}
auto data = op["data"];
std::string targetId = data.value("annotationId", "");
if (targetId.empty()) {
spdlog::error("update_annotation missing 'annotationId'");
return std::unexpected(EngineError::InvalidFormat);
}
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
spdlog::error("Failed to load page index {} for update_annotation", pageIndex);
return std::unexpected(EngineError::Unknown);
}
int count = FPDFPage_GetAnnotCount(page);
FPDF_ANNOTATION targetAnnot = nullptr;
for (int i = 0; i < count; ++i) {
FPDF_ANNOTATION annot = FPDFPage_GetAnnot(page, i);
if (!annot) continue;
std::string id;
unsigned long len = FPDFAnnot_GetStringValue(annot, "NM", nullptr, 0);
if (len > 2) {
std::vector<uint8_t> buf(len);
FPDFAnnot_GetStringValue(annot, "NM", reinterpret_cast<FPDF_WCHAR*>(buf.data()), len);
id = utf16le_to_utf8(reinterpret_cast<const char16_t*>(buf.data()), len / sizeof(char16_t));
while (!id.empty() && id.back() == '\0') id.pop_back();
}
if (id.empty()) {
id = "anno_" + std::to_string(pageIndex) + "_" + std::to_string(i);
}
if (id == targetId) {
targetAnnot = annot;
break;
}
FPDFPage_CloseAnnot(annot);
}
if (targetAnnot) {
if (data.contains("x") && data.contains("y") && data.contains("width") && data.contains("height")) {
double pageHeight = FPDF_GetPageHeightF(page);
float x = static_cast<float>(data["x"].get<double>());
float y = static_cast<float>(data["y"].get<double>());
float width = static_cast<float>(data["width"].get<double>());
float height = static_cast<float>(data["height"].get<double>());
FS_RECTF rect;
rect.left = x;
rect.right = x + width;
rect.top = static_cast<float>(pageHeight - y);
rect.bottom = static_cast<float>(pageHeight - (y + height));
FPDFAnnot_SetRect(targetAnnot, &rect);
}
if (data.contains("color")) {
std::string colorStr = data["color"].get<std::string>();
unsigned int r = 0, g = 0, b = 0;
parseHexColor(colorStr, r, g, b);
FPDFAnnot_SetColor(targetAnnot, FPDFANNOT_COLORTYPE_Color, r, g, b, 255);
}
if (data.contains("thickness")) {
float thickness = static_cast<float>(data["thickness"].get<double>());
FPDFAnnot_SetBorder(targetAnnot, 0.0f, 0.0f, thickness);
}
if (data.contains("text")) {
std::string text = data["text"].get<std::string>();
auto utf16 = utf8_to_utf16le(text);
FPDFAnnot_SetStringValue(targetAnnot, "Contents", reinterpret_cast<FPDF_WIDESTRING>(utf16.data()));
}
FPDFPage_CloseAnnot(targetAnnot);
} else {
spdlog::warn("update_annotation: annotation '{}' not found on page {}", targetId, pageIndex);
}
FPDF_ClosePage(page);
return {};
#else
(void)op; (void)pageIndex;
return std::unexpected(EngineError::Unknown);
#endif
}
}
+170
View File
@@ -0,0 +1,170 @@
#include "parser/pdfium_internal.hpp"
namespace pdfengine::parser {
std::expected<void, EngineError> PdfiumDocument::applyOp_imageOverlay(const nlohmann::json& op, int pageIndex) {
#ifdef PDFENGINE_WITH_PDFIUM
if (!op.contains("data") || !op["data"].is_object()) {
spdlog::error("image_overlay operation missing 'data' object");
return std::unexpected(EngineError::InvalidFormat);
}
auto data = op["data"];
double x = data.value("x", 0.0);
double y = data.value("y", 0.0);
double width = data.value("width", 100.0);
double height = data.value("height", 100.0);
int pixelWidth = data.value("pixelWidth", 0);
int pixelHeight = data.value("pixelHeight", 0);
std::string rawPixelData = data.value("rawPixelData", "");
std::string pixelDataPath = data.value("pixelDataPath", "");
std::vector<uint8_t> decodedBytes;
if (!pixelDataPath.empty()) {
std::ifstream infile(pixelDataPath, std::ios::binary);
if (!infile) {
spdlog::error("Failed to open pixel data path: {}", pixelDataPath);
return std::unexpected(EngineError::Unknown);
}
infile.seekg(0, std::ios::end);
std::streamsize size = infile.tellg();
infile.seekg(0, std::ios::beg);
decodedBytes.resize(static_cast<size_t>(size));
if (!infile.read(reinterpret_cast<char*>(decodedBytes.data()), size)) {
spdlog::error("Failed to read pixel data from path: {}", pixelDataPath);
return std::unexpected(EngineError::Unknown);
}
infile.close();
std::error_code ec;
std::filesystem::remove(pixelDataPath, ec);
} else if (!rawPixelData.empty()) {
decodedBytes = base64Decode(rawPixelData);
} else {
spdlog::warn("image_overlay operation contains invalid raw pixels or dimensions");
return {};
}
if (decodedBytes.size() != static_cast<size_t>(pixelWidth * pixelHeight * 4)) {
spdlog::error("Decoded image bytes size mismatch. Expected: {}, Got: {}",
pixelWidth * pixelHeight * 4, decodedBytes.size());
return std::unexpected(EngineError::InvalidFormat);
}
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
spdlog::error("Failed to load page index {} for image insertion", pageIndex);
return std::unexpected(EngineError::Unknown);
}
FPDF_PAGEOBJECT imgObj = FPDFPageObj_NewImageObj(doc_);
if (!imgObj) {
spdlog::error("Failed to create new image object");
FPDF_ClosePage(page);
return std::unexpected(EngineError::Unknown);
}
FPDF_BITMAP bitmap = FPDFBitmap_Create(pixelWidth, pixelHeight, 4);
if (!bitmap) {
spdlog::error("Failed to create FPDF_BITMAP");
FPDFPageObj_Destroy(imgObj);
FPDF_ClosePage(page);
return std::unexpected(EngineError::Unknown);
}
uint8_t* dest = static_cast<uint8_t*>(FPDFBitmap_GetBuffer(bitmap));
std::memcpy(dest, decodedBytes.data(), decodedBytes.size());
if (!FPDFImageObj_SetBitmap(&page, 1, imgObj, bitmap)) {
spdlog::error("Failed to set bitmap on image object");
FPDFBitmap_Destroy(bitmap);
FPDFPageObj_Destroy(imgObj);
FPDF_ClosePage(page);
return std::unexpected(EngineError::Unknown);
}
FPDFPageObj_Transform(imgObj, width, 0.0, 0.0, height, x, y);
FPDFPage_InsertObject(page, imgObj);
if (!FPDFPage_GenerateContent(page)) {
spdlog::error("Failed to generate page content after image insertion");
FPDFBitmap_Destroy(bitmap);
FPDF_ClosePage(page);
return std::unexpected(EngineError::Unknown);
}
FPDFBitmap_Destroy(bitmap);
FPDF_ClosePage(page);
return {};
#else
(void)op; (void)pageIndex;
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<void, EngineError> PdfiumDocument::applyOp_redaction(const nlohmann::json& op, int pageIndex) {
#ifdef PDFENGINE_WITH_PDFIUM
if (!op.contains("data") || !op["data"].is_object()) {
spdlog::error("redaction operation missing 'data' object");
return std::unexpected(EngineError::InvalidFormat);
}
auto data = op["data"];
double x = data.value("x", 0.0);
double y = data.value("y", 0.0);
double width = data.value("width", 0.0);
double height = data.value("height", 0.0);
std::string fillColor = data.value("fillColor", "#ffffff");
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
spdlog::error("Failed to load page index {} for redaction", pageIndex);
return std::unexpected(EngineError::Unknown);
}
int count = FPDFPage_CountObjects(page);
for (int i = count - 1; i >= 0; --i) {
FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page, i);
if (obj) {
float left = 0, bottom = 0, right = 0, top = 0;
if (FPDFPageObj_GetBounds(obj, &left, &bottom, &right, &top)) {
if (!(left > x + width || right < x || bottom > y + height || top < y)) {
FPDFPage_RemoveObject(page, obj);
FPDFPageObj_Destroy(obj);
}
}
}
}
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 redaction cover rectangle object");
FPDF_ClosePage(page);
return std::unexpected(EngineError::Unknown);
}
unsigned int r = 255, g = 255, b = 255;
parseHexColor(fillColor, r, g, b);
FPDFPageObj_SetFillColor(rectObj, r, g, b, 255);
FPDFPath_SetDrawMode(rectObj, FPDF_FILLMODE_WINDING, 0);
FPDFPage_InsertObject(page, rectObj);
if (!FPDFPage_GenerateContent(page)) {
spdlog::error("Failed to generate page content after redaction");
FPDF_ClosePage(page);
return std::unexpected(EngineError::Unknown);
}
FPDF_ClosePage(page);
return {};
#else
(void)op; (void)pageIndex;
return std::unexpected(EngineError::Unknown);
#endif
}
}
+179
View File
@@ -0,0 +1,179 @@
#include "parser/pdfium_internal.hpp"
namespace pdfengine::parser {
std::expected<void, EngineError> PdfiumDocument::applyOp_updateField(const nlohmann::json& op, int pageIndex) {
#ifdef PDFENGINE_WITH_PDFIUM
if (!op.contains("data") || !op["data"].is_object()) {
spdlog::error("update_field operation missing 'data' object");
return std::unexpected(EngineError::InvalidFormat);
}
auto data = op["data"];
const bool isBool = data["value"].is_boolean();
const bool boolVal = isBool && data["value"].get<bool>();
std::string strVal;
if (data["value"].is_string()) strVal = data["value"].get<std::string>();
else if (!isBool) {
spdlog::error("update_field value must be a string or boolean");
return std::unexpected(EngineError::InvalidFormat);
}
std::string targetId = data.value("annotationId", op.value("id", ""));
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
spdlog::error("Failed to load page index {} for update_field", pageIndex);
return std::unexpected(EngineError::Unknown);
}
FPDF_FORMFILLINFO formInfo{};
formInfo.version = 2;
FPDF_FORMHANDLE form = FPDFDOC_InitFormFillEnvironment(doc_, &formInfo);
int count = FPDFPage_GetAnnotCount(page);
FPDF_ANNOTATION target = nullptr;
for (int i = 0; i < count; ++i) {
FPDF_ANNOTATION annot = FPDFPage_GetAnnot(page, i);
if (!annot) continue;
std::string id;
unsigned long len = FPDFAnnot_GetStringValue(annot, "NM", nullptr, 0);
if (len > 2) {
std::vector<uint8_t> buf(len);
FPDFAnnot_GetStringValue(annot, "NM", reinterpret_cast<FPDF_WCHAR*>(buf.data()), len);
id = utf16le_to_utf8(reinterpret_cast<const char16_t*>(buf.data()), len / sizeof(char16_t));
while (!id.empty() && id.back() == '\0') id.pop_back();
}
if (id.empty()) id = "anno_" + std::to_string(pageIndex) + "_" + std::to_string(i);
if (id == targetId) { target = annot; break; }
FPDFPage_CloseAnnot(annot);
}
if (target) {
int fieldType = form ? FPDFAnnot_GetFormFieldType(form, target) : -1;
if (form) FORM_OnAfterLoadPage(page, form);
if (fieldType == 2 || fieldType == 3) {
std::string state = boolVal ? "Yes" : "Off";
auto u = utf8_to_utf16le(state);
FPDFAnnot_SetStringValue(target, "V", reinterpret_cast<FPDF_WIDESTRING>(u.data()));
FPDFAnnot_SetStringValue(target, "AS", reinterpret_cast<FPDF_WIDESTRING>(u.data()));
} else if (form && FORM_SetFocusedAnnot(form, target)) {
if (fieldType == 4 || fieldType == 5) {
int optCount = FPDFAnnot_GetOptionCount(form, target);
int sel = -1;
for (int o = 0; o < optCount; ++o) {
unsigned long ol = FPDFAnnot_GetOptionLabel(form, target, o, nullptr, 0);
if (ol <= 2) continue;
std::vector<FPDF_WCHAR> ob(ol / 2);
FPDFAnnot_GetOptionLabel(form, target, o, ob.data(), ol);
std::string label = utf16le_to_utf8(reinterpret_cast<const char16_t*>(ob.data()), ob.size());
while (!label.empty() && label.back() == '\0') label.pop_back();
if (label == strVal) { sel = o; break; }
}
if (sel >= 0) {
FORM_SetIndexSelected(form, page, sel, 1);
} else {
auto u = utf8_to_utf16le(strVal);
FPDFAnnot_SetStringValue(target, "V", reinterpret_cast<FPDF_WIDESTRING>(u.data()));
}
} else {
FORM_SelectAllText(form, page);
auto u = utf8_to_utf16le(strVal);
FORM_ReplaceSelection(form, page, reinterpret_cast<FPDF_WIDESTRING>(u.data()));
}
FORM_ForceToKillFocus(form);
} else {
std::string v = isBool ? (boolVal ? "Yes" : "Off") : strVal;
auto u = utf8_to_utf16le(v);
FPDFAnnot_SetStringValue(target, "V", reinterpret_cast<FPDF_WIDESTRING>(u.data()));
}
if (form) FORM_OnBeforeClosePage(page, form);
FPDFPage_CloseAnnot(target);
} else {
spdlog::warn("update_field: field '{}' not found on page {}", targetId, pageIndex);
}
if (form) FPDFDOC_ExitFormFillEnvironment(form);
FPDF_ClosePage(page);
return {};
#else
(void)op; (void)pageIndex;
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<void, EngineError> PdfiumDocument::applyOp_pageRotation(const nlohmann::json& op, int pageIndex) {
#ifdef PDFENGINE_WITH_PDFIUM
if (!op.contains("data") || !op["data"].is_object()) {
spdlog::error("page_rotation operation missing 'data' object");
return std::unexpected(EngineError::InvalidFormat);
}
auto data = op["data"];
int rotation = data.value("rotation", 0);
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
spdlog::error("Failed to load page index {} for page rotation", pageIndex);
return std::unexpected(EngineError::Unknown);
}
int currentCode = FPDFPage_GetRotation(page);
int currentDegrees = currentCode * 90;
int newDegrees = currentDegrees + rotation;
newDegrees = (newDegrees % 360 + 360) % 360;
int newCode = newDegrees / 90;
FPDFPage_SetRotation(page, newCode);
FPDF_ClosePage(page);
return {};
#else
(void)op; (void)pageIndex;
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<void, EngineError> PdfiumDocument::applyOp_pageDeletion(const nlohmann::json& op, int pageIndex) {
#ifdef PDFENGINE_WITH_PDFIUM
if (pageCount() <= 1) {
spdlog::error("Cannot delete the only page in the document");
return std::unexpected(EngineError::Unknown);
}
FPDFPage_Delete(doc_, pageIndex);
return {};
#else
(void)op; (void)pageIndex;
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<void, EngineError> PdfiumDocument::applyOp_pageReorder(const nlohmann::json& op, int pageIndex) {
#ifdef PDFENGINE_WITH_PDFIUM
if (!op.contains("data") || !op["data"].is_object()) {
spdlog::error("page_reorder operation missing 'data' object");
return std::unexpected(EngineError::InvalidFormat);
}
auto data = op["data"];
if (!data.contains("destPageIndex")) {
spdlog::error("page_reorder data missing 'destPageIndex'");
return std::unexpected(EngineError::InvalidFormat);
}
int destPageIndex = data["destPageIndex"];
if (destPageIndex < 0 || destPageIndex >= pageCount()) {
spdlog::error("Destination page index {} out of bounds (total pages: {})", destPageIndex, pageCount());
return std::unexpected(EngineError::PageOutOfBounds);
}
int fromIndex = pageIndex;
if (!FPDF_MovePages(doc_, &fromIndex, 1, destPageIndex)) {
spdlog::error("FPDF_MovePages failed from {} to {}", fromIndex, destPageIndex);
return std::unexpected(EngineError::Unknown);
}
return {};
#else
(void)op; (void)pageIndex;
return std::unexpected(EngineError::Unknown);
#endif
}
}
+472
View File
@@ -0,0 +1,472 @@
#include "parser/pdfium_internal.hpp"
namespace pdfengine::parser {
std::expected<void, EngineError> PdfiumDocument::applyOp_reflow(const nlohmann::json& op, int pageIndex) {
#ifdef PDFENGINE_WITH_PDFIUM
if (!op.contains("data") || !op["data"].is_object()) {
spdlog::error("reflow_paragraph missing 'data'");
return std::unexpected(EngineError::InvalidFormat);
}
auto data = op["data"];
struct RunStyle { std::string text; std::string internalFontId; double fontSize; unsigned int r, g, b; std::vector<double> advances; };
std::vector<RunStyle> runs;
auto parseHex = [](const std::string& hex, unsigned int& r, unsigned int& g, unsigned int& b) {
r = 0; g = 0; b = 0;
if (hex.size() >= 7 && hex[0] == '#') {
auto hv = [](char ch) -> int {
if (ch >= '0' && ch <= '9') return ch - '0';
if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10;
if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10;
return 0;
};
r = static_cast<unsigned int>(hv(hex[1]) * 16 + hv(hex[2]));
g = static_cast<unsigned int>(hv(hex[3]) * 16 + hv(hex[4]));
b = static_cast<unsigned int>(hv(hex[5]) * 16 + hv(hex[6]));
}
};
auto parseRun = [&](const nlohmann::json& rj) {
RunStyle rs;
rs.text = rj.value("text", "");
rs.internalFontId = rj.value("internalFontId", "");
rs.fontSize = rj.value("fontSize", 12.0);
parseHex(rj.value("color", std::string("#000000")), rs.r, rs.g, rs.b);
if (rj.contains("advances") && rj["advances"].is_array()) {
for (const auto& a : rj["advances"]) rs.advances.push_back(a.get<double>());
}
runs.push_back(std::move(rs));
};
std::vector<std::vector<int>> providedLines;
bool hasProvidedLines = data.contains("lines") && data["lines"].is_array() && !data["lines"].empty();
if (hasProvidedLines) {
for (const auto& lineJson : data["lines"]) {
std::vector<int> lineRunIdxs;
if (lineJson.is_array()) {
for (const auto& rj : lineJson) { lineRunIdxs.push_back(static_cast<int>(runs.size())); parseRun(rj); }
}
providedLines.push_back(std::move(lineRunIdxs));
}
} else if (data.contains("runs") && data["runs"].is_array()) {
for (const auto& rj : data["runs"]) parseRun(rj);
}
std::vector<int> objectIndices;
if (data.contains("objectIndices") && data["objectIndices"].is_array()) {
for (auto& idx : data["objectIndices"]) objectIndices.push_back(idx.get<int>());
}
double columnLeft = data.value("columnLeft", 0.0);
double columnRight = data.value("columnRight", 0.0);
double firstBaselineY = data.value("firstBaselineY", 0.0);
double leading = data.value("leading", 0.0);
int oldLineCount = data.value("oldLineCount", 1);
std::string align = data.value("align", std::string("left"));
std::vector<double> lineBaselineY, lineX;
if (data.contains("lineBaselineY") && data["lineBaselineY"].is_array())
for (const auto& v : data["lineBaselineY"]) lineBaselineY.push_back(v.get<double>());
if (data.contains("lineX") && data["lineX"].is_array())
for (const auto& v : data["lineX"]) lineX.push_back(v.get<double>());
double columnWidth = columnRight - columnLeft;
double pushColumnLeft = data.value("pushColumnLeft", columnLeft);
std::string paraId;
if (data.contains("paraId") && data["paraId"].is_string())
paraId = data["paraId"].get<std::string>();
if (runs.empty() || objectIndices.empty() || columnWidth <= 1.0 || leading <= 0.0) {
spdlog::warn("reflow_paragraph: insufficient layout data, skipping");
return {};
}
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
spdlog::error("Failed to load page {} for reflow_paragraph", pageIndex);
return std::unexpected(EngineError::Unknown);
}
PageBand startBand = repagComputeBand(page, pushColumnLeft, columnRight);
double bottomLimitY = startBand.valid ? startBand.bottomLimitY : -1e18;
std::vector<double> anchoredCentersStart =
startBand.valid ? repagAnchoredCenters(page, pushColumnLeft, columnRight, bottomLimitY)
: std::vector<double>{};
if (!paraId.empty()) {
int cont = repagRemoveContinuations(doc_, pageIndex, paraId, pushColumnLeft, columnRight);
if (cont > 0)
spdlog::info("reflow_paragraph: removed continuation of paraId={} on {} page(s)", paraId, cont);
}
std::vector<int> paragraphSet = objectIndices;
{
float ul = 0, ub = 0, ur = 0, ut = 0; bool haveUnion = false;
for (int idx : objectIndices) {
FPDF_PAGEOBJECT o = FPDFPage_GetObject(page, idx);
if (!o) continue;
float l = 0, b = 0, r = 0, t = 0;
if (!FPDFPageObj_GetBounds(o, &l, &b, &r, &t)) continue;
if (!haveUnion) { ul = l; ub = b; ur = r; ut = t; haveUnion = true; }
else {
if (l < ul) ul = l; if (b < ub) ub = b;
if (r > ur) ur = r; if (t > ut) ut = t;
}
}
if (haveUnion) {
const float eps = 0.5f;
int nObjs = FPDFPage_CountObjects(page);
int adopted = 0;
for (int k = 0; k < nObjs; ++k) {
if (std::find(objectIndices.begin(), objectIndices.end(), k) != objectIndices.end()) continue;
FPDF_PAGEOBJECT o = FPDFPage_GetObject(page, k);
if (!o || FPDFPageObj_GetType(o) != FPDF_PAGEOBJ_TEXT) continue;
float l = 0, b = 0, r = 0, t = 0;
if (!FPDFPageObj_GetBounds(o, &l, &b, &r, &t)) continue;
if (l >= ul - eps && b >= ub - eps && r <= ur + eps && t <= ut + eps) {
paragraphSet.push_back(k);
adopted++;
spdlog::debug("reflow_paragraph: adopted leftover text object idx={} inside paragraph bbox (not in objectIndices) -> prevents bulge/merge + font substitution", k);
}
}
if (adopted > 0)
spdlog::debug("reflow_paragraph: geometric backstop adopted {} object(s) the model omitted", adopted);
}
}
if (!paraId.empty()) {
int nObjs = FPDFPage_CountObjects(page);
int adoptedById = 0;
for (int k = 0; k < nObjs; ++k) {
if (std::find(paragraphSet.begin(), paragraphSet.end(), k) != paragraphSet.end()) continue;
FPDF_PAGEOBJECT o = FPDFPage_GetObject(page, k);
if (o && repagGetParaId(o) == paraId) { paragraphSet.push_back(k); adoptedById++; }
}
if (adoptedById > 0)
spdlog::debug("reflow_paragraph: adopted {} anchor-page object(s) by paraId", adoptedById);
}
{
std::unordered_map<std::string, double> exactByBaseName;
std::vector<double> exactSizes;
for (int idx : paragraphSet) {
FPDF_PAGEOBJECT o = FPDFPage_GetObject(page, idx);
if (!o || FPDFPageObj_GetType(o) != FPDF_PAGEOBJ_TEXT) continue;
FS_MATRIX mtx;
if (!FPDFPageObj_GetMatrix(o, &mtx)) continue;
float nominal = 0.0f;
if (!FPDFTextObj_GetFontSize(o, &nominal)) continue;
double scale = std::sqrt(static_cast<double>(mtx.a) * mtx.a +
static_cast<double>(mtx.b) * mtx.b);
double exact = static_cast<double>(nominal) * scale;
if (exact <= 0.1) continue;
exactSizes.push_back(exact);
FPDF_FONT fo = FPDFTextObj_GetFont(o);
if (!fo) continue;
size_t nl = FPDFFont_GetBaseFontName(fo, nullptr, 0);
if (nl == 0) continue;
std::vector<char> nb(nl);
if (FPDFFont_GetBaseFontName(fo, nb.data(), nl) == 0) continue;
std::string bn(nb.data());
if (!exactByBaseName.count(bn)) exactByBaseName[bn] = exact;
}
double paraExact = 0.0;
if (!exactSizes.empty()) {
std::sort(exactSizes.begin(), exactSizes.end());
paraExact = exactSizes[exactSizes.size() / 2];
}
if (paraExact > 0.1) {
for (auto& rs : runs) {
std::string bn = baseNameFromInternalFontId(rs.internalFontId);
auto it = exactByBaseName.find(bn);
rs.fontSize = (it != exactByBaseName.end() && it->second > 0.1) ? it->second : paraExact;
}
spdlog::info("reflow_paragraph: size-exact override paraExact={:.2f} ({} font(s))",
paraExact, exactByBaseName.size());
}
}
std::vector<EmissionFont> runFonts(runs.size());
auto toCodepoints = [](const std::string& s) {
auto u16 = utf8_to_utf16le(s);
std::vector<uint32_t> cps;
for (size_t i = 0; i < u16.size();) {
uint32_t cp = u16[i];
if (cp >= 0xD800 && cp <= 0xDBFF && i + 1 < u16.size()) {
uint32_t low = u16[i + 1];
if (low >= 0xDC00 && low <= 0xDFFF) { cp = 0x10000 + ((cp - 0xD800) << 10) + (low - 0xDC00); i += 2; }
else i += 1;
} else i += 1;
cps.push_back(cp);
}
return cps;
};
{
std::unordered_map<std::string, std::vector<uint32_t>> fontCps;
for (const auto& rs : runs) {
auto cps = toCodepoints(rs.text);
auto& dst = fontCps[rs.internalFontId];
dst.insert(dst.end(), cps.begin(), cps.end());
}
auto resolveOrigFont = [&](const std::string& fid) -> FPDF_FONT {
const std::string expected = baseNameFromInternalFontId(fid);
for (int idx : paragraphSet) {
FPDF_PAGEOBJECT o = FPDFPage_GetObject(page, idx);
if (!o || FPDFPageObj_GetType(o) != FPDF_PAGEOBJ_TEXT) continue;
FPDF_FONT fo = FPDFTextObj_GetFont(o);
if (!fo) continue;
size_t nl = FPDFFont_GetBaseFontName(fo, nullptr, 0);
if (nl == 0) continue;
std::vector<char> nb(nl);
if (FPDFFont_GetBaseFontName(fo, nb.data(), nl) == 0) continue;
if (std::string(nb.data()) == expected) return fo;
}
return nullptr;
};
std::unordered_map<std::string, EmissionFont> fontByFid;
for (const auto& rs : runs) {
if (!fontByFid.count(rs.internalFontId))
fontByFid[rs.internalFontId] = loadEmissionFont(pageIndex, rs.internalFontId, rs.fontSize, fontCps[rs.internalFontId], paragraphSet, resolveOrigFont(rs.internalFontId));
}
for (size_t ri = 0; ri < runs.size(); ++ri) runFonts[ri] = fontByFid[runs[ri].internalFontId];
for (size_t ri = 0; ri < runs.size(); ++ri) {
const fonts::FontFace* face = runFonts[ri].measureFace
? runFonts[ri].measureFace.get()
: (runFonts[ri].resolved ? &runFonts[ri].resolved->getFontFace() : nullptr);
runs[ri].text = normalizeReflowText(runs[ri].text, face);
}
}
fonts::HbShaper shaper;
constexpr unsigned int kRefSize = 1000;
auto perCharAdvances = [&](size_t runIdx, const std::string& text) -> std::vector<double> {
std::vector<double> out(text.size(), 0.0);
if (text.empty()) return out;
auto& rf = runFonts[runIdx];
double size = runs[runIdx].fontSize > 0 ? runs[runIdx].fontSize : 12.0;
double scale = size / static_cast<double>(kRefSize);
fonts::FontFace* face = rf.measureFace ? rf.measureFace.get()
: (rf.resolved ? &rf.resolved->getFontFace() : nullptr);
if (face) {
auto glyphs = shaper.shapeRun(text, *face, kRefSize);
if (glyphs.size() == text.size()) {
for (size_t i = 0; i < text.size(); ++i) out[i] = glyphs[i].advanceX * scale;
return out;
}
double w = 0.0; for (auto& gph : glyphs) w += gph.advanceX;
double per = w * scale / static_cast<double>(text.size());
for (auto& v : out) v = per;
return out;
}
for (auto& v : out) v = size * 0.5;
return out;
};
std::vector<std::vector<double>> runCharAdv(runs.size());
std::vector<char> runPerChar(runs.size(), 0);
for (size_t ri = 0; ri < runs.size(); ++ri) {
if (runs[ri].advances.size() == runs[ri].text.size() && !runs[ri].text.empty()) {
runCharAdv[ri] = runs[ri].advances;
auto natural = perCharAdvances(ri, runs[ri].text);
bool diverges = natural.size() != runCharAdv[ri].size();
for (size_t c = 0; !diverges && c < natural.size(); ++c)
if (std::abs(natural[c] - runCharAdv[ri][c]) > 0.05) diverges = true;
runPerChar[ri] = diverges ? 1 : 0;
} else {
runCharAdv[ri] = perCharAdvances(ri, runs[ri].text);
}
}
auto charAdvAt = [&](int ri, size_t off) -> double {
const auto& v = runCharAdv[static_cast<size_t>(ri)];
return off < v.size() ? v[off] : 0.0;
};
auto isSpace = [](char ch) { return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'; };
struct Seg { int runIdx; std::string text; double width; size_t off; };
struct Word { std::vector<Seg> segs; double width; double spaceAfter; };
std::vector<Word> words;
auto tokenize = [&](const std::vector<int>& runIdxs) -> std::vector<size_t> {
std::string ft; std::vector<int> sof; std::vector<size_t> cof;
for (int ri : runIdxs) {
size_t off = 0;
for (char ch : runs[ri].text) { ft.push_back(ch); sof.push_back(ri); cof.push_back(off++); }
}
std::vector<size_t> out;
size_t i = 0;
while (i < ft.size()) {
if (isSpace(ft[i])) { i++; continue; }
Word w; w.width = 0.0; w.spaceAfter = 0.0;
while (i < ft.size() && !isSpace(ft[i])) {
int st = sof[i]; size_t segOff = cof[i]; std::string frag; double fw = 0.0;
while (i < ft.size() && !isSpace(ft[i]) && sof[i] == st) { frag.push_back(ft[i]); fw += charAdvAt(st, cof[i]); i++; }
w.segs.push_back({st, frag, fw, segOff}); w.width += fw;
}
while (i < ft.size() && isSpace(ft[i])) { w.spaceAfter += charAdvAt(sof[i], cof[i]); i++; }
out.push_back(words.size());
words.push_back(std::move(w));
}
return out;
};
std::vector<std::vector<size_t>> lines;
if (hasProvidedLines) {
for (const auto& lineRunIdxs : providedLines) {
auto wi = tokenize(lineRunIdxs);
if (!wi.empty()) lines.push_back(std::move(wi));
}
} else {
std::vector<int> allRuns(runs.size());
for (size_t ri = 0; ri < runs.size(); ++ri) allRuns[ri] = static_cast<int>(ri);
auto allWords = tokenize(allRuns);
std::vector<size_t> cur; double curW = 0.0;
for (size_t k = 0; k < allWords.size(); ++k) {
size_t wi = allWords[k];
double gap = cur.empty() ? 0.0 : words[allWords[k - 1]].spaceAfter;
if (!cur.empty() && curW + gap + words[wi].width > columnWidth) {
lines.push_back(cur); cur.clear();
cur.push_back(wi); curW = words[wi].width;
} else {
cur.push_back(wi); curW += gap + words[wi].width;
}
}
if (!cur.empty()) lines.push_back(cur);
}
if (words.empty() || lines.empty()) { FPDF_ClosePage(page); return {}; }
int newLineCount = static_cast<int>(lines.size());
double deltaH = (newLineCount - oldLineCount) * leading;
double paragraphBottomBaseline = firstBaselineY - (oldLineCount - 1) * leading;
if (std::abs(deltaH) > 0.01) {
double threshold = paragraphBottomBaseline - 0.5 * leading;
int nObjs = FPDFPage_CountObjects(page);
int pushed = 0;
for (int k = 0; k < nObjs; ++k) {
if (std::find(paragraphSet.begin(), paragraphSet.end(), k) != paragraphSet.end()) continue;
FPDF_PAGEOBJECT o = FPDFPage_GetObject(page, k);
if (!o) continue;
float l = 0, bo = 0, rr = 0, tt = 0;
if (!FPDFPageObj_GetBounds(o, &l, &bo, &rr, &tt)) continue;
double cY = (bo + tt) / 2.0;
if (cY < threshold && cY >= bottomLimitY && rr > pushColumnLeft && l < columnRight) {
FPDFPageObj_Transform(o, 1.0, 0.0, 0.0, 1.0, 0.0, -deltaH);
pushed++;
}
}
spdlog::info("reflow_paragraph: lines {}->{}, deltaH={}, pushed {} objects", oldLineCount, newLineCount, deltaH, pushed);
}
std::sort(paragraphSet.begin(), paragraphSet.end(), std::greater<int>());
int minIndex = paragraphSet.back();
for (int idx : paragraphSet) {
FPDF_PAGEOBJECT o = FPDFPage_GetObject(page, idx);
if (o) { FPDFPage_RemoveObject(page, o); FPDFPageObj_Destroy(o); }
}
nlohmann::json layoutLines = nlohmann::json::array();
for (size_t li = 0; li < lines.size(); ++li) {
double baselineY = (li < lineBaselineY.size())
? lineBaselineY[li] : firstBaselineY - static_cast<double>(li) * leading;
auto& lw = lines[li];
double naturalW = 0.0;
for (size_t k = 0; k < lw.size(); ++k) {
naturalW += words[lw[k]].width;
if (k > 0) naturalW += words[lw[k - 1]].spaceAfter;
}
bool justifyThis = (align == "justify") && (li + 1 < lines.size()) && lw.size() > 1;
double extraPerGap = 0.0;
if (justifyThis) { double slack = columnWidth - naturalW; if (slack > 0) extraPerGap = slack / static_cast<double>(lw.size() - 1); }
std::string lineText;
std::vector<double> adv;
double lineFontSize = 0.0;
double x = (li < lineX.size()) ? lineX[li] : columnLeft;
if (li >= lineX.size()) {
if (align == "right") x = columnLeft + (columnWidth - naturalW);
else if (align == "center") x = columnLeft + (columnWidth - naturalW) / 2.0;
}
const double lineStartX = x;
for (size_t k = 0; k < lw.size(); ++k) {
size_t wi = lw[k];
if (k > 0) {
double gap = words[lw[k - 1]].spaceAfter + (justifyThis ? extraPerGap : 0.0);
x += gap;
lineText.push_back(' ');
adv.push_back(gap);
}
double segX = x;
for (auto& seg : words[wi].segs) {
if (lineFontSize <= 0.0) lineFontSize = runs[seg.runIdx].fontSize;
FPDF_FONT font = runFonts[seg.runIdx].font;
auto emitObj = [&](const std::string& s, double atX) {
if (!font || s.empty()) return;
FPDF_PAGEOBJECT obj = FPDFPageObj_CreateTextObj(doc_, font, static_cast<float>(runs[seg.runIdx].fontSize));
if (!obj) return;
FPDFPageObj_SetFillColor(obj, runs[seg.runIdx].r, runs[seg.runIdx].g, runs[seg.runIdx].b, 255);
auto u16 = utf8_to_utf16le(s); u16.push_back(0);
FPDFText_SetText(obj, reinterpret_cast<FPDF_WIDESTRING>(u16.data()));
FPDFPageObj_Transform(obj, 1.0, 0.0, 0.0, 1.0, atX, baselineY);
repagSetParaId(doc_, obj, paraId);
FPDFPage_InsertObjectAtIndex(page, obj, minIndex);
};
if (runPerChar[seg.runIdx]) {
double gx = segX;
for (size_t c = 0; c < seg.text.size(); ++c) {
double a = charAdvAt(seg.runIdx, seg.off + c);
if (seg.text[c] != ' ') emitObj(std::string(1, seg.text[c]), gx);
lineText.push_back(seg.text[c]); adv.push_back(a);
gx += a;
}
} else {
emitObj(seg.text, segX);
for (size_t c = 0; c < seg.text.size(); ++c) { lineText.push_back(seg.text[c]); adv.push_back(charAdvAt(seg.runIdx, seg.off + c)); }
}
segX += seg.width;
}
x += words[wi].width;
}
layoutLines.push_back({
{"baselineY", baselineY}, {"x0", lineStartX},
{"fontSize", lineFontSize > 0 ? lineFontSize : leading / 1.2},
{"text", lineText}, {"adv", adv},
});
}
{
int pageOff = 0; double cur = 0.0; bool flowing = false;
for (auto& ln : layoutLines) {
double b = ln["baselineY"].get<double>();
if (!flowing && startBand.valid && b < startBand.bottomLimitY - 0.01) {
flowing = true; pageOff = 1; cur = startBand.placementTopY;
}
if (flowing) {
if (cur < startBand.bottomLimitY - 0.01) { pageOff++; cur = startBand.placementTopY; }
ln["baselineY"] = cur; cur -= leading;
}
ln["pageIndex"] = pageIndex + pageOff;
}
}
lastReflowLayout_ = nlohmann::json{
{"columnLeft", columnLeft}, {"anchorPage", pageIndex}, {"lines", layoutLines}}.dump();
if (!FPDFPage_GenerateContent(page)) {
spdlog::error("Failed to generate page content after reflow_paragraph");
}
FPDF_ClosePage(page);
lastReflowOverflowed_ = false;
if (startBand.valid) {
int moved = 0;
int added = repaginateForward(doc_, pageIndex, columnLeft, columnRight,
pushColumnLeft, leading, anchoredCentersStart, &moved);
int removed = repaginateBackward(doc_, pageIndex, columnLeft, columnRight,
pushColumnLeft, leading);
lastReflowOverflowed_ = (moved > 0 || added > 0 || removed > 0);
if (added > 0 || removed > 0)
spdlog::info("reflow_paragraph: cross-page reflow added {} / removed {} page(s)",
added, removed);
}
return {};
#else
(void)op; (void)pageIndex;
return std::unexpected(EngineError::Unknown);
#endif
}
}
+610
View File
@@ -0,0 +1,610 @@
#include "parser/pdfium_internal.hpp"
namespace pdfengine::parser {
std::expected<void, EngineError> PdfiumDocument::applyOp_replaceText(const nlohmann::json& op, int pageIndex) {
#ifdef PDFENGINE_WITH_PDFIUM
std::vector<int> objectIndices;
std::string newText = "";
std::string internalFontId = "";
double fontSize = -1.0;
bool disableJustify = false;
if (op.contains("data") && op["data"].is_object()) {
auto data = op["data"];
if (data.contains("objectIndices") && data["objectIndices"].is_array()) {
for (auto& idx : data["objectIndices"]) {
objectIndices.push_back(idx.get<int>());
}
}
newText = data.value("text", "");
internalFontId = data.value("internalFontId", "");
if (data.contains("fontSize")) {
fontSize = data["fontSize"].get<double>();
}
disableJustify = data.value("disableJustify", false);
} else {
if (op.contains("objectIndices") && op["objectIndices"].is_array()) {
for (auto& idx : op["objectIndices"]) {
objectIndices.push_back(idx.get<int>());
}
}
newText = op.value("text", "");
internalFontId = op.value("internalFontId", "");
if (op.contains("fontSize")) {
fontSize = op["fontSize"].get<double>();
}
}
if (objectIndices.empty()) {
spdlog::warn("replace_text has empty objectIndices, nothing to replace");
return {};
}
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
spdlog::error("Failed to load page index {} for replace_text", pageIndex);
return std::unexpected(EngineError::Unknown);
}
std::sort(objectIndices.begin(), objectIndices.end(), std::greater<int>());
int minIndex = objectIndices.back();
FPDF_PAGEOBJECT origObj = FPDFPage_GetObject(page, minIndex);
if (!origObj) {
spdlog::error("Failed to get original text object at index {}", minIndex);
FPDF_ClosePage(page);
return std::unexpected(EngineError::Unknown);
}
double a = 1.0, b = 0.0, c = 0.0, d = 1.0, e = 0.0, f = 0.0;
FS_MATRIX matrix;
if (FPDFPageObj_GetMatrix(origObj, &matrix)) {
a = matrix.a;
b = matrix.b;
c = matrix.c;
d = matrix.d;
e = matrix.e;
f = matrix.f;
}
unsigned int r = 0, g = 0, b_color = 0, a_color = 255;
FPDFPageObj_GetFillColor(origObj, &r, &g, &b_color, &a_color);
if (fontSize < 0.0) {
float sizeVal = 12.0f;
if (FPDFTextObj_GetFontSize(origObj, &sizeVal)) {
fontSize = sizeVal;
} else {
fontSize = 12.0;
}
}
FPDF_TEXT_RENDERMODE renderMode = static_cast<FPDF_TEXT_RENDERMODE>(FPDFTextObj_GetTextRenderMode(origObj));
std::string fontName = "Helvetica";
std::string origFontName = "";
bool bold = false;
bool italic = false;
FPDF_FONT origFont = FPDFTextObj_GetFont(origObj);
if (origFont) {
size_t nameLen = FPDFFont_GetBaseFontName(origFont, nullptr, 0);
if (nameLen > 0) {
std::vector<char> nameBuf(nameLen);
if (FPDFFont_GetBaseFontName(origFont, nameBuf.data(), nameLen) > 0) {
origFontName = nameBuf.data();
std::string baseName(nameBuf.data());
std::string lowerName = baseName;
std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
bold = (lowerName.find("bold") != std::string::npos);
italic = (lowerName.find("italic") != std::string::npos || lowerName.find("oblique") != std::string::npos);
if (lowerName.find("times") != std::string::npos) {
if (bold && italic) fontName = "Times-BoldItalic";
else if (bold) fontName = "Times-Bold";
else if (italic) fontName = "Times-Italic";
else fontName = "Times-Roman";
} else if (lowerName.find("courier") != std::string::npos) {
if (bold && italic) fontName = "Courier-BoldOblique";
else if (bold) fontName = "Courier-Bold";
else if (italic) fontName = "Courier-Oblique";
else fontName = "Courier";
} else {
if (bold && italic) fontName = "Helvetica-BoldOblique";
else if (bold) fontName = "Helvetica-Bold";
else if (italic) fontName = "Helvetica-Oblique";
else fontName = "Helvetica";
}
}
}
}
if (!internalFontId.empty()) {
std::string lowerId = internalFontId;
std::transform(lowerId.begin(), lowerId.end(), lowerId.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
if (lowerId.find("bolditalic") != std::string::npos) { bold = true; italic = true; }
else if (lowerId.find("bold") != std::string::npos) { bold = true; }
else if (lowerId.find("italic") != std::string::npos) { italic = true; }
else if (lowerId.find("oblique") != std::string::npos) { italic = true; }
if (lowerId.find("times") != std::string::npos) {
if (bold && italic) fontName = "Times-BoldItalic";
else if (bold) fontName = "Times-Bold";
else if (italic) fontName = "Times-Italic";
else fontName = "Times-Roman";
} else if (lowerId.find("courier") != std::string::npos) {
if (bold && italic) fontName = "Courier-BoldOblique";
else if (bold) fontName = "Courier-Bold";
else if (italic) fontName = "Courier-Oblique";
else fontName = "Courier";
} else if (lowerId.find("helvetica") != std::string::npos) {
if (bold && italic) fontName = "Helvetica-BoldOblique";
else if (bold) fontName = "Helvetica-Bold";
else if (italic) fontName = "Helvetica-Oblique";
else fontName = "Helvetica";
}
}
std::optional<FontInfo> matchedFontInfo;
auto fontsRes = getFonts(pageIndex, pageIndex);
if (fontsRes.has_value()) {
for (const auto& fontInfoEntry : *fontsRes) {
if ((!internalFontId.empty() && fontInfoEntry.internalFontId == internalFontId) ||
(!origFontName.empty() && fontInfoEntry.fontName == origFontName)) {
matchedFontInfo = fontInfoEntry;
break;
}
}
}
std::shared_ptr<fonts::pdf_fonts::Font> resolvedFont = nullptr;
if (matchedFontInfo.has_value()) {
auto resolvedFontRes = getResolvedFont(*matchedFontInfo);
if (resolvedFontRes.has_value()) {
resolvedFont = *resolvedFontRes;
spdlog::info("Font Engine: resolved font '{}'", matchedFontInfo->fontName);
} else {
spdlog::warn("Font Engine: failed to resolve font '{}': {}", matchedFontInfo->fontName, resolvedFontRes.error());
}
}
bool fontSupportsAll = true;
double totalWidth = 0.0;
auto utf16 = utf8_to_utf16le(newText);
std::vector<uint32_t> unicodeCodepoints;
for (size_t i = 0; i < utf16.size(); ) {
uint32_t cp = utf16[i];
if (cp >= 0xD800 && cp <= 0xDBFF && i + 1 < utf16.size()) {
uint32_t low = utf16[i + 1];
if (low >= 0xDC00 && low <= 0xDFFF) {
cp = 0x10000 + ((cp - 0xD800) << 10) + (low - 0xDC00);
i += 2;
} else {
i += 1;
}
} else {
i += 1;
}
unicodeCodepoints.push_back(cp);
}
bool isSubsetFont = matchedFontInfo && matchedFontInfo->isSubset;
bool subsetLacksGlyphs = false;
if (resolvedFont) {
for (uint32_t cp : unicodeCodepoints) {
if (!resolvedFont->hasGlyph(cp)) {
if (isSubsetFont) {
subsetLacksGlyphs = true;
} else {
fontSupportsAll = false;
}
spdlog::warn("Font Engine: Glyph for codepoint {} not found in font {}", cp, matchedFontInfo ? matchedFontInfo->fontName : "Unknown");
}
}
} else {
fontSupportsAll = false;
}
bool shapedSuccessful = false;
if (resolvedFont) {
try {
fonts::HbShaper shaper;
unsigned int uFontSize = static_cast<unsigned int>(fontSize > 0.0 ? fontSize : 12.0);
auto shapedGlyphs = shaper.shapeRun(newText, resolvedFont->getFontFace(), uFontSize);
if (!shapedGlyphs.empty()) {
totalWidth = 0.0;
for (const auto& sg : shapedGlyphs) {
totalWidth += sg.advanceX;
}
shapedSuccessful = true;
spdlog::info("Font Engine: HarfBuzz shaped '{}' glyphs, total advance width = {}", shapedGlyphs.size(), totalWidth);
}
} catch (const std::exception& e) {
spdlog::warn("Font Engine: HarfBuzz shaping failed: {}", e.what());
} catch (...) {
spdlog::warn("Font Engine: HarfBuzz shaping failed with unknown exception");
}
}
if (!shapedSuccessful && resolvedFont) {
totalWidth = 0.0;
for (uint32_t cp : unicodeCodepoints) {
double w = resolvedFont->getAdvanceWidth(cp, fontSize);
totalWidth += w;
}
spdlog::info("Font Engine: FreeType fallback total advance width = {}", totalWidth);
}
float origLeft = 999999.0f, origRight = -999999.0f;
float origBottom = 999999.0f, origTop = -999999.0f;
bool hasOrigBounds = false;
for (int idx : objectIndices) {
FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page, idx);
if (obj) {
float left = 0.0f, bottom = 0.0f, right = 0.0f, top = 0.0f;
if (FPDFPageObj_GetBounds(obj, &left, &bottom, &right, &top)) {
if (left < origLeft) origLeft = left;
if (right > origRight) origRight = right;
if (bottom < origBottom) origBottom = bottom;
if (top > origTop) origTop = top;
hasOrigBounds = true;
}
}
}
double origWidth = 0.0;
double origCenterY = 0.0;
if (hasOrigBounds) {
origWidth = origRight - origLeft;
origCenterY = (origBottom + origTop) / 2.0;
}
bool axisAligned = (std::abs(b) < 1e-6 && std::abs(c) < 1e-6 && a > 0.0 && d > 0.0);
double colRight = origRight;
bool sawSibling = false;
if (axisAligned && hasOrigBounds) {
int nObjForCol = FPDFPage_CountObjects(page);
for (int k = 0; k < nObjForCol; ++k) {
if (std::find(objectIndices.begin(), objectIndices.end(), k) != objectIndices.end()) continue;
FPDF_PAGEOBJECT o = FPDFPage_GetObject(page, k);
if (!o || FPDFPageObj_GetType(o) != FPDF_PAGEOBJ_TEXT) continue;
float l = 0, bo = 0, rr = 0, tt = 0;
if (!FPDFPageObj_GetBounds(o, &l, &bo, &rr, &tt)) continue;
if (std::abs(l - origLeft) <= 3.0) { sawSibling = true; if (rr > colRight) colRight = rr; }
}
}
double justifyTol = (std::max)(4.0, (colRight - origLeft) * 0.02);
bool wasJustified = !disableJustify && axisAligned && resolvedFont && hasOrigBounds && sawSibling &&
(colRight - origLeft) > 20.0 && (origRight >= colRight - justifyTol);
double deltaX = 0.0;
if (hasOrigBounds) {
deltaX = totalWidth - origWidth;
spdlog::info("Reflow Engine: origWidth = {}, newWidth = {}, deltaX = {}", origWidth, totalWidth, deltaX);
}
if (!wasJustified && !disableJustify && hasOrigBounds && std::abs(deltaX) > 0.001) {
int pageObjCount = FPDFPage_CountObjects(page);
double tolerance = (std::max)(5.0, fontSize * 0.5);
int reflowedCount = 0;
for (int k = 0; k < pageObjCount; ++k) {
if (std::find(objectIndices.begin(), objectIndices.end(), k) != objectIndices.end()) {
continue;
}
FPDF_PAGEOBJECT otherObj = FPDFPage_GetObject(page, k);
if (otherObj && FPDFPageObj_GetType(otherObj) == FPDF_PAGEOBJ_TEXT) {
float otherLeft = 0.0f, otherBottom = 0.0f, otherRight = 0.0f, otherTop = 0.0f;
if (FPDFPageObj_GetBounds(otherObj, &otherLeft, &otherBottom, &otherRight, &otherTop)) {
double otherCenterY = (otherBottom + otherTop) / 2.0;
if (std::abs(otherCenterY - origCenterY) <= tolerance) {
if (otherLeft >= (origRight - 2.0f)) {
FPDFPageObj_Transform(otherObj, 1.0, 0.0, 0.0, 1.0, deltaX, 0.0);
reflowedCount++;
}
}
}
}
}
spdlog::info("Reflow Engine: shifted {} subsequent text objects on the same line by {}", reflowedCount, deltaX);
}
for (int idx : objectIndices) {
FPDF_PAGEOBJECT objToRemove = FPDFPage_GetObject(page, idx);
if (objToRemove) {
FPDFPage_RemoveObject(page, objToRemove);
FPDFPageObj_Destroy(objToRemove);
}
}
FPDF_FONT font = nullptr;
std::string cacheKey = "";
bool useEmbedded = false;
bool useSystem = false;
if (resolvedFont && matchedFontInfo) {
if (matchedFontInfo->isEmbedded && !isSubsetFont && fontSupportsAll) {
cacheKey = matchedFontInfo->internalFontId;
useEmbedded = true;
} else if (matchedFontInfo->isEmbedded && isSubsetFont && !subsetLacksGlyphs) {
cacheKey = matchedFontInfo->internalFontId;
useEmbedded = true;
} else if (!matchedFontInfo->isEmbedded) {
cacheKey = "standard_" + fontName;
} else {
cacheKey = "system_embed_" + matchedFontInfo->fontName + "_" + (bold ? "B" : "") + (italic ? "I" : "");
useSystem = true;
}
} else {
cacheKey = "standard_" + fontName;
}
{
std::lock_guard<std::mutex> lock(loadedFontsMutex_);
if (loadedFontsCache_.count(cacheKey)) {
font = loadedFontsCache_[cacheKey];
spdlog::info("Font Engine: Reusing cached FPDF_FONT for key '{}'", cacheKey);
}
}
FPDF_FONT reconFont = nullptr;
const fonts::pdf_fonts::ReconstructedFont* reconRf = nullptr;
if (matchedFontInfo && matchedFontInfo->isEmbedded && classifyFontFidelity(*matchedFontInfo) != "exact") {
reconRf = lookupReconFont(matchedFontInfo->internalFontId);
if (reconRf && reconRf->ok) {
std::lock_guard<std::mutex> lock(loadedFontsMutex_);
const std::string rkey = "recon_" + matchedFontInfo->internalFontId;
if (loadedFontsCache_.count(rkey)) reconFont = loadedFontsCache_[rkey];
else {
loadedFontDataBuffers_[rkey] = reconRf->sfnt;
const auto& bytes = loadedFontDataBuffers_[rkey];
reconFont = FPDFText_LoadFont(doc_, bytes.data(), static_cast<uint32_t>(bytes.size()), FPDF_FONT_TRUETYPE, true);
if (reconFont) loadedFontsCache_[rkey] = reconFont;
}
if (reconFont) {
bool fullyCovered = true;
for (uint32_t cp : unicodeCodepoints)
if (cp >= 0x20 && !reconRf->coveredUnicode.count(cp)) { fullyCovered = false; break; }
if (fullyCovered && !font) {
font = reconFont;
spdlog::info("Tier-2: replace_text exact reconstructed font '{}'", matchedFontInfo->internalFontId);
}
}
}
}
if (!font) {
if (useEmbedded) {
auto fontDataRes = getFontData(matchedFontInfo->internalFontId);
if (fontDataRes.has_value()) {
std::lock_guard<std::mutex> lock(loadedFontsMutex_);
loadedFontDataBuffers_[cacheKey] = fontDataRes.value();
const auto& bytes = loadedFontDataBuffers_[cacheKey];
font = FPDFText_LoadFont(doc_, bytes.data(), static_cast<uint32_t>(bytes.size()), FPDF_FONT_TRUETYPE, false);
if (font) {
spdlog::info("Font Engine: Loaded embedded font '{}' (cache key: {})", matchedFontInfo->fontName, cacheKey);
}
}
} else if (useSystem) {
std::string fontPath = fonts::pdf_fonts::FontFallback::getInstance().getFallbackFontPath(
matchedFontInfo->normalizedFamily.empty() ? matchedFontInfo->fontName : matchedFontInfo->normalizedFamily,
bold,
italic
);
std::ifstream fs(fontPath, std::ios::binary);
if (fs) {
std::vector<uint8_t> fileBytes((std::istreambuf_iterator<char>(fs)), std::istreambuf_iterator<char>());
if (!fileBytes.empty()) {
const size_t fullSize = fileBytes.size();
std::lock_guard<std::mutex> lock(loadedFontsMutex_);
loadedFontDataBuffers_[cacheKey] = std::move(fileBytes);
const auto& bytes = loadedFontDataBuffers_[cacheKey];
font = FPDFText_LoadFont(doc_, bytes.data(), static_cast<uint32_t>(bytes.size()), FPDF_FONT_TRUETYPE, true);
if (font) {
spdlog::info("Font Engine: Embedded FULL CID system font '{}' ({} bytes) from '{}'",
matchedFontInfo->fontName, fullSize, fontPath);
}
}
} else {
spdlog::warn("Font Engine: Failed to open system font file '{}' for embedding", fontPath);
}
}
if (!font) {
spdlog::info("Font Engine: Loading standard PDF font for replace_text: {}", fontName);
font = FPDFText_LoadStandardFont(doc_, fontName.c_str());
if (!font) {
font = FPDFText_LoadStandardFont(doc_, "Helvetica");
}
}
if (font) {
std::lock_guard<std::mutex> lock(loadedFontsMutex_);
loadedFontsCache_[cacheKey] = font;
}
}
if (font) {
constexpr unsigned int kRef = 1000;
double emToPage = (fontSize > 0.0 ? fontSize : 1.0) * a / static_cast<double>(kRef);
auto pageWidthOf = [&](const std::string& s) -> double {
if (s.empty() || !resolvedFont) return 0.0;
double sum = 0.0;
try {
fonts::HbShaper sh;
auto gl = sh.shapeRun(s, resolvedFont->getFontFace(), kRef);
for (const auto& gg : gl) sum += gg.advanceX;
} catch (...) {
for (unsigned char ch : s) sum += resolvedFont->getAdvanceWidth(ch, kRef);
}
return sum * emToPage;
};
std::vector<std::string> words;
if (wasJustified) {
std::string cur;
for (char ch : newText) {
if (ch == ' ') { if (!cur.empty()) { words.push_back(cur); cur.clear(); } }
else cur.push_back(ch);
}
if (!cur.empty()) words.push_back(cur);
}
auto measuredWidth = [&](const std::vector<unsigned short>& u16le) -> double {
FPDF_PAGEOBJECT m = FPDFPageObj_CreateTextObj(doc_, font, static_cast<float>(fontSize));
if (!m) return 0.0;
FPDFText_SetText(m, reinterpret_cast<FPDF_WIDESTRING>(u16le.data()));
FPDFPageObj_Transform(m, a, b, c, d, 0.0, 0.0);
float l = 0, bo = 0, rr = 0, tt = 0;
double w = FPDFPageObj_GetBounds(m, &l, &bo, &rr, &tt) ? (rr - l) : 0.0;
FPDFPageObj_Destroy(m);
return w;
};
if (wasJustified && words.size() > 1) {
std::vector<double> wpx;
wpx.reserve(words.size());
double estWords = 0.0;
for (const auto& w : words) { double ww = pageWidthOf(w); wpx.push_back(ww); estWords += ww; }
double estSpace = pageWidthOf(" ");
int gaps = static_cast<int>(words.size()) - 1;
double estTotal = estWords + gaps * estSpace;
double actualFull = measuredWidth(utf16);
double k = (estTotal > 1e-6 && actualFull > 1e-6) ? actualFull / estTotal : 1.0;
double targetW = colRight - e;
double slack = targetW - actualFull;
double extraPerGap = (slack > 0.0) ? slack / gaps : 0.0;
spdlog::info("replace_text: justify {} words targetW={:.1f} actualW={:.1f} extraPerGap={:.2f}",
words.size(), targetW, actualFull, extraPerGap);
double penX = e;
for (size_t wi = 0; wi < words.size(); ++wi) {
FPDF_PAGEOBJECT wobj = FPDFPageObj_CreateTextObj(doc_, font, static_cast<float>(fontSize));
if (wobj) {
FPDFPageObj_SetFillColor(wobj, r, g, b_color, a_color);
FPDFTextObj_SetTextRenderMode(wobj, renderMode);
auto wu = utf8_to_utf16le(words[wi]); wu.push_back(0);
FPDFText_SetText(wobj, reinterpret_cast<FPDF_WIDESTRING>(wu.data()));
FPDFPageObj_Transform(wobj, a, b, c, d, penX, f);
FPDFPage_InsertObjectAtIndex(page, wobj, minIndex);
}
penX += (wpx[wi] + estSpace) * k + extraPerGap;
}
} else {
bool didHybrid = false;
if (reconFont && reconRf && font && font != reconFont && !unicodeCodepoints.empty()) {
fonts::FontFace reconFace; bool haveReconFace = reconFace.loadFromMemory(reconRf->sfnt);
fonts::FontFace subFace; bool haveSubFace = false;
{
std::lock_guard<std::mutex> lock(loadedFontsMutex_);
auto it = loadedFontDataBuffers_.find(cacheKey);
if (it != loadedFontDataBuffers_.end() && !it->second.empty())
haveSubFace = subFace.loadFromMemory(it->second);
}
auto segWidthPage = [&](fonts::FontFace* face, const std::string& s) -> double {
if (!face || s.empty()) return 0.0;
double sum = 0.0;
try { fonts::HbShaper sh; for (const auto& gg : sh.shapeRun(s, *face, kRef)) sum += gg.advanceX; }
catch (...) { return 0.0; }
return sum * emToPage;
};
auto isCov = [&](uint32_t cp){ return cp < 0x20 || reconRf->coveredUnicode.count(cp) != 0; };
auto cpToU16 = [](uint32_t cp, std::vector<unsigned short>& dst){
if (cp <= 0xFFFF) dst.push_back(static_cast<unsigned short>(cp));
else { cp -= 0x10000; dst.push_back(static_cast<unsigned short>(0xD800 + (cp >> 10)));
dst.push_back(static_cast<unsigned short>(0xDC00 + (cp & 0x3FF))); }
};
auto cpToU8 = [](uint32_t cp, std::string& d){
if (cp < 0x80) d += static_cast<char>(cp);
else if (cp < 0x800) { d += static_cast<char>(0xC0 | (cp >> 6)); d += static_cast<char>(0x80 | (cp & 0x3F)); }
else if (cp < 0x10000) { d += static_cast<char>(0xE0 | (cp >> 12)); d += static_cast<char>(0x80 | ((cp >> 6) & 0x3F)); d += static_cast<char>(0x80 | (cp & 0x3F)); }
else { d += static_cast<char>(0xF0 | (cp >> 18)); d += static_cast<char>(0x80 | ((cp >> 12) & 0x3F)); d += static_cast<char>(0x80 | ((cp >> 6) & 0x3F)); d += static_cast<char>(0x80 | (cp & 0x3F)); }
};
double penX = e;
size_t i = 0;
while (i < unicodeCodepoints.size()) {
bool cov = isCov(unicodeCodepoints[i]);
std::vector<unsigned short> seg; std::string seg8;
while (i < unicodeCodepoints.size() && isCov(unicodeCodepoints[i]) == cov) {
cpToU16(unicodeCodepoints[i], seg); cpToU8(unicodeCodepoints[i], seg8); ++i;
}
seg.push_back(0);
FPDF_FONT segFont = cov ? reconFont : font;
fonts::FontFace* segFace = cov ? (haveReconFace ? &reconFace : nullptr)
: (haveSubFace ? &subFace : nullptr);
FPDF_PAGEOBJECT obj = FPDFPageObj_CreateTextObj(doc_, segFont, static_cast<float>(fontSize));
if (obj) {
FPDFPageObj_SetFillColor(obj, r, g, b_color, a_color);
FPDFTextObj_SetTextRenderMode(obj, renderMode);
FPDFText_SetText(obj, reinterpret_cast<FPDF_WIDESTRING>(seg.data()));
FPDFPageObj_Transform(obj, a, b, c, d, penX, f);
FPDFPage_InsertObjectAtIndex(page, obj, minIndex);
}
double adv = segWidthPage(segFace, seg8);
if (adv <= 0.0) { // shaping unavailable -> fall back to ink bbox
if (obj) { float l=0,bo=0,rr=0,tt=0; if (FPDFPageObj_GetBounds(obj,&l,&bo,&rr,&tt)) adv = rr - l; }
}
penX += adv;
}
didHybrid = true;
spdlog::info("Tier-2: replace_text HYBRID emission for '{}' (mixed embedded/substitute)", internalFontId);
}
if (!didHybrid)
{
double aScale = a;
if (disableJustify && hasOrigBounds && axisAligned) {
double newW = measuredWidth(utf16);
double rowTol = (std::max)(5.0, (origTop - origBottom) * 0.5);
double nextLeft = 1e18;
int nObj = FPDFPage_CountObjects(page);
for (int k = 0; k < nObj; ++k) {
if (std::find(objectIndices.begin(), objectIndices.end(), k) != objectIndices.end()) continue;
FPDF_PAGEOBJECT o = FPDFPage_GetObject(page, k);
if (!o || FPDFPageObj_GetType(o) != FPDF_PAGEOBJ_TEXT) continue;
float l = 0, bo = 0, rr = 0, tt = 0;
if (!FPDFPageObj_GetBounds(o, &l, &bo, &rr, &tt)) continue;
if (std::abs((bo + tt) / 2.0 - origCenterY) <= rowTol && l > origRight + 1.0 && l < nextLeft) {
nextLeft = l;
}
}
if (nextLeft < 1e17) {
double avail = nextLeft - e - 2.0;
if (avail > 1.0 && newW > avail) aScale = a * (avail / newW);
}
}
FPDF_PAGEOBJECT newTextObj = FPDFPageObj_CreateTextObj(doc_, font, static_cast<float>(fontSize));
if (newTextObj) {
FPDFPageObj_SetFillColor(newTextObj, r, g, b_color, a_color);
FPDFTextObj_SetTextRenderMode(newTextObj, renderMode);
FPDFText_SetText(newTextObj, reinterpret_cast<FPDF_WIDESTRING>(utf16.data()));
FPDFPageObj_Transform(newTextObj, aScale, b, c, d, e, f);
FPDFPage_InsertObjectAtIndex(page, newTextObj, minIndex);
} else {
spdlog::error("Failed to create new text object");
}
}
}
}
if (!FPDFPage_GenerateContent(page)) {
spdlog::error("Failed to generate page content after replace_text");
}
FPDF_ClosePage(page);
return {};
#else
(void)op; (void)pageIndex;
return std::unexpected(EngineError::Unknown);
#endif
}
}
+648
View File
@@ -0,0 +1,648 @@
#include "parser/pdfium_internal.hpp"
namespace pdfengine::parser {
#ifdef PDFENGINE_WITH_PDFIUM
void PdfiumDocument::registerAuxFont(const std::string& internalFontId, const std::vector<uint8_t>& sfnt) {
fonts::pdf_fonts::ReconstructedFont rf;
if (!sfnt.empty()) {
rf.ok = true;
fonts::FontFace face;
if (face.loadFromMemory(sfnt))
for (uint32_t u : face.coveredCodepoints()) rf.coveredUnicode.insert(u);
rf.sfnt = sfnt;
}
std::lock_guard<std::mutex> lock(reconstructedFontsMutex_);
reconstructedFonts_[internalFontId] = std::move(rf);
}
std::expected<std::vector<uint8_t>, EngineError>
PdfiumDocument::getReconstructedFontData(const std::string& internalFontId) {
const auto* rf = lookupReconFont(internalFontId);
if (rf && rf->ok && !rf->sfnt.empty()) return rf->sfnt;
return std::unexpected(EngineError::Unknown);
}
const fonts::pdf_fonts::ReconstructedFont* PdfiumDocument::lookupReconFont(const std::string& internalFontId) {
#ifdef PDFENGINE_WITH_QPDF
return getReconstructedEmbeddedFont(internalFontId);
#else
std::lock_guard<std::mutex> lock(reconstructedFontsMutex_);
auto it = reconstructedFonts_.find(internalFontId);
return it != reconstructedFonts_.end() ? &it->second : nullptr;
#endif
}
#endif
#if defined(PDFENGINE_WITH_PDFIUM) && defined(PDFENGINE_WITH_QPDF)
const fonts::pdf_fonts::ReconstructedFont*
PdfiumDocument::getReconstructedEmbeddedFont(const std::string& internalFontId) {
{
std::lock_guard<std::mutex> lock(reconstructedFontsMutex_);
auto it = reconstructedFonts_.find(internalFontId);
if (it != reconstructedFonts_.end()) return &it->second;
}
fonts::pdf_fonts::ReconstructedFont rf;
auto progRes = getFontData(internalFontId);
if (progRes.has_value() && !progRes.value().empty() && !memoryBuffer_.empty()) {
std::string baseName = baseNameFromInternalFontId(internalFontId);
qpdf_layer::QpdfFontExtractor fx;
auto mapRes = fx.extractMapping(memoryBuffer_, baseName);
if (mapRes.has_value() && mapRes->ok) {
rf = fonts::pdf_fonts::EmbeddedFontReconstructor::reconstruct(
progRes.value(), mapRes->codeToUnicode, mapRes->identityCidToGid, mapRes->codeToGid);
spdlog::info("Tier-2: reconstruct '{}' ok={} covered={}", internalFontId, rf.ok, rf.coveredUnicode.size());
}
}
std::lock_guard<std::mutex> lock(reconstructedFontsMutex_);
auto res = reconstructedFonts_.emplace(internalFontId, std::move(rf));
return &res.first->second;
}
#endif
#ifdef PDFENGINE_WITH_PDFIUM
PdfiumDocument::EmissionFont PdfiumDocument::loadEmissionFont(
int pageIndex, const std::string& internalFontId, double fontSize,
const std::vector<uint32_t>& codepoints, const std::vector<int>& srcObjects,
FPDF_FONT reuseFont) {
EmissionFont out;
(void)fontSize;
std::string fontName = "Helvetica";
bool bold = false, italic = false;
{
std::string lowerId = internalFontId;
std::transform(lowerId.begin(), lowerId.end(), lowerId.begin(),
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
if (lowerId.find("bolditalic") != std::string::npos) { bold = true; italic = true; }
else if (lowerId.find("bold") != std::string::npos) { bold = true; }
else if (lowerId.find("italic") != std::string::npos) { italic = true; }
else if (lowerId.find("oblique") != std::string::npos) { italic = true; }
if (lowerId.find("times") != std::string::npos) {
fontName = bold && italic ? "Times-BoldItalic" : bold ? "Times-Bold" : italic ? "Times-Italic" : "Times-Roman";
} else if (lowerId.find("courier") != std::string::npos) {
fontName = bold && italic ? "Courier-BoldOblique" : bold ? "Courier-Bold" : italic ? "Courier-Oblique" : "Courier";
} else {
fontName = bold && italic ? "Helvetica-BoldOblique" : bold ? "Helvetica-Bold" : italic ? "Helvetica-Oblique" : "Helvetica";
}
}
std::optional<FontInfo> matchedFontInfo;
auto fontsRes = getFonts(pageIndex, pageIndex);
if (fontsRes.has_value()) {
for (const auto& fi : *fontsRes) {
if (!internalFontId.empty() && fi.internalFontId == internalFontId) { matchedFontInfo = fi; break; }
}
}
if (matchedFontInfo.has_value()) {
auto r = getResolvedFont(*matchedFontInfo);
if (r.has_value()) out.resolved = *r;
}
bool fontSupportsAll = true, subsetLacksGlyphs = false;
bool isSubsetFont = matchedFontInfo && matchedFontInfo->isSubset;
if (out.resolved) {
for (uint32_t cp : codepoints) {
if (!out.resolved->hasGlyph(cp)) {
if (isSubsetFont) subsetLacksGlyphs = true; else fontSupportsAll = false;
}
}
} else {
fontSupportsAll = false;
}
std::string cacheKey;
bool useEmbedded = false, useSystem = false;
if (matchedFontInfo && matchedFontInfo->isEmbedded) {
bool subsetProvenLacking = out.resolved && isSubsetFont && subsetLacksGlyphs;
bool fullProvenLacking = out.resolved && !isSubsetFont && !fontSupportsAll;
if (!subsetProvenLacking && !fullProvenLacking) { cacheKey = matchedFontInfo->internalFontId; useEmbedded = true; }
else { cacheKey = "system_embed_" + matchedFontInfo->fontName + std::string("_") + (bold ? "B" : "") + (italic ? "I" : ""); useSystem = true; }
} else if (matchedFontInfo && !matchedFontInfo->isEmbedded) {
cacheKey = "standard_" + fontName;
} else {
cacheKey = "standard_" + fontName;
}
{
uint64_t h = 1469598103934665603ull;
for (uint32_t cp : codepoints) { h ^= cp; h *= 1099511628211ull; }
cacheKey += "#" + std::to_string(h);
}
if (useEmbedded && reuseFont && !isSubsetFont) {
out.font = reuseFont;
if (auto perObj = getFontDataFromObjects(pageIndex, srcObjects, internalFontId);
perObj.has_value() && !perObj->empty()) {
auto mf = std::make_shared<fonts::FontFace>();
if (mf->loadFromMemory(*perObj)) out.measureFace = mf;
}
return out;
}
{
std::lock_guard<std::mutex> lock(loadedFontsMutex_);
if (loadedFontsCache_.count(cacheKey)) {
out.font = loadedFontsCache_[cacheKey];
auto it = loadedMeasureFaces_.find(cacheKey);
if (it != loadedMeasureFaces_.end()) out.measureFace = it->second;
}
}
if (out.font) return out;
if (matchedFontInfo && matchedFontInfo->isEmbedded && classifyFontFidelity(*matchedFontInfo) != "exact") {
const auto* rf = lookupReconFont(internalFontId);
if (rf && rf->ok) {
bool covered = true;
for (uint32_t cp : codepoints) {
if (cp >= 0x20 && !rf->coveredUnicode.count(cp)) { covered = false; break; }
}
if (covered) {
std::lock_guard<std::mutex> lock(loadedFontsMutex_);
const std::string rkey = "recon_" + internalFontId;
if (loadedFontsCache_.count(rkey)) {
out.font = loadedFontsCache_[rkey];
auto mit = loadedMeasureFaces_.find(rkey);
if (mit != loadedMeasureFaces_.end()) out.measureFace = mit->second;
} else {
loadedFontDataBuffers_[rkey] = rf->sfnt;
const auto& bytes = loadedFontDataBuffers_[rkey];
out.font = FPDFText_LoadFont(doc_, bytes.data(), static_cast<uint32_t>(bytes.size()), FPDF_FONT_TRUETYPE, true);
if (out.font) {
loadedFontsCache_[rkey] = out.font;
auto mf = std::make_shared<fonts::FontFace>();
if (mf->loadFromMemory(bytes)) { loadedMeasureFaces_[rkey] = mf; out.measureFace = mf; }
}
}
if (out.font) { spdlog::info("Tier-2: emit reconstructed embedded font '{}'", internalFontId); return out; }
}
}
}
if (useEmbedded) {
std::vector<uint8_t> sourceBytes;
if (auto perObj = getFontDataFromObjects(pageIndex, srcObjects, matchedFontInfo->internalFontId);
perObj.has_value() && !perObj->empty()) {
sourceBytes = std::move(*perObj);
} else if (auto fontDataRes = getFontData(matchedFontInfo->internalFontId);
fontDataRes.has_value() && !fontDataRes.value().empty()) {
sourceBytes = std::move(fontDataRes.value());
}
if (!sourceBytes.empty()) {
std::lock_guard<std::mutex> lock(loadedFontsMutex_);
loadedFontDataBuffers_[cacheKey] = std::move(sourceBytes);
const auto& bytes = loadedFontDataBuffers_[cacheKey];
out.font = FPDFText_LoadFont(doc_, bytes.data(), static_cast<uint32_t>(bytes.size()), FPDF_FONT_TRUETYPE, true);
}
} else if (useSystem) {
std::string fontPath = fonts::pdf_fonts::FontFallback::getInstance().getFallbackFontPath(
matchedFontInfo->normalizedFamily.empty() ? matchedFontInfo->fontName : matchedFontInfo->normalizedFamily, bold, italic);
std::ifstream fs(fontPath, std::ios::binary);
if (fs) {
std::vector<uint8_t> fileBytes((std::istreambuf_iterator<char>(fs)), std::istreambuf_iterator<char>());
if (!fileBytes.empty()) {
std::lock_guard<std::mutex> lock(loadedFontsMutex_);
loadedFontDataBuffers_[cacheKey] = std::move(fileBytes);
const auto& bytes = loadedFontDataBuffers_[cacheKey];
out.font = FPDFText_LoadFont(doc_, bytes.data(), static_cast<uint32_t>(bytes.size()), FPDF_FONT_TRUETYPE, true);
}
}
}
if (!out.font) {
out.font = FPDFText_LoadStandardFont(doc_, fontName.c_str());
if (!out.font) out.font = FPDFText_LoadStandardFont(doc_, "Helvetica");
}
if (out.font) {
std::lock_guard<std::mutex> lock(loadedFontsMutex_);
loadedFontsCache_[cacheKey] = out.font;
auto bit = loadedFontDataBuffers_.find(cacheKey);
if (bit != loadedFontDataBuffers_.end() && !bit->second.empty()) {
auto mf = std::make_shared<fonts::FontFace>();
if (mf->loadFromMemory(bit->second)) { loadedMeasureFaces_[cacheKey] = mf; out.measureFace = mf; }
}
}
return out;
}
#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);
}
auto fontPdfDataMap = buildFontPdfDataMap(page_, textPage_);
std::vector<FontInfo> pageFonts;
int charCount = FPDFText_CountChars(textPage_);
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 "";
};
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;
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);
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;
}
}
}
std::unordered_map<std::string, bool> fontIsVertical;
for (const auto& [fname, stats] : fontStats) {
bool vertical = false;
if (stats.verticalSteps > 0 || stats.horizontalSteps > 0) {
vertical = stats.verticalSteps > stats.horizontalSteps;
} else {
vertical = (stats.totalFilteredDeltaY > 2.0 * stats.totalFilteredDeltaX) &&
(stats.totalFilteredDeltaY > 0.5);
}
fontIsVertical[fname] = vertical;
}
for (int i = 0; i < charCount; ++i) {
const std::string& fontName = charInfos[i].fontName;
if (fontName.empty()) {
continue;
}
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);
deduceFontMetadata(f);
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(), [](unsigned char c) { return static_cast<char>(::tolower(c)); });
f.isEmbedded = pd.isEmbedded;
f.flags = static_cast<uint32_t>(pd.flags);
if (pd.ascent != 0.0) f.ascent = pd.ascent;
if (pd.descent != 0.0) f.descent = pd.descent;
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;
}
if (lname.find("symbol") != std::string::npos) {
f.hasToUnicode = false;
} else {
f.hasToUnicode = pd.hasUnicodeMapping;
}
if (f.isEmbedded) {
f.sourceType = "Embedded";
f.substitutedFrom = "";
f.substitutedTo = "";
}
f.internalFontId = makeInternalFontId(f);
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);
}
if (!f.isVertical) {
auto vit = fontIsVertical.find(fontName);
if (vit != fontIsVertical.end() && vit->second) {
f.isVertical = true;
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);
}
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>();
}
if (startPage == 0 && endPage == total - 1 && hasCachedFonts_) {
return cachedFonts_;
}
std::vector<FontInfo> aggregated;
for (int i = startPage; i <= endPage; ++i) {
auto pageRes = const_cast<PdfiumDocument*>(this)->getPage(i);
if (!pageRes) {
spdlog::error("Failed to load page index {} for font diagnostics", i);
continue;
}
auto pageFontsRes = pageRes.value()->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);
}
}
}
}
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;
});
if (startPage == 0 && endPage == total - 1) {
cachedFonts_ = aggregated;
hasCachedFonts_ = true;
}
return aggregated;
#else
(void)startPage;
(void)endPage;
return std::unexpected(EngineError::Unknown);
#endif
}
std::optional<std::vector<uint8_t>>
PdfiumDocument::getFontDataFromObjects(int pageIndex, const std::vector<int>& objectIndices,
const std::string& internalFontId) const {
#ifdef PDFENGINE_WITH_PDFIUM
ensure_pdfium_initialized();
if (!doc_ || objectIndices.empty()) return std::nullopt;
const std::string expected = baseNameFromInternalFontId(internalFontId);
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) return std::nullopt;
std::optional<std::vector<uint8_t>> result;
for (int idx : objectIndices) {
FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page, idx);
if (!obj || FPDFPageObj_GetType(obj) != FPDF_PAGEOBJ_TEXT) continue;
FPDF_FONT font = FPDFTextObj_GetFont(obj);
if (!font) continue;
size_t nameLen = FPDFFont_GetBaseFontName(font, nullptr, 0);
if (nameLen == 0) continue;
std::vector<char> nameBuf(nameLen);
if (FPDFFont_GetBaseFontName(font, nameBuf.data(), nameLen) == 0) continue;
if (std::string(nameBuf.data()) != expected) continue;
size_t buflen = 0;
FPDFFont_GetFontData(font, nullptr, 0, &buflen);
if (buflen == 0) continue;
std::vector<uint8_t> buffer(buflen);
size_t actualLen = 0;
if (FPDFFont_GetFontData(font, buffer.data(), buflen, &actualLen) && actualLen > 0) {
result = std::move(buffer);
break;
}
}
FPDF_ClosePage(page);
return result;
#else
(void)pageIndex; (void)objectIndices; (void)internalFontId;
return std::nullopt;
#endif
}
std::expected<std::vector<uint8_t>, EngineError> PdfiumDocument::getFontData(const std::string& internalFontId) const {
#ifdef PDFENGINE_WITH_PDFIUM
ensure_pdfium_initialized();
if (!doc_) {
return std::unexpected(EngineError::Unknown);
}
std::string expectedFontName = baseNameFromInternalFontId(internalFontId);
int numPages = FPDF_GetPageCount(doc_);
int startPage = 0;
{
std::lock_guard<std::mutex> lock(fontsMutex_);
if (fontDataCache_.count(expectedFontName)) {
const auto& cachedBuf = fontDataCache_[expectedFontName];
if (!cachedBuf.empty()) {
return cachedBuf;
} else {
return std::unexpected(EngineError::FileNotFound);
}
}
startPage = fontDataScannedPages_;
}
for (int i = startPage; i < numPages; ++i) {
FPDF_PAGE page = FPDF_LoadPage(doc_, i);
if (!page) {
std::lock_guard<std::mutex> lock(fontsMutex_);
fontDataScannedPages_ = i + 1;
continue;
}
int objectCount = FPDFPage_CountObjects(page);
for (int j = 0; j < objectCount; ++j) {
FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page, j);
if (!obj || FPDFPageObj_GetType(obj) != FPDF_PAGEOBJ_TEXT) continue;
FPDF_FONT font = FPDFTextObj_GetFont(obj);
if (!font) continue;
size_t nameLen = FPDFFont_GetBaseFontName(font, nullptr, 0);
if (nameLen > 0) {
std::vector<char> nameBuf(nameLen);
if (FPDFFont_GetBaseFontName(font, nameBuf.data(), nameLen) > 0) {
std::string fontName(nameBuf.data());
std::lock_guard<std::mutex> lock(fontsMutex_);
if (fontDataCache_.find(fontName) == fontDataCache_.end()) {
size_t buflen = 0;
FPDFFont_GetFontData(font, nullptr, 0, &buflen);
if (buflen > 0) {
std::vector<uint8_t> buffer(buflen);
size_t actual_len = 0;
if (FPDFFont_GetFontData(font, buffer.data(), buflen, &actual_len)) {
fontDataCache_[fontName] = buffer;
} else {
fontDataCache_[fontName] = std::vector<uint8_t>();
}
} else {
fontDataCache_[fontName] = std::vector<uint8_t>();
}
}
if (fontName == expectedFontName) {
const auto& cachedBuf = fontDataCache_[fontName];
if (!cachedBuf.empty()) {
FPDF_ClosePage(page);
fontDataScannedPages_ = i;
return cachedBuf;
}
}
}
}
}
FPDF_ClosePage(page);
std::lock_guard<std::mutex> lock(fontsMutex_);
fontDataScannedPages_ = i + 1;
}
{
std::lock_guard<std::mutex> lock(fontsMutex_);
if (fontDataCache_.find(expectedFontName) == fontDataCache_.end()) {
fontDataCache_[expectedFontName] = std::vector<uint8_t>();
}
}
return std::unexpected(EngineError::FileNotFound);
#else
(void)internalFontId;
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<std::shared_ptr<fonts::pdf_fonts::Font>, std::string> PdfiumDocument::getResolvedFont(const FontInfo& fontInfo) {
#ifdef PDFENGINE_WITH_PDFIUM
std::lock_guard<std::mutex> lock(resolvedFontsMutex_);
if (resolvedFontsCache_.count(fontInfo.internalFontId)) {
return resolvedFontsCache_[fontInfo.internalFontId];
}
if (!fontResolver_) {
fontResolver_ = std::make_unique<::pdfengine::fonts::loader::FontResolver>(shared_from_this());
}
auto result = fontResolver_->resolveFont(fontInfo);
if (!result) {
return std::unexpected(result.error());
}
std::shared_ptr<fonts::pdf_fonts::Font> sharedFont(std::move(result.value()));
resolvedFontsCache_[fontInfo.internalFontId] = sharedFont;
return sharedFont;
#else
return std::unexpected(std::string("EngineError::Unknown"));
#endif
}
}
+607
View File
@@ -0,0 +1,607 @@
#include "parser/pdfium_internal.hpp"
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) {
if (i + 1 < length) {
char16_t low = utf16[i + 1];
if (low >= 0xDC00 && low <= 0xDFFF) {
cp = 0x10000 + (((c - 0xD800) << 10) | (low - 0xDC00));
i++;
}
}
}
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;
}
static int cp1252ToByte(unsigned int cp) {
if (cp <= 0x7F) return static_cast<int>(cp);
if (cp >= 0xA0 && cp <= 0xFF) return static_cast<int>(cp);
switch (cp) {
case 0x20AC: return 0x80; case 0x201A: return 0x82; case 0x0192: return 0x83;
case 0x201E: return 0x84; case 0x2026: return 0x85; case 0x2020: return 0x86;
case 0x2021: return 0x87; case 0x02C6: return 0x88; case 0x2030: return 0x89;
case 0x0160: return 0x8A; case 0x2039: return 0x8B; case 0x0152: return 0x8C;
case 0x017D: return 0x8E; case 0x2018: return 0x91; case 0x2019: return 0x92;
case 0x201C: return 0x93; case 0x201D: return 0x94; case 0x2022: return 0x95;
case 0x2013: return 0x96; case 0x2014: return 0x97; case 0x02DC: return 0x98;
case 0x2122: return 0x99; case 0x0161: return 0x9A; case 0x203A: return 0x9B;
case 0x0153: return 0x9C; case 0x017E: return 0x9E; case 0x0178: return 0x9F;
default: return -1;
}
}
static std::vector<unsigned int> utf8_to_codepoints(const std::string& s) {
std::vector<unsigned int> cps;
for (size_t i = 0; i < s.size();) {
unsigned char c = static_cast<unsigned char>(s[i]);
unsigned int cp = c; int 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 { cp = c; extra = 0; }
if (i + extra >= s.size()) { cps.push_back(c); i++; continue; }
bool ok = true;
for (int j = 1; j <= extra; ++j) { unsigned char n = static_cast<unsigned char>(s[i + j]); if ((n & 0xC0) != 0x80) { ok = false; break; } cp = (cp << 6) | (n & 0x3F); }
if (!ok) { cps.push_back(c); i++; continue; }
cps.push_back(cp); i += extra + 1;
}
return cps;
}
std::string repairMojibake(const std::string& s) {
auto cps = utf8_to_codepoints(s);
std::string out;
auto emit = [&](unsigned int cp) { out += code_point_to_utf8(cp); };
for (size_t i = 0; i < cps.size();) {
int b0 = cp1252ToByte(cps[i]);
int need = 0;
if (b0 >= 0xC0 && b0 <= 0xDF) need = 1;
else if (b0 >= 0xE0 && b0 <= 0xEF) need = 2;
else if (b0 >= 0xF0 && b0 <= 0xF7) need = 3;
if (need > 0 && i + need < cps.size()) {
std::string bytes; bytes.push_back(static_cast<char>(b0));
bool ok = true;
for (int j = 1; j <= need; ++j) { int b = cp1252ToByte(cps[i + j]); if (b < 0x80 || b > 0xBF) { ok = false; break; } bytes.push_back(static_cast<char>(b)); }
if (ok) { auto rd = utf8_to_codepoints(bytes); if (rd.size() == 1 && rd[0] > 0x7F) { emit(rd[0]); i += need + 1; continue; } }
}
emit(cps[i]); i++;
}
return out;
}
std::string normalizeReflowText(const std::string& s, const fonts::FontFace* face) {
auto cps = utf8_to_codepoints(s);
std::string out;
auto keepIfCovered = [&](unsigned int cp, const char* ascii) {
if (face && face->coversUnicode(cp)) out += code_point_to_utf8(cp);
else out += ascii;
};
for (unsigned int cp : cps) {
switch (cp) {
case 0x2010: case 0x2011: case 0x2012: case 0x2013: case 0x2014: case 0x2015:
keepIfCovered(cp, "-"); break;
case 0x2018: case 0x2019: case 0x201B: keepIfCovered(cp, "'"); break;
case 0x201C: case 0x201D: case 0x201F: keepIfCovered(cp, "\""); break;
case 0x2026: keepIfCovered(cp, "..."); break;
case 0x00A0: case 0x2002: case 0x2003: case 0x2009: case 0x202F: out += ' '; break;
default: out += code_point_to_utf8(cp); break;
}
}
return out;
}
}
namespace pdfengine::parser {
std::string makeInternalFontId(const pdfengine::FontInfo& f) {
if (f.isSubset && !f.subsetTag.empty()) return f.fontName;
return f.fontName + "_" + f.type + "_" + std::to_string(f.flags);
}
std::string classifyFontFidelity(const pdfengine::FontInfo& f) {
if (!f.isEmbedded) return "exact";
std::string fam = f.normalizedFamily;
std::transform(fam.begin(), fam.end(), fam.begin(), [](unsigned char c){ return std::tolower(c); });
auto has = [&](const char* s){ return fam.find(s) != std::string::npos; };
if (fam.empty() || has("times") || has("arial") || has("helvetica") || has("courier") ||
has("symbol") || has("zapf"))
return "exact";
const bool reconstructable =
f.hasToUnicode &&
(f.type.find("TrueType") != std::string::npos ||
f.type.find("Type0") != std::string::npos ||
f.type.find("CIDFontType2") != std::string::npos);
return reconstructable ? "partial" : "substituted";
}
std::string baseNameFromInternalFontId(const std::string& internalFontId) {
std::string expected = internalFontId;
size_t lastUnderscore = expected.rfind('_');
if (lastUnderscore != std::string::npos && lastUnderscore > 0) {
size_t secondLast = expected.rfind('_', lastUnderscore - 1);
if (secondLast != std::string::npos) {
std::string typePart = expected.substr(secondLast + 1, lastUnderscore - secondLast - 1);
if (typePart == "TrueType" || typePart == "Type1" ||
typePart == "CIDFontType0" || typePart == "CIDFontType2") {
expected = expected.substr(0, secondLast);
}
}
}
return expected;
}
#ifdef PDFENGINE_WITH_PDFIUM
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) {
std::string name = fontName;
if (name.size() > 7 && name[6] == '+') {
name = name.substr(7);
}
size_t sep = name.find_first_of("-,");
if (sep != std::string::npos) {
name = name.substr(0, sep);
}
auto cleanName = name;
auto lower = name;
std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return static_cast<char>(::tolower(c)); });
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);
}
}
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(), [](unsigned char c) { return static_cast<char>(::tolower(c)); });
if (f.fontName.size() > 7 && f.fontName[6] == '+') {
f.isSubset = true;
f.subsetTag = f.fontName.substr(0, 6);
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);
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";
}
if (lowerName.find("symbol") != std::string::npos) {
f.hasToUnicode = false;
} else {
f.hasToUnicode = true;
}
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";
}
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 {
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";
}
if (f.isSubset) {
f.isEmbedded = true;
f.sourceType = "Embedded";
f.substitutedFrom = "";
f.substitutedTo = "";
} else {
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; }
}
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);
}
}
f.internalFontId = makeInternalFontId(f);
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 {
f.ascent = 905.0;
f.descent = -211.0;
f.capHeight = 728.0;
}
}
std::unordered_map<std::string, FontPdfData>
buildFontPdfDataMap(FPDF_PAGE page, FPDF_TEXTPAGE textPage) {
std::unordered_map<std::string, FontPdfData> result;
if (!page) return result;
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;
size_t 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;
if (result.count(fontName)) continue;
FontPdfData data;
data.valid = true;
data.isEmbedded = (FPDFFont_GetIsEmbedded(font) != 0);
data.flags = FPDFFont_GetFlags(font);
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;
}
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);
if (it == result.end() || it->second.hasUnicodeMapping) continue;
unsigned int cp = FPDFText_GetUnicode(textPage, ci);
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;
}
}
}
std::vector<uint8_t> base64Decode(const std::string& encoded) {
std::vector<uint8_t> decoded;
int T[256];
std::fill(std::begin(T), std::end(T), -1);
for (int i = 0; i < 64; ++i) {
T[static_cast<unsigned char>("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[i])] = i;
}
int val = 0;
int valb = -8;
for (char c : encoded) {
unsigned char uc = static_cast<unsigned char>(c);
if (T[uc] == -1) continue;
val = (val << 6) + T[uc];
valb += 6;
if (valb >= 0) {
decoded.push_back(static_cast<uint8_t>((val >> valb) & 0xFF));
valb -= 8;
}
}
return decoded;
}
}
+109
View File
@@ -0,0 +1,109 @@
#pragma once
#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 <fpdf_formfill.h>
#include <png.h>
#include "parser/pdfium_loader.hpp"
#endif
#include "fonts/loader/font_resolver.hpp"
#include "fonts/pdf_fonts/font.hpp"
#include "pdfengine/hardened_limits.h"
#include "fonts/pdf_fonts/font_fallback.hpp"
#include "fonts/pdf_fonts/font_subset.hpp"
#include "fonts/shaping/hb_shaper.hpp"
#include "fonts/face/font_face.hpp"
#include "decoration_builder.hpp"
#include "qpdf/qpdf_extractor.hpp"
#include "qpdf/qpdf_font_extractor.hpp"
#include "parser/lexer.hpp"
#include "parser/parser.hpp"
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
#include <fstream>
#include <filesystem>
#include <csetjmp>
#include <algorithm>
#include <cctype>
#include <cmath>
#include <string>
#include <vector>
#include <unordered_map>
#include <cstdint>
namespace pdfengine::parser {
std::string makeInternalFontId(const pdfengine::FontInfo& f);
std::string classifyFontFidelity(const pdfengine::FontInfo& f);
std::string baseNameFromInternalFontId(const std::string& internalFontId);
void parseHexColor(const std::string& hex, unsigned int& r, unsigned int& g, unsigned int& b);
std::vector<uint8_t> base64Decode(const std::string& encoded);
std::string repairMojibake(const std::string& s);
std::string normalizeReflowText(const std::string& s, const fonts::FontFace* face);
#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 FontPdfData {
bool valid = false;
bool isEmbedded = false;
int flags = 0;
double ascent = 0.0;
double descent = 0.0;
bool hasUnicodeMapping = false;
};
struct PageBand {
double pageW = 0.0, pageH = 0.0;
double placementTopY = 0.0;
double bottomLimitY = 0.0;
bool valid = false;
};
std::vector<uint8_t> encodeBgraToPng(const uint8_t* bgra, int width, int height, int stride);
pdfengine::EngineError mapPdfiumError(unsigned long err, bool passwordProvided);
void ensure_pdfium_initialized();
void deduceFontMetadata(pdfengine::FontInfo& f);
std::unordered_map<std::string, FontPdfData> buildFontPdfDataMap(FPDF_PAGE page, FPDF_TEXTPAGE textPage);
void repagSetParaId(FPDF_DOCUMENT doc, FPDF_PAGEOBJECT obj, const std::string& paraId);
std::string repagGetParaId(FPDF_PAGEOBJECT obj);
void walkOutline(FPDF_DOCUMENT doc, FPDF_BOOKMARK bm, int level,
std::vector<PdfDocument::OutlineItem>& out);
PageBand repagComputeBand(FPDF_PAGE page, double pushColLeft, double colRight);
std::vector<double> repagAnchoredCenters(FPDF_PAGE page, double pushColLeft, double colRight,
double bottomLimitY);
int repaginateForward(FPDF_DOCUMENT doc, int startPage,
double colLeft, double colRight, double pushColLeft, double leading,
const std::vector<double>& anchoredCentersStart, int* movedOut = nullptr);
int repaginateBackward(FPDF_DOCUMENT doc, int startPage,
double colLeft, double colRight, double pushColLeft, double leading);
int repagRemoveContinuations(FPDF_DOCUMENT doc, int anchorPage, const std::string& paraId,
double pushColLeft, double colRight);
#endif
}
+390
View File
@@ -0,0 +1,390 @@
#include "parser/pdfium_internal.hpp"
namespace pdfengine::parser {
PdfiumPage::PdfiumPage(NativeDocHandle docHandle, NativePageHandle pageHandle, int pageIndex,
std::shared_ptr<PdfiumDocument> owner)
: doc_(docHandle), page_(pageHandle), pageIndex_(pageIndex), ownerDoc_(std::move(owner)) {
}
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
doc_ = other.doc_;
page_ = other.page_;
textPage_ = other.textPage_;
pageIndex_ = other.pageIndex_;
ownerDoc_ = std::move(other.ownerDoc_);
other.doc_ = nullptr;
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);
}
if (!limits::pageDimensionsOk(width(), height())) {
return std::unexpected(EngineError::RenderFailed);
}
if (!limits::objectCountOk(FPDFPage_CountObjects(page_))) {
spdlog::error("Refusing to render page: object count exceeds hardened limit ({})",
limits::kMaxObjects);
return std::unexpected(EngineError::RenderFailed);
}
double scale = dpi / 72.0;
int w = static_cast<int>(width() * scale);
int h = static_cast<int>(height() * scale);
if (!limits::rasterSizeOk(w, h)) {
return std::unexpected(EngineError::RenderFailed);
}
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<PageImage, EngineError> PdfiumPage::renderRegionRaw(int dpi, double yTopPt, double heightPt) const {
#ifdef PDFENGINE_WITH_PDFIUM
if (!page_) return std::unexpected(EngineError::Unknown);
if (!limits::pageDimensionsOk(width(), height())) return std::unexpected(EngineError::RenderFailed);
if (!limits::objectCountOk(FPDFPage_CountObjects(page_))) return std::unexpected(EngineError::RenderFailed);
const double scale = dpi / 72.0;
const int w = static_cast<int>(width() * scale);
const int fullH = static_cast<int>(height() * scale);
int yTopPx = static_cast<int>(yTopPt * scale);
if (yTopPx < 0) yTopPx = 0;
if (yTopPx > fullH) yTopPx = fullH;
int regionH = heightPt > 0 ? static_cast<int>(heightPt * scale) : (fullH - yTopPx);
if (regionH > fullH - yTopPx) regionH = fullH - yTopPx;
if (w <= 0 || regionH <= 0) return std::unexpected(EngineError::RenderFailed);
if (!limits::rasterSizeOk(w, regionH)) return std::unexpected(EngineError::RenderFailed);
FPDF_BITMAP bitmap = FPDFBitmap_Create(w, regionH, 1);
if (!bitmap) return std::unexpected(EngineError::RenderFailed);
FPDFBitmap_FillRect(bitmap, 0, 0, w, regionH, 0xFFFFFFFF);
FPDF_RenderPageBitmap(bitmap, page_, 0, -yTopPx, w, fullH, 0, 0);
const auto* bgra = static_cast<const uint8_t*>(FPDFBitmap_GetBuffer(bitmap));
const int stride = FPDFBitmap_GetStride(bitmap);
std::vector<uint8_t> rgba(static_cast<size_t>(w) * regionH * 4);
for (int y = 0; y < regionH; ++y) {
const uint8_t* src = bgra + static_cast<size_t>(y) * stride;
uint8_t* dst = rgba.data() + static_cast<size_t>(y) * w * 4;
for (int x = 0; x < w; ++x) {
dst[x * 4 + 0] = src[x * 4 + 2];
dst[x * 4 + 1] = src[x * 4 + 1];
dst[x * 4 + 2] = src[x * 4 + 0];
dst[x * 4 + 3] = src[x * 4 + 3];
}
}
FPDFBitmap_Destroy(bitmap);
return PageImage{w, regionH, std::move(rgba)};
#else
(void)dpi; (void)yTopPt; (void)heightPt;
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
}
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
}
std::expected<std::string, EngineError> PdfiumPage::extractDisplayListJson() const {
#if defined(PDFENGINE_WITH_PDFIUM) && defined(PDFENGINE_WITH_QPDF)
if (!ownerDoc_) return std::unexpected(EngineError::Unknown);
const auto& buffer = ownerDoc_->getMemoryBuffer();
if (buffer.empty()) return std::unexpected(EngineError::FileNotFound);
qpdf_layer::QpdfExtractor extractor;
auto stream = extractor.extractPageStreamFromMemory(buffer, pageIndex_);
if (!stream) return std::unexpected(EngineError::InvalidFormat);
Lexer lexer(stream->decodedContent);
auto tokens = lexer.tokenize();
ContentParser parser(tokens);
auto operations = parser.parse();
nlohmann::json j_array = nlohmann::json::array();
for (const auto& op : operations) {
nlohmann::json j_op = nlohmann::json::object();
j_op["op"] = op.op;
if (!op.operands.empty()) {
nlohmann::json j_args = nlohmann::json::array();
for (const auto& arg : op.operands) {
if (arg->type == AstNodeType::Number) {
j_args.push_back(arg->numberValue);
} else if (arg->type == AstNodeType::Name || arg->type == AstNodeType::String) {
j_args.push_back(arg->stringValue);
} else {
j_args.push_back("<other>");
}
}
j_op["args"] = j_args;
}
j_array.push_back(j_op);
}
return j_array.dump();
#else
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<std::vector<uint8_t>, EngineError> PdfiumPage::extractImageXObject(const std::string& name) const {
(void)name;
return std::unexpected(EngineError::Unknown);
}
}
+634
View File
@@ -0,0 +1,634 @@
#include "parser/pdfium_internal.hpp"
namespace pdfengine::parser {
std::expected<PageModel, EngineError> PdfiumPage::extractDocumentModel() const {
#ifdef PDFENGINE_WITH_PDFIUM
if (!page_) {
return std::unexpected(EngineError::Unknown);
}
ensureTextPageLoaded();
if (!textPage_) {
return std::unexpected(EngineError::Unknown);
}
PageModel model;
model.width = width();
model.height = height();
model.pageIndex = pageIndex_;
int charCount = FPDFText_CountChars(textPage_);
if (charCount <= 0) {
return model;
}
std::unordered_map<std::string, FontInfo> fontMap;
if (auto fontsRes = getFonts()) {
for (const auto& f : *fontsRes) {
fontMap[f.fontName] = f;
}
}
std::vector<Glyph> documentGlyphs;
documentGlyphs.reserve(charCount);
std::unordered_map<FPDF_PAGEOBJECT, int> objToIndex;
int objCount = FPDFPage_CountObjects(page_);
for (int i = 0; i < objCount; ++i) {
objToIndex[FPDFPage_GetObject(page_, i)] = i;
}
for (int i = 0; i < charCount; ++i) {
unsigned int codeUnit = FPDFText_GetUnicode(textPage_, i);
unsigned int cp = codeUnit;
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);
}
}
std::string utf8_char = code_point_to_utf8(cp);
if (utf8_char.empty() || cp == '\r' || cp == '\n') {
if (cp > 0xFFFF) ++i;
continue;
}
Glyph g;
g.text = std::move(utf8_char);
g.unicode = cp;
g.srcIndex = i;
double left, right, bottom, top;
FPDFText_GetCharBox(textPage_, i, &left, &right, &bottom, &top);
g.bboxX = (std::min)(left, right);
g.bboxY = (std::min)(bottom, top);
g.bboxW = std::abs(right - left);
g.bboxH = std::abs(top - bottom);
FPDFText_GetCharOrigin(textPage_, i, &g.originX, &g.originY);
g.angle = FPDFText_GetCharAngle(textPage_, i);
g.fontSize = FPDFText_GetFontSize(textPage_, i);
int flags = 0;
unsigned long len = FPDFText_GetFontInfo(textPage_, i, nullptr, 0, &flags);
if (len > 0) {
std::vector<char> buf(len);
if (FPDFText_GetFontInfo(textPage_, i, buf.data(), len, &flags) > 0) {
g.fontName = std::string(buf.data());
}
}
g.flags = flags;
FPDF_PAGEOBJECT textObj = FPDFText_GetTextObject(textPage_, i);
if (textObj) {
auto it = objToIndex.find(textObj);
if (it != objToIndex.end()) {
g.pageObjectIndex = it->second;
}
}
documentGlyphs.push_back(g);
if (cp > 0xFFFF) ++i;
}
std::sort(documentGlyphs.begin(), documentGlyphs.end(), [](const Glyph& a, const Glyph& b) {
if (std::abs(a.originY - b.originY) > 1.0) {
return a.originY > b.originY;
}
return a.originX < b.originX;
});
std::vector<TextLine> lines;
if (!documentGlyphs.empty()) {
TextLine currentLine;
currentLine.angle = documentGlyphs[0].angle;
double currentOriginY = documentGlyphs[0].originY;
for (const auto& g : documentGlyphs) {
if (currentLine.glyphs.empty()) {
currentLine.glyphs.push_back(g);
continue;
}
if (std::abs(g.angle - currentLine.angle) < 0.1 &&
std::abs(g.originY - currentOriginY) < 1.0) {
currentLine.glyphs.push_back(g);
} else {
lines.push_back(std::move(currentLine));
currentLine = TextLine();
currentLine.angle = g.angle;
currentOriginY = g.originY;
currentLine.glyphs.push_back(g);
}
}
if (!currentLine.glyphs.empty()) {
lines.push_back(std::move(currentLine));
}
}
for (auto& line : lines) {
std::sort(line.glyphs.begin(), line.glyphs.end(), [](const Glyph& a, const Glyph& b) {
if (a.originX != b.originX) return a.originX < b.originX;
return a.srcIndex < b.srcIndex;
});
std::vector<double> gaps;
for (size_t i = 1; i < line.glyphs.size(); ++i) {
double gap = line.glyphs[i].bboxX - (line.glyphs[i-1].bboxX + line.glyphs[i-1].bboxW);
if (gap > 0) {
gaps.push_back(gap);
}
}
double p25Gap = 0.0;
if (!gaps.empty()) {
std::sort(gaps.begin(), gaps.end());
p25Gap = gaps[gaps.size() / 4];
}
double lineEm = 0.0;
{
std::vector<double> hs;
for (const auto& gg : line.glyphs) if (gg.bboxH > 0.1) hs.push_back(gg.bboxH);
if (!hs.empty()) {
std::sort(hs.begin(), hs.end());
double capH = hs[(hs.size() * 9) / 10];
lineEm = capH / 0.7;
}
}
TextRun currentRun;
if (!line.glyphs.empty()) {
const Glyph* firstG = &line.glyphs[0];
currentRun.fontName = firstG->fontName;
currentRun.fontSize = firstG->fontSize;
currentRun.flags = firstG->flags;
if (auto it = fontMap.find(currentRun.fontName); it != fontMap.end()) {
currentRun.internalFontId = it->second.internalFontId;
currentRun.isEmbedded = it->second.isEmbedded;
currentRun.type = it->second.type;
currentRun.fontFidelity = classifyFontFidelity(it->second);
}
currentRun.glyphs.push_back(*firstG);
currentRun.text += firstG->text;
for (size_t i = 1; i < line.glyphs.size(); ++i) {
const auto& prevG = line.glyphs[i-1];
const auto& currG = line.glyphs[i];
double gap = currG.bboxX - (prevG.bboxX + prevG.bboxW);
double em = (std::max)(static_cast<double>(currG.fontSize), lineEm);
double spaceThreshold = em * 0.2;
if (p25Gap > spaceThreshold) {
spaceThreshold = (std::min)(p25Gap * 1.5, em * 0.38);
}
bool addSpace = gap > spaceThreshold && prevG.text != " " && currG.text != " ";
bool breakRun = currG.fontName != currentRun.fontName ||
std::abs(currG.fontSize - currentRun.fontSize) > 0.1 ||
currG.flags != currentRun.flags;
if (addSpace) {
Glyph spaceGlyph;
spaceGlyph.text = " ";
spaceGlyph.unicode = ' ';
spaceGlyph.fontSize = currG.fontSize;
spaceGlyph.fontName = currG.fontName;
spaceGlyph.flags = currG.flags;
spaceGlyph.originX = prevG.bboxX + prevG.bboxW;
spaceGlyph.originY = currG.originY;
spaceGlyph.angle = currG.angle;
spaceGlyph.bboxX = spaceGlyph.originX;
spaceGlyph.bboxY = currG.bboxY;
spaceGlyph.bboxW = gap;
spaceGlyph.bboxH = currG.bboxH;
if (breakRun) {
for (const auto& g : currentRun.glyphs) {
if (g.pageObjectIndex != -1 && std::find(currentRun.objectIndices.begin(), currentRun.objectIndices.end(), g.pageObjectIndex) == currentRun.objectIndices.end()) {
currentRun.objectIndices.push_back(g.pageObjectIndex);
}
}
line.runs.push_back(std::move(currentRun));
currentRun = TextRun();
currentRun.fontName = currG.fontName;
currentRun.fontSize = currG.fontSize;
currentRun.flags = currG.flags;
if (auto it = fontMap.find(currentRun.fontName); it != fontMap.end()) {
currentRun.internalFontId = it->second.internalFontId;
currentRun.isEmbedded = it->second.isEmbedded;
currentRun.type = it->second.type;
currentRun.fontFidelity = classifyFontFidelity(it->second);
}
}
currentRun.glyphs.push_back(spaceGlyph);
currentRun.text += spaceGlyph.text;
} else if (breakRun) {
for (const auto& g : currentRun.glyphs) {
if (g.pageObjectIndex != -1 && std::find(currentRun.objectIndices.begin(), currentRun.objectIndices.end(), g.pageObjectIndex) == currentRun.objectIndices.end()) {
currentRun.objectIndices.push_back(g.pageObjectIndex);
}
}
line.runs.push_back(std::move(currentRun));
currentRun = TextRun();
currentRun.fontName = currG.fontName;
currentRun.fontSize = currG.fontSize;
currentRun.flags = currG.flags;
if (auto it = fontMap.find(currentRun.fontName); it != fontMap.end()) {
currentRun.internalFontId = it->second.internalFontId;
currentRun.isEmbedded = it->second.isEmbedded;
currentRun.type = it->second.type;
currentRun.fontFidelity = classifyFontFidelity(it->second);
}
}
currentRun.glyphs.push_back(currG);
currentRun.text += currG.text;
}
if (!currentRun.glyphs.empty()) {
for (const auto& g : currentRun.glyphs) {
if (g.pageObjectIndex != -1 && std::find(currentRun.objectIndices.begin(), currentRun.objectIndices.end(), g.pageObjectIndex) == currentRun.objectIndices.end()) {
currentRun.objectIndices.push_back(g.pageObjectIndex);
}
}
line.runs.push_back(std::move(currentRun));
}
}
}
for (auto& line : lines) {
for (auto& run : line.runs) {
std::string repaired = pdfengine::parser::repairMojibake(run.text);
if (repaired != run.text) run.text = std::move(repaired);
}
}
std::vector<Paragraph> paragraphs;
auto lineStyleKey = [](const TextLine& ln) -> std::pair<bool,bool> {
for (const auto& r : ln.runs) {
if (r.text.empty()) continue;
std::string n = r.fontName + "|" + r.internalFontId;
std::transform(n.begin(), n.end(), n.begin(), [](unsigned char c){ return static_cast<char>(std::tolower(c)); });
bool bold = n.find("bold") != std::string::npos;
bool sans = n.find("arial") != std::string::npos || n.find("helvetica") != std::string::npos;
return {bold, sans};
}
return {false, false};
};
if (!lines.empty()) {
Paragraph currentPara;
currentPara.lines.push_back(std::move(lines[0]));
for (size_t i = 1; i < lines.size(); ++i) {
auto& prevLine = currentPara.lines.back();
auto& currLine = lines[i];
double prevY = prevLine.glyphs.empty() ? 0 : prevLine.glyphs[0].originY;
double currY = currLine.glyphs.empty() ? 0 : currLine.glyphs[0].originY;
double fontSize = currLine.runs.empty() ? 12.0 : currLine.runs[0].fontSize;
double capH = 0.0;
for (const auto& g : currLine.glyphs) if (g.bboxH > capH) capH = g.bboxH;
if (capH > 0.0) fontSize = (std::max)(fontSize, capH / 0.7);
double vGap = std::abs(prevY - currY);
bool styleChanged = lineStyleKey(currLine) != lineStyleKey(prevLine);
if (vGap > fontSize * 1.5 || styleChanged) {
paragraphs.push_back(std::move(currentPara));
currentPara = Paragraph();
}
currentPara.lines.push_back(std::move(currLine));
}
if (!currentPara.lines.empty()) {
paragraphs.push_back(std::move(currentPara));
}
}
auto computeRunBBox = [](TextRun& r) {
if (r.glyphs.empty()) return;
double minX = r.glyphs[0].bboxX;
double minY = r.glyphs[0].bboxY;
double maxX = r.glyphs[0].bboxX + r.glyphs[0].bboxW;
double maxY = r.glyphs[0].bboxY + r.glyphs[0].bboxH;
for (size_t i = 1; i < r.glyphs.size(); ++i) {
minX = (std::min)(minX, r.glyphs[i].bboxX);
minY = (std::min)(minY, r.glyphs[i].bboxY);
maxX = (std::max)(maxX, r.glyphs[i].bboxX + r.glyphs[i].bboxW);
maxY = (std::max)(maxY, r.glyphs[i].bboxY + r.glyphs[i].bboxH);
}
r.x = minX;
r.y = minY;
r.w = maxX - minX;
r.h = maxY - minY;
};
auto computeLineBBox = [](TextLine& l) {
if (l.runs.empty()) return;
double minX = l.runs[0].x;
double minY = l.runs[0].y;
double maxX = l.runs[0].x + l.runs[0].w;
double maxY = l.runs[0].y + l.runs[0].h;
for (size_t i = 1; i < l.runs.size(); ++i) {
minX = (std::min)(minX, l.runs[i].x);
minY = (std::min)(minY, l.runs[i].y);
maxX = (std::max)(maxX, l.runs[i].x + l.runs[i].w);
maxY = (std::max)(maxY, l.runs[i].y + l.runs[i].h);
}
l.x = minX;
l.y = minY;
l.w = maxX - minX;
l.h = maxY - minY;
};
auto computeParaBBox = [](Paragraph& p) {
if (p.lines.empty()) return;
double minX = p.lines[0].x;
double minY = p.lines[0].y;
double maxX = p.lines[0].x + p.lines[0].w;
double maxY = p.lines[0].y + p.lines[0].h;
for (size_t i = 1; i < p.lines.size(); ++i) {
minX = (std::min)(minX, p.lines[i].x);
minY = (std::min)(minY, p.lines[i].y);
maxX = (std::max)(maxX, p.lines[i].x + p.lines[i].w);
maxY = (std::max)(maxY, p.lines[i].y + p.lines[i].h);
}
p.x = minX;
p.y = minY;
p.w = maxX - minX;
p.h = maxY - minY;
};
auto hex2 = [](unsigned int c) -> std::string {
static const char* h = "0123456789abcdef";
c &= 0xFF;
return std::string{h[(c >> 4) & 0xF], h[c & 0xF]};
};
auto computeRunColor = [&](TextRun& r) {
if (r.objectIndices.empty()) return;
std::vector<int> idxs = r.objectIndices;
std::sort(idxs.begin(), idxs.end());
for (int idx : idxs) {
FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page_, idx);
if (!obj || FPDFPageObj_GetType(obj) != FPDF_PAGEOBJ_TEXT) continue;
unsigned int cr = 0, cg = 0, cb = 0, ca = 0;
bool got = (FPDFPageObj_GetFillColor(obj, &cr, &cg, &cb, &ca) && ca != 0) ||
(FPDFPageObj_GetStrokeColor(obj, &cr, &cg, &cb, &ca) && ca != 0);
if (got) {
r.fillColor = "#" + hex2(cr) + hex2(cg) + hex2(cb);
}
return;
}
};
auto computeRunParaId = [&](TextRun& r) {
if (r.objectIndices.empty()) return;
int minIdx = r.objectIndices[0];
for (int idx : r.objectIndices) if (idx < minIdx) minIdx = idx;
FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page_, minIdx);
if (obj) r.paraId = repagGetParaId(obj);
};
for (auto& p : paragraphs) {
for (auto& l : p.lines) {
l.baselineY = l.glyphs.empty() ? 0.0 : l.glyphs[0].originY;
for (auto& r : l.runs) {
computeRunBBox(r);
computeRunColor(r);
computeRunParaId(r);
}
computeLineBBox(l);
}
computeParaBBox(p);
}
model.paragraphs = std::move(paragraphs);
return model;
#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);
}
}
}
}
return result;
#else
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<std::vector<PdfPage::AnnotationInfo>, EngineError> PdfiumPage::extractAnnotations() const {
#ifdef PDFENGINE_WITH_PDFIUM
if (!page_) {
return std::unexpected(EngineError::Unknown);
}
FPDF_FORMFILLINFO formInfo;
memset(&formInfo, 0, sizeof(formInfo));
formInfo.version = 1;
FPDF_FORMHANDLE formHandle = FPDFDOC_InitFormFillEnvironment(doc_, &formInfo);
std::vector<PdfPage::AnnotationInfo> result;
int count = FPDFPage_GetAnnotCount(page_);
for (int i = 0; i < count; ++i) {
FPDF_ANNOTATION annot = FPDFPage_GetAnnot(page_, i);
if (!annot) continue;
PdfPage::AnnotationInfo info;
info.pageIndex = pageIndex_;
unsigned long len = FPDFAnnot_GetStringValue(annot, "NM", nullptr, 0);
if (len > 2) {
std::vector<uint8_t> buf(len);
FPDFAnnot_GetStringValue(annot, "NM", 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();
info.id = text;
}
if (info.id.empty()) {
info.id = "anno_" + std::to_string(pageIndex_) + "_" + std::to_string(i);
}
int subtype = FPDFAnnot_GetSubtype(annot);
if (subtype == FPDF_ANNOT_HIGHLIGHT) {
info.type = "highlight";
} else if (subtype == FPDF_ANNOT_FREETEXT || subtype == FPDF_ANNOT_TEXT) {
info.type = "comment";
} else if (subtype == FPDF_ANNOT_INK) {
info.type = "ink";
} else if (subtype == FPDF_ANNOT_STRIKEOUT) {
info.type = "strikeout";
} else if (subtype == FPDF_ANNOT_UNDERLINE) {
info.type = "underline";
} else if (subtype == FPDF_ANNOT_SQUIGGLY) {
info.type = "squiggly";
} else if (subtype == FPDF_ANNOT_WIDGET) {
info.type = "widget";
int fieldType = FPDFAnnot_GetFormFieldType(formHandle, annot);
if (fieldType == 1 || fieldType == 2 || fieldType == 3) {
info.fieldType = "Btn";
} else if (fieldType == 4 || fieldType == 5) {
info.fieldType = "Ch";
} else if (fieldType == 6) {
info.fieldType = "Tx";
} else if (fieldType == 7) {
info.fieldType = "Sig";
} else {
info.fieldType = "Unknown";
}
info.fieldFlags = FPDFAnnot_GetFormFieldFlags(formHandle, annot);
unsigned long nameLen = FPDFAnnot_GetFormFieldName(formHandle, annot, nullptr, 0);
if (nameLen > 2) {
std::vector<FPDF_WCHAR> nameBuf(nameLen / 2);
FPDFAnnot_GetFormFieldName(formHandle, annot, nameBuf.data(), nameLen);
std::string text = utf16le_to_utf8(reinterpret_cast<const char16_t*>(nameBuf.data()), nameBuf.size());
while (!text.empty() && text.back() == '\0') text.pop_back();
info.fieldName = text;
}
unsigned long valLen = FPDFAnnot_GetFormFieldValue(formHandle, annot, nullptr, 0);
if (valLen > 2) {
std::vector<FPDF_WCHAR> valBuf(valLen / 2);
FPDFAnnot_GetFormFieldValue(formHandle, annot, valBuf.data(), valLen);
std::string text = utf16le_to_utf8(reinterpret_cast<const char16_t*>(valBuf.data()), valBuf.size());
while (!text.empty() && text.back() == '\0') text.pop_back();
info.fieldValue = text;
}
if (info.fieldType == "Ch") {
int optCount = FPDFAnnot_GetOptionCount(formHandle, annot);
for (int o = 0; o < optCount; ++o) {
unsigned long optLen = FPDFAnnot_GetOptionLabel(formHandle, annot, o, nullptr, 0);
if (optLen > 2) {
std::vector<FPDF_WCHAR> optBuf(optLen / 2);
FPDFAnnot_GetOptionLabel(formHandle, annot, o, optBuf.data(), optLen);
std::string optText = utf16le_to_utf8(reinterpret_cast<const char16_t*>(optBuf.data()), optBuf.size());
while (!optText.empty() && optText.back() == '\0') optText.pop_back();
info.fieldOptions.push_back(optText);
}
}
}
} else {
info.type = "unknown";
}
if (info.type != "unknown") {
FS_RECTF rect;
if (FPDFAnnot_GetRect(annot, &rect)) {
int dw = static_cast<int>(std::round(width()));
int dh = static_cast<int>(std::round(height()));
DevicePoint topLeft = pageToDevice({rect.left, rect.top}, dw, dh, 0);
DevicePoint bottomRight = pageToDevice({rect.right, rect.bottom}, dw, dh, 0);
info.x = (std::min)(topLeft.x, bottomRight.x);
info.y = (std::min)(topLeft.y, bottomRight.y);
info.width = std::abs(bottomRight.x - topLeft.x);
info.height = std::abs(bottomRight.y - topLeft.y);
}
len = FPDFAnnot_GetStringValue(annot, "T", nullptr, 0);
if (len > 2) {
std::vector<uint8_t> buf(len);
FPDFAnnot_GetStringValue(annot, "T", 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();
info.author = text;
}
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();
info.content = text;
}
len = FPDFAnnot_GetStringValue(annot, "M", nullptr, 0);
if (len <= 2) {
len = FPDFAnnot_GetStringValue(annot, "CreationDate", nullptr, 0);
if (len > 2) {
std::vector<uint8_t> buf(len);
FPDFAnnot_GetStringValue(annot, "CreationDate", 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();
info.timestamp = text;
}
} else {
std::vector<uint8_t> buf(len);
FPDFAnnot_GetStringValue(annot, "M", 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();
info.timestamp = text;
}
unsigned int R = 0, G = 0, B = 0, A = 0;
if (FPDFAnnot_GetColor(annot, FPDFANNOT_COLORTYPE_Color, &R, &G, &B, &A)) {
char hex[10];
snprintf(hex, sizeof(hex), "#%02x%02x%02x", R, G, B);
info.color = hex;
}
if (subtype == FPDF_ANNOT_INK) {
const double pageH = height();
unsigned long strokeCount = FPDFAnnot_GetInkListCount(annot);
for (unsigned long s = 0; s < strokeCount; ++s) {
unsigned long ptCount = FPDFAnnot_GetInkListPath(annot, s, nullptr, 0);
if (ptCount == 0) continue;
std::vector<FS_POINTF> pts(ptCount);
FPDFAnnot_GetInkListPath(annot, s, pts.data(), ptCount);
std::vector<Point2D> stroke;
stroke.reserve(ptCount);
for (const auto& p : pts) {
stroke.push_back(Point2D{static_cast<double>(p.x), pageH - static_cast<double>(p.y)});
}
if (!stroke.empty()) info.paths.push_back(std::move(stroke));
}
}
result.push_back(info);
}
FPDFPage_CloseAnnot(annot);
}
FPDFDOC_ExitFormFillEnvironment(formHandle);
return result;
#else
return std::unexpected(EngineError::Unknown);
#endif
}
}
+286
View File
@@ -0,0 +1,286 @@
#include "parser/pdfium_internal.hpp"
namespace pdfengine::parser {
#ifdef PDFENGINE_WITH_PDFIUM
void repagSetParaId(FPDF_DOCUMENT doc, FPDF_PAGEOBJECT obj, const std::string& paraId) {
if (paraId.empty() || !obj) return;
FPDF_PAGEOBJECTMARK mark = FPDFPageObj_AddMark(obj, "PDFPARA");
if (mark) FPDFPageObjMark_SetStringParam(doc, obj, mark, "id", paraId.c_str());
}
std::string repagGetParaId(FPDF_PAGEOBJECT obj) {
if (!obj) return "";
int n = FPDFPageObj_CountMarks(obj);
for (int i = 0; i < n; ++i) {
FPDF_PAGEOBJECTMARK mark = FPDFPageObj_GetMark(obj, i);
if (!mark) continue;
unsigned long nl = 0;
FPDFPageObjMark_GetName(mark, nullptr, 0, &nl);
if (nl < 2) continue;
std::vector<unsigned short> nb(nl / 2);
FPDFPageObjMark_GetName(mark, reinterpret_cast<FPDF_WCHAR*>(nb.data()), nl, &nl);
std::string name = utf16le_to_utf8(reinterpret_cast<const char16_t*>(nb.data()), nb.size());
while (!name.empty() && name.back() == '\0') name.pop_back();
if (name != "PDFPARA") continue;
unsigned long vl = 0;
if (!FPDFPageObjMark_GetParamStringValue(mark, "id", nullptr, 0, &vl) || vl < 2) continue;
std::vector<unsigned short> vb(vl / 2);
if (!FPDFPageObjMark_GetParamStringValue(mark, "id", reinterpret_cast<FPDF_WCHAR*>(vb.data()), vl, &vl))
continue;
std::string val = utf16le_to_utf8(reinterpret_cast<const char16_t*>(vb.data()), vb.size());
while (!val.empty() && val.back() == '\0') val.pop_back();
return val;
}
return "";
}
void walkOutline(FPDF_DOCUMENT doc, FPDF_BOOKMARK bm, int level,
std::vector<PdfDocument::OutlineItem>& out) {
while (bm && level <= 32 && out.size() < 5000) {
PdfDocument::OutlineItem item;
item.level = level;
unsigned long len = FPDFBookmark_GetTitle(bm, nullptr, 0);
if (len > 2) {
std::vector<unsigned short> buf(len / 2);
FPDFBookmark_GetTitle(bm, buf.data(), len);
item.title = utf16le_to_utf8(reinterpret_cast<const char16_t*>(buf.data()), buf.size());
while (!item.title.empty() && item.title.back() == '\0') item.title.pop_back();
}
FPDF_DEST dest = FPDFBookmark_GetDest(doc, bm);
if (!dest) {
FPDF_ACTION action = FPDFBookmark_GetAction(bm);
if (action) dest = FPDFAction_GetDest(doc, action);
}
item.pageIndex = dest ? static_cast<int>(FPDFDest_GetDestPageIndex(doc, dest)) : -1;
out.push_back(std::move(item));
FPDF_BOOKMARK child = FPDFBookmark_GetFirstChild(doc, bm);
if (child) walkOutline(doc, child, level + 1, out);
bm = FPDFBookmark_GetNextSibling(doc, bm);
}
}
inline bool repagInColumn(FPDF_PAGEOBJECT o, double pushColLeft, double colRight) {
float l = 0, b = 0, r = 0, t = 0;
if (!FPDFPageObj_GetBounds(o, &l, &b, &r, &t)) return false;
return (r > pushColLeft && l < colRight);
}
PageBand repagComputeBand(FPDF_PAGE page, double pushColLeft, double colRight) {
PageBand band;
band.pageW = FPDF_GetPageWidthF(page);
band.pageH = FPDF_GetPageHeightF(page);
double maxTop = -1e18; bool any = false;
int n = FPDFPage_CountObjects(page);
for (int k = 0; k < n; ++k) {
FPDF_PAGEOBJECT o = FPDFPage_GetObject(page, k);
if (!o) continue;
float l = 0, b = 0, r = 0, t = 0;
if (!FPDFPageObj_GetBounds(o, &l, &b, &r, &t)) continue;
if (r > pushColLeft && l < colRight) { if (t > maxTop) { maxTop = t; } any = true; }
}
if (!any) return band;
band.placementTopY = maxTop;
double limit = band.pageH - maxTop;
if (limit < 18.0) limit = 18.0;
if (limit > band.pageH * 0.25) limit = band.pageH * 0.25;
band.bottomLimitY = limit;
band.valid = true;
return band;
}
std::vector<double> repagAnchoredCenters(FPDF_PAGE page, double pushColLeft, double colRight,
double bottomLimitY) {
std::vector<double> out;
int n = FPDFPage_CountObjects(page);
for (int k = 0; k < n; ++k) {
FPDF_PAGEOBJECT o = FPDFPage_GetObject(page, k);
if (!o || !repagInColumn(o, pushColLeft, colRight)) continue;
float l = 0, b = 0, r = 0, t = 0;
if (!FPDFPageObj_GetBounds(o, &l, &b, &r, &t)) continue;
double cy = (b + t) / 2.0;
if (cy < bottomLimitY) out.push_back(cy);
}
return out;
}
inline bool repagIsAnchored(double cy, const std::vector<double>& anchored, double tol = 1.0) {
for (double ac : anchored) if (std::abs(ac - cy) < tol) return true;
return false;
}
int repaginateForward(FPDF_DOCUMENT doc, int startPage,
double colLeft, double colRight, double pushColLeft, double leading,
const std::vector<double>& anchoredCentersStart, int* movedOut) {
(void)colLeft;
const int kMaxPagesAdded = 200;
int pagesAdded = 0;
int movedTotal = 0;
int curPage = startPage;
std::vector<double> anchored = anchoredCentersStart;
while (true) {
FPDF_PAGE P = FPDF_LoadPage(doc, curPage);
if (!P) break;
PageBand band = repagComputeBand(P, pushColLeft, colRight);
if (!band.valid) { FPDF_ClosePage(P); break; }
std::vector<FPDF_PAGEOBJECT> overflow;
double blockTop = -1e18, blockBottom = 1e18;
int n = FPDFPage_CountObjects(P);
for (int k = 0; k < n; ++k) {
FPDF_PAGEOBJECT o = FPDFPage_GetObject(P, k);
if (!o || !repagInColumn(o, pushColLeft, colRight)) continue;
float l = 0, b = 0, r = 0, t = 0;
if (!FPDFPageObj_GetBounds(o, &l, &b, &r, &t)) continue;
double cy = (b + t) / 2.0;
if (cy >= band.bottomLimitY) continue;
if (repagIsAnchored(cy, anchored)) continue;
overflow.push_back(o);
if (t > blockTop) blockTop = t;
if (b < blockBottom) blockBottom = b;
}
if (overflow.empty()) { FPDF_ClosePage(P); break; }
int total = FPDF_GetPageCount(doc);
FPDF_PAGE Q = nullptr;
bool createdNew = false;
if (curPage + 1 >= total) {
if (pagesAdded + 1 > kMaxPagesAdded) {
spdlog::warn("repaginateForward: hit page-add cap ({}), stopping", kMaxPagesAdded);
FPDF_ClosePage(P); break;
}
Q = FPDFPage_New(doc, curPage + 1, band.pageW, band.pageH);
createdNew = true;
pagesAdded++;
} else {
Q = FPDF_LoadPage(doc, curPage + 1);
}
if (!Q) { FPDF_ClosePage(P); break; }
PageBand bandQ = createdNew ? band : repagComputeBand(Q, pushColLeft, colRight);
double placeTopQ = bandQ.valid ? bandQ.placementTopY : band.placementTopY;
double limitQ = bandQ.valid ? bandQ.bottomLimitY : band.bottomLimitY;
double blockH = blockTop - blockBottom;
if (blockH < 0) blockH = 0;
std::vector<double> anchoredQ =
createdNew ? std::vector<double>{}
: repagAnchoredCenters(Q, pushColLeft, colRight, limitQ);
if (!createdNew) {
double shift = blockH + leading;
int nq = FPDFPage_CountObjects(Q);
for (int k = 0; k < nq; ++k) {
FPDF_PAGEOBJECT o = FPDFPage_GetObject(Q, k);
if (!o || !repagInColumn(o, pushColLeft, colRight)) continue;
float l = 0, b = 0, r = 0, t = 0;
if (!FPDFPageObj_GetBounds(o, &l, &b, &r, &t)) continue;
double cy = (b + t) / 2.0;
if (repagIsAnchored(cy, anchoredQ)) continue;
FPDFPageObj_Transform(o, 1.0, 0.0, 0.0, 1.0, 0.0, -shift);
}
}
double translateDy = placeTopQ - blockTop;
for (FPDF_PAGEOBJECT o : overflow) {
FPDFPage_RemoveObject(P, o);
FPDFPageObj_Transform(o, 1.0, 0.0, 0.0, 1.0, 0.0, translateDy);
FPDFPage_InsertObject(Q, o);
}
FPDFPage_GenerateContent(P);
FPDFPage_GenerateContent(Q);
spdlog::info("repaginateForward: page {} -> {} moved {} object(s), blockH={:.1f}{}",
curPage, curPage + 1, overflow.size(), blockH, createdNew ? " (new page)" : "");
FPDF_ClosePage(P);
FPDF_ClosePage(Q);
movedTotal += static_cast<int>(overflow.size());
anchored = anchoredQ;
curPage += 1;
}
if (movedOut) *movedOut = movedTotal;
return pagesAdded;
}
int repaginateBackward(FPDF_DOCUMENT doc, int startPage,
double colLeft, double colRight, double pushColLeft, double leading) {
(void)colLeft; (void)colRight; (void)pushColLeft; (void)leading;
int pagesRemoved = 0;
for (int pg = FPDF_GetPageCount(doc) - 1; pg > startPage; --pg) {
FPDF_PAGE p = FPDF_LoadPage(doc, pg);
if (!p) break;
int n = FPDFPage_CountObjects(p);
FPDF_ClosePage(p);
if (n == 0) { FPDFPage_Delete(doc, pg); pagesRemoved++; }
else break;
}
return pagesRemoved;
}
void repagCompactToTop(FPDF_DOCUMENT doc, int pageIdx, double pushColLeft, double colRight) {
FPDF_PAGE p = FPDF_LoadPage(doc, pageIdx);
if (!p) return;
PageBand band = repagComputeBand(p, pushColLeft, colRight);
if (!band.valid) { FPDF_ClosePage(p); return; }
std::vector<double> anchored = repagAnchoredCenters(p, pushColLeft, colRight, band.bottomLimitY);
double curTop = -1e18; bool any = false;
int n = FPDFPage_CountObjects(p);
for (int k = 0; k < n; ++k) {
FPDF_PAGEOBJECT o = FPDFPage_GetObject(p, k);
if (!o || !repagInColumn(o, pushColLeft, colRight)) continue;
float l=0,b=0,r=0,t=0; if (!FPDFPageObj_GetBounds(o,&l,&b,&r,&t)) continue;
double cy=(b+t)/2.0; if (repagIsAnchored(cy, anchored)) continue;
if (t > curTop) { curTop = t; any = true; }
}
if (any) {
double up = band.placementTopY - curTop;
if (up > 0.01) {
for (int k = 0; k < n; ++k) {
FPDF_PAGEOBJECT o = FPDFPage_GetObject(p, k);
if (!o || !repagInColumn(o, pushColLeft, colRight)) continue;
float l=0,b=0,r=0,t=0; if (!FPDFPageObj_GetBounds(o,&l,&b,&r,&t)) continue;
double cy=(b+t)/2.0; if (repagIsAnchored(cy, anchored)) continue;
FPDFPageObj_Transform(o, 1.0, 0.0, 0.0, 1.0, 0.0, up);
}
FPDFPage_GenerateContent(p);
}
}
FPDF_ClosePage(p);
}
int repagRemoveContinuations(FPDF_DOCUMENT doc, int anchorPage, const std::string& paraId,
double pushColLeft, double colRight) {
if (paraId.empty()) return 0;
int touched = 0;
int total = FPDF_GetPageCount(doc);
for (int pg = 0; pg < total; ++pg) {
if (pg == anchorPage) continue;
FPDF_PAGE p = FPDF_LoadPage(doc, pg);
if (!p) continue;
std::vector<FPDF_PAGEOBJECT> toDel;
int n = FPDFPage_CountObjects(p);
for (int k = 0; k < n; ++k) {
FPDF_PAGEOBJECT o = FPDFPage_GetObject(p, k);
if (o && repagGetParaId(o) == paraId) toDel.push_back(o);
}
if (toDel.empty()) { FPDF_ClosePage(p); continue; }
for (auto o : toDel) { FPDFPage_RemoveObject(p, o); FPDFPageObj_Destroy(o); }
FPDFPage_GenerateContent(p);
FPDF_ClosePage(p);
repagCompactToTop(doc, pg, pushColLeft, colRight);
touched++;
}
return touched;
}
#endif
}
+10 -7
View File
@@ -1,11 +1,16 @@
# Engine test suite. Phase 0: a single smoke test that backs Gate G0.
add_executable(pdfengine_smoke
smoke_test.cpp
fonts_test.cpp
font_rendering_test.cpp
font_loader_descriptor_test.cpp
font_encoding_test.cpp
font_types_test.cpp
graphics_state_test.cpp
display_list_test.cpp
document_test.cpp
document_load_test.cpp
page_render_test.cpp
document_edit_test.cpp
font_diagnostics_test.cpp
text_encoding_test.cpp
skia_renderer_test.cpp
lexer_test.cpp
parser_test.cpp
@@ -46,7 +51,6 @@ target_include_directories(pdfengine_smoke
"${CMAKE_CURRENT_SOURCE_DIR}/../src"
)
# Locate and normalize corpus path
set(TEST_CORPUS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../corpus" CACHE PATH "Path to test corpus")
file(TO_CMAKE_PATH "${TEST_CORPUS_DIR}" TEST_CORPUS_DIR_NORM)
target_compile_definitions(pdfengine_smoke PRIVATE TEST_CORPUS_DIR="${TEST_CORPUS_DIR_NORM}")
@@ -54,5 +58,4 @@ target_compile_definitions(pdfengine_smoke PRIVATE TEST_CORPUS_DIR="${TEST_CORPU
pdfengine_set_warnings(pdfengine_smoke)
pdfengine_enable_sanitizers(pdfengine_smoke)
# Registers each TEST() with CTest so `ctest --preset ...` runs them.
gtest_discover_tests(pdfengine_smoke)
gtest_discover_tests(pdfengine_smoke)
+689
View File
@@ -0,0 +1,689 @@
#include "document_test_helpers.hpp"
namespace pdfengine {
TEST(DocumentEditTest, ApplyEditsAndIncrementalSave) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("basic", "hello_world.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "hello_world.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto doc = *docRes;
std::string editsJson = R"({
"version": "1.0",
"operations": [
{
"id": "op_test_1",
"type": "text_overlay",
"pageIndex": 0,
"data": {
"text": "UniqueEditedTextAnnotation123",
"x": 100.0,
"y": 150.0,
"width": 200.0,
"height": 20.0,
"fontSize": 14.0,
"fontFamily": "Helvetica",
"color": "#000000"
}
}
]
})";
auto editRes = doc->applyEdits(editsJson);
ASSERT_TRUE(editRes.has_value());
auto saveRes = doc->saveIncremental();
ASSERT_TRUE(saveRes.has_value());
const auto& savedBytes = *saveRes;
ASSERT_FALSE(savedBytes.empty());
auto newDocRes = PdfDocument::loadFromMemory(savedBytes);
ASSERT_TRUE(newDocRes.has_value());
auto newDoc = *newDocRes;
EXPECT_EQ(newDoc->pageCount(), 1);
auto newPageRes = newDoc->getPage(0);
ASSERT_TRUE(newPageRes.has_value());
auto newPage = *newPageRes;
auto textRes = newPage->extractText();
ASSERT_TRUE(textRes.has_value());
EXPECT_NE(textRes->find("UniqueEditedTextAnnotation123"), std::string::npos);
}
TEST(DocumentEditTest, ApplyRedactionAndFullSave) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("basic", "hello_world.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "hello_world.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto doc = *docRes;
{
auto pageRes = doc->getPage(0);
ASSERT_TRUE(pageRes.has_value());
auto textRes = (*pageRes)->extractText();
ASSERT_TRUE(textRes.has_value());
EXPECT_NE(textRes->find("Hello"), std::string::npos);
}
std::string editsJson = R"({
"version": "1.0",
"operations": [
{
"id": "op_redact_test_1",
"type": "redaction",
"pageIndex": 0,
"data": {
"x": 0.0,
"y": 0.0,
"width": 612.0,
"height": 792.0,
"fillColor": "#ffffff"
}
}
]
})";
auto editRes = doc->applyEdits(editsJson);
ASSERT_TRUE(editRes.has_value());
auto saveRes = doc->saveFull();
ASSERT_TRUE(saveRes.has_value());
const auto& savedBytes = *saveRes;
ASSERT_FALSE(savedBytes.empty());
auto newDocRes = PdfDocument::loadFromMemory(savedBytes);
ASSERT_TRUE(newDocRes.has_value());
auto newDoc = *newDocRes;
auto newPageRes = newDoc->getPage(0);
ASSERT_TRUE(newPageRes.has_value());
auto newPage = *newPageRes;
auto textRes = newPage->extractText();
ASSERT_TRUE(textRes.has_value());
EXPECT_EQ(textRes->find("Hello"), std::string::npos);
EXPECT_EQ(textRes->find("world"), std::string::npos);
}
TEST(DocumentEditTest, ApplyImageOverlayAndIncrementalSave) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("basic", "hello_world.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "hello_world.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto doc = *docRes;
std::string editsJson = R"({
"version": "1.0",
"operations": [
{
"id": "op_test_img_1",
"type": "image_overlay",
"pageIndex": 0,
"data": {
"x": 100.0,
"y": 150.0,
"width": 200.0,
"height": 150.0,
"pixelWidth": 2,
"pixelHeight": 2,
"rawPixelData": "AAD//wAA//8AAP//AAD//w=="
}
}
]
})";
auto editRes = doc->applyEdits(editsJson);
ASSERT_TRUE(editRes.has_value());
auto saveRes = doc->saveIncremental();
ASSERT_TRUE(saveRes.has_value());
const auto& savedBytes = *saveRes;
ASSERT_FALSE(savedBytes.empty());
auto newDocRes = PdfDocument::loadFromMemory(savedBytes);
ASSERT_TRUE(newDocRes.has_value());
auto newDoc = *newDocRes;
EXPECT_EQ(newDoc->pageCount(), 1);
}
TEST(DocumentEditTest, ApplyPageRotationAndIncrementalSave) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("basic", "about_blank.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "about_blank.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto doc = *docRes;
auto pageRes = doc->getPage(0);
ASSERT_TRUE(pageRes.has_value());
double origW = (*pageRes)->width();
double origH = (*pageRes)->height();
EXPECT_GT(origW, 0.0);
EXPECT_GT(origH, origW);
std::string editsJson1 = R"({
"version": "1.0",
"operations": [
{
"id": "op_test_rot_1",
"type": "page_rotation",
"pageIndex": 0,
"data": {
"rotation": 90
}
}
]
})";
auto editRes1 = doc->applyEdits(editsJson1);
ASSERT_TRUE(editRes1.has_value());
std::string editsJson2 = R"({
"version": "1.0",
"operations": [
{
"id": "op_test_rot_2",
"type": "page_rotation",
"pageIndex": 0,
"data": {
"rotation": 90
}
}
]
})";
auto editRes2 = doc->applyEdits(editsJson2);
ASSERT_TRUE(editRes2.has_value());
auto saveRes = doc->saveIncremental();
ASSERT_TRUE(saveRes.has_value());
const auto& savedBytes = *saveRes;
ASSERT_FALSE(savedBytes.empty());
auto newDocRes = PdfDocument::loadFromMemory(savedBytes);
ASSERT_TRUE(newDocRes.has_value());
auto newDoc = *newDocRes;
EXPECT_EQ(newDoc->pageCount(), 1);
auto newPageRes = newDoc->getPage(0);
ASSERT_TRUE(newPageRes.has_value());
double rotatedW = (*newPageRes)->width();
double rotatedH = (*newPageRes)->height();
EXPECT_NEAR(rotatedW, origW, 0.01);
EXPECT_NEAR(rotatedH, origH, 0.01);
std::string editsJson3 = R"({
"version": "1.0",
"operations": [
{
"id": "op_test_rot_3",
"type": "page_rotation",
"pageIndex": 0,
"data": {
"rotation": -90
}
}
]
})";
auto editRes3 = newDoc->applyEdits(editsJson3);
ASSERT_TRUE(editRes3.has_value());
auto saveRes3 = newDoc->saveIncremental();
ASSERT_TRUE(saveRes3.has_value());
const auto& savedBytes3 = *saveRes3;
auto finalDocRes = PdfDocument::loadFromMemory(savedBytes3);
ASSERT_TRUE(finalDocRes.has_value());
auto finalPageRes = (*finalDocRes)->getPage(0);
ASSERT_TRUE(finalPageRes.has_value());
double finalW = (*finalPageRes)->width();
double finalH = (*finalPageRes)->height();
EXPECT_NEAR(finalW, origH, 0.01);
EXPECT_NEAR(finalH, origW, 0.01);
}
TEST(DocumentEditTest, ApplyPageDeletionAndIncrementalSave) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("basic", "hello_world_2_pages.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "hello_world_2_pages.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto doc = *docRes;
EXPECT_EQ(doc->pageCount(), 2);
std::string editsJson = R"({
"version": "1.0",
"operations": [
{
"id": "op_del_test_1",
"type": "page_deletion",
"pageIndex": 1,
"data": {}
}
]
})";
auto editRes = doc->applyEdits(editsJson);
ASSERT_TRUE(editRes.has_value());
EXPECT_EQ(doc->pageCount(), 1);
auto saveRes = doc->saveIncremental();
ASSERT_TRUE(saveRes.has_value());
const auto& savedBytes = *saveRes;
ASSERT_FALSE(savedBytes.empty());
auto newDocRes = PdfDocument::loadFromMemory(savedBytes);
ASSERT_TRUE(newDocRes.has_value());
EXPECT_EQ((*newDocRes)->pageCount(), 1);
}
TEST(DocumentEditTest, ApplyPageReorderAndIncrementalSave) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("basic", "hello_world_2_pages.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "hello_world_2_pages.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto doc = *docRes;
EXPECT_EQ(doc->pageCount(), 2);
std::string editsJson = R"({
"version": "1.0",
"operations": [
{
"id": "op_reorder_test_1",
"type": "page_reorder",
"pageIndex": 1,
"data": {
"destPageIndex": 0
}
}
]
})";
auto editRes = doc->applyEdits(editsJson);
ASSERT_TRUE(editRes.has_value());
EXPECT_EQ(doc->pageCount(), 2);
auto saveRes = doc->saveIncremental();
ASSERT_TRUE(saveRes.has_value());
const auto& savedBytes = *saveRes;
ASSERT_FALSE(savedBytes.empty());
auto newDocRes = PdfDocument::loadFromMemory(savedBytes);
ASSERT_TRUE(newDocRes.has_value());
EXPECT_EQ((*newDocRes)->pageCount(), 2);
}
TEST(DocumentEditTest, ReplaceTextMVPStandardFont) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("basic", "hello_world.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "hello_world.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto doc = *docRes;
auto pageRes = doc->getPage(0);
ASSERT_TRUE(pageRes.has_value());
auto pageObj = *pageRes;
auto modelRes = pageObj->extractDocumentModel();
ASSERT_TRUE(modelRes.has_value());
const auto& model = *modelRes;
std::vector<int> objectIndices;
for (const auto& p : model.paragraphs) {
for (const auto& line : p.lines) {
for (const auto& run : line.runs) {
if (run.text.find("Hello") != std::string::npos) {
objectIndices = run.objectIndices;
break;
}
}
if (!objectIndices.empty()) break;
}
if (!objectIndices.empty()) break;
}
ASSERT_FALSE(objectIndices.empty()) << "Could not find a text object in hello_world.pdf";
std::string indicesStr = "";
for (size_t i = 0; i < objectIndices.size(); ++i) {
indicesStr += std::to_string(objectIndices[i]);
if (i + 1 < objectIndices.size()) indicesStr += ",";
}
std::string editsJson = R"({
"version": "1.0",
"operations": [
{
"id": "op_mvp_1",
"type": "replace_text",
"pageIndex": 0,
"objectIndices": [)" + indicesStr + R"(],
"text": "Greeting, universe!"
}
]
})";
auto editRes = doc->applyEdits(editsJson);
ASSERT_TRUE(editRes.has_value());
auto saveRes = doc->saveIncremental();
ASSERT_TRUE(saveRes.has_value());
const auto& savedBytes = *saveRes;
ASSERT_FALSE(savedBytes.empty());
auto newDocRes = PdfDocument::loadFromMemory(savedBytes);
ASSERT_TRUE(newDocRes.has_value());
auto newDoc = *newDocRes;
auto newPageRes = newDoc->getPage(0);
ASSERT_TRUE(newPageRes.has_value());
auto newPage = *newPageRes;
auto textRes = newPage->extractText();
ASSERT_TRUE(textRes.has_value());
EXPECT_NE(textRes->find("Greeting, universe!"), std::string::npos);
EXPECT_EQ(textRes->find("Hello"), std::string::npos);
}
TEST(DocumentEditTest, ReplaceTextRuntimeFontEngine) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("fonts", "latin_extended.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "latin_extended.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto doc = *docRes;
auto pageRes = doc->getPage(0);
ASSERT_TRUE(pageRes.has_value());
auto pageObj = *pageRes;
auto modelRes = pageObj->extractDocumentModel();
ASSERT_TRUE(modelRes.has_value());
const auto& model = *modelRes;
std::vector<int> objectIndices;
std::string originalFontId = "";
for (const auto& p : model.paragraphs) {
for (const auto& line : p.lines) {
for (const auto& run : line.runs) {
if (run.fontName.find("Roboto-Regular") != std::string::npos) {
objectIndices = run.objectIndices;
originalFontId = run.internalFontId;
break;
}
}
if (!objectIndices.empty()) break;
}
if (!objectIndices.empty()) break;
}
ASSERT_FALSE(objectIndices.empty()) << "Could not find target text run in latin_extended.pdf";
std::string indicesStr = "";
for (size_t i = 0; i < objectIndices.size(); ++i) {
indicesStr += std::to_string(objectIndices[i]);
if (i + 1 < objectIndices.size()) indicesStr += ",";
}
std::string editsJson = R"({
"version": "1.0",
"operations": [
{
"id": "op_engine_1",
"type": "replace_text",
"pageIndex": 0,
"objectIndices": [)" + indicesStr + R"(],
"text": "Font Engine Active!",
"internalFontId": ")" + originalFontId + R"("
}
]
})";
auto editRes = doc->applyEdits(editsJson);
ASSERT_TRUE(editRes.has_value());
auto saveRes = doc->saveIncremental();
ASSERT_TRUE(saveRes.has_value());
const auto& savedBytes = *saveRes;
ASSERT_FALSE(savedBytes.empty());
auto newDocRes = PdfDocument::loadFromMemory(savedBytes);
ASSERT_TRUE(newDocRes.has_value());
auto newDoc = *newDocRes;
auto newPageRes = newDoc->getPage(0);
ASSERT_TRUE(newPageRes.has_value());
auto newPage = *newPageRes;
auto textRes = newPage->extractText();
ASSERT_TRUE(textRes.has_value());
EXPECT_NE(textRes->find("Font Engine Active!"), std::string::npos);
}
TEST(DocumentEditTest, ReplaceTextFontReuseAndEmbedding) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("basic", "hello_world.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "hello_world.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto doc = *docRes;
auto pageRes = doc->getPage(0);
ASSERT_TRUE(pageRes.has_value());
auto pageObj = *pageRes;
auto modelRes = pageObj->extractDocumentModel();
ASSERT_TRUE(modelRes.has_value());
const auto& model = *modelRes;
std::vector<int> objectIndices;
std::string originalFontId = "";
for (const auto& p : model.paragraphs) {
for (const auto& line : p.lines) {
for (const auto& run : line.runs) {
if (!run.objectIndices.empty()) {
objectIndices = run.objectIndices;
originalFontId = run.internalFontId;
break;
}
}
if (!objectIndices.empty()) break;
}
if (!objectIndices.empty()) break;
}
ASSERT_FALSE(objectIndices.empty()) << "Could not find a text run in hello_world.pdf";
std::string indicesStr = "";
for (size_t i = 0; i < objectIndices.size(); ++i) {
indicesStr += std::to_string(objectIndices[i]);
if (i + 1 < objectIndices.size()) indicesStr += ",";
}
std::string editsJson = R"({
"version": "1.0",
"operations": [
{
"id": "op_reuse_1",
"type": "replace_text",
"pageIndex": 0,
"objectIndices": [)" + indicesStr + R"(],
"text": "Embedded Arial",
"internalFontId": ")" + originalFontId + R"("
}
]
})";
auto editRes = doc->applyEdits(editsJson);
ASSERT_TRUE(editRes.has_value());
auto saveRes = doc->saveIncremental();
ASSERT_TRUE(saveRes.has_value());
const auto& savedBytes = *saveRes;
ASSERT_FALSE(savedBytes.empty());
auto newDocRes = PdfDocument::loadFromMemory(savedBytes);
ASSERT_TRUE(newDocRes.has_value());
auto newDoc = *newDocRes;
auto fontsRes = newDoc->getFonts(0, 0);
ASSERT_TRUE(fontsRes.has_value());
bool foundEmbeddedArial = false;
for (const auto& f : *fontsRes) {
if (f.isEmbedded && (f.fontName.find("Arial") != std::string::npos || f.fontName.find("LiberationSans") != std::string::npos)) {
foundEmbeddedArial = true;
}
}
std::cout << "Font Embedding Test: foundEmbeddedArial = " << foundEmbeddedArial << std::endl;
}
TEST(DocumentEditTest, ReplaceTextHarfBuzzShapingAndReflow) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("fonts", "latin_extended.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "latin_extended.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto doc = *docRes;
auto pageRes = doc->getPage(0);
ASSERT_TRUE(pageRes.has_value());
auto pageObj = *pageRes;
auto modelRes = pageObj->extractDocumentModel();
ASSERT_TRUE(modelRes.has_value());
const auto& model = *modelRes;
std::vector<int> targetIndices;
std::string originalFontId = "";
std::string runBText = "";
double runBOrigX = 0.0;
double runBOrigY = 0.0;
for (const auto& p : model.paragraphs) {
for (const auto& line : p.lines) {
if (line.runs.size() >= 2) {
const auto& runA = line.runs[0];
const auto& runB = line.runs[1];
if (runA.fontName.find("Roboto-Regular") != std::string::npos &&
!runA.objectIndices.empty() &&
runB.x > runA.x) {
targetIndices = runA.objectIndices;
originalFontId = runA.internalFontId;
runBText = runB.text;
runBOrigX = runB.x;
runBOrigY = runB.y;
break;
}
}
}
if (!targetIndices.empty()) break;
}
if (targetIndices.empty()) {
GTEST_SKIP() << "Could not find a suitable line with multiple runs to test reflow.";
}
std::string indicesStr = "";
for (size_t i = 0; i < targetIndices.size(); ++i) {
indicesStr += std::to_string(targetIndices[i]);
if (i + 1 < targetIndices.size()) indicesStr += ",";
}
std::string editsJson = R"({
"version": "1.0",
"operations": [
{
"id": "op_reflow_1",
"type": "replace_text",
"pageIndex": 0,
"objectIndices": [)" + indicesStr + R"(],
"text": "This is an extremely long replacement text to force the Reflow Engine to shift subsequent runs!",
"internalFontId": ")" + originalFontId + R"("
}
]
})";
auto editRes = doc->applyEdits(editsJson);
ASSERT_TRUE(editRes.has_value());
auto saveRes = doc->saveIncremental();
ASSERT_TRUE(saveRes.has_value());
const auto& savedBytes = *saveRes;
ASSERT_FALSE(savedBytes.empty());
auto newDocRes = PdfDocument::loadFromMemory(savedBytes);
ASSERT_TRUE(newDocRes.has_value());
auto newDoc = *newDocRes;
auto newPageRes = newDoc->getPage(0);
ASSERT_TRUE(newPageRes.has_value());
auto newPage = *newPageRes;
auto newModelRes = newPage->extractDocumentModel();
ASSERT_TRUE(newModelRes.has_value());
const auto& newModel = *newModelRes;
bool foundRunB = false;
double runBNewX = 0.0;
for (const auto& p : newModel.paragraphs) {
for (const auto& line : p.lines) {
for (const auto& run : line.runs) {
if (run.text == runBText && std::abs(run.y - runBOrigY) < 5.0) {
foundRunB = true;
runBNewX = run.x;
break;
}
}
if (foundRunB) break;
}
if (foundRunB) break;
}
ASSERT_TRUE(foundRunB) << "Could not find the subsequent text run '" << runBText << "' in the reflowed document.";
EXPECT_GT(runBNewX, runBOrigX + 10.0) << "The subsequent text run did not shift to the right by at least 10 points.";
std::cout << "Reflow Engine verified: '" << runBText << "' shifted from X=" << runBOrigX << " to X=" << runBNewX << std::endl;
}
}
+153
View File
@@ -0,0 +1,153 @@
#include "document_test_helpers.hpp"
namespace pdfengine {
TEST(DocumentLoadTest, NonExistentFileReturnsFileNotFound) {
SKIP_IF_NO_PDFIUM();
auto result = PdfDocument::loadFromFile("nonexistent_file_12345.pdf");
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), EngineError::FileNotFound);
}
TEST(DocumentLoadTest, InvalidFileReturnsInvalidFormat) {
SKIP_IF_NO_PDFIUM();
std::string path = "invalid_format_test.pdf";
{
std::ofstream out(path, std::ios::binary);
out << "NOT A PDF FILE!";
}
auto result = PdfDocument::loadFromFile(path);
std::filesystem::remove(path);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), EngineError::InvalidFormat);
}
TEST(DocumentLoadTest, EncryptedPdfRequiresPassword) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("edge-cases", "encrypted.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "encrypted.pdf not found in corpus.";
}
auto result = PdfDocument::loadFromFile(path.string());
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), EngineError::PasswordRequired);
}
TEST(DocumentLoadTest, EncryptedPdfInvalidPassword) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("edge-cases", "encrypted.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "encrypted.pdf not found in corpus.";
}
auto result = PdfDocument::loadFromFile(path.string(), "wrong_password");
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), EngineError::InvalidPassword);
}
TEST(DocumentLoadTest, EncryptedPdfCorrectPassword) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("edge-cases", "encrypted.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "encrypted.pdf not found in corpus.";
}
std::vector<std::string> candidates = {"tessy", "test", "password", "123456", "1234", "foobar", "user", "owner", ""};
bool success = false;
for (const auto& pw : candidates) {
auto result = PdfDocument::loadFromFile(path.string(), pw);
if (result.has_value()) {
EXPECT_GT((*result)->pageCount(), 0);
success = true;
break;
}
}
EXPECT_TRUE(success) << "Failed to open encrypted.pdf with any of the candidate passwords.";
}
TEST(DocumentLoadTest, LoadFromMemorySuccess) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("basic", "hello_world.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "hello_world.pdf not found in corpus.";
}
auto buffer = readFile(path);
ASSERT_FALSE(buffer.empty());
auto result = PdfDocument::loadFromMemory(buffer);
ASSERT_TRUE(result.has_value());
EXPECT_EQ((*result)->pageCount(), 1);
}
TEST(DocumentLoadTest, LoadFromMemoryEmptyBuffer) {
SKIP_IF_NO_PDFIUM();
std::vector<uint8_t> empty_buf;
auto result = PdfDocument::loadFromMemory(empty_buf);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), EngineError::InvalidFormat);
}
TEST(PageCountTest, CorpusPageCounts) {
SKIP_IF_NO_PDFIUM();
struct ExpectedPageCount {
std::string folder;
std::string file;
int count;
};
std::vector<ExpectedPageCount> targets = {
{"basic", "about_blank.pdf", 1},
{"basic", "black.pdf", 1},
{"basic", "hello_world.pdf", 1},
{"basic", "hello_world_2_pages.pdf", 2},
{"basic", "rectangles_multi_pages.pdf", 5}
};
for (const auto& t : targets) {
auto path = getCorpusPath(t.folder, t.file);
if (!std::filesystem::exists(path)) {
continue;
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value()) << "Failed to open " << t.file;
EXPECT_EQ((*docRes)->pageCount(), t.count) << "Mismatch for " << t.file;
}
auto verifyCorpusFolder = [](const std::string& folder, const std::vector<std::string>& files) {
for (const auto& f : files) {
auto path = getCorpusPath(folder, f);
if (!std::filesystem::exists(path)) {
ADD_FAILURE() << "Missing corpus file: " << path.string();
continue;
}
std::vector<std::string> passwords = {""};
if (f == "encrypted.pdf") {
passwords = {"tessy", "test", "password", "123456", "1234", "foobar", "user", "owner"};
}
bool success = false;
EngineError lastErr = EngineError::Unknown;
for (const auto& pw : passwords) {
auto docRes = PdfDocument::loadFromFile(path.string(), pw);
if (docRes.has_value()) {
EXPECT_GE((*docRes)->pageCount(), 0) << "Page count negative for " << f;
success = true;
break;
}
lastErr = docRes.error();
}
EXPECT_TRUE(success) << "Failed to open " << f << " error: " << static_cast<int>(lastErr);
}
};
verifyCorpusFolder("basic", corpus_basic);
verifyCorpusFolder("fonts", corpus_fonts);
verifyCorpusFolder("edge-cases", corpus_edge);
}
}
File diff suppressed because it is too large Load Diff
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include <gtest/gtest.h>
#include <pdfengine/pdf_document.hpp>
#include <pdfengine/pdf_engine.hpp>
#include "parser/pdfium_document.hpp"
#include "fonts/pdf_fonts/encoding/cjk_collection_db.hpp"
#include <filesystem>
#include <fstream>
#include <vector>
#include <string>
#include <thread>
#include <atomic>
#include <chrono>
#include "fonts/cache/glyph_cache.hpp"
#include "fonts/pdf_fonts/font.hpp"
#ifndef TEST_CORPUS_DIR
#define TEST_CORPUS_DIR "../../corpus"
#endif
namespace {
#define SKIP_IF_NO_PDFIUM() \
if (!pdfengine::engineHasPdfium()) { \
GTEST_SKIP() << "Skipping PDFium tests because PDFium is not linked."; \
}
std::filesystem::path getCorpusPath(const std::string& subfolder, const std::string& filename) {
return std::filesystem::path(TEST_CORPUS_DIR) / subfolder / filename;
}
std::vector<uint8_t> readFile(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file.is_open()) {
return {};
}
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> buffer(size);
if (file.read(reinterpret_cast<char*>(buffer.data()), size)) {
return buffer;
}
return {};
}
const std::vector<std::string> corpus_basic = {
"about_blank.pdf", "black.pdf", "clip_path.pdf", "dashed_lines.pdf",
"hello_world.pdf", "hello_world_2_pages.pdf", "many_rectangles.pdf",
"rectangles.pdf", "rectangles_multi_pages.pdf", "whitespace.pdf"
};
const std::vector<std::string> corpus_fonts = {
"latin_extended.pdf", "rotated_text.pdf", "rotated_text_90.pdf",
"text_font.pdf", "utf-8.pdf", "vertical_text.pdf", "hebrew_mirrored.pdf"
};
const std::vector<std::string> corpus_edge = {
"annots.pdf", "bookmarks.pdf", "combobox_form.pdf", "embedded_attachments.pdf",
"empty_xref.pdf", "encrypted.pdf", "linearized.pdf", "listbox_form.pdf",
"no_page_count.pdf", "page_labels.pdf", "text_form.pdf", "unsupported_feature.pdf",
"zero_length_stream.pdf"
};
}
+601
View File
@@ -0,0 +1,601 @@
#include "document_test_helpers.hpp"
namespace pdfengine {
TEST(FontDiagnosticsTest, IntrospectionAccuracy) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("basic", "hello_world.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "hello_world.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto doc = *docRes;
auto pageRes = doc->getPage(0);
ASSERT_TRUE(pageRes.has_value());
auto page = *pageRes;
auto fontsRes = page->getFonts();
ASSERT_TRUE(fontsRes.has_value());
auto fonts = *fontsRes;
if (!fonts.empty()) {
auto firstFont = fonts[0];
EXPECT_FALSE(firstFont.fontName.empty());
EXPECT_FALSE(firstFont.type.empty());
EXPECT_FALSE(firstFont.normalizedFamily.empty());
EXPECT_FALSE(firstFont.internalFontId.empty());
}
auto docFontsRes = doc->getFonts();
ASSERT_TRUE(docFontsRes.has_value());
auto docFonts = *docFontsRes;
EXPECT_EQ(docFonts.size(), fonts.size());
}
TEST(FontDiagnosticsTest, SubsetAndVerticalTextIntrospection) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("fonts", "vertical_text.pdf");
if (!std::filesystem::exists(path)) {
path = getCorpusPath("fonts", "utf-8.pdf");
}
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "vertical_text.pdf or utf-8.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto doc = *docRes;
auto fontsRes = doc->getFonts();
ASSERT_TRUE(fontsRes.has_value());
for (const auto& f : *fontsRes) {
if (f.isSubset) {
EXPECT_FALSE(f.subsetTag.empty());
EXPECT_EQ(f.subsetTag.size(), 6);
}
if (f.isVertical) {
EXPECT_TRUE(f.isVertical);
EXPECT_NE(f.encoding.find("Identity-V"), std::string::npos);
}
}
}
TEST(FontDiagnosticsTest, CacheInvalidationAfterEdits) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("basic", "hello_world.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "hello_world.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto doc = *docRes;
auto fontsRes1 = doc->getFonts();
ASSERT_TRUE(fontsRes1.has_value());
std::string editsJson = R"({
"version": "1.0",
"operations": [
{
"id": "op_test_2",
"type": "text_overlay",
"pageIndex": 0,
"data": {
"text": "IntrospectionDiagnosticsNewText",
"x": 10.0,
"y": 20.0,
"width": 200.0,
"height": 20.0,
"fontSize": 12.0,
"fontFamily": "Helvetica",
"color": "#000000"
}
}
]
})";
auto editRes = doc->applyEdits(editsJson);
ASSERT_TRUE(editRes.has_value());
auto fontsRes2 = doc->getFonts();
ASSERT_TRUE(fontsRes2.has_value());
}
TEST(FontDiagnosticsTest, ConcurrencyThreadSafety) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("basic", "hello_world.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "hello_world.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto doc = *docRes;
std::vector<std::thread> threads;
for (int i = 0; i < 8; ++i) {
threads.emplace_back([&doc]() {
auto res = doc->getFonts();
ASSERT_TRUE(res.has_value());
});
}
for (auto& t : threads) {
t.join();
}
}
TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) {
SKIP_IF_NO_PDFIUM();
{
auto path = getCorpusPath("fonts", "utf-8.pdf");
if (std::filesystem::exists(path)) {
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto doc = *docRes;
auto fontsRes = doc->getFonts();
ASSERT_TRUE(fontsRes.has_value());
auto fonts = *fontsRes;
for (const auto& f : fonts) {
EXPECT_FALSE(f.fontName.empty());
EXPECT_FALSE(f.type.empty());
EXPECT_FALSE(f.normalizedFamily.empty());
EXPECT_FALSE(f.internalFontId.empty());
if (f.isSubset) {
EXPECT_EQ(f.subsetTag.size(), 6);
for (char c : f.subsetTag) {
EXPECT_TRUE(std::isupper(static_cast<unsigned char>(c)));
}
EXPECT_EQ(f.sourceType, "Embedded");
EXPECT_TRUE(f.isEmbedded);
EXPECT_EQ(f.internalFontId, f.fontName);
} else {
EXPECT_TRUE(f.subsetTag.empty());
EXPECT_EQ(f.internalFontId, f.fontName + "_" + f.type + "_" + std::to_string(f.flags));
}
if (f.sourceType == "SystemFallback") {
EXPECT_FALSE(f.isEmbedded);
EXPECT_TRUE(f.substitutedFrom.empty());
EXPECT_TRUE(f.substitutedTo.empty());
} else if (f.sourceType == "Substituted") {
EXPECT_FALSE(f.isEmbedded);
EXPECT_EQ(f.substitutedFrom, f.fontName);
#if defined(_WIN32)
EXPECT_EQ(f.substitutedTo, "Arial");
#else
EXPECT_EQ(f.substitutedTo, "Liberation Sans");
#endif
}
EXPECT_GT(f.ascent, 0.0);
EXPECT_LT(f.descent, 0.0);
EXPECT_GT(f.capHeight, 0.0);
}
}
}
{
auto path = getCorpusPath("fonts", "vertical_text.pdf");
if (std::filesystem::exists(path)) {
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto doc = *docRes;
auto fontsRes = doc->getFonts();
ASSERT_TRUE(fontsRes.has_value());
bool foundVertical = false;
for (const auto& f : *fontsRes) {
if (f.isVertical) {
foundVertical = true;
EXPECT_TRUE(f.encoding.find("-V") != std::string::npos || f.cmapName.find("-V") != std::string::npos);
}
}
EXPECT_TRUE(foundVertical);
}
}
{
auto path1 = getCorpusPath("fonts", "vertical_identity_v.pdf");
if (std::filesystem::exists(path1)) {
auto docRes = PdfDocument::loadFromFile(path1.string());
ASSERT_TRUE(docRes.has_value());
auto fontsRes = (*docRes)->getFonts();
ASSERT_TRUE(fontsRes.has_value());
bool foundVertical = false;
for (const auto& f : *fontsRes) {
if (f.isVertical) foundVertical = true;
}
EXPECT_TRUE(foundVertical) << "Failed to detect vertical font in vertical_identity_v.pdf";
}
}
{
auto path = getCorpusPath("fonts", "utf-8.pdf");
if (!std::filesystem::exists(path)) {
path = getCorpusPath("basic", "hello_world.pdf");
}
if (std::filesystem::exists(path)) {
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto doc = *docRes;
auto pageRes = doc->getPage(0);
ASSERT_TRUE(pageRes.has_value());
auto page = *pageRes;
auto boundsRes = page->extractTextWithBounds();
ASSERT_TRUE(boundsRes.has_value());
const auto& glyphs = *boundsRes;
ASSERT_FALSE(glyphs.empty());
std::vector<double> uniqueSizes;
for (const auto& glyph : glyphs) {
if (glyph.text != " " && glyph.text != "\r" && glyph.text != "\n" && glyph.text != "\t") {
EXPECT_GT(glyph.w, 0.0);
EXPECT_GT(glyph.h, 0.0);
}
EXPECT_GT(glyph.fontSize, 0.0);
EXPECT_LT(glyph.fontSize, 100.0);
if (std::find(uniqueSizes.begin(), uniqueSizes.end(), glyph.fontSize) == uniqueSizes.end()) {
uniqueSizes.push_back(glyph.fontSize);
}
}
if (path.filename().string() == "utf-8.pdf") {
EXPECT_GE(uniqueSizes.size(), 2u);
}
}
}
}
TEST(FontDiagnosticsTest, RealPDFiumEmbeddingAndTypeAccuracy) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("fonts", "text_font.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "text_font.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value()) << "Failed to open text_font.pdf";
auto doc = *docRes;
auto pageRes = doc->getPage(0);
ASSERT_TRUE(pageRes.has_value());
auto fontsRes = (*pageRes)->getFonts();
ASSERT_TRUE(fontsRes.has_value());
const auto& fonts = *fontsRes;
ASSERT_FALSE(fonts.empty()) << "text_font.pdf must expose at least one font";
for (const auto& f : fonts) {
EXPECT_FALSE(f.fontName.empty());
EXPECT_FALSE(f.type.empty());
static const std::vector<std::string> kValidTypes = {
"Type1", "TrueType", "CIDFontType0", "CIDFontType2"
};
bool typeValid = std::find(kValidTypes.begin(), kValidTypes.end(), f.type)
!= kValidTypes.end();
EXPECT_TRUE(typeValid) << "Unexpected type '" << f.type << "' for font '" << f.fontName << "'";
if (f.isSubset) {
EXPECT_TRUE(f.isEmbedded)
<< "Subset font '" << f.fontName
<< "' must be embedded (FPDFFont_GetIsEmbedded should return 1)";
EXPECT_EQ(f.sourceType, "Embedded")
<< "sourceType must be 'Embedded' when isEmbedded=true";
EXPECT_TRUE(f.substitutedFrom.empty());
EXPECT_TRUE(f.substitutedTo.empty());
}
if (f.isEmbedded) {
EXPECT_EQ(f.sourceType, "Embedded");
}
}
}
TEST(FontDiagnosticsTest, RealPDFiumMetricsAccuracy) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("fonts", "text_font.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "text_font.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto fontsRes = (*docRes)->getFonts();
ASSERT_TRUE(fontsRes.has_value());
for (const auto& f : *fontsRes) {
EXPECT_GT(f.ascent, 0.0)
<< "ascent must be positive for font '" << f.fontName << "'";
EXPECT_LT(f.descent, 0.0)
<< "descent must be negative for font '" << f.fontName << "'";
EXPECT_GT(f.capHeight, 0.0)
<< "capHeight must be positive for font '" << f.fontName << "'";
EXPECT_LE(f.capHeight, f.ascent + 1.0)
<< "capHeight should not exceed ascent for font '" << f.fontName << "'";
EXPECT_LT(f.ascent, 1500.0) << "Implausibly large ascent for '" << f.fontName << "'";
EXPECT_GT(f.descent, -1500.0) << "Implausibly deep descent for '" << f.fontName << "'";
}
}
TEST(FontDiagnosticsTest, ToUnicodePresenceAccuracy) {
SKIP_IF_NO_PDFIUM();
{
auto path = getCorpusPath("fonts", "with_tounicode.pdf");
if (std::filesystem::exists(path)) {
auto docRes = PdfDocument::loadFromFile(path.string());
if (docRes.has_value()) {
auto pageRes = (*docRes)->getPage(0);
if (pageRes.has_value()) {
auto fontsRes = (*pageRes)->getFonts();
if (fontsRes.has_value() && !fontsRes->empty()) {
bool anyTrue = false;
for (const auto& f : *fontsRes) {
if (f.hasToUnicode) { anyTrue = true; break; }
}
EXPECT_TRUE(anyTrue)
<< "At least one font in with_tounicode.pdf must have hasToUnicode=true";
}
}
}
}
}
{
auto path = getCorpusPath("fonts", "no_tounicode.pdf");
if (std::filesystem::exists(path)) {
auto docRes = PdfDocument::loadFromFile(path.string());
if (docRes.has_value()) {
auto pageRes = (*docRes)->getPage(0);
if (pageRes.has_value()) {
auto fontsRes = (*pageRes)->getFonts();
if (fontsRes.has_value() && !fontsRes->empty()) {
for (const auto& f : *fontsRes) {
EXPECT_FALSE(f.hasToUnicode)
<< "Font '" << f.fontName
<< "' in no_tounicode.pdf must have hasToUnicode=false";
}
}
}
}
}
}
{
auto path = getCorpusPath("fonts", "latin_extended.pdf");
if (std::filesystem::exists(path)) {
auto docRes = PdfDocument::loadFromFile(path.string());
if (docRes.has_value()) {
auto pageRes = (*docRes)->getPage(0);
if (pageRes.has_value()) {
auto fontsRes = (*pageRes)->getFonts();
if (fontsRes.has_value() && !fontsRes->empty()) {
bool anyTrue = false;
for (const auto& f : *fontsRes) {
if (f.hasToUnicode) { anyTrue = true; break; }
}
EXPECT_TRUE(anyTrue)
<< "At least one font in latin_extended.pdf must decode to Unicode";
}
}
}
}
}
}
TEST(GlyphCacheTest, ConcurrencyBench) {
using namespace pdfengine::fonts;
FontFace face;
bool loaded = face.loadFromFile("C:\\Windows\\Fonts\\arial.ttf");
if (!loaded) {
GTEST_SKIP() << "Skipping benchmark: Arial font not found.";
}
GlyphCache cache(1000);
auto run_benchmark = [&](int num_threads, int ops_per_thread) {
std::atomic<int> start_flag{0};
std::vector<std::thread> threads;
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back([&, i]() {
while (start_flag.load() == 0) { std::this_thread::yield(); }
for (int op = 0; op < ops_per_thread; ++op) {
unsigned int glyphIndex = (op + i) % 2000;
unsigned int fontSize = 12 + (op % 5);
auto hit = cache.get(face, glyphIndex, fontSize);
if (!hit) {
GlyphBitmap bmp;
bmp.width = 10; bmp.height = 10;
cache.insert(face, glyphIndex, fontSize, bmp);
}
}
});
}
auto start_time = std::chrono::high_resolution_clock::now();
start_flag.store(1);
for (auto& t : threads) { t.join(); }
auto end_time = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> diff = end_time - start_time;
return diff.count();
};
run_benchmark(2, 1000);
cache.clear();
double time_10 = run_benchmark(10, 10000);
std::cout << "[ BENCHMARK ] 10 Threads Time: " << time_10 << " seconds (" << (100000.0 / time_10) << " ops/sec)\n";
cache.clear();
double time_50 = run_benchmark(50, 10000);
std::cout << "[ BENCHMARK ] 50 Threads Time: " << time_50 << " seconds (" << (500000.0 / time_50) << " ops/sec)\n";
EXPECT_LE(cache.size(), 1000 + 16);
}
TEST(FontDiagnosticsTest, EmbeddedFontResolutionAndReloadingVerification) {
SKIP_IF_NO_PDFIUM();
std::vector<std::string> testFiles = {
"text_font.pdf",
"embedded_truetype.pdf",
"embedded_cid_font.pdf",
"subset_font.pdf",
"latin_extended.pdf"
};
bool foundAnyEmbedded = false;
for (const auto& fileName : testFiles) {
auto path = getCorpusPath("fonts", fileName);
if (!std::filesystem::exists(path)) {
continue;
}
std::cout << "\n========================================\n";
std::cout << "Testing PDF: " << fileName << "\n";
std::cout << "========================================\n";
auto docRes = PdfDocument::loadFromFile(path.string());
if (!docRes.has_value()) {
std::cout << "Failed to load document: " << fileName << std::endl;
continue;
}
auto doc = *docRes;
auto fontsRes = doc->getFonts();
if (!fontsRes.has_value()) {
std::cout << "Failed to get fonts for: " << fileName << std::endl;
continue;
}
const auto& fonts = *fontsRes;
for (const auto& fontInfo : fonts) {
std::cout << "Font: " << fontInfo.fontName
<< ", type: " << fontInfo.type
<< ", isEmbedded: " << (fontInfo.isEmbedded ? "yes" : "no")
<< ", flags: " << fontInfo.flags << std::endl;
if (fontInfo.isEmbedded) {
foundAnyEmbedded = true;
auto resolvedFontRes = doc->getResolvedFont(fontInfo);
if (!resolvedFontRes.has_value()) {
std::cout << " Failed to resolve font: " << resolvedFontRes.error() << std::endl;
continue;
}
auto resolvedFont = *resolvedFontRes;
std::cout << " Resolved font successfully." << std::endl;
auto face = static_cast<FT_Face>(resolvedFont->getFontFace().getFace());
if (face) {
std::cout << " FreeType Face Num Glyphs: " << face->num_glyphs << std::endl;
std::cout << " FreeType Charmaps Count: " << face->num_charmaps << std::endl;
for (int i = 0; i < face->num_charmaps; ++i) {
FT_CharMap cm = face->charmaps[i];
std::cout << " Charmap " << i << ": platform_id=" << cm->platform_id
<< ", encoding_id=" << cm->encoding_id << std::endl;
FT_Error err = FT_Set_Charmap(face, cm);
if (err) {
std::cout << " FT_Set_Charmap failed: " << err << std::endl;
continue;
}
FT_UInt gindex;
FT_ULong charcode = FT_Get_First_Char(face, &gindex);
std::cout << " Mapped characters under charmap " << i << ": ";
int count = 0;
while (gindex != 0 && count < 10) {
std::cout << charcode << "->" << gindex << " ";
charcode = FT_Get_Next_Char(face, charcode, &gindex);
count++;
}
std::cout << std::endl;
}
if (face->num_charmaps > 0) {
FT_Set_Charmap(face, face->charmaps[0]);
}
std::cout << " Glyph names: ";
for (int i = 0; i < face->num_glyphs; ++i) {
char nameBuf[64] = {0};
if (FT_Get_Glyph_Name(face, i, nameBuf, sizeof(nameBuf)) == 0) {
std::cout << i << ":" << nameBuf << " ";
} else {
std::cout << i << ":[unknown] ";
}
}
std::cout << std::endl;
} else {
std::cout << " No FreeType Face available." << std::endl;
}
EXPECT_TRUE(resolvedFont->isEmbedded());
std::vector<uint32_t> testChars = {32, 48, 65, 97};
for (uint32_t cp : testChars) {
bool hasG = resolvedFont->hasGlyph(cp);
double w = resolvedFont->getAdvanceWidth(cp, 12.0);
std::cout << " char(" << cp << "): hasGlyph=" << (hasG ? "yes" : "no")
<< ", advanceWidth=" << w << std::endl;
}
auto metrics = resolvedFont->getMetrics(12.0);
std::cout << " Metrics: ascent=" << metrics.ascent << ", descent=" << metrics.descent << ", capHeight=" << metrics.capHeight << std::endl;
EXPECT_NE(metrics.ascent, 0.0);
EXPECT_NE(metrics.descent, 0.0);
EXPECT_NE(metrics.capHeight, 0.0);
if (fileName == "text_font.pdf") {
EXPECT_TRUE(resolvedFont->hasGlyph(1));
double w = resolvedFont->getAdvanceWidth(1, 12.0);
EXPECT_GT(w, 0.0);
std::cout << " [VERIFIED] text_font.pdf char(1): hasGlyph=yes, advanceWidth=" << w << std::endl;
}
if (face->num_glyphs > 1) {
bool foundNonZeroWidth = false;
for (int gid = 1; gid < face->num_glyphs; ++gid) {
FT_Error err = FT_Load_Glyph(face, gid, FT_LOAD_DEFAULT);
if (err == 0) {
double directWidth = static_cast<double>(face->glyph->advance.x) / 64.0;
if (directWidth > 0.0) {
foundNonZeroWidth = true;
std::cout << " [VERIFIED] Direct glyph " << gid << " load: advanceWidth=" << directWidth << std::endl;
break;
}
}
}
EXPECT_TRUE(foundNonZeroWidth) << "Expected to find at least one glyph with a non-zero advance width";
}
}
}
}
EXPECT_TRUE(foundAnyEmbedded) << "Expected to find at least one embedded font in test files";
}
}
+351
View File
@@ -0,0 +1,351 @@
#include "fonts_test_helpers.hpp"
namespace pdfengine::fonts {
TEST(EncodingTest, PredefinedEncodingTest) {
using namespace pdfengine::fonts::pdf_fonts;
PredefinedEncoding winAnsi(SimpleEncodingType::WinAnsi);
EXPECT_EQ(winAnsi.getType(), SimpleEncodingType::WinAnsi);
EXPECT_EQ(winAnsi.decode(65), 65);
EXPECT_EQ(winAnsi.decode(128), 0x20AC);
EXPECT_EQ(winAnsi.decode(169), 169);
EXPECT_EQ(winAnsi.decode(300), 0);
PredefinedEncoding macRoman(SimpleEncodingType::MacRoman);
EXPECT_EQ(macRoman.getType(), SimpleEncodingType::MacRoman);
EXPECT_EQ(macRoman.decode(65), 65);
EXPECT_EQ(macRoman.decode(128), 0x00C4);
EXPECT_EQ(macRoman.decode(300), 0);
PredefinedEncoding identity(SimpleEncodingType::Identity);
EXPECT_EQ(identity.getType(), SimpleEncodingType::Identity);
EXPECT_EQ(identity.decode(65), 65);
EXPECT_EQ(identity.decode(128), 128);
EXPECT_EQ(identity.decode(1000), 1000);
}
TEST(EncodingTest, CustomEncodingWithDifferences) {
using namespace pdfengine::fonts::pdf_fonts;
auto baseEncoding = std::make_unique<PredefinedEncoding>(SimpleEncodingType::WinAnsi);
CustomEncoding custom(std::move(baseEncoding));
EXPECT_EQ(custom.decode(65), 65);
custom.addDifference(120, "quotesingle");
EXPECT_EQ(custom.decode(120), 0x0027);
custom.addDifference(121, "uni0041");
EXPECT_EQ(custom.decode(121), 0x0041);
custom.addDifference(122, "u0042");
EXPECT_EQ(custom.decode(122), 0x0042);
custom.addDifference(123, "nonexistentglyphname123");
EXPECT_EQ(custom.decode(123), 123);
}
TEST(EncodingTest, ToUnicodeCMapbfchar) {
using namespace pdfengine::fonts::pdf_fonts;
ToUnicodeCMap cmap;
std::string cmapStream =
"/CIDInit /ProcSet findresource begin\n"
"12 dict begin\n"
"begincmap\n"
"/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n"
"/CMapName /Custom-ToUnicode def\n"
"1 begincodespacerange\n"
"<0000> <FFFF>\n"
"endcodespacerange\n"
"2 beginbfchar\n"
"<0001> <0041>\n"
"<0002> <0042>\n"
"endbfchar\n"
"endcmap\n"
"CMapName currentdict /CMap defineresource pop\n"
"end\n"
"end\n";
ASSERT_TRUE(cmap.parseCMapStream(cmapStream));
EXPECT_EQ(cmap.decode(1), 0x0041);
EXPECT_EQ(cmap.decode(2), 0x0042);
EXPECT_EQ(cmap.decode(3), 0);
}
TEST(EncodingTest, ToUnicodeCMapbfrange) {
using namespace pdfengine::fonts::pdf_fonts;
ToUnicodeCMap cmap;
std::string cmapStream =
"begincmap\n"
"2 beginbfrange\n"
"<0001> <0005> <0041>\n"
"<0010> <0012> [<0061> <0062> <0063>]\n"
"endbfrange\n"
"endcmap\n";
ASSERT_TRUE(cmap.parseCMapStream(cmapStream));
EXPECT_EQ(cmap.decode(1), 0x0041);
EXPECT_EQ(cmap.decode(3), 0x0043);
EXPECT_EQ(cmap.decode(5), 0x0045);
EXPECT_EQ(cmap.decode(0x10), 0x0061);
EXPECT_EQ(cmap.decode(0x11), 0x0062);
EXPECT_EQ(cmap.decode(0x12), 0x0063);
}
TEST(EncodingTest, ToUnicodeMalformedCMap) {
using namespace pdfengine::fonts::pdf_fonts;
ToUnicodeCMap cmap;
std::string garbageStream = "This is a garbage string with no valid CMap elements";
EXPECT_FALSE(cmap.parseCMapStream(garbageStream));
std::string partialStream =
"begincmap\n"
"beginbfchar\n"
"<0001> <0041>\n"
"<0002> /invalid\n"
"<0003> <0043>\n"
"endbfchar\n"
"endcmap\n";
EXPECT_TRUE(cmap.parseCMapStream(partialStream));
EXPECT_EQ(cmap.decode(1), 0x0041);
EXPECT_EQ(cmap.decode(2), 0);
EXPECT_EQ(cmap.decode(3), 0x0043);
}
TEST(EncodingTest, FontLoaderWithEncoding) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run PDF font loader with encoding test.";
}
std::ifstream file(fontPath, std::ios::binary | std::ios::ate);
ASSERT_TRUE(file.is_open());
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> buffer(size);
ASSERT_TRUE(file.read(reinterpret_cast<char*>(buffer.data()), size));
auto encoding = std::make_unique<pdfengine::fonts::pdf_fonts::PredefinedEncoding>(
pdfengine::fonts::pdf_fonts::SimpleEncodingType::WinAnsi
);
auto pdfFont = pdfengine::fonts::pdf_fonts::FontLoader::loadTrueTypeFromMemory(
"Arial-With-Encoding", buffer, nullptr, std::move(encoding)
);
ASSERT_NE(pdfFont, nullptr);
EXPECT_EQ(pdfFont->getBaseFont(), "Arial-With-Encoding");
const auto* retrievedEncoding = pdfFont->getEncoding();
ASSERT_NE(retrievedEncoding, nullptr);
EXPECT_EQ(retrievedEncoding->decode(128), 0x20AC);
}
TEST(FontSubsetTest, SubsetTagParsingAndStripping) {
using namespace pdfengine::fonts::pdf_fonts;
std::string subsetName = "KTJHQO+Arial";
EXPECT_TRUE(FontSubset::hasSubsetPrefix(subsetName));
EXPECT_EQ(FontSubset::getSubsetPrefix(subsetName), "KTJHQO");
EXPECT_EQ(FontSubset::stripSubsetPrefix(subsetName), "Arial");
std::string normalName = "Arial";
EXPECT_FALSE(FontSubset::hasSubsetPrefix(normalName));
EXPECT_EQ(FontSubset::getSubsetPrefix(normalName), "");
EXPECT_EQ(FontSubset::stripSubsetPrefix(normalName), "Arial");
}
TEST(FontSubsetTest, PrefixFormatValidation) {
using namespace pdfengine::fonts::pdf_fonts;
EXPECT_TRUE(FontSubset::hasSubsetPrefix("ABCDEF+Helvetica"));
EXPECT_FALSE(FontSubset::hasSubsetPrefix("abcDEF+Helvetica"));
EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCdef+Helvetica"));
EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABC123+Helvetica"));
EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDE_+Helvetica"));
EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDE+Helvetica"));
EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDEFG+Helvetica"));
}
TEST(FontSubsetTest, GIDRemappingTranslations) {
using namespace pdfengine::fonts::pdf_fonts;
FontSubset subset("KTJHQO+Arial");
EXPECT_TRUE(subset.isSubset());
EXPECT_EQ(subset.getFullFontName(), "KTJHQO+Arial");
EXPECT_EQ(subset.getBaseFontName(), "Arial");
EXPECT_EQ(subset.getPrefix(), "KTJHQO");
EXPECT_EQ(subset.mapSubsetToOriginal(5), 5);
EXPECT_FALSE(subset.hasGlyphMapping(5));
subset.addGlyphMapping(1, 41);
subset.addGlyphMapping(2, 42);
subset.addGlyphMapping(3, 43);
EXPECT_EQ(subset.getMappingCount(), 3u);
EXPECT_TRUE(subset.hasGlyphMapping(1));
EXPECT_TRUE(subset.hasGlyphMapping(2));
EXPECT_EQ(subset.mapSubsetToOriginal(1), 41);
EXPECT_EQ(subset.mapSubsetToOriginal(2), 42);
EXPECT_EQ(subset.mapSubsetToOriginal(3), 43);
EXPECT_EQ(subset.mapSubsetToOriginal(4), 4);
}
TEST(FontSubsetTest, CoreFontSubsettingIntegration) {
using namespace pdfengine::fonts::pdf_fonts;
TrueTypeFont fontTT("KTJHQO+Arial", false);
const auto* ttSubset = fontTT.getSubsetInfo();
ASSERT_NE(ttSubset, nullptr);
EXPECT_TRUE(ttSubset->isSubset());
EXPECT_EQ(ttSubset->getBaseFontName(), "Arial");
EXPECT_EQ(ttSubset->getPrefix(), "KTJHQO");
Type1Font fontT1("SUBSET+Courier", false);
const auto* t1Subset = fontT1.getSubsetInfo();
ASSERT_NE(t1Subset, nullptr);
EXPECT_TRUE(t1Subset->isSubset());
EXPECT_EQ(t1Subset->getBaseFontName(), "Courier");
EXPECT_EQ(t1Subset->getPrefix(), "SUBSET");
CIDFont fontCID("CJKTAG+SimSun", FontType::CIDFontType2, false);
const auto* cidSubset = fontCID.getSubsetInfo();
ASSERT_NE(cidSubset, nullptr);
EXPECT_TRUE(cidSubset->isSubset());
EXPECT_EQ(cidSubset->getBaseFontName(), "SimSun");
EXPECT_EQ(cidSubset->getPrefix(), "CJKTAG");
}
TEST(TextExtractionLayerTest, SimpleCharacterDecoding) {
using namespace pdfengine::fonts::pdf_fonts;
auto font = FontLoader::loadType1SystemFallback("Helvetica");
ASSERT_NE(font, nullptr);
EXPECT_EQ(font->decodeToUnicode(65), 65u);
EXPECT_EQ(font->decodeToUnicode(97), 97u);
EXPECT_EQ(font->decodeToUnicode(48), 48u);
}
TEST(TextExtractionLayerTest, EncodingAndToUnicodeDecoding) {
using namespace pdfengine::fonts::pdf_fonts;
auto baseEncoding = std::make_unique<PredefinedEncoding>(SimpleEncodingType::WinAnsi);
auto customEnc = std::make_unique<CustomEncoding>(std::move(baseEncoding));
customEnc->addDifference(128, "euro");
auto font = FontLoader::loadType1SystemFallback("Helvetica", nullptr, std::move(customEnc));
ASSERT_NE(font, nullptr);
EXPECT_EQ(font->decodeToUnicode(128), 0x20ACu);
auto cmap = std::make_unique<ToUnicodeCMap>();
cmap->addMapping(1, 0x0041);
cmap->addMapping(2, 0x0042);
cmap->addMapping(3, 0x20AC);
auto fontCMap = FontLoader::loadType1SystemFallback("Helvetica", nullptr, std::move(cmap));
ASSERT_NE(fontCMap, nullptr);
EXPECT_EQ(fontCMap->decodeToUnicode(1), 0x0041u);
EXPECT_EQ(fontCMap->decodeToUnicode(2), 0x0042u);
EXPECT_EQ(fontCMap->decodeToUnicode(3), 0x20ACu);
}
TEST(TextExtractionLayerTest, SubsetFontTextExtraction) {
using namespace pdfengine::fonts::pdf_fonts;
auto font = FontLoader::loadType1SystemFallback("KTJHQO+Helvetica");
ASSERT_NE(font, nullptr);
const auto* subsetConst = font->getSubsetInfo();
ASSERT_NE(subsetConst, nullptr);
auto* subset = const_cast<FontSubset*>(subsetConst);
FT_Face face = font->getFontFace().getFace();
ASSERT_NE(face, nullptr);
FT_UInt originalGid = FT_Get_Char_Index(face, 'A');
ASSERT_GT(originalGid, 0u);
subset->addGlyphMapping(5, originalGid);
EXPECT_EQ(font->decodeToUnicode(5), 65u);
}
TEST(TextExtractionLayerTest, CIDFontTextExtraction) {
using namespace pdfengine::fonts::pdf_fonts;
auto font = FontLoader::loadCIDFontSystemFallback("SimSun", FontType::CIDFontType2);
ASSERT_NE(font, nullptr);
CIDFont* cidFont = dynamic_cast<CIDFont*>(font.get());
ASSERT_NE(cidFont, nullptr);
FT_Face face = cidFont->getFontFace().getFace();
ASSERT_NE(face, nullptr);
FT_UInt gid65 = 65;
FT_ULong expectedChar65 = 0;
FT_UInt gindex;
FT_ULong charcode = FT_Get_First_Char(face, &gindex);
while (gindex != 0) {
if (gindex == gid65) {
expectedChar65 = charcode;
break;
}
charcode = FT_Get_Next_Char(face, charcode, &gindex);
}
if (expectedChar65 != 0) {
EXPECT_EQ(cidFont->decodeToUnicode(65), expectedChar65);
} else {
EXPECT_EQ(cidFont->decodeToUnicode(65), 65u);
}
FT_UInt gidA = FT_Get_Char_Index(face, 'A');
if (gidA > 0) {
std::unordered_map<uint32_t, uint32_t> cidToGid = {
{500, gidA}
};
cidFont->setCIDToGIDMap(cidToGid);
EXPECT_EQ(cidFont->decodeToUnicode(500), 65u);
}
}
TEST(TextExtractionLayerTest, StringUtf8Conversion) {
using namespace pdfengine::fonts::pdf_fonts;
auto font = FontLoader::loadType1SystemFallback("Helvetica");
ASSERT_NE(font, nullptr);
std::vector<uint32_t> codes = {65, 66, 67};
EXPECT_EQ(font->decodeStringToUnicode(codes), "ABC");
auto cmap = std::make_unique<ToUnicodeCMap>();
cmap->addMapping(10, 0x65E5);
cmap->addMapping(11, 0x672C);
cmap->addMapping(12, 0x8A9E);
auto fontCMap = FontLoader::loadType1SystemFallback("Helvetica", nullptr, std::move(cmap));
ASSERT_NE(fontCMap, nullptr);
std::vector<uint32_t> cjkCodes = {10, 11, 12};
std::string decodedCjk = fontCMap->decodeStringToUnicode(cjkCodes);
EXPECT_EQ(decodedCjk, "日本語");
}
}
@@ -0,0 +1,173 @@
#include "fonts_test_helpers.hpp"
namespace pdfengine::fonts {
TEST(FontLoaderTest, FontFaceLoadFromMemorySuccess) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run load from memory success test.";
}
std::ifstream file(fontPath, std::ios::binary | std::ios::ate);
ASSERT_TRUE(file.is_open());
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> buffer(size);
ASSERT_TRUE(file.read(reinterpret_cast<char*>(buffer.data()), size));
FontFace face;
ASSERT_TRUE(face.loadFromMemory(buffer));
ASSERT_NE(face.getFace(), nullptr);
unsigned int glyphIndex = FT_Get_Char_Index(face.getFace(), 'M');
ASSERT_GT(glyphIndex, 0u);
auto glyph = face.renderGlyph(glyphIndex, 16);
ASSERT_TRUE(glyph.has_value());
EXPECT_GT(glyph->width, 0);
EXPECT_GT(glyph->height, 0);
EXPECT_GT(glyph->advance, 0.0);
}
TEST(FontLoaderTest, FontFaceLoadFromMemoryInvalid) {
FontFace face;
std::vector<uint8_t> emptyData;
EXPECT_FALSE(face.loadFromMemory(emptyData));
EXPECT_EQ(face.getFace(), nullptr);
std::vector<uint8_t> corruptData = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
EXPECT_FALSE(face.loadFromMemory(corruptData));
EXPECT_EQ(face.getFace(), nullptr);
}
TEST(FontLoaderTest, FontLoaderTrueTypeSuccess) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run PDF font loader test.";
}
std::ifstream file(fontPath, std::ios::binary | std::ios::ate);
ASSERT_TRUE(file.is_open());
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> buffer(size);
ASSERT_TRUE(file.read(reinterpret_cast<char*>(buffer.data()), size));
auto pdfFont = pdfengine::fonts::pdf_fonts::FontLoader::loadTrueTypeFromMemory("Arial", buffer);
ASSERT_NE(pdfFont, nullptr);
EXPECT_EQ(pdfFont->getBaseFont(), "Arial");
EXPECT_EQ(pdfFont->getType(), pdfengine::fonts::pdf_fonts::FontType::TrueType);
EXPECT_TRUE(pdfFont->isEmbedded());
HbShaper shaper;
auto glyphs = shaper.shapeRun("Test Memory Load", pdfFont->getFontFace(), 16);
EXPECT_FALSE(glyphs.empty());
}
TEST(FontDescriptorTest, DescriptorDefaultValues) {
pdfengine::fonts::pdf_fonts::FontDescriptor desc;
EXPECT_EQ(desc.getFontName(), "");
EXPECT_EQ(desc.getFlags(), 0);
EXPECT_EQ(desc.getItalicAngle(), 0.0);
EXPECT_EQ(desc.getAscent(), 0.0);
EXPECT_EQ(desc.getDescent(), 0.0);
EXPECT_EQ(desc.getCapHeight(), 0.0);
EXPECT_EQ(desc.getStemV(), 0.0);
pdfengine::fonts::pdf_fonts::FontBBox bbox = desc.getFontBBox();
EXPECT_EQ(bbox.llx, 0);
EXPECT_EQ(bbox.lly, 0);
EXPECT_EQ(bbox.urx, 0);
EXPECT_EQ(bbox.ury, 0);
EXPECT_FALSE(desc.isFixedPitch());
EXPECT_FALSE(desc.isSerif());
EXPECT_FALSE(desc.isSymbolic());
EXPECT_FALSE(desc.isItalic());
}
TEST(FontDescriptorTest, DescriptorParsingSuccess) {
std::string dict =
"<< /Type /FontDescriptor\n"
" /FontName /ArialMT\n"
" /Flags 32\n"
" /FontBBox [-166 -225 1000 931]\n"
" /ItalicAngle 0\n"
" /Ascent 905\n"
" /Descent -211\n"
" /CapHeight 728\n"
" /StemV 94\n"
">>";
pdfengine::fonts::pdf_fonts::FontDescriptor desc;
ASSERT_TRUE(desc.parseFromDictionaryString(dict));
EXPECT_EQ(desc.getFontName(), "ArialMT");
EXPECT_EQ(desc.getFlags(), 32);
pdfengine::fonts::pdf_fonts::FontBBox bbox = desc.getFontBBox();
EXPECT_EQ(bbox.llx, -166);
EXPECT_EQ(bbox.lly, -225);
EXPECT_EQ(bbox.urx, 1000);
EXPECT_EQ(bbox.ury, 931);
EXPECT_DOUBLE_EQ(desc.getItalicAngle(), 0.0);
EXPECT_DOUBLE_EQ(desc.getAscent(), 905.0);
EXPECT_DOUBLE_EQ(desc.getDescent(), -211.0);
EXPECT_DOUBLE_EQ(desc.getCapHeight(), 728.0);
EXPECT_DOUBLE_EQ(desc.getStemV(), 94.0);
EXPECT_FALSE(desc.isFixedPitch());
EXPECT_TRUE(desc.isNonsymbolic());
EXPECT_FALSE(desc.isItalic());
}
TEST(FontDescriptorTest, DescriptorParsingMalformed) {
pdfengine::fonts::pdf_fonts::FontDescriptor desc;
EXPECT_FALSE(desc.parseFromDictionaryString("/Flags 32 >>"));
EXPECT_FALSE(desc.parseFromDictionaryString("<< /Flags 32"));
EXPECT_FALSE(desc.parseFromDictionaryString("<< /FontBBox [-166 -225] >>"));
EXPECT_FALSE(desc.parseFromDictionaryString("<< /Ascent abc >>"));
EXPECT_FALSE(desc.parseFromDictionaryString("<< /Ascent >>"));
}
TEST(FontDescriptorTest, FontLoaderWithDescriptor) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run PDF font loader test.";
}
std::ifstream file(fontPath, std::ios::binary | std::ios::ate);
ASSERT_TRUE(file.is_open());
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> buffer(size);
ASSERT_TRUE(file.read(reinterpret_cast<char*>(buffer.data()), size));
auto descriptor = std::make_unique<pdfengine::fonts::pdf_fonts::FontDescriptor>();
descriptor->setFontName("Arial-BoldMT");
descriptor->setFlags(96);
descriptor->setAscent(905.0);
descriptor->setDescent(-211.0);
auto pdfFont = pdfengine::fonts::pdf_fonts::FontLoader::loadTrueTypeFromMemory("Arial-Bold", buffer, std::move(descriptor));
ASSERT_NE(pdfFont, nullptr);
EXPECT_EQ(pdfFont->getBaseFont(), "Arial-Bold");
EXPECT_TRUE(pdfFont->isEmbedded());
const auto* retrievedDesc = pdfFont->getDescriptor();
ASSERT_NE(retrievedDesc, nullptr);
EXPECT_EQ(retrievedDesc->getFontName(), "Arial-BoldMT");
EXPECT_EQ(retrievedDesc->getFlags(), 96);
EXPECT_TRUE(retrievedDesc->isItalic());
EXPECT_TRUE(retrievedDesc->isNonsymbolic());
EXPECT_DOUBLE_EQ(retrievedDesc->getAscent(), 905.0);
EXPECT_DOUBLE_EQ(retrievedDesc->getDescent(), -211.0);
}
}
+454
View File
@@ -0,0 +1,454 @@
#include "fonts_test_helpers.hpp"
namespace pdfengine::fonts {
TEST(FontTest, FontFaceInitialization) {
FontFace face;
EXPECT_EQ(face.getFace(), nullptr);
}
TEST(FontTest, FontFaceLoadNonExistentFile) {
FontFace face;
EXPECT_FALSE(face.loadFromFile("this_file_does_not_exist_12345.ttf"));
EXPECT_EQ(face.getFace(), nullptr);
}
TEST(FontTest, FontFaceMoveSemantics) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run move semantics test.";
}
FontFace face1;
ASSERT_TRUE(face1.loadFromFile(fontPath));
FT_Face rawFace = face1.getFace();
ASSERT_NE(rawFace, nullptr);
FontFace face2(std::move(face1));
EXPECT_EQ(face1.getFace(), nullptr);
EXPECT_EQ(face2.getFace(), rawFace);
FontFace face3;
face3 = std::move(face2);
EXPECT_EQ(face2.getFace(), nullptr);
EXPECT_EQ(face3.getFace(), rawFace);
}
TEST(FontTest, HbShaperEmptyInput) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run empty input shaper test.";
}
FontFace face;
ASSERT_TRUE(face.loadFromFile(fontPath));
HbShaper shaper;
auto glyphs = shaper.shapeRun("", face, 16);
EXPECT_TRUE(glyphs.empty());
}
TEST(FontTest, HbShaperNullFace) {
FontFace face;
HbShaper shaper;
auto glyphs = shaper.shapeRun("Hello", face, 16);
EXPECT_TRUE(glyphs.empty());
}
TEST(FontTest, HbShaperShapeTextSuccess) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
std::cout << "[ WARNING ] Skipping shape success test: no system font found." << std::endl;
GTEST_SKIP() << "No system font found to run text shaping test.";
}
FontFace face;
ASSERT_TRUE(face.loadFromFile(fontPath));
ASSERT_NE(face.getFace(), nullptr);
HbShaper shaper;
std::string testText = "Hello World!";
auto glyphs = shaper.shapeRun(testText, face, 16);
EXPECT_FALSE(glyphs.empty());
for (const auto& g : glyphs) {
EXPECT_GT(g.advanceX, 0.0);
}
}
TEST(FontTest, FontFaceRenderGlyphNullFace) {
FontFace face;
auto glyph = face.renderGlyph(0, 16);
EXPECT_FALSE(glyph.has_value());
}
TEST(FontTest, FontFaceRenderGlyphSuccess) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run render glyph success test.";
}
FontFace face;
ASSERT_TRUE(face.loadFromFile(fontPath));
ASSERT_NE(face.getFace(), nullptr);
unsigned int glyphIndex = FT_Get_Char_Index(face.getFace(), 'A');
ASSERT_GT(glyphIndex, 0u);
auto glyphOpt = face.renderGlyph(glyphIndex, 24);
ASSERT_TRUE(glyphOpt.has_value());
const auto& glyph = *glyphOpt;
EXPECT_GT(glyph.width, 0);
EXPECT_GT(glyph.height, 0);
EXPECT_EQ(glyph.pixels.size(), static_cast<size_t>(glyph.width * glyph.height));
EXPECT_GT(glyph.advance, 0.0);
}
TEST(FontTest, GlyphCacheBasicGetInsert) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run cache test.";
}
FontFace face;
ASSERT_TRUE(face.loadFromFile(fontPath));
GlyphCache cache(10);
EXPECT_EQ(cache.capacity(), 10u);
EXPECT_EQ(cache.size(), 0u);
auto miss = cache.get(face, 12, 16);
EXPECT_FALSE(miss.has_value());
GlyphBitmap bitmap;
bitmap.width = 10;
bitmap.height = 12;
bitmap.pixels = std::vector<unsigned char>(120, 255);
bitmap.advance = 8.5;
cache.insert(face, 12, 16, bitmap);
EXPECT_EQ(cache.size(), 1u);
auto hit = cache.get(face, 12, 16);
ASSERT_TRUE(hit.has_value());
EXPECT_EQ(hit->width, 10);
EXPECT_EQ(hit->height, 12);
EXPECT_EQ(hit->advance, 8.5);
EXPECT_EQ(hit->pixels.size(), 120u);
}
TEST(FontTest, GlyphCacheEvictionPolicy) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run cache eviction test.";
}
FontFace face;
ASSERT_TRUE(face.loadFromFile(fontPath));
GlyphCache cache(2);
GlyphBitmap bmp1{ .width = 1 };
GlyphBitmap bmp2{ .width = 2 };
GlyphBitmap bmp3{ .width = 3 };
cache.insert(face, 1, 16, bmp1);
cache.insert(face, 2, 16, bmp2);
EXPECT_EQ(cache.size(), 2u);
cache.insert(face, 3, 16, bmp3);
EXPECT_EQ(cache.size(), 2u);
EXPECT_FALSE(cache.get(face, 1, 16).has_value());
EXPECT_TRUE(cache.get(face, 2, 16).has_value());
EXPECT_TRUE(cache.get(face, 3, 16).has_value());
}
TEST(FontTest, GlyphCacheLRUPolicy) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run cache LRU test.";
}
FontFace face;
ASSERT_TRUE(face.loadFromFile(fontPath));
GlyphCache cache(2);
GlyphBitmap bmp1{ .width = 1 };
GlyphBitmap bmp2{ .width = 2 };
GlyphBitmap bmp3{ .width = 3 };
cache.insert(face, 1, 16, bmp1);
cache.insert(face, 2, 16, bmp2);
auto hit = cache.get(face, 1, 16);
ASSERT_TRUE(hit.has_value());
cache.insert(face, 3, 16, bmp3);
EXPECT_EQ(cache.size(), 2u);
EXPECT_TRUE(cache.get(face, 1, 16).has_value());
EXPECT_FALSE(cache.get(face, 2, 16).has_value());
EXPECT_TRUE(cache.get(face, 3, 16).has_value());
}
TEST(FontTest, FontPipelineIntegration) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run pipeline integration test.";
}
FontFace face;
ASSERT_TRUE(face.loadFromFile(fontPath));
HbShaper shaper;
GlyphCache cache(100);
std::vector<std::string> testTexts = {"Hello World", "office", "سلام"};
unsigned int fontSize = 16;
for (const auto& text : testTexts) {
auto shapedGlyphs = shaper.shapeRun(text, face, fontSize);
EXPECT_FALSE(shapedGlyphs.empty());
for (const auto& sg : shapedGlyphs) {
auto cachedBmp = cache.get(face, sg.glyphIndex, fontSize);
if (!cachedBmp.has_value()) {
auto renderedOpt = face.renderGlyph(sg.glyphIndex, fontSize);
ASSERT_TRUE(renderedOpt.has_value());
cache.insert(face, sg.glyphIndex, fontSize, *renderedOpt);
EXPECT_EQ(renderedOpt->pixels.size(), static_cast<size_t>(renderedOpt->width * renderedOpt->height));
}
auto hitBmp = cache.get(face, sg.glyphIndex, fontSize);
ASSERT_TRUE(hitBmp.has_value());
EXPECT_EQ(hitBmp->pixels.size(), static_cast<size_t>(hitBmp->width * hitBmp->height));
EXPECT_GE(hitBmp->advance, 0.0);
}
}
}
TEST(FontTest, UnicodeAndRtlShaping) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run Unicode and RTL shaping test.";
}
FontFace face;
ASSERT_TRUE(face.loadFromFile(fontPath));
HbShaper shaper;
unsigned int fontSize = 16;
{
std::string arabicText = "سلام";
auto glyphs = shaper.shapeRun(arabicText, face, fontSize);
EXPECT_FALSE(glyphs.empty());
for (const auto& g : glyphs) {
EXPECT_TRUE(g.advanceX != 0.0 || g.advanceY != 0.0 || g.offsetX != 0.0 || g.offsetY != 0.0 || g.glyphIndex != 999999u);
}
}
{
std::string hindiText = "नमस्ते";
auto glyphs = shaper.shapeRun(hindiText, face, fontSize);
EXPECT_FALSE(glyphs.empty());
for (const auto& g : glyphs) {
EXPECT_TRUE(g.advanceX != 0.0 || g.advanceY != 0.0 || g.glyphIndex != 999999u);
}
}
{
std::string ligatureText = "office";
auto glyphs = shaper.shapeRun(ligatureText, face, fontSize);
EXPECT_FALSE(glyphs.empty());
EXPECT_LE(glyphs.size(), ligatureText.length());
for (const auto& g : glyphs) {
EXPECT_TRUE(g.advanceX != 0.0 || g.advanceY != 0.0 || g.glyphIndex != 999999u);
}
}
}
TEST(FontTest, CachePerformanceTest) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run cache performance test.";
}
FontFace face;
ASSERT_TRUE(face.loadFromFile(fontPath));
HbShaper shaper;
GlyphCache cache(100);
std::string text = "Hello Hello Hello Hello";
unsigned int fontSize = 16;
{
auto glyphs = shaper.shapeRun(text, face, fontSize);
for (const auto& g : glyphs) {
auto cached = cache.get(face, g.glyphIndex, fontSize);
if (!cached.has_value()) {
auto rendered = face.renderGlyph(g.glyphIndex, fontSize);
if (rendered.has_value()) {
cache.insert(face, g.glyphIndex, fontSize, *rendered);
}
}
}
}
cache.resetStats();
for (int i = 0; i < 1000; ++i) {
auto glyphs = shaper.shapeRun(text, face, fontSize);
for (const auto& g : glyphs) {
auto cached = cache.get(face, g.glyphIndex, fontSize);
if (!cached.has_value()) {
auto rendered = face.renderGlyph(g.glyphIndex, fontSize);
if (rendered.has_value()) {
cache.insert(face, g.glyphIndex, fontSize, *rendered);
}
}
}
}
double hitRateVal = cache.hitRate();
std::cout << "[ INFO ] Cache Hit Rate for repetitive text: " << (hitRateVal * 100.0) << "%" << std::endl;
EXPECT_GT(hitRateVal, 0.90);
}
TEST(FontTest, EngineStressTest) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run engine stress test.";
}
FontFace face;
ASSERT_TRUE(face.loadFromFile(fontPath));
HbShaper shaper;
GlyphCache cache(32);
std::string base = "The quick brown fox jumps over the lazy dog. 1234567890!@#$%^&*() ";
std::string longText;
longText.reserve(10000);
while (longText.length() < 10000) {
longText += base;
}
unsigned int fontSize = 16;
auto glyphs = shaper.shapeRun(longText, face, fontSize);
EXPECT_FALSE(glyphs.empty());
for (const auto& g : glyphs) {
auto cached = cache.get(face, g.glyphIndex, fontSize);
if (!cached.has_value()) {
auto rendered = face.renderGlyph(g.glyphIndex, fontSize);
if (rendered.has_value()) {
cache.insert(face, g.glyphIndex, fontSize, *rendered);
}
}
}
EXPECT_LE(cache.size(), cache.capacity());
EXPECT_GT(cache.size(), 0u);
}
TEST(FontTest, VisualBitmapDebugging) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run visual bitmap debugging test.";
}
FontFace face;
ASSERT_TRUE(face.loadFromFile(fontPath));
unsigned int glyphIndex = FT_Get_Char_Index(face.getFace(), 'A');
ASSERT_GT(glyphIndex, 0u);
auto renderedOpt = face.renderGlyph(glyphIndex, 48);
ASSERT_TRUE(renderedOpt.has_value());
std::string filename = "A.pgm";
std::filesystem::remove(filename);
ASSERT_TRUE(saveGlyphAsPGM(*renderedOpt, filename));
EXPECT_TRUE(std::filesystem::exists(filename));
EXPECT_GT(std::filesystem::file_size(filename), 0u);
}
TEST(FontTest, MetricsValidation) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run metrics validation test.";
}
FontFace face;
ASSERT_TRUE(face.loadFromFile(fontPath));
FT_Face ftFace = face.getFace();
ASSERT_NE(ftFace, nullptr);
unsigned int glyphIndex = FT_Get_Char_Index(ftFace, 'B');
ASSERT_GT(glyphIndex, 0u);
unsigned int fontSize = 24;
auto renderedOpt = face.renderGlyph(glyphIndex, fontSize);
ASSERT_TRUE(renderedOpt.has_value());
ASSERT_EQ(FT_Set_Pixel_Sizes(ftFace, 0, fontSize), 0);
ASSERT_EQ(FT_Load_Glyph(ftFace, glyphIndex, FT_LOAD_RENDER), 0);
FT_GlyphSlot slot = ftFace->glyph;
EXPECT_EQ(renderedOpt->width, static_cast<int>(slot->bitmap.width));
EXPECT_EQ(renderedOpt->height, static_cast<int>(slot->bitmap.rows));
EXPECT_EQ(renderedOpt->bearingX, slot->bitmap_left);
EXPECT_EQ(renderedOpt->bearingY, slot->bitmap_top);
EXPECT_DOUBLE_EQ(renderedOpt->advance, static_cast<double>(slot->advance.x) / 64.0);
}
TEST(FontTest, CacheRecencyStress) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run cache recency stress test.";
}
FontFace face;
ASSERT_TRUE(face.loadFromFile(fontPath));
GlyphCache cache(5);
std::vector<GlyphBitmap> bmps;
for (int i = 0; i < 10; ++i) {
GlyphBitmap b;
b.width = i;
bmps.push_back(b);
}
for (unsigned int i = 0; i < 5; ++i) {
cache.insert(face, i, 16, bmps[i]);
}
EXPECT_EQ(cache.size(), 5u);
ASSERT_TRUE(cache.get(face, 0, 16).has_value());
ASSERT_TRUE(cache.get(face, 2, 16).has_value());
cache.insert(face, 5, 16, bmps[5]);
EXPECT_FALSE(cache.get(face, 1, 16).has_value());
EXPECT_TRUE(cache.get(face, 5, 16).has_value());
ASSERT_TRUE(cache.get(face, 3, 16).has_value());
cache.insert(face, 6, 16, bmps[6]);
EXPECT_FALSE(cache.get(face, 4, 16).has_value());
EXPECT_TRUE(cache.get(face, 0, 16).has_value());
EXPECT_TRUE(cache.get(face, 2, 16).has_value());
EXPECT_TRUE(cache.get(face, 3, 16).has_value());
EXPECT_TRUE(cache.get(face, 5, 16).has_value());
EXPECT_TRUE(cache.get(face, 6, 16).has_value());
}
}
+277
View File
@@ -0,0 +1,277 @@
#include "fonts_test_helpers.hpp"
namespace pdfengine::fonts {
TEST(Type1FontTest, EmbeddedType1FontLoading) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run embedded Type1 test.";
}
std::ifstream file(fontPath, std::ios::binary | std::ios::ate);
ASSERT_TRUE(file.is_open());
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> buffer(size);
ASSERT_TRUE(file.read(reinterpret_cast<char*>(buffer.data()), size));
auto font = pdfengine::fonts::pdf_fonts::FontLoader::loadType1FromMemory("LegacyType1", buffer);
ASSERT_NE(font, nullptr);
EXPECT_EQ(font->getBaseFont(), "LegacyType1");
EXPECT_EQ(font->getType(), pdfengine::fonts::pdf_fonts::FontType::Type1);
EXPECT_TRUE(font->isEmbedded());
}
TEST(Type1FontTest, NonEmbeddedType1SystemFallback) {
auto fontHelv = pdfengine::fonts::pdf_fonts::FontLoader::loadType1SystemFallback("Helvetica");
ASSERT_NE(fontHelv, nullptr);
EXPECT_EQ(fontHelv->getBaseFont(), "Helvetica");
EXPECT_EQ(fontHelv->getType(), pdfengine::fonts::pdf_fonts::FontType::Type1);
EXPECT_FALSE(fontHelv->isEmbedded());
auto fontTimes = pdfengine::fonts::pdf_fonts::FontLoader::loadType1SystemFallback("Times-BoldItalic");
ASSERT_NE(fontTimes, nullptr);
EXPECT_EQ(fontTimes->getBaseFont(), "Times-BoldItalic");
EXPECT_FALSE(fontTimes->isEmbedded());
auto fontCourier = pdfengine::fonts::pdf_fonts::FontLoader::loadType1SystemFallback("Courier-Oblique");
ASSERT_NE(fontCourier, nullptr);
EXPECT_EQ(fontCourier->getBaseFont(), "Courier-Oblique");
EXPECT_FALSE(fontCourier->isEmbedded());
auto fontUnknown = pdfengine::fonts::pdf_fonts::FontLoader::loadType1SystemFallback("UnknownLegacyFont");
ASSERT_NE(fontUnknown, nullptr);
EXPECT_EQ(fontUnknown->getBaseFont(), "UnknownLegacyFont");
EXPECT_FALSE(fontUnknown->isEmbedded());
}
TEST(CIDFontTest, CompositeFontInitializationAndTypes) {
using namespace pdfengine::fonts::pdf_fonts;
auto desc = std::make_unique<FontDescriptor>();
desc->setFontName("SimSun-Descriptor");
CIDFont font0("SimSun", FontType::CIDFontType0, false, std::move(desc));
EXPECT_EQ(font0.getBaseFont(), "SimSun");
EXPECT_EQ(font0.getType(), FontType::CIDFontType0);
EXPECT_FALSE(font0.isEmbedded());
EXPECT_EQ(font0.getDescriptor()->getFontName(), "SimSun-Descriptor");
CIDFont font2("MS-Gothic", FontType::CIDFontType2, true);
EXPECT_EQ(font2.getBaseFont(), "MS-Gothic");
EXPECT_EQ(font2.getType(), FontType::CIDFontType2);
EXPECT_TRUE(font2.isEmbedded());
}
TEST(CIDFontTest, CIDToGIDTranslations) {
using namespace pdfengine::fonts::pdf_fonts;
CIDFont font("SimSun", FontType::CIDFontType2, false);
EXPECT_TRUE(font.isIdentityMap());
EXPECT_EQ(font.mapCIDToGID(100), 100);
EXPECT_EQ(font.mapCIDToGID(5000), 5000);
std::unordered_map<uint32_t, uint32_t> customMap = {
{10, 100},
{20, 200},
{30, 300}
};
font.setCIDToGIDMap(customMap);
EXPECT_FALSE(font.isIdentityMap());
EXPECT_EQ(font.mapCIDToGID(10), 100);
EXPECT_EQ(font.mapCIDToGID(20), 200);
EXPECT_EQ(font.mapCIDToGID(30), 300);
EXPECT_EQ(font.mapCIDToGID(40), 0);
font.setIdentityCIDToGIDMap();
EXPECT_TRUE(font.isIdentityMap());
EXPECT_EQ(font.mapCIDToGID(10), 10);
}
TEST(CIDFontTest, NonEmbeddedCIDFontSystemFallback) {
using namespace pdfengine::fonts::pdf_fonts;
auto fontSimSun = FontLoader::loadCIDFontSystemFallback("SimSun", FontType::CIDFontType2);
ASSERT_NE(fontSimSun, nullptr);
EXPECT_EQ(fontSimSun->getBaseFont(), "SimSun");
EXPECT_EQ(fontSimSun->getType(), FontType::CIDFontType2);
EXPECT_FALSE(fontSimSun->isEmbedded());
auto fontGothic = FontLoader::loadCIDFontSystemFallback("HeiseiMin-W3", FontType::CIDFontType0);
ASSERT_NE(fontGothic, nullptr);
EXPECT_EQ(fontGothic->getBaseFont(), "HeiseiMin-W3");
EXPECT_EQ(fontGothic->getType(), FontType::CIDFontType0);
EXPECT_FALSE(fontGothic->isEmbedded());
HbShaper shaper;
auto glyphs = shaper.shapeRun("日本語漢字", fontGothic->getFontFace(), 16);
EXPECT_FALSE(glyphs.empty());
}
TEST(FontFallbackTest, SingletonInstanceIsUnique) {
using namespace pdfengine::fonts::pdf_fonts;
auto& instance1 = FontFallback::getInstance();
auto& instance2 = FontFallback::getInstance();
EXPECT_EQ(&instance1, &instance2);
}
TEST(FontFallbackTest, StandardFontFallbacks) {
using namespace pdfengine::fonts::pdf_fonts;
auto& fallback = FontFallback::getInstance();
std::string path1 = fallback.getFallbackFontPath("Helvetica");
EXPECT_FALSE(path1.empty());
EXPECT_TRUE(std::filesystem::exists(path1));
#if defined(_WIN32)
EXPECT_TRUE(containsCI(path1, "arial") || containsCI(path1, "liberationsans"));
#elif defined(__APPLE__)
EXPECT_TRUE(containsCI(path1, "arial") || containsCI(path1, "helvetica") ||
containsCI(path1, "liberationsans"));
#else
EXPECT_TRUE(containsCI(path1, "liberationsans") || containsCI(path1, "dejavusans"));
#endif
std::string path2 = fallback.getFallbackFontPath("Times-Roman");
EXPECT_FALSE(path2.empty());
EXPECT_TRUE(std::filesystem::exists(path2));
#if defined(_WIN32)
EXPECT_TRUE(containsCI(path2, "times") || containsCI(path2, "liberationserif") ||
containsCI(path2, "tinos"));
#elif defined(__APPLE__)
EXPECT_TRUE(containsCI(path2, "times") || containsCI(path2, "liberationserif") ||
containsCI(path2, "tinos"));
#else
EXPECT_TRUE(containsCI(path2, "liberationserif") || containsCI(path2, "dejavuserif") ||
containsCI(path2, "tinos"));
#endif
}
TEST(FontFallbackTest, StyleModifierResolutions) {
using namespace pdfengine::fonts::pdf_fonts;
auto& fallback = FontFallback::getInstance();
std::string pathBold = fallback.getFallbackFontPath("Helvetica", true, false);
EXPECT_FALSE(pathBold.empty());
EXPECT_TRUE(std::filesystem::exists(pathBold));
#if defined(_WIN32)
EXPECT_TRUE(containsCI(pathBold, "arialbd") || containsCI(pathBold, "liberationsans-bold"));
#endif
std::string pathBoldItalic = fallback.getFallbackFontPath("Times", true, true);
EXPECT_FALSE(pathBoldItalic.empty());
EXPECT_TRUE(std::filesystem::exists(pathBoldItalic));
#if defined(_WIN32)
EXPECT_TRUE(containsCI(pathBoldItalic, "timesbi") ||
containsCI(pathBoldItalic, "liberationserif-bolditalic") ||
containsCI(pathBoldItalic, "tinos-bolditalic"));
#endif
}
TEST(FontFallbackTest, CustomFallbackRegistration) {
using namespace pdfengine::fonts::pdf_fonts;
auto& fallback = FontFallback::getInstance();
fallback.resetToDefaults();
std::string standardPath = fallback.getFallbackFontPath("Helvetica");
std::filesystem::path overrideFont =
std::filesystem::temp_directory_path() / "pdfengine_custom_fallback.ttf";
{ std::ofstream(overrideFont) << "stub-font"; }
fallback.registerFallback("helvetica", overrideFont.string());
std::string overridenPath = fallback.getFallbackFontPath("Helvetica");
EXPECT_EQ(overridenPath, overrideFont.string());
fallback.resetToDefaults();
std::string restoredPath = fallback.getFallbackFontPath("Helvetica");
EXPECT_EQ(restoredPath, standardPath);
std::filesystem::remove(overrideFont);
}
TEST(FontSubstitutionAndWidthsTest, WidthMatchingAndSubstitutionVerification) {
using namespace pdfengine::fonts::pdf_fonts;
auto font = FontLoader::loadType1SystemFallback("Helvetica");
ASSERT_NE(font, nullptr);
EXPECT_FALSE(font->hasWidths());
std::vector<double> pdfWidths = { 600.0, 500.0, 550.0, 400.0 };
font->setWidths(65, 68, pdfWidths);
EXPECT_TRUE(font->hasWidths());
double fontSize = 12.0;
double expectedWidthA = (600.0 / 1000.0) * fontSize;
double expectedWidthB = (500.0 / 1000.0) * fontSize;
double expectedWidthC = (550.0 / 1000.0) * fontSize;
double expectedWidthD = (400.0 / 1000.0) * fontSize;
EXPECT_NEAR(font->getCharWidth(65, fontSize), expectedWidthA, 1e-5);
EXPECT_NEAR(font->getCharWidth(66, fontSize), expectedWidthB, 1e-5);
EXPECT_NEAR(font->getCharWidth(67, fontSize), expectedWidthC, 1e-5);
EXPECT_NEAR(font->getCharWidth(68, fontSize), expectedWidthD, 1e-5);
EXPECT_EQ(font->getCharWidth(999, fontSize), 0.0);
}
TEST(CIDAdvancedMappingTest, IdentityVSupport) {
using namespace pdfengine::fonts::pdf_fonts;
PredefinedEncoding identityV(SimpleEncodingType::Identity_V);
EXPECT_EQ(identityV.getType(), SimpleEncodingType::Identity_V);
EXPECT_EQ(identityV.decode(65), 65);
EXPECT_EQ(identityV.decode(1000), 1000);
}
TEST(CIDAdvancedMappingTest, VerticalMetricsResolution) {
using namespace pdfengine::fonts::pdf_fonts;
auto font = FontLoader::loadType1SystemFallback("Helvetica");
ASSERT_NE(font, nullptr);
double fontSize = 12.0;
EXPECT_EQ(font->isVertical(), false);
EXPECT_EQ(font->getCharHeight(65, fontSize), fontSize);
font->setVertical(true);
EXPECT_EQ(font->isVertical(), true);
std::vector<double> verticalAdvances = { 1000.0, 800.0, 900.0 };
font->setVerticalMetrics(65, 67, verticalAdvances);
EXPECT_TRUE(font->hasVerticalMetrics());
EXPECT_NEAR(font->getCharHeight(65, fontSize), 12.0, 1e-5);
EXPECT_NEAR(font->getCharHeight(66, fontSize), 9.6, 1e-5);
EXPECT_NEAR(font->getCharHeight(67, fontSize), 10.8, 1e-5);
EXPECT_NEAR(font->getCharHeight(999, fontSize), 12.0, 1e-5);
}
TEST(CIDAdvancedMappingTest, CjkCollectionResolutionDB) {
using namespace pdfengine::fonts::pdf_fonts;
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1010), 0x3041);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1092), 0x3093);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1125), 0x30A1);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1205), 0x30F6);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1206), 0x4E00);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-GB1", 1), 0x3000);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-GB1", 2), 0x3001);
}
TEST(CIDAdvancedMappingTest, HarfBuzzVerticalShapingSignature) {
using namespace pdfengine::fonts;
EXPECT_EQ(static_cast<int>(HbShaper::WritingMode::Horizontal), 0);
EXPECT_EQ(static_cast<int>(HbShaper::WritingMode::Vertical), 1);
}
}
File diff suppressed because it is too large Load Diff
+80
View File
@@ -0,0 +1,80 @@
#pragma once
#include "fonts/face/font_face.hpp"
#include "fonts/shaping/hb_shaper.hpp"
#include "fonts/cache/glyph_cache.hpp"
#include "fonts/pdf_fonts/font.hpp"
#include "fonts/pdf_fonts/types/truetype_font.hpp"
#include "fonts/pdf_fonts/types/type1_font.hpp"
#include "fonts/pdf_fonts/types/cid_font.hpp"
#include "fonts/pdf_fonts/font_loader.hpp"
#include "fonts/pdf_fonts/encoding/encoding.hpp"
#include "fonts/pdf_fonts/encoding/cjk_collection_db.hpp"
#include "fonts/pdf_fonts/font_fallback.hpp"
#include "fonts/pdf_fonts/font_subset.hpp"
#include <gtest/gtest.h>
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
namespace {
bool containsCI(const std::string& haystack, const std::string& needle) {
auto it = std::search(
haystack.begin(), haystack.end(), needle.begin(), needle.end(),
[](char a, char b) {
return std::tolower(static_cast<unsigned char>(a)) ==
std::tolower(static_cast<unsigned char>(b));
});
return it != haystack.end();
}
bool saveGlyphAsPGM(const pdfengine::fonts::GlyphBitmap& bitmap, const std::string& filename) {
if (bitmap.width == 0 || bitmap.height == 0 || bitmap.pixels.empty()) {
return false;
}
std::ofstream out(filename, std::ios::binary);
if (!out) {
return false;
}
out << "P5\n" << bitmap.width << " " << bitmap.height << "\n255\n";
out.write(reinterpret_cast<const char*>(bitmap.pixels.data()), bitmap.pixels.size());
return true;
}
std::string getSystemFontPath() {
#if defined(_WIN32)
std::vector<std::string> paths = {
"C:\\Windows\\Fonts\\arial.ttf",
"C:\\Windows\\Fonts\\consola.ttf",
"C:\\Windows\\Fonts\\tahoma.ttf"
};
#elif defined(__APPLE__)
std::vector<std::string> paths = {
"/Library/Fonts/Arial.ttf",
"/System/Library/Fonts/Geneva.ttf",
"/System/Library/Fonts/Helvetica.ttc",
"/System/Library/Fonts/Supplemental/Arial.ttf"
};
#else
std::vector<std::string> paths = {
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
"/usr/share/fonts/truetype/freefont/FreeSans.ttf"
};
#endif
for (const auto& path : paths) {
if (std::filesystem::exists(path)) {
return path;
}
}
return "";
}
}
+174
View File
@@ -0,0 +1,174 @@
#include "document_test_helpers.hpp"
namespace pdfengine {
TEST(PageRenderTest, RenderAtDifferentDPI) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("basic", "hello_world.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "hello_world.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto pageRes = (*docRes)->getPage(0);
ASSERT_TRUE(pageRes.has_value());
auto page = *pageRes;
double ptWidth = page->width();
double ptHeight = page->height();
EXPECT_GT(ptWidth, 0.0);
EXPECT_GT(ptHeight, 0.0);
std::vector<int> dpis = {72, 144, 288};
for (int dpi : dpis) {
auto imgRes = page->render(dpi);
ASSERT_TRUE(imgRes.has_value()) << "Failed to render at DPI " << dpi;
double scale = dpi / 72.0;
int expectedW = static_cast<int>(ptWidth * scale);
int expectedH = static_cast<int>(ptHeight * scale);
EXPECT_EQ(imgRes->width, expectedW);
EXPECT_EQ(imgRes->height, expectedH);
ASSERT_GE(imgRes->data.size(), 8U);
EXPECT_EQ(imgRes->data[0], 0x89);
EXPECT_EQ(imgRes->data[1], 'P');
EXPECT_EQ(imgRes->data[2], 'N');
EXPECT_EQ(imgRes->data[3], 'G');
EXPECT_EQ(imgRes->data[4], 0x0D);
EXPECT_EQ(imgRes->data[5], 0x0A);
EXPECT_EQ(imgRes->data[6], 0x1A);
EXPECT_EQ(imgRes->data[7], 0x0A);
}
}
TEST(PageRenderTest, InvalidPageIndexReturnsPageOutOfBounds) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("basic", "hello_world.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "hello_world.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto pageRes = (*docRes)->getPage(1);
ASSERT_FALSE(pageRes.has_value());
EXPECT_EQ(pageRes.error(), EngineError::PageOutOfBounds);
auto pageResNeg = (*docRes)->getPage(-1);
ASSERT_FALSE(pageResNeg.has_value());
EXPECT_EQ(pageResNeg.error(), EngineError::PageOutOfBounds);
}
TEST(CoordinateTransformTest, PageToDeviceAndBackRoundtrip) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("basic", "hello_world.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "hello_world.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto pageRes = (*docRes)->getPage(0);
ASSERT_TRUE(pageRes.has_value());
auto page = *pageRes;
int deviceW = 1024;
int deviceH = 768;
std::vector<int> rotations = {0, 90, 180, 270};
std::vector<Point2D> testPoints = {
{0.0, 0.0},
{50.0, 50.0},
{100.0, 200.0},
{page->width() / 2.0, page->height() / 2.0},
{page->width() - 10.0, page->height() - 10.0}
};
for (int rotation : rotations) {
for (const auto& pt : testPoints) {
auto devPt = page->pageToDevice(pt, deviceW, deviceH, rotation);
auto pagePt = page->deviceToPage(devPt, deviceW, deviceH, rotation);
EXPECT_NEAR(pt.x, pagePt.x, 2.0) << "Failed roundtrip for x at rotation " << rotation;
EXPECT_NEAR(pt.y, pagePt.y, 2.0) << "Failed roundtrip for y at rotation " << rotation;
}
}
}
TEST(TextExtractionTest, ExtractSimpleText) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("basic", "hello_world.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "hello_world.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto pageRes = (*docRes)->getPage(0);
ASSERT_TRUE(pageRes.has_value());
auto textRes = (*pageRes)->extractText();
ASSERT_TRUE(textRes.has_value());
std::string text = *textRes;
EXPECT_NE(text.find("Hello"), std::string::npos);
EXPECT_NE(text.find("world"), std::string::npos);
}
TEST(TextExtractionTest, ExtractUtf8ExtendedText) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("fonts", "latin_extended.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "latin_extended.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto pageRes = (*docRes)->getPage(0);
ASSERT_TRUE(pageRes.has_value());
auto textRes = (*pageRes)->extractText();
ASSERT_TRUE(textRes.has_value());
std::string text = *textRes;
EXPECT_FALSE(text.empty());
}
TEST(TextExtractionTest, ExtractTextWithBounds) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("basic", "hello_world.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "hello_world.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto pageRes = (*docRes)->getPage(0);
ASSERT_TRUE(pageRes.has_value());
auto boundsRes = (*pageRes)->extractTextWithBounds();
ASSERT_TRUE(boundsRes.has_value());
const auto& glyphs = *boundsRes;
ASSERT_FALSE(glyphs.empty());
bool found_hello = false;
for (const auto& glyph : glyphs) {
EXPECT_FALSE(glyph.text.empty());
if (glyph.text != " " && glyph.text != "\r" && glyph.text != "\n" && glyph.text != "\t") {
EXPECT_GT(glyph.w, 0.0);
EXPECT_GT(glyph.h, 0.0);
}
EXPECT_GT(glyph.fontSize, 0.0);
if (glyph.text == "H" || glyph.text == "e" || glyph.text == "l" || glyph.text == "o") {
found_hello = true;
}
}
EXPECT_TRUE(found_hello);
}
}
+74
View File
@@ -0,0 +1,74 @@
#include "document_test_helpers.hpp"
namespace pdfengine {
TEST(UtfConversionTest, EmojiSurrogatePairs) {
std::string utf8_grinning = "\xF0\x9F\x98\x80";
auto utf16 = pdfengine::parser::utf8_to_utf16le(utf8_grinning);
ASSERT_EQ(utf16.size(), 3);
EXPECT_EQ(utf16[0], 0xD83D);
EXPECT_EQ(utf16[1], 0xDE00);
EXPECT_EQ(utf16[2], 0x0000);
std::string utf8_out = pdfengine::parser::utf16le_to_utf8(reinterpret_cast<const char16_t*>(utf16.data()), utf16.size() - 1);
EXPECT_EQ(utf8_out, utf8_grinning);
std::string utf8_rocket = "\xF0\x9F\x9A\x80";
utf16 = pdfengine::parser::utf8_to_utf16le(utf8_rocket);
ASSERT_EQ(utf16.size(), 3);
EXPECT_EQ(utf16[0], 0xD83D);
EXPECT_EQ(utf16[1], 0xDE80);
EXPECT_EQ(utf16[2], 0x0000);
utf8_out = pdfengine::parser::utf16le_to_utf8(reinterpret_cast<const char16_t*>(utf16.data()), utf16.size() - 1);
EXPECT_EQ(utf8_out, utf8_rocket);
}
TEST(UtfConversionTest, CJKExtensionB) {
std::string utf8_cjk = "\xF0\xA0\x80\x80";
auto utf16 = pdfengine::parser::utf8_to_utf16le(utf8_cjk);
ASSERT_EQ(utf16.size(), 3);
EXPECT_EQ(utf16[0], 0xD840);
EXPECT_EQ(utf16[1], 0xDC00);
EXPECT_EQ(utf16[2], 0x0000);
std::string utf8_out = pdfengine::parser::utf16le_to_utf8(reinterpret_cast<const char16_t*>(utf16.data()), utf16.size() - 1);
EXPECT_EQ(utf8_out, utf8_cjk);
}
TEST(UtfConversionTest, RoundtripMixed) {
std::string mixed = "A\xF0\x9F\x98\x80""B\xF0\xA0\x80\x80""C";
auto utf16 = pdfengine::parser::utf8_to_utf16le(mixed);
std::string mixed_out = pdfengine::parser::utf16le_to_utf8(reinterpret_cast<const char16_t*>(utf16.data()), utf16.size() - 1);
EXPECT_EQ(mixed_out, mixed);
}
TEST(CjkResolutionTest, AdobeCNS1) {
using pdfengine::fonts::pdf_fonts::CjkCollectionDB;
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 100), 0x4E00);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 112), 0x4E2D);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 130), 0x4ED7);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 131), 0x4ED8);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 135), 0x4EDF);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 140), 0x4F01);
EXPECT_EQ(CjkCollectionDB::resolveCID("Identity-H-CNS1", 137), 0x4EE3);
}
TEST(CjkResolutionTest, AdobeKorea1) {
using pdfengine::fonts::pdf_fonts::CjkCollectionDB;
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 101), 0xAC00);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 119), 0xAC1C);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 150), 0xAC90);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 151), 0xAC94);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 156), 0xACA9);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 160), 0xACBD);
EXPECT_EQ(CjkCollectionDB::resolveCID("UniKS-UTF16-H-Korea1", 153), 0xACA0);
}
}
File diff suppressed because one or more lines are too long
Binary file not shown.
+1 -1
View File
@@ -18,7 +18,7 @@ function getModule(): Promise<PdfiumModule | null> {
if (!modulePromise) {
modulePromise = (async () => {
try {
const V = '20260625-tier2b';
const V = '20260626-auxfont';
const resp = await fetch(`/pdfium-engine.mjs?v=${V}`, { cache: 'no-store' });
if (!resp.ok) throw new Error(`pdfium-engine.mjs ${resp.status}`);
const blobUrl = URL.createObjectURL(new Blob([await resp.text()], { type: 'text/javascript' }));
-900
View File
@@ -1,900 +0,0 @@
import hashlib
from fastapi import APIRouter, File, HTTPException, Response, UploadFile, status
from pydantic import BaseModel
from app.services import engine
from app.services.store import document_store
router = APIRouter(prefix="/documents", tags=["documents"])
class PageInfoResponse(BaseModel):
index: int
width: float
height: float
class PermissionsResponse(BaseModel):
isEncrypted: bool = False
encryption: str = "None"
securityRevision: int = -1
ownerUnlocked: bool = False
canPrint: bool = True
canPrintHighRes: bool = True
canModify: bool = True
canCopy: bool = True
canAnnotate: bool = True
canFillForms: bool = True
canExtractForAccessibility: bool = True
canAssemble: bool = True
class DocumentInfoResponse(BaseModel):
id: str
filename: str
sizeBytes: int
totalPages: int
pageWidth: float
pageHeight: float
uploadedAt: str
status: str
pages: list[PageInfoResponse] = []
permissions: PermissionsResponse = PermissionsResponse()
def make_document_response(d: dict) -> DocumentInfoResponse:
pages_list = []
if "doc_instance" in d:
doc = d["doc_instance"]
for i in range(doc.page_count):
try:
page = doc.get_page(i)
pages_list.append(PageInfoResponse(index=i, width=page.width, height=page.height))
except Exception:
pass
perms = d.get("permissions")
return DocumentInfoResponse(
id=d["id"],
filename=d["filename"],
sizeBytes=d["sizeBytes"],
totalPages=d["totalPages"],
pageWidth=d.get("pageWidth", 612.0),
pageHeight=d.get("pageHeight", 792.0),
uploadedAt=d["uploadedAt"],
status=d["status"],
pages=pages_list,
permissions=PermissionsResponse(**perms) if perms else PermissionsResponse(),
)
@router.post("", response_model=DocumentInfoResponse, status_code=status.HTTP_201_CREATED)
async def upload_document(file: UploadFile = File(...), password: str = "") -> DocumentInfoResponse:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
bytes_data = await file.read()
try:
pdfengine = engine.require()
doc = pdfengine.PdfDocument.load_from_memory(bytes_data, password)
info = document_store.add_document(file.filename, bytes_data, doc)
return make_document_response(info)
except ValueError as e:
detail = str(e)
if "Password required" in detail:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Password required"
)
elif "Invalid password" in detail:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid password")
else:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=detail)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=f"Failed to load PDF: {e!s}"
)
@router.get("", response_model=list[DocumentInfoResponse])
def list_documents() -> list[DocumentInfoResponse]:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
docs = document_store.list_documents()
return [make_document_response(d) for d in docs]
@router.get("/{document_id}", response_model=DocumentInfoResponse)
def get_document(document_id: str) -> DocumentInfoResponse:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
d = document_store.get_document(document_id)
if not d:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
return make_document_response(d)
@router.delete("/{document_id}")
def delete_document(document_id: str):
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
deleted = document_store.delete_document(document_id)
if not deleted:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
return {"success": True}
class DocumentMetadataResponse(BaseModel):
title: str
author: str
creator: str
producer: str
creation_date: str
modification_date: str
@router.get("/{document_id}/metadata", response_model=DocumentMetadataResponse)
def get_document_metadata(document_id: str) -> DocumentMetadataResponse:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
d = document_store.get_document(document_id)
if not d:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
try:
doc = d["doc_instance"]
meta = doc.metadata
return DocumentMetadataResponse(
title=meta.title,
author=meta.author,
creator=meta.creator,
producer=meta.producer,
creation_date=meta.creation_date,
modification_date=meta.modification_date,
)
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
class FontInfoResponse(BaseModel):
fontName: str
type: str
isEmbedded: bool
isSubset: bool
isVertical: bool
encoding: str
hasToUnicode: bool
cmapName: str
cidSystemInfo: str
subsetTag: str
sourceType: str
substitutedFrom: str
substitutedTo: str
normalizedFamily: str
internalFontId: str
flags: int
ascent: float
descent: float
capHeight: float
@router.get("/{document_id}/fonts", response_model=list[FontInfoResponse])
def get_document_fonts(
document_id: str, start_page: int = 0, end_page: int = -1
) -> list[FontInfoResponse]:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
doc_info = document_store.get_document(document_id)
if not doc_info:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
try:
doc = doc_info["doc_instance"]
fonts = doc.get_fonts(start_page, end_page)
return [
FontInfoResponse(
fontName=f.font_name,
type=f.type,
isEmbedded=f.is_embedded,
isSubset=f.is_subset,
isVertical=f.is_vertical,
encoding=f.encoding,
hasToUnicode=f.has_to_unicode,
cmapName=f.cmap_name,
cidSystemInfo=f.cid_system_info,
subsetTag=f.subset_tag,
sourceType=f.source_type,
substitutedFrom=f.substituted_from,
substitutedTo=f.substituted_to,
normalizedFamily=f.normalized_family,
internalFontId=f.internal_font_id,
flags=f.flags,
ascent=f.ascent,
descent=f.descent,
capHeight=f.cap_height,
)
for f in fonts
]
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except IndexError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except Exception as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
_SFNT_TTF_MAGIC = (b"\x00\x01\x00\x00", b"true", b"ttcf")
_SFNT_OTF_MAGIC = b"OTTO"
@router.get("/{document_id}/font")
def get_font_bytes(document_id: str, internal_font_id: str) -> Response:
"""Raw embedded font bytes for an in-place-editing preview.
Returns the font only when it's a browser-loadable sfnt (TrueType / OpenType-CFF).
Type1/PFB, non-embedded, and unknown fonts return 404 so the frontend falls back to
a base-14 CSS font. The lookup is sandboxed to fonts inside the loaded document.
"""
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
if not internal_font_id or len(internal_font_id) > 256:
return Response(status_code=status.HTTP_204_NO_CONTENT)
doc_info = document_store.get_document(document_id)
if not doc_info:
return Response(status_code=status.HTTP_204_NO_CONTENT)
try:
data = bytes(doc_info["doc_instance"].get_font_data(internal_font_id))
except Exception:
data = b""
if not data:
return Response(status_code=status.HTTP_204_NO_CONTENT)
magic = data[:4]
if magic in _SFNT_TTF_MAGIC:
media_type = "font/ttf"
elif magic == _SFNT_OTF_MAGIC:
media_type = "font/otf"
else:
return Response(status_code=status.HTTP_204_NO_CONTENT)
etag = '"' + hashlib.sha256(data).hexdigest()[:32] + '"'
return Response(
content=data,
media_type=media_type,
headers={"Cache-Control": "public, max-age=31536000, immutable", "ETag": etag},
)
@router.get("/{document_id}/font-reconstructed")
def get_reconstructed_font_bytes(document_id: str, internal_font_id: str) -> Response:
"""Tier-2: a cmap-augmented copy of an embedded font (original glyph program + synthesized
Unicode cmap) so the WASM live preview can reuse the document's real glyphs and match the
saved result. 204 when reconstruction isn't possible -> frontend falls back to Tier-1."""
if not engine.is_available():
return Response(status_code=status.HTTP_204_NO_CONTENT)
if not internal_font_id or len(internal_font_id) > 256:
return Response(status_code=status.HTTP_204_NO_CONTENT)
doc_info = document_store.get_document(document_id)
if not doc_info:
return Response(status_code=status.HTTP_204_NO_CONTENT)
try:
data = bytes(doc_info["doc_instance"].get_reconstructed_font_data(internal_font_id))
except Exception:
data = b""
if not data or data[:4] not in _SFNT_TTF_MAGIC:
return Response(status_code=status.HTTP_204_NO_CONTENT)
etag = '"' + hashlib.sha256(data).hexdigest()[:32] + '"'
return Response(
content=data,
media_type="font/ttf",
headers={"Cache-Control": "public, max-age=31536000, immutable", "ETag": etag},
)
@router.get("/{document_id}/raw")
def get_document_raw(document_id: str) -> Response:
"""Raw PDF bytes of the current version — loaded into the in-browser WASM engine for the
pixel-identical live-edit preview. Gated on copy permission (same as export)."""
doc_info = document_store.get_document(document_id)
if not doc_info:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
perms = doc_info.get("permissions") or {}
if perms.get("canCopy", True) is False:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not permitted (canCopy).")
data = doc_info.get("bytes_data")
if not data:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document bytes unavailable")
return Response(content=bytes(data), media_type="application/pdf")
class SearchRect(BaseModel):
x: float
y: float
w: float
h: float
class SearchMatch(BaseModel):
pageIndex: int
rects: list[SearchRect]
text: str
@router.get("/{document_id}/search", response_model=list[SearchMatch])
def search_document(
document_id: str,
q: str,
case_sensitive: bool = False,
whole_words: bool = False,
) -> list[SearchMatch]:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
if not q:
return []
doc_info = document_store.get_document(document_id)
if not doc_info:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
def _is_word_char(ch: str) -> bool:
return ch.isalnum() or ch == "_"
def _find_all(haystack: str, needle: str) -> list[int]:
"""Return start indices of all non-overlapping occurrences of needle in haystack."""
results: list[int] = []
start = 0
needle_len = len(needle)
while True:
pos = haystack.find(needle, start)
if pos == -1:
break
if whole_words:
before_ok = pos == 0 or not _is_word_char(haystack[pos - 1])
after_ok = (pos + needle_len) >= len(haystack) or not _is_word_char(
haystack[pos + needle_len]
)
if before_ok and after_ok:
results.append(pos)
else:
results.append(pos)
start = pos + 1
return results
try:
doc = doc_info["doc_instance"]
matches = []
search_needle = q if case_sensitive else q.lower()
query_len = len(search_needle)
for page_idx in range(doc.page_count):
page = doc.get_page(page_idx)
glyphs = page.extract_text_with_bounds()
if not glyphs:
continue
text_str = ""
char_to_glyph: list[int] = []
for i, g in enumerate(glyphs):
s = g.get("text", "")
start_len = len(text_str)
text_str += s
for _ in range(len(text_str) - start_len):
char_to_glyph.append(i)
search_text = text_str if case_sensitive else text_str.lower()
for idx in _find_all(search_text, search_needle):
if idx + query_len - 1 >= len(char_to_glyph):
continue
start_glyph_idx = char_to_glyph[idx]
end_glyph_idx = char_to_glyph[idx + query_len - 1]
rects = []
current_rect = None
for g_idx in range(start_glyph_idx, end_glyph_idx + 1):
g = glyphs[g_idx]
dom_y = page.height - (g["y"] + g["h"])
if current_rect is None:
current_rect = {"x": g["x"], "y": dom_y, "w": g["w"], "h": g["h"]}
else:
if abs(dom_y - current_rect["y"]) < g.get("fontSize", 12) * 0.5:
max_x = max(current_rect["x"] + current_rect["w"], g["x"] + g["w"])
current_rect["w"] = max_x - current_rect["x"]
current_rect["y"] = min(current_rect["y"], dom_y)
current_rect["h"] = max(current_rect["h"], g["h"])
else:
rects.append(SearchRect(**current_rect))
current_rect = {"x": g["x"], "y": dom_y, "w": g["w"], "h": g["h"]}
if current_rect:
rects.append(SearchRect(**current_rect))
matches.append(
SearchMatch(
pageIndex=page_idx, rects=rects, text=text_str[idx : idx + query_len]
)
)
return matches
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
class GlyphModel(BaseModel):
text: str
unicode: int
font_name: str
flags: int
font_size: float
origin_x: float
origin_y: float
bbox_x: float
bbox_y: float
bbox_w: float
bbox_h: float
angle: float
page_object_index: int = -1
class TextRunModel(BaseModel):
text: str
font_name: str
flags: int
font_size: float
internal_font_id: str
is_embedded: bool
type: str
glyphs: list[GlyphModel]
x: float
y: float
w: float
h: float
object_indices: list[int] = []
color: str = "#000000"
font_fidelity: str = "exact"
class TextLineModel(BaseModel):
runs: list[TextRunModel]
baseline_y: float
x: float
y: float
w: float
h: float
class ParagraphModel(BaseModel):
lines: list[TextLineModel]
x: float
y: float
w: float
h: float
class PageModelResponse(BaseModel):
paragraphs: list[ParagraphModel]
width: float
height: float
page_index: int
@router.get("/{document_id}/pages/{page_index}/model", response_model=PageModelResponse)
def get_page_model(document_id: str, page_index: int) -> PageModelResponse:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
doc_info = document_store.get_document(document_id)
if not doc_info:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
try:
doc = doc_info["doc_instance"]
page = doc.get_page(page_index)
model = page.extract_document_model()
paragraphs = []
for p in model.paragraphs:
lines = []
for line in p.lines:
runs = []
for r in line.runs:
glyphs = []
for g in r.glyphs:
glyphs.append(
GlyphModel(
text=g.text,
unicode=g.unicode,
font_name=g.font_name,
flags=g.flags,
font_size=g.font_size,
origin_x=g.origin_x,
origin_y=g.origin_y,
bbox_x=g.bbox_x,
bbox_y=g.bbox_y,
bbox_w=g.bbox_w,
bbox_h=g.bbox_h,
angle=g.angle,
page_object_index=g.page_object_index,
)
)
runs.append(
TextRunModel(
text=r.text,
font_name=r.font_name,
flags=r.flags,
font_size=r.font_size,
internal_font_id=r.internal_font_id,
is_embedded=r.is_embedded,
type=r.type,
glyphs=glyphs,
x=r.x,
y=r.y,
w=r.w,
h=r.h,
object_indices=r.object_indices,
color=getattr(r, "fill_color", "#000000") or "#000000",
font_fidelity=getattr(r, "font_fidelity", "exact") or "exact",
)
)
lines.append(
TextLineModel(
runs=runs,
baseline_y=line.baseline_y,
x=line.x,
y=line.y,
w=line.w,
h=line.h,
)
)
paragraphs.append(ParagraphModel(lines=lines, x=p.x, y=p.y, w=p.w, h=p.h))
return PageModelResponse(
paragraphs=paragraphs,
width=model.width,
height=model.height,
page_index=model.page_index,
)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except IndexError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
class AnnotationResponse(BaseModel):
id: str
type: str
x: float
y: float
width: float
height: float
color: str
author: str
content: str
timestamp: str | None = None
pageIndex: int
paths: list[list[dict[str, float]]] = []
fieldName: str | None = None
fieldValue: str | None = None
fieldType: str | None = None
fieldFlags: int | None = None
fieldOptions: list[str] | None = None
@router.get("/{document_id}/annotations", response_model=list[AnnotationResponse])
def get_document_annotations(document_id: str) -> list[AnnotationResponse]:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
doc_info = document_store.get_document(document_id)
if not doc_info:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
try:
doc = doc_info["doc_instance"]
all_annots = []
for i in range(doc.page_count):
try:
page = doc.get_page(i)
annots = page.extract_annotations()
for a in annots:
all_annots.append(AnnotationResponse(
id=a.id,
type=a.type,
x=a.x,
y=a.y,
width=a.width,
height=a.height,
color=a.color,
author=a.author,
content=a.content,
timestamp=getattr(a, "timestamp", None),
pageIndex=a.page_index,
paths=[[{"x": p.x, "y": p.y} for p in stroke] for stroke in getattr(a, "paths", [])],
fieldName=getattr(a, "field_name", None),
fieldValue=getattr(a, "field_value", None),
fieldType=getattr(a, "field_type", None),
fieldFlags=getattr(a, "field_flags", None),
fieldOptions=getattr(a, "field_options", None),
))
except Exception:
pass
return all_annots
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
class OutlineItemResponse(BaseModel):
title: str
pageIndex: int
level: int
@router.get("/{document_id}/outline", response_model=list[OutlineItemResponse])
def get_document_outline(document_id: str) -> list[OutlineItemResponse]:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
d = document_store.get_document(document_id)
if not d:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
try:
doc = d["doc_instance"]
items = doc.extract_outline()
return [
OutlineItemResponse(title=it["title"], pageIndex=it["pageIndex"], level=it["level"])
for it in items
]
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
@router.get("/{document_id}/export")
def export_document(document_id: str):
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
d = document_store.get_document(document_id)
if not d:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
perms = d.get("permissions") or {}
if perms.get("canCopy", True) is False:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Exporting is not permitted by this document's restrictions (canCopy).",
)
try:
doc = d["doc_instance"]
bytes_data = doc.save_full()
filename = d["filename"]
if not filename.endswith(".pdf"):
filename += ".pdf"
import sys
import os
roaming_path = os.path.join(os.environ.get("APPDATA", "C:\\Users\\azeem\\AppData\\Roaming"), "Python", "Python312", "site-packages")
if roaming_path not in sys.path:
sys.path.append(roaming_path)
import pypdf
import io
reader = pypdf.PdfReader(io.BytesIO(bytes_data))
writer = pypdf.PdfWriter()
writer.append(reader)
acro_form = writer.root_object.get("/AcroForm")
if acro_form is not None:
acro_form_dict = acro_form.get_object()
acro_form_dict[pypdf.generic.NameObject("/NeedAppearances")] = pypdf.generic.BooleanObject(True)
out_stream = io.BytesIO()
writer.write(out_stream)
bytes_data = out_stream.getvalue()
return Response(
content=bytes_data,
media_type="application/pdf",
headers={
"Content-Disposition": f'attachment; filename="{filename}"',
"Content-Length": str(len(bytes_data)),
},
)
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
class TextObjectResponse(BaseModel):
text: str
fontName: str
fontSize: float
tm: list[float]
@router.get("/{document_id}/pages/{page_index}/text_objects", response_model=list[TextObjectResponse])
def get_text_objects(document_id: str, page_index: int) -> list[TextObjectResponse]:
if not engine.is_available():
raise HTTPException(status_code=501, detail="Engine not available")
doc_info = document_store.get_document(document_id)
if not doc_info:
raise HTTPException(status_code=404, detail="Document not found")
_perms = doc_info.get("permissions") or {}
if _perms.get("canModify", True) is False:
raise HTTPException(status_code=403, detail="Raw Text editing is not permitted by this document's restrictions (canModify).")
pdfengine = engine.require()
import os
import tempfile
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
tmp.write(doc_info["bytes_data"])
tmp_path = tmp.name
try:
editor = pdfengine.StreamEditor(tmp_path)
objects = editor.extract_text_objects(page_index)
result = []
for obj in objects:
text_str = obj["text"].decode("latin-1") if isinstance(obj["text"], bytes) else obj["text"]
result.append(TextObjectResponse(
text=text_str,
fontName=obj["fontName"],
fontSize=obj["fontSize"],
tm=obj["tm"]
))
return result
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
class UpdateTextObjectRequest(BaseModel):
new_text: str
@router.put("/{document_id}/pages/{page_index}/text_objects/{object_index}")
def replace_text_object(document_id: str, page_index: int, object_index: int, req: UpdateTextObjectRequest) -> dict:
if not engine.is_available():
raise HTTPException(status_code=501, detail="Engine not available")
doc_info = document_store.get_document(document_id)
if not doc_info:
raise HTTPException(status_code=404, detail="Document not found")
_perms = doc_info.get("permissions") or {}
if _perms.get("canModify", True) is False:
raise HTTPException(status_code=403, detail="Raw Text editing is not permitted by this document's restrictions (canModify).")
pdfengine = engine.require()
import tempfile
import os
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
tmp.write(doc_info["bytes_data"])
tmp_path = tmp.name
out_path = tmp_path + ".out.pdf"
try:
editor = pdfengine.StreamEditor(tmp_path)
try:
new_text_bytes = req.new_text.encode("latin-1")
except UnicodeEncodeError as enc_err:
raise HTTPException(
status_code=400,
detail="Some characters can't be encoded in this run's font. Raw Text supports same-charset edits only — use Edit text to add new characters.",
) from enc_err
success = editor.replace_text_object(page_index, object_index, new_text_bytes, out_path)
if not success:
raise HTTPException(status_code=400, detail="Failed to replace text object (not found or identical)")
with open(out_path, "rb") as f:
new_bytes = f.read()
new_doc = pdfengine.PdfDocument.load_from_memory(new_bytes, "")
new_info = document_store.add_document(
filename=doc_info["filename"], bytes_data=new_bytes, doc_instance=new_doc,
permissions=doc_info.get("permissions"),
)
return {"success": True, "newDocumentId": new_info["id"]}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
if os.path.exists(out_path):
os.remove(out_path)
@router.get("/{document_id}/pages/{page_index}/display_list")
def get_display_list(document_id: str, page_index: int):
if not engine.is_available():
raise HTTPException(status_code=501, detail="Engine not available")
doc_info = document_store.get_document(document_id)
if not doc_info:
raise HTTPException(status_code=404, detail="Document not found")
try:
doc = doc_info["doc_instance"]
page = doc.get_page(page_index)
json_str = page.extract_display_list()
return Response(content=json_str, media_type="application/json")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{document_id}/pages/{page_index}/xobjects/{name}")
def get_image_xobject(document_id: str, page_index: int, name: str):
if not engine.is_available():
raise HTTPException(status_code=501, detail="Engine not available")
doc_info = document_store.get_document(document_id)
if not doc_info:
raise HTTPException(status_code=404, detail="Document not found")
try:
doc = doc_info["doc_instance"]
page = doc.get_page(page_index)
img_bytes = page.extract_image_xobject(name)
if not img_bytes:
raise HTTPException(status_code=404, detail="Image not found")
return Response(content=img_bytes, media_type="image/png")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
+20
View File
@@ -0,0 +1,20 @@
"""Documents resource router.
Previously a single 900-line ``documents.py``. Split into focused sub-routers,
all mounted under the unchanged ``/documents`` prefix so every public URL is
preserved exactly. ``main.py`` still does ``app.include_router(documents.router)``.
The ``crud`` module owns the base router (it holds the prefix), and the other
sub-routers mount into it.
"""
from app.schemas.font import FontInfoResponse
from . import content, export, fonts, metadata, text_objects
from .crud import router
router.include_router(metadata.router)
router.include_router(fonts.router)
router.include_router(content.router)
router.include_router(export.router)
router.include_router(text_objects.router)
+163
View File
@@ -0,0 +1,163 @@
from fastapi import APIRouter, HTTPException, Response, status
from app.schemas.annotation import AnnotationResponse
from app.schemas.page_model import PageModelResponse
from app.schemas.search import SearchMatch
from app.services import engine
from app.services.page_model import build_page_model_response
from app.services.search import compute_search_matches
from app.services.store import document_store
router = APIRouter(tags=["documents"])
@router.get("/{document_id}/raw")
def get_document_raw(document_id: str) -> Response:
"""Raw PDF bytes of the current version — loaded into the in-browser WASM engine for the
pixel-identical live-edit preview. Gated on copy permission (same as export)."""
doc_info = document_store.get_document(document_id)
if not doc_info:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
perms = doc_info.get("permissions") or {}
if perms.get("canCopy", True) is False:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not permitted (canCopy).")
data = doc_info.get("bytes_data")
if not data:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document bytes unavailable")
return Response(content=bytes(data), media_type="application/pdf")
@router.get("/{document_id}/search", response_model=list[SearchMatch])
def search_document(
document_id: str,
q: str,
case_sensitive: bool = False,
whole_words: bool = False,
) -> list[SearchMatch]:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
if not q:
return []
doc_info = document_store.get_document(document_id)
if not doc_info:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
try:
doc = doc_info["doc_instance"]
return compute_search_matches(doc, q, case_sensitive, whole_words)
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
@router.get("/{document_id}/pages/{page_index}/model", response_model=PageModelResponse)
def get_page_model(document_id: str, page_index: int) -> PageModelResponse:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
doc_info = document_store.get_document(document_id)
if not doc_info:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
try:
doc = doc_info["doc_instance"]
page = doc.get_page(page_index)
model = page.extract_document_model()
return build_page_model_response(model)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except IndexError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
@router.get("/{document_id}/annotations", response_model=list[AnnotationResponse])
def get_document_annotations(document_id: str) -> list[AnnotationResponse]:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
doc_info = document_store.get_document(document_id)
if not doc_info:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
try:
doc = doc_info["doc_instance"]
all_annots = []
for i in range(doc.page_count):
try:
page = doc.get_page(i)
annots = page.extract_annotations()
for a in annots:
all_annots.append(AnnotationResponse(
id=a.id,
type=a.type,
x=a.x,
y=a.y,
width=a.width,
height=a.height,
color=a.color,
author=a.author,
content=a.content,
timestamp=getattr(a, "timestamp", None),
pageIndex=a.page_index,
paths=[[{"x": p.x, "y": p.y} for p in stroke] for stroke in getattr(a, "paths", [])],
fieldName=getattr(a, "field_name", None),
fieldValue=getattr(a, "field_value", None),
fieldType=getattr(a, "field_type", None),
fieldFlags=getattr(a, "field_flags", None),
fieldOptions=getattr(a, "field_options", None),
))
except Exception:
pass
return all_annots
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
@router.get("/{document_id}/pages/{page_index}/display_list")
def get_display_list(document_id: str, page_index: int):
if not engine.is_available():
raise HTTPException(status_code=501, detail="Engine not available")
doc_info = document_store.get_document(document_id)
if not doc_info:
raise HTTPException(status_code=404, detail="Document not found")
try:
doc = doc_info["doc_instance"]
page = doc.get_page(page_index)
json_str = page.extract_display_list()
return Response(content=json_str, media_type="application/json")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{document_id}/pages/{page_index}/xobjects/{name}")
def get_image_xobject(document_id: str, page_index: int, name: str):
if not engine.is_available():
raise HTTPException(status_code=501, detail="Engine not available")
doc_info = document_store.get_document(document_id)
if not doc_info:
raise HTTPException(status_code=404, detail="Document not found")
try:
doc = doc_info["doc_instance"]
page = doc.get_page(page_index)
img_bytes = page.extract_image_xobject(name)
if not img_bytes:
raise HTTPException(status_code=404, detail="Image not found")
return Response(content=img_bytes, media_type="image/png")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
+107
View File
@@ -0,0 +1,107 @@
from fastapi import APIRouter, File, HTTPException, UploadFile, status
from app.schemas.document import (
DocumentInfoResponse,
PageInfoResponse,
PermissionsResponse,
)
from app.services import engine
from app.services.store import document_store
router = APIRouter(prefix="/documents", tags=["documents"])
def make_document_response(d: dict) -> DocumentInfoResponse:
pages_list = []
if "doc_instance" in d:
doc = d["doc_instance"]
for i in range(doc.page_count):
try:
page = doc.get_page(i)
pages_list.append(PageInfoResponse(index=i, width=page.width, height=page.height))
except Exception:
pass
perms = d.get("permissions")
return DocumentInfoResponse(
id=d["id"],
filename=d["filename"],
sizeBytes=d["sizeBytes"],
totalPages=d["totalPages"],
pageWidth=d.get("pageWidth", 612.0),
pageHeight=d.get("pageHeight", 792.0),
uploadedAt=d["uploadedAt"],
status=d["status"],
pages=pages_list,
permissions=PermissionsResponse(**perms) if perms else PermissionsResponse(),
)
@router.post("", response_model=DocumentInfoResponse, status_code=status.HTTP_201_CREATED)
async def upload_document(file: UploadFile = File(...), password: str = "") -> DocumentInfoResponse:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
bytes_data = await file.read()
try:
pdfengine = engine.require()
doc = pdfengine.PdfDocument.load_from_memory(bytes_data, password)
info = document_store.add_document(file.filename, bytes_data, doc)
return make_document_response(info)
except ValueError as e:
detail = str(e)
if "Password required" in detail:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Password required"
)
elif "Invalid password" in detail:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid password")
else:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=detail)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=f"Failed to load PDF: {e!s}"
)
@router.get("", response_model=list[DocumentInfoResponse])
def list_documents() -> list[DocumentInfoResponse]:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
docs = document_store.list_documents()
return [make_document_response(d) for d in docs]
@router.get("/{document_id}", response_model=DocumentInfoResponse)
def get_document(document_id: str) -> DocumentInfoResponse:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
d = document_store.get_document(document_id)
if not d:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
return make_document_response(d)
@router.delete("/{document_id}")
def delete_document(document_id: str):
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
deleted = document_store.delete_document(document_id)
if not deleted:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
return {"success": True}
+46
View File
@@ -0,0 +1,46 @@
from fastapi import APIRouter, HTTPException, Response, status
from app.services import engine
from app.services.export import apply_need_appearances
from app.services.store import document_store
router = APIRouter(tags=["documents"])
@router.get("/{document_id}/export")
def export_document(document_id: str):
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
d = document_store.get_document(document_id)
if not d:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
perms = d.get("permissions") or {}
if perms.get("canCopy", True) is False:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Exporting is not permitted by this document's restrictions (canCopy).",
)
try:
doc = d["doc_instance"]
bytes_data = doc.save_full()
filename = d["filename"]
if not filename.endswith(".pdf"):
filename += ".pdf"
bytes_data = apply_need_appearances(bytes_data)
return Response(
content=bytes_data,
media_type="application/pdf",
headers={
"Content-Disposition": f'attachment; filename="{filename}"',
"Content-Length": str(len(bytes_data)),
},
)
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
+109
View File
@@ -0,0 +1,109 @@
import hashlib
from fastapi import APIRouter, HTTPException, Response, status
from app.schemas.font import FontInfoResponse
from app.services import engine
from app.services.font import font_info_to_response
from app.services.store import document_store
router = APIRouter(tags=["documents"])
_SFNT_TTF_MAGIC = (b"\x00\x01\x00\x00", b"true", b"ttcf")
_SFNT_OTF_MAGIC = b"OTTO"
@router.get("/{document_id}/fonts", response_model=list[FontInfoResponse])
def get_document_fonts(
document_id: str, start_page: int = 0, end_page: int = -1
) -> list[FontInfoResponse]:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
doc_info = document_store.get_document(document_id)
if not doc_info:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
try:
doc = doc_info["doc_instance"]
fonts = doc.get_fonts(start_page, end_page)
return [font_info_to_response(f) for f in fonts]
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except IndexError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except Exception as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.get("/{document_id}/font")
def get_font_bytes(document_id: str, internal_font_id: str) -> Response:
"""Raw embedded font bytes for an in-place-editing preview.
Returns the font only when it's a browser-loadable sfnt (TrueType / OpenType-CFF).
Type1/PFB, non-embedded, and unknown fonts return 404 so the frontend falls back to
a base-14 CSS font. The lookup is sandboxed to fonts inside the loaded document.
"""
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
if not internal_font_id or len(internal_font_id) > 256:
return Response(status_code=status.HTTP_204_NO_CONTENT)
doc_info = document_store.get_document(document_id)
if not doc_info:
return Response(status_code=status.HTTP_204_NO_CONTENT)
try:
data = bytes(doc_info["doc_instance"].get_font_data(internal_font_id))
except Exception:
data = b""
if not data:
return Response(status_code=status.HTTP_204_NO_CONTENT)
magic = data[:4]
if magic in _SFNT_TTF_MAGIC:
media_type = "font/ttf"
elif magic == _SFNT_OTF_MAGIC:
media_type = "font/otf"
else:
return Response(status_code=status.HTTP_204_NO_CONTENT)
etag = '"' + hashlib.sha256(data).hexdigest()[:32] + '"'
return Response(
content=data,
media_type=media_type,
headers={"Cache-Control": "public, max-age=31536000, immutable", "ETag": etag},
)
@router.get("/{document_id}/font-reconstructed")
def get_reconstructed_font_bytes(document_id: str, internal_font_id: str) -> Response:
"""Tier-2: a cmap-augmented copy of an embedded font (original glyph program + synthesized
Unicode cmap) so the WASM live preview can reuse the document's real glyphs and match the
saved result. 204 when reconstruction isn't possible -> frontend falls back to Tier-1."""
if not engine.is_available():
return Response(status_code=status.HTTP_204_NO_CONTENT)
if not internal_font_id or len(internal_font_id) > 256:
return Response(status_code=status.HTTP_204_NO_CONTENT)
doc_info = document_store.get_document(document_id)
if not doc_info:
return Response(status_code=status.HTTP_204_NO_CONTENT)
try:
data = bytes(doc_info["doc_instance"].get_reconstructed_font_data(internal_font_id))
except Exception:
data = b""
if not data or data[:4] not in _SFNT_TTF_MAGIC:
return Response(status_code=status.HTTP_204_NO_CONTENT)
etag = '"' + hashlib.sha256(data).hexdigest()[:32] + '"'
return Response(
content=data,
media_type="font/ttf",
headers={"Cache-Control": "public, max-age=31536000, immutable", "ETag": etag},
)
+56
View File
@@ -0,0 +1,56 @@
from fastapi import APIRouter, HTTPException, status
from app.schemas.document import DocumentMetadataResponse, OutlineItemResponse
from app.services import engine
from app.services.store import document_store
router = APIRouter(tags=["documents"])
@router.get("/{document_id}/metadata", response_model=DocumentMetadataResponse)
def get_document_metadata(document_id: str) -> DocumentMetadataResponse:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
d = document_store.get_document(document_id)
if not d:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
try:
doc = d["doc_instance"]
meta = doc.metadata
return DocumentMetadataResponse(
title=meta.title,
author=meta.author,
creator=meta.creator,
producer=meta.producer,
creation_date=meta.creation_date,
modification_date=meta.modification_date,
)
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
@router.get("/{document_id}/outline", response_model=list[OutlineItemResponse])
def get_document_outline(document_id: str) -> list[OutlineItemResponse]:
if not engine.is_available():
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Engine bridge (bindings/python) not yet available.",
)
d = document_store.get_document(document_id)
if not d:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
try:
doc = d["doc_instance"]
items = doc.extract_outline()
return [
OutlineItemResponse(title=it["title"], pageIndex=it["pageIndex"], level=it["level"])
for it in items
]
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
@@ -0,0 +1,106 @@
import os
import tempfile
from fastapi import APIRouter, HTTPException
from app.schemas.text_object import TextObjectResponse, UpdateTextObjectRequest
from app.services import engine
from app.services.store import document_store
router = APIRouter(tags=["documents"])
@router.get(
"/{document_id}/pages/{page_index}/text_objects",
response_model=list[TextObjectResponse],
)
def get_text_objects(document_id: str, page_index: int) -> list[TextObjectResponse]:
if not engine.is_available():
raise HTTPException(status_code=501, detail="Engine not available")
doc_info = document_store.get_document(document_id)
if not doc_info:
raise HTTPException(status_code=404, detail="Document not found")
_perms = doc_info.get("permissions") or {}
if _perms.get("canModify", True) is False:
raise HTTPException(status_code=403, detail="Raw Text editing is not permitted by this document's restrictions (canModify).")
pdfengine = engine.require()
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
tmp.write(doc_info["bytes_data"])
tmp_path = tmp.name
try:
editor = pdfengine.StreamEditor(tmp_path)
objects = editor.extract_text_objects(page_index)
result = []
for obj in objects:
text_str = obj["text"].decode("latin-1") if isinstance(obj["text"], bytes) else obj["text"]
result.append(TextObjectResponse(
text=text_str,
fontName=obj["fontName"],
fontSize=obj["fontSize"],
tm=obj["tm"]
))
return result
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
@router.put("/{document_id}/pages/{page_index}/text_objects/{object_index}")
def replace_text_object(document_id: str, page_index: int, object_index: int, req: UpdateTextObjectRequest) -> dict:
if not engine.is_available():
raise HTTPException(status_code=501, detail="Engine not available")
doc_info = document_store.get_document(document_id)
if not doc_info:
raise HTTPException(status_code=404, detail="Document not found")
_perms = doc_info.get("permissions") or {}
if _perms.get("canModify", True) is False:
raise HTTPException(status_code=403, detail="Raw Text editing is not permitted by this document's restrictions (canModify).")
pdfengine = engine.require()
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
tmp.write(doc_info["bytes_data"])
tmp_path = tmp.name
out_path = tmp_path + ".out.pdf"
try:
editor = pdfengine.StreamEditor(tmp_path)
try:
new_text_bytes = req.new_text.encode("latin-1")
except UnicodeEncodeError as enc_err:
raise HTTPException(
status_code=400,
detail="Some characters can't be encoded in this run's font. Raw Text supports same-charset edits only — use Edit text to add new characters.",
) from enc_err
success = editor.replace_text_object(page_index, object_index, new_text_bytes, out_path)
if not success:
raise HTTPException(status_code=400, detail="Failed to replace text object (not found or identical)")
with open(out_path, "rb") as f:
new_bytes = f.read()
new_doc = pdfengine.PdfDocument.load_from_memory(new_bytes, "")
new_info = document_store.add_document(
filename=doc_info["filename"], bytes_data=new_bytes, doc_instance=new_doc,
permissions=doc_info.get("permissions"),
)
return {"success": True, "newDocumentId": new_info["id"]}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
if os.path.exists(out_path):
os.remove(out_path)
+3 -25
View File
@@ -2,8 +2,9 @@ from typing import Annotated
from fastapi import APIRouter, HTTPException, Path, Query, Response, status
from app.routers.documents import FontInfoResponse
from app.schemas.font import FontInfoResponse
from app.services import engine
from app.services.font import font_info_to_response
from app.services.store import document_store
router = APIRouter(prefix="/documents/{document_id}/pages", tags=["render"])
@@ -179,30 +180,7 @@ def get_page_fonts(document_id: str, page_index: int) -> list[FontInfoResponse]:
doc = doc_info["doc_instance"]
page = doc.get_page(page_index)
fonts = page.get_fonts()
return [
FontInfoResponse(
fontName=f.font_name,
type=f.type,
isEmbedded=f.is_embedded,
isSubset=f.is_subset,
isVertical=f.is_vertical,
encoding=f.encoding,
hasToUnicode=f.has_to_unicode,
cmapName=f.cmap_name,
cidSystemInfo=f.cid_system_info,
subsetTag=f.subset_tag,
sourceType=f.source_type,
substitutedFrom=f.substituted_from,
substitutedTo=f.substituted_to,
normalizedFamily=f.normalized_family,
internalFontId=f.internal_font_id,
flags=f.flags,
ascent=f.ascent,
descent=f.descent,
capHeight=f.cap_height,
)
for f in fonts
]
return [font_info_to_response(f) for f in fonts]
except IndexError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Page index out of range")
except Exception as e:
+6
View File
@@ -0,0 +1,6 @@
"""Pydantic request/response schemas for the gateway API.
Extracted from the router modules so they can be shared across routers without
forcing cross-router imports (which previously created an import cycle between
``render`` and ``documents``).
"""
+22
View File
@@ -0,0 +1,22 @@
from pydantic import BaseModel
class AnnotationResponse(BaseModel):
id: str
type: str
x: float
y: float
width: float
height: float
color: str
author: str
content: str
timestamp: str | None = None
pageIndex: int
paths: list[list[dict[str, float]]] = []
fieldName: str | None = None
fieldValue: str | None = None
fieldType: str | None = None
fieldFlags: int | None = None
fieldOptions: list[str] | None = None
+50
View File
@@ -0,0 +1,50 @@
from pydantic import BaseModel
class PageInfoResponse(BaseModel):
index: int
width: float
height: float
class PermissionsResponse(BaseModel):
isEncrypted: bool = False
encryption: str = "None"
securityRevision: int = -1
ownerUnlocked: bool = False
canPrint: bool = True
canPrintHighRes: bool = True
canModify: bool = True
canCopy: bool = True
canAnnotate: bool = True
canFillForms: bool = True
canExtractForAccessibility: bool = True
canAssemble: bool = True
class DocumentInfoResponse(BaseModel):
id: str
filename: str
sizeBytes: int
totalPages: int
pageWidth: float
pageHeight: float
uploadedAt: str
status: str
pages: list[PageInfoResponse] = []
permissions: PermissionsResponse = PermissionsResponse()
class DocumentMetadataResponse(BaseModel):
title: str
author: str
creator: str
producer: str
creation_date: str
modification_date: str
class OutlineItemResponse(BaseModel):
title: str
pageIndex: int
level: int
+23
View File
@@ -0,0 +1,23 @@
from pydantic import BaseModel
class FontInfoResponse(BaseModel):
fontName: str
type: str
isEmbedded: bool
isSubset: bool
isVertical: bool
encoding: str
hasToUnicode: bool
cmapName: str
cidSystemInfo: str
subsetTag: str
sourceType: str
substitutedFrom: str
substitutedTo: str
normalizedFamily: str
internalFontId: str
flags: int
ascent: float
descent: float
capHeight: float
+59
View File
@@ -0,0 +1,59 @@
from pydantic import BaseModel
class GlyphModel(BaseModel):
text: str
unicode: int
font_name: str
flags: int
font_size: float
origin_x: float
origin_y: float
bbox_x: float
bbox_y: float
bbox_w: float
bbox_h: float
angle: float
page_object_index: int = -1
class TextRunModel(BaseModel):
text: str
font_name: str
flags: int
font_size: float
internal_font_id: str
is_embedded: bool
type: str
glyphs: list[GlyphModel]
x: float
y: float
w: float
h: float
object_indices: list[int] = []
color: str = "#000000"
font_fidelity: str = "exact"
class TextLineModel(BaseModel):
runs: list[TextRunModel]
baseline_y: float
x: float
y: float
w: float
h: float
class ParagraphModel(BaseModel):
lines: list[TextLineModel]
x: float
y: float
w: float
h: float
class PageModelResponse(BaseModel):
paragraphs: list[ParagraphModel]
width: float
height: float
page_index: int
+13
View File
@@ -0,0 +1,13 @@
from pydantic import BaseModel
class SearchRect(BaseModel):
x: float
y: float
w: float
h: float
class SearchMatch(BaseModel):
pageIndex: int
rects: list[SearchRect]
text: str
+12
View File
@@ -0,0 +1,12 @@
from pydantic import BaseModel
class TextObjectResponse(BaseModel):
text: str
fontName: str
fontSize: float
tm: list[float]
class UpdateTextObjectRequest(BaseModel):
new_text: str
+44
View File
@@ -0,0 +1,44 @@
"""Export pipeline: full-save + a pypdf pass that sets /NeedAppearances.
NOTE: ``pypdf`` is intentionally imported lazily inside the function. It is not a
declared gateway dependency and may only be resolvable via the roaming
site-packages path appended below, so importing it at module load would prevent
the whole app from starting on machines without it. Behavior here is preserved
verbatim from the original ``routers.documents.export_document``; if pypdf is
later added to ``pyproject.toml`` dependencies, the sys.path augmentation can be
removed and the import hoisted to module scope.
"""
def apply_need_appearances(bytes_data: bytes) -> bytes:
"""Round-trip the PDF through pypdf to set AcroForm /NeedAppearances=true."""
import os
import sys
roaming_path = os.path.join(
os.environ.get("APPDATA", "C:\\Users\\azeem\\AppData\\Roaming"),
"Python",
"Python312",
"site-packages",
)
if roaming_path not in sys.path:
sys.path.append(roaming_path)
import io
import pypdf
reader = pypdf.PdfReader(io.BytesIO(bytes_data))
writer = pypdf.PdfWriter()
writer.append(reader)
acro_form = writer.root_object.get("/AcroForm")
if acro_form is not None:
acro_form_dict = acro_form.get_object()
acro_form_dict[pypdf.generic.NameObject("/NeedAppearances")] = pypdf.generic.BooleanObject(
True
)
out_stream = io.BytesIO()
writer.write(out_stream)
return out_stream.getvalue()
+32
View File
@@ -0,0 +1,32 @@
"""Font helpers shared by the documents and render routers."""
from app.schemas.font import FontInfoResponse
def font_info_to_response(f) -> FontInfoResponse:
"""Map an engine font-info object to the API response model.
Previously this mapping was duplicated verbatim in both
``routers.documents.get_document_fonts`` and ``routers.render.get_page_fonts``.
"""
return FontInfoResponse(
fontName=f.font_name,
type=f.type,
isEmbedded=f.is_embedded,
isSubset=f.is_subset,
isVertical=f.is_vertical,
encoding=f.encoding,
hasToUnicode=f.has_to_unicode,
cmapName=f.cmap_name,
cidSystemInfo=f.cid_system_info,
subsetTag=f.subset_tag,
sourceType=f.source_type,
substitutedFrom=f.substituted_from,
substitutedTo=f.substituted_to,
normalizedFamily=f.normalized_family,
internalFontId=f.internal_font_id,
flags=f.flags,
ascent=f.ascent,
descent=f.descent,
capHeight=f.cap_height,
)
+77
View File
@@ -0,0 +1,77 @@
"""Map an engine page model to the API ``PageModelResponse``.
Extracted verbatim from ``routers.documents.get_page_model``.
"""
from app.schemas.page_model import (
GlyphModel,
PageModelResponse,
ParagraphModel,
TextLineModel,
TextRunModel,
)
def build_page_model_response(model) -> PageModelResponse:
paragraphs = []
for p in model.paragraphs:
lines = []
for line in p.lines:
runs = []
for r in line.runs:
glyphs = []
for g in r.glyphs:
glyphs.append(
GlyphModel(
text=g.text,
unicode=g.unicode,
font_name=g.font_name,
flags=g.flags,
font_size=g.font_size,
origin_x=g.origin_x,
origin_y=g.origin_y,
bbox_x=g.bbox_x,
bbox_y=g.bbox_y,
bbox_w=g.bbox_w,
bbox_h=g.bbox_h,
angle=g.angle,
page_object_index=g.page_object_index,
)
)
runs.append(
TextRunModel(
text=r.text,
font_name=r.font_name,
flags=r.flags,
font_size=r.font_size,
internal_font_id=r.internal_font_id,
is_embedded=r.is_embedded,
type=r.type,
glyphs=glyphs,
x=r.x,
y=r.y,
w=r.w,
h=r.h,
object_indices=r.object_indices,
color=getattr(r, "fill_color", "#000000") or "#000000",
font_fidelity=getattr(r, "font_fidelity", "exact") or "exact",
)
)
lines.append(
TextLineModel(
runs=runs,
baseline_y=line.baseline_y,
x=line.x,
y=line.y,
w=line.w,
h=line.h,
)
)
paragraphs.append(ParagraphModel(lines=lines, x=p.x, y=p.y, w=p.w, h=p.h))
return PageModelResponse(
paragraphs=paragraphs,
width=model.width,
height=model.height,
page_index=model.page_index,
)
+97
View File
@@ -0,0 +1,97 @@
"""Full-text search over a loaded document's glyph bounds.
Extracted verbatim from ``routers.documents.search_document`` so the router stays
a thin HTTP wrapper. The router remains responsible for the engine-availability /
document-existence checks and for translating exceptions into HTTP errors.
"""
from app.schemas.search import SearchMatch, SearchRect
def compute_search_matches(
doc,
q: str,
case_sensitive: bool = False,
whole_words: bool = False,
) -> list[SearchMatch]:
def _is_word_char(ch: str) -> bool:
return ch.isalnum() or ch == "_"
def _find_all(haystack: str, needle: str) -> list[int]:
"""Return start indices of all non-overlapping occurrences of needle in haystack."""
results: list[int] = []
start = 0
needle_len = len(needle)
while True:
pos = haystack.find(needle, start)
if pos == -1:
break
if whole_words:
before_ok = pos == 0 or not _is_word_char(haystack[pos - 1])
after_ok = (pos + needle_len) >= len(haystack) or not _is_word_char(
haystack[pos + needle_len]
)
if before_ok and after_ok:
results.append(pos)
else:
results.append(pos)
start = pos + 1
return results
matches = []
search_needle = q if case_sensitive else q.lower()
query_len = len(search_needle)
for page_idx in range(doc.page_count):
page = doc.get_page(page_idx)
glyphs = page.extract_text_with_bounds()
if not glyphs:
continue
text_str = ""
char_to_glyph: list[int] = []
for i, g in enumerate(glyphs):
s = g.get("text", "")
start_len = len(text_str)
text_str += s
for _ in range(len(text_str) - start_len):
char_to_glyph.append(i)
search_text = text_str if case_sensitive else text_str.lower()
for idx in _find_all(search_text, search_needle):
if idx + query_len - 1 >= len(char_to_glyph):
continue
start_glyph_idx = char_to_glyph[idx]
end_glyph_idx = char_to_glyph[idx + query_len - 1]
rects = []
current_rect = None
for g_idx in range(start_glyph_idx, end_glyph_idx + 1):
g = glyphs[g_idx]
dom_y = page.height - (g["y"] + g["h"])
if current_rect is None:
current_rect = {"x": g["x"], "y": dom_y, "w": g["w"], "h": g["h"]}
else:
if abs(dom_y - current_rect["y"]) < g.get("fontSize", 12) * 0.5:
max_x = max(current_rect["x"] + current_rect["w"], g["x"] + g["w"])
current_rect["w"] = max_x - current_rect["x"]
current_rect["y"] = min(current_rect["y"], dom_y)
current_rect["h"] = max(current_rect["h"], g["h"])
else:
rects.append(SearchRect(**current_rect))
current_rect = {"x": g["x"], "y": dom_y, "w": g["w"], "h": g["h"]}
if current_rect:
rects.append(SearchRect(**current_rect))
matches.append(
SearchMatch(
pageIndex=page_idx, rects=rects, text=text_str[idx : idx + query_len]
)
)
return matches
-461
View File
@@ -1,461 +0,0 @@
#include "pdf_engine_facade.hpp"
#include "wasm_rasterizer.hpp"
#include <string_view>
#include <iostream>
#include <cmath>
#include <algorithm>
WasmMockPage::WasmMockPage(int pageIndex) : m_pageIndex(pageIndex) {
m_displayList.fillRect(0.0f, 0.0f, 800.0f, 1100.0f);
float gridSpacing = 50.0f;
for (float x = gridSpacing; x < 800.0f; x += gridSpacing) {
m_displayList.fillRect(x, 0.0f, 1.0f, 1100.0f);
}
for (float y = gridSpacing; y < 1100.0f; y += gridSpacing) {
m_displayList.fillRect(0.0f, y, 800.0f, 1.0f);
}
float borderWidth = 4.0f;
m_displayList.fillRect(0.0f, 0.0f, 800.0f, borderWidth);
m_displayList.fillRect(0.0f, 1100.0f - borderWidth, 800.0f, borderWidth);
m_displayList.fillRect(0.0f, 0.0f, borderWidth, 1100.0f);
m_displayList.fillRect(800.0f - borderWidth, 0.0f, borderWidth, 1100.0f);
float centerX = 400.0f;
float centerY = 550.0f;
float size = 120.0f;
if (m_pageIndex == 0) {
m_displayList.fillRect(centerX - size / 2.0f, centerY - size / 2.0f, size, size);
} else {
m_displayList.saveState();
m_displayList.setTransform(pdfengine::Matrix(1.0f, 0.0f, 0.0f, 1.0f, centerX, centerY));
float angle = 45.0f * 3.14159265f / 180.0f;
float cosVal = std::cos(angle);
float sinVal = std::sin(angle);
m_displayList.setTransform(pdfengine::Matrix(cosVal, sinVal, -sinVal, cosVal, 0.0f, 0.0f));
m_displayList.fillRect(-size / 2.0f, -size / 2.0f, size, size);
m_displayList.restoreState();
}
m_displayList.drawText("Page " + std::to_string(m_pageIndex + 1), 60.0f, 80.0f);
}
std::expected<pdfengine::PageImage, pdfengine::EngineError> WasmMockPage::render(int dpi) const {
(void)dpi;
return std::unexpected(pdfengine::EngineError::Unknown);
}
std::expected<std::string, pdfengine::EngineError> WasmMockPage::extractText() const {
return "Mock text on page " + std::to_string(m_pageIndex + 1);
}
std::expected<std::vector<pdfengine::GlyphBounds>, pdfengine::EngineError> WasmMockPage::extractTextWithBounds() const {
std::vector<pdfengine::GlyphBounds> glyphs;
std::string text = "Page " + std::to_string(m_pageIndex + 1);
double startX = 60.0;
double startY = 80.0;
for (size_t i = 0; i < text.length(); ++i) {
pdfengine::GlyphBounds gb;
gb.text = std::string(1, text[i]);
gb.x = startX + i * 6.0;
gb.y = startY;
gb.w = 6.0;
gb.h = 8.0;
gb.fontSize = 12.0;
glyphs.push_back(gb);
}
return glyphs;
}
std::expected<pdfengine::PageModel, pdfengine::EngineError> WasmMockPage::extractDocumentModel() const {
pdfengine::PageModel model;
model.width = width();
model.height = height();
model.pageIndex = m_pageIndex;
pdfengine::Paragraph p;
p.x = 60.0;
p.y = 80.0;
p.w = 100.0;
p.h = 20.0;
pdfengine::TextLine line;
line.x = 60.0;
line.y = 80.0;
line.w = 100.0;
line.h = 20.0;
line.baselineY = 80.0;
pdfengine::TextRun run;
run.text = "Page " + std::to_string(m_pageIndex + 1);
run.fontName = "Helvetica";
run.fontSize = 12.0;
run.x = 60.0;
run.y = 80.0;
run.w = 100.0;
run.h = 20.0;
double startX = 60.0;
double startY = 80.0;
for (size_t i = 0; i < run.text.length(); ++i) {
pdfengine::Glyph g;
g.text = std::string(1, run.text[i]);
g.unicode = run.text[i];
g.fontName = "Helvetica";
g.fontSize = 12.0;
g.originX = startX + i * 6.0;
g.originY = startY;
g.bboxX = g.originX;
g.bboxY = g.originY;
g.bboxW = 6.0;
g.bboxH = 8.0;
run.glyphs.push_back(g);
}
line.runs.push_back(run);
p.lines.push_back(line);
model.paragraphs.push_back(p);
return model;
}
std::expected<std::vector<std::string>, pdfengine::EngineError> WasmMockPage::extractAnnotationsText() const {
return std::vector<std::string>();
}
std::expected<std::vector<pdfengine::PdfPage::AnnotationInfo>, pdfengine::EngineError> WasmMockPage::extractAnnotations() const {
return std::vector<pdfengine::PdfPage::AnnotationInfo>();
}
std::expected<double, pdfengine::EngineError> WasmMockPage::getGlyphWidth(const std::string& fontName, uint32_t charcode, double fontSize) const {
(void)fontName; (void)charcode;
return 6.0 * (fontSize / 12.0);
}
std::expected<std::vector<pdfengine::FontInfo>, pdfengine::EngineError> WasmMockPage::getFonts() const {
pdfengine::FontInfo info;
info.fontName = "Helvetica";
info.type = "Type1";
info.isEmbedded = false;
info.isSubset = false;
info.isVertical = false;
info.encoding = "WinAnsiEncoding";
info.hasToUnicode = false;
info.sourceType = "Substituted";
info.substitutedFrom = "Helvetica";
info.substitutedTo = "Liberation Sans Regular";
info.normalizedFamily= "Arial";
info.internalFontId = "mock-page-" + std::to_string(m_pageIndex) + "-helvetica";
info.flags = 32;
info.ascent = 718.0;
info.descent = -207.0;
info.capHeight = 718.0;
return std::vector<pdfengine::FontInfo>{info};
}
pdfengine::DevicePoint WasmMockPage::pageToDevice(const pdfengine::Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate) const noexcept {
(void)rotate;
double scaleX = static_cast<double>(deviceWidth) / width();
double scaleY = static_cast<double>(deviceHeight) / height();
int dx = static_cast<int>(pagePoint.x * scaleX);
int dy = static_cast<int>((height() - pagePoint.y) * scaleY);
return {dx, dy};
}
pdfengine::Point2D WasmMockPage::deviceToPage(const pdfengine::DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate) const noexcept {
(void)rotate;
double scaleX = width() / static_cast<double>(deviceWidth);
double scaleY = height() / static_cast<double>(deviceHeight);
double px = devicePoint.x * scaleX;
double py = height() - (devicePoint.y * scaleY);
return {px, py};
}
WasmMockDocument::WasmMockDocument(int pageCount, std::vector<uint8_t> data)
: m_pageCount(pageCount), m_data(std::move(data)) {}
pdfengine::DocumentMetadata WasmMockDocument::metadata() const noexcept {
pdfengine::DocumentMetadata meta;
meta.title = "WASM Mock Document";
meta.author = "Emscripten Engine";
meta.creator = "PdfEngine SDK";
meta.producer = "WebAssembly Facade";
meta.creationDate = "D:20260601090000";
meta.modificationDate = "D:20260601090000";
return meta;
}
std::expected<std::shared_ptr<pdfengine::PdfPage>, pdfengine::EngineError> WasmMockDocument::getPage(int pageIndex) {
if (pageIndex < 0 || pageIndex >= m_pageCount) {
return std::unexpected(pdfengine::EngineError::PageOutOfBounds);
}
auto it = m_pages.find(pageIndex);
if (it == m_pages.end()) {
auto page = std::make_shared<WasmMockPage>(pageIndex);
m_pages[pageIndex] = page;
return page;
}
return it->second;
}
std::expected<std::vector<pdfengine::FontInfo>, pdfengine::EngineError> WasmMockDocument::getFonts(int startPage, int endPage) const {
int last = (endPage < 0) ? m_pageCount - 1 : std::min(endPage, m_pageCount - 1);
std::vector<pdfengine::FontInfo> result;
for (int i = startPage; i <= last; ++i) {
bool found = false;
for (const auto& existing : result) {
if (existing.fontName == "Helvetica") { found = true; break; }
}
if (!found) {
pdfengine::FontInfo info;
info.fontName = "Helvetica";
info.type = "Type1";
info.isEmbedded = false;
info.isSubset = false;
info.isVertical = false;
info.encoding = "WinAnsiEncoding";
info.hasToUnicode = false;
info.sourceType = "Substituted";
info.substitutedFrom = "Helvetica";
info.substitutedTo = "Liberation Sans Regular";
info.normalizedFamily= "Arial";
info.internalFontId = "mock-doc-helvetica";
info.flags = 32;
info.ascent = 718.0;
info.descent = -207.0;
info.capHeight = 718.0;
result.push_back(info);
}
}
return result;
}
std::expected<void, pdfengine::EngineError> WasmMockDocument::applyEdits(const std::string& editsJson) {
(void)editsJson;
return {};
}
std::expected<std::vector<uint8_t>, pdfengine::EngineError> WasmMockDocument::saveIncremental() const {
return m_data;
}
std::expected<std::vector<uint8_t>, pdfengine::EngineError> WasmMockDocument::getFontData(const std::string& internalFontId) const {
(void)internalFontId;
return std::unexpected(pdfengine::EngineError::Unknown);
}
std::expected<std::shared_ptr<pdfengine::fonts::pdf_fonts::Font>, std::string> WasmMockDocument::getResolvedFont(const pdfengine::FontInfo& fontInfo) {
(void)fontInfo;
return std::unexpected("Not implemented in WASM mock");
}
std::expected<std::vector<uint8_t>, pdfengine::EngineError> WasmMockDocument::saveFull() const {
return m_data;
}
PdfEngineFacade::PdfEngineFacade() : m_nextHandle(1) {}
PdfEngineFacade::~PdfEngineFacade() = default;
int PdfEngineFacade::loadDocument(const uint8_t* buffer, int size) {
if (!buffer || size <= 0) {
return 0;
}
int pages = 0;
if (size > 4 && buffer[0] == '%' && buffer[1] == 'P' && buffer[2] == 'D' && buffer[3] == 'F') {
std::string_view sv(reinterpret_cast<const char*>(buffer), size);
size_t pos = 0;
while ((pos = sv.find("/Type /Page", pos)) != std::string_view::npos) {
if (pos + 11 < sv.size() && sv[pos + 11] != 's') {
pages++;
}
pos += 11;
}
}
if (pages == 0) {
pages = 3;
}
std::vector<uint8_t> docBytes(buffer, buffer + size);
auto doc = std::make_shared<WasmMockDocument>(pages, std::move(docBytes));
int handle = m_nextHandle++;
m_documents[handle] = std::move(doc);
return handle;
}
bool PdfEngineFacade::renderPage(int docHandle, int pageIndex, float scale, uint8_t* outputBuffer, int width, int height) {
auto it = m_documents.find(docHandle);
if (it == m_documents.end()) {
return false;
}
const auto& doc = it->second;
auto pageRes = doc->getPage(pageIndex);
if (!pageRes.has_value()) {
return false;
}
auto page = *pageRes;
auto mockPage = std::dynamic_pointer_cast<WasmMockPage>(page);
if (!mockPage) {
return false;
}
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
int idx = (y * width + x) * 4;
outputBuffer[idx + 0] = 250;
outputBuffer[idx + 1] = 250;
outputBuffer[idx + 2] = 245;
outputBuffer[idx + 3] = 255;
}
}
WasmRasterizer rasterizer(outputBuffer, width, height, scale);
mockPage->getDisplayList().replay(rasterizer);
return true;
}
void PdfEngineFacade::freeDocument(int docHandle) {
m_documents.erase(docHandle);
}
const char* PdfEngineFacade::buildInfo() {
return "PdfEngine WASM Facade (Phase 1/2 Enabled)";
}
bool PdfEngineFacade::hasSkia() {
return false;
}
namespace {
std::string fontInfosToJsonLocal(const std::vector<pdfengine::FontInfo>& fonts) {
std::string json = "[";
for (size_t i = 0; i < fonts.size(); ++i) {
const auto& f = fonts[i];
if (i > 0) json += ",";
json += "{";
json += "\"fontName\":\"" + f.fontName + "\",";
json += "\"type\":\"" + f.type + "\",";
json += "\"isEmbedded\":" + std::string(f.isEmbedded ? "true" : "false") + ",";
json += "\"isSubset\":" + std::string(f.isSubset ? "true" : "false") + ",";
json += "\"isVertical\":" + std::string(f.isVertical ? "true" : "false") + ",";
json += "\"encoding\":\"" + f.encoding + "\",";
json += "\"hasToUnicode\":" + std::string(f.hasToUnicode ? "true" : "false") + ",";
json += "\"cmapName\":\"" + f.cmapName + "\",";
json += "\"cidSystemInfo\":\"" + f.cidSystemInfo + "\",";
json += "\"subsetTag\":\"" + f.subsetTag + "\",";
json += "\"sourceType\":\"" + f.sourceType + "\",";
json += "\"substitutedFrom\":\"" + f.substitutedFrom + "\",";
json += "\"substitutedTo\":\"" + f.substitutedTo + "\",";
json += "\"normalizedFamily\":\"" + f.normalizedFamily + "\",";
json += "\"internalFontId\":\"" + f.internalFontId + "\",";
json += "\"ascent\":" + std::to_string(f.ascent) + ",";
json += "\"descent\":" + std::to_string(f.descent) + ",";
json += "\"capHeight\":" + std::to_string(f.capHeight);
json += "}";
}
json += "]";
return json;
}
std::string escapeJsonString(const std::string& input) {
std::string output;
for (char c : input) {
if (c == '"') output += "\\\"";
else if (c == '\\') output += "\\\\";
else if (c == '\b') output += "\\b";
else if (c == '\f') output += "\\f";
else if (c == '\n') output += "\\n";
else if (c == '\r') output += "\\r";
else if (c == '\t') output += "\\t";
else if (static_cast<unsigned char>(c) < 32) {
char buf[16];
snprintf(buf, sizeof(buf), "\\u%04x", c);
output += buf;
} else {
output += c;
}
}
return output;
}
std::string glyphBoundsToJsonLocal(const std::vector<pdfengine::GlyphBounds>& glyphs) {
std::string json = "[";
for (size_t i = 0; i < glyphs.size(); ++i) {
const auto& g = glyphs[i];
if (i > 0) json += ",";
json += "{";
json += "\"text\":\"" + escapeJsonString(g.text) + "\",";
json += "\"x\":" + std::to_string(g.x) + ",";
json += "\"y\":" + std::to_string(g.y) + ",";
json += "\"w\":" + std::to_string(g.w) + ",";
json += "\"h\":" + std::to_string(g.h) + ",";
json += "\"fontSize\":" + std::to_string(g.fontSize);
json += "}";
}
json += "]";
return json;
}
}
std::string PdfEngineFacade::getDocumentFonts(int docHandle, int startPage, int endPage) {
auto it = m_documents.find(docHandle);
if (it == m_documents.end()) {
return "[]";
}
auto result = it->second->getFonts(startPage, endPage);
if (!result.has_value()) {
return "[]";
}
return fontInfosToJsonLocal(*result);
}
std::string PdfEngineFacade::getPageFonts(int docHandle, int pageIndex) {
auto it = m_documents.find(docHandle);
if (it == m_documents.end()) {
return "[]";
}
auto pageRes = it->second->getPage(pageIndex);
if (!pageRes.has_value()) {
return "[]";
}
auto result = (*pageRes)->getFonts();
if (!result.has_value()) {
return "[]";
}
return fontInfosToJsonLocal(*result);
}
std::string PdfEngineFacade::getPageTextJson(int docHandle, int pageIndex) {
auto it = m_documents.find(docHandle);
if (it == m_documents.end()) {
return "[]";
}
auto pageRes = it->second->getPage(pageIndex);
if (!pageRes.has_value()) {
return "[]";
}
auto result = (*pageRes)->extractTextWithBounds();
if (!result.has_value()) {
return "[]";
}
return glyphBoundsToJsonLocal(*result);
}
-80
View File
@@ -1,80 +0,0 @@
#pragma once
#include <pdfengine/pdf_document.hpp>
#include <pdfengine/display_list.hpp>
#include <vector>
#include <unordered_map>
#include <memory>
#include <cstdint>
#include <string>
class WasmMockPage : public pdfengine::PdfPage {
public:
explicit WasmMockPage(int pageIndex);
~WasmMockPage() override = default;
[[nodiscard]] double width() const noexcept override { return 800.0; }
[[nodiscard]] double height() const noexcept override { return 1100.0; }
[[nodiscard]] std::expected<pdfengine::PageImage, pdfengine::EngineError> render(int dpi = 96) const override;
[[nodiscard]] std::expected<std::string, pdfengine::EngineError> extractText() const override;
[[nodiscard]] std::expected<std::vector<pdfengine::GlyphBounds>, pdfengine::EngineError> extractTextWithBounds() const override;
[[nodiscard]] std::expected<pdfengine::PageModel, pdfengine::EngineError> extractDocumentModel() const override;
[[nodiscard]] std::expected<std::vector<pdfengine::FontInfo>, pdfengine::EngineError> getFonts() const override;
[[nodiscard]] std::expected<std::vector<std::string>, pdfengine::EngineError> extractAnnotationsText() const override;
[[nodiscard]] std::expected<std::vector<pdfengine::PdfPage::AnnotationInfo>, pdfengine::EngineError> extractAnnotations() const override;
[[nodiscard]] std::expected<double, pdfengine::EngineError> getGlyphWidth(const std::string& fontName, uint32_t charcode, double fontSize) const override;
[[nodiscard]] pdfengine::DevicePoint pageToDevice(const pdfengine::Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept override;
[[nodiscard]] pdfengine::Point2D deviceToPage(const pdfengine::DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept override;
[[nodiscard]] const pdfengine::DisplayList& getDisplayList() const { return m_displayList; }
private:
int m_pageIndex;
pdfengine::DisplayList m_displayList;
};
class WasmMockDocument : public pdfengine::PdfDocument {
public:
WasmMockDocument(int pageCount, std::vector<uint8_t> data);
~WasmMockDocument() override = default;
[[nodiscard]] int pageCount() const noexcept override { return m_pageCount; }
[[nodiscard]] pdfengine::DocumentMetadata metadata() const noexcept override;
[[nodiscard]] std::expected<std::vector<OutlineItem>, pdfengine::EngineError> extractOutline() const override { return std::vector<OutlineItem>{}; }
[[nodiscard]] std::expected<std::shared_ptr<pdfengine::PdfPage>, pdfengine::EngineError> getPage(int pageIndex) override;
[[nodiscard]] std::expected<std::vector<pdfengine::FontInfo>, pdfengine::EngineError> getFonts(int startPage = 0, int endPage = -1) const override;
std::expected<void, pdfengine::EngineError> applyEdits(const std::string& editsJson) override;
[[nodiscard]] std::expected<std::vector<uint8_t>, pdfengine::EngineError> saveIncremental() const override;
[[nodiscard]] std::expected<std::vector<uint8_t>, pdfengine::EngineError> getFontData(const std::string& internalFontId) const override;
[[nodiscard]] std::expected<std::shared_ptr<pdfengine::fonts::pdf_fonts::Font>, std::string> getResolvedFont(const pdfengine::FontInfo& fontInfo) override;
[[nodiscard]] std::expected<std::vector<uint8_t>, pdfengine::EngineError> saveFull() const override;
private:
int m_pageCount;
std::vector<uint8_t> m_data;
std::unordered_map<int, std::shared_ptr<WasmMockPage>> m_pages;
};
class PdfEngineFacade {
public:
PdfEngineFacade();
~PdfEngineFacade();
int loadDocument(const uint8_t* buffer, int size);
bool renderPage(int docHandle, int pageIndex, float scale, uint8_t* outputBuffer, int width, int height);
void freeDocument(int docHandle);
std::string getDocumentFonts(int docHandle, int startPage, int endPage);
std::string getPageFonts(int docHandle, int pageIndex);
std::string getPageTextJson(int docHandle, int pageIndex);
static const char* buildInfo();
static bool hasSkia();
private:
int m_nextHandle;
std::unordered_map<int, std::shared_ptr<pdfengine::PdfDocument>> m_documents;
};
+11 -2
View File
@@ -10,11 +10,16 @@
namespace {
struct DocEntry {
std::shared_ptr<pdfengine::PdfDocument> doc;
std::vector<uint8_t> bytes;
std::vector<uint8_t> bytes;
std::unordered_map<std::string, std::vector<uint8_t>> auxFonts;
};
std::unordered_map<int, DocEntry> g_docs;
int g_nextHandle = 1;
void applyAuxFonts(pdfengine::PdfDocument& fresh, const DocEntry& entry) {
for (const auto& [fid, bytes] : entry.auxFonts) fresh.registerAuxFont(fid, bytes);
}
std::vector<uint8_t> g_lastPng;
int g_lastW = 0, g_lastH = 0;
std::string g_lastLayout;
@@ -63,6 +68,7 @@ EMSCRIPTEN_KEEPALIVE int previewRender(int handle, int pageIndex, int dpi, const
if (it == g_docs.end()) return -1;
auto fresh = pdfengine::PdfDocument::loadFromMemory(it->second.bytes, "");
if (!fresh) return -1;
applyAuxFonts(**fresh, it->second);
g_lastLayout.clear();
if (editsJson && editsJson[0]) {
auto r = (*fresh)->applyEdits(editsJson);
@@ -78,6 +84,7 @@ EMSCRIPTEN_KEEPALIVE int previewRenderRegion(int handle, int pageIndex, int dpi,
if (it == g_docs.end()) return -1;
auto fresh = pdfengine::PdfDocument::loadFromMemory(it->second.bytes, "");
if (!fresh) return -1;
applyAuxFonts(**fresh, it->second);
g_lastLayout.clear();
if (editsJson && editsJson[0]) {
auto r = (*fresh)->applyEdits(editsJson);
@@ -100,6 +107,7 @@ EMSCRIPTEN_KEEPALIVE int previewRenderPaginated(int handle, int pageIndex, int d
if (it == g_docs.end()) return -1;
auto fresh = pdfengine::PdfDocument::loadFromMemory(it->second.bytes, "");
if (!fresh) return -1;
applyAuxFonts(**fresh, it->second);
g_lastLayout.clear();
g_regions.clear();
bool overflowed = false;
@@ -149,10 +157,11 @@ EMSCRIPTEN_KEEPALIVE void registerAuxFont(int handle, const char* fid, const uin
if (it == g_docs.end() || !fid || !data || size <= 0) return;
std::vector<uint8_t> bytes(data, data + size);
it->second.doc->registerAuxFont(std::string(fid), bytes);
it->second.auxFonts[std::string(fid)] = std::move(bytes);
}
EMSCRIPTEN_KEEPALIVE void freeDocument(int handle) { g_docs.erase(handle); }
EMSCRIPTEN_KEEPALIVE const char* engineBuildInfo() { return "pdfengine-wasm+pdfium"; }
}
}