2026-06-29 10:51:04 +05:30
|
|
|
#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;
|
2026-06-30 21:18:46 +05:30
|
|
|
|
|
|
|
|
std::shared_ptr<fonts::FontFace> measureFace;
|
|
|
|
|
{
|
|
|
|
|
std::vector<uint8_t> mb;
|
|
|
|
|
if (auto perObj = getFontDataFromObjects(pageIndex, objectIndices, internalFontId);
|
|
|
|
|
perObj.has_value() && !perObj->empty()) mb = std::move(*perObj);
|
|
|
|
|
else if (!internalFontId.empty()) {
|
|
|
|
|
if (auto fd = getFontData(internalFontId); fd.has_value() && !fd->empty())
|
|
|
|
|
mb = std::move(fd.value());
|
|
|
|
|
}
|
|
|
|
|
if (!mb.empty()) {
|
|
|
|
|
auto mf = std::make_shared<fonts::FontFace>();
|
|
|
|
|
if (mf->loadFromMemory(mb)) measureFace = mf;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 10:51:04 +05:30
|
|
|
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;
|
2026-06-30 21:18:46 +05:30
|
|
|
constexpr unsigned int kRefMeasure = 1000;
|
|
|
|
|
fonts::FontFace* mface = measureFace ? measureFace.get()
|
|
|
|
|
: (resolvedFont ? &resolvedFont->getFontFace() : nullptr);
|
|
|
|
|
if (mface) {
|
2026-06-29 10:51:04 +05:30
|
|
|
try {
|
|
|
|
|
fonts::HbShaper shaper;
|
2026-06-30 21:18:46 +05:30
|
|
|
auto shapedGlyphs = shaper.shapeRun(newText, *mface, kRefMeasure);
|
2026-06-29 10:51:04 +05:30
|
|
|
if (!shapedGlyphs.empty()) {
|
2026-06-30 21:18:46 +05:30
|
|
|
double sum = 0.0;
|
|
|
|
|
for (const auto& sg : shapedGlyphs) sum += sg.advanceX;
|
|
|
|
|
totalWidth = sum * (fontSize > 0.0 ? fontSize : 12.0) / static_cast<double>(kRefMeasure);
|
2026-06-29 10:51:04 +05:30
|
|
|
shapedSuccessful = true;
|
2026-06-30 21:18:46 +05:30
|
|
|
spdlog::info("Font Engine: measured {} chars -> width {:.2f} ({} face)",
|
|
|
|
|
newText.size(), totalWidth, measureFace ? "embedded" : "resolved");
|
2026-06-29 10:51:04 +05:30
|
|
|
}
|
|
|
|
|
} catch (const std::exception& e) {
|
2026-06-30 21:18:46 +05:30
|
|
|
spdlog::warn("Font Engine: shaping failed: {}", e.what());
|
2026-06-29 10:51:04 +05:30
|
|
|
} catch (...) {
|
2026-06-30 21:18:46 +05:30
|
|
|
spdlog::warn("Font Engine: shaping failed (unknown)");
|
2026-06-29 10:51:04 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2026-06-30 21:18:46 +05:30
|
|
|
bool sawSibling = false;
|
|
|
|
|
bool hasFollowingText = false;
|
|
|
|
|
double rowTolFollow = (std::max)(4.0, (origTop - origBottom) * 0.6);
|
2026-06-29 10:51:04 +05:30
|
|
|
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; }
|
2026-06-30 21:18:46 +05:30
|
|
|
if (std::abs((bo + tt) / 2.0 - origCenterY) <= rowTolFollow && l > origRight + 1.0)
|
|
|
|
|
hasFollowingText = true;
|
2026-06-29 10:51:04 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
double justifyTol = (std::max)(4.0, (colRight - origLeft) * 0.02);
|
|
|
|
|
bool wasJustified = !disableJustify && axisAligned && resolvedFont && hasOrigBounds && sawSibling &&
|
2026-06-30 21:18:46 +05:30
|
|
|
!hasFollowingText &&
|
2026-06-29 10:51:04 +05:30
|
|
|
(colRight - origLeft) > 20.0 && (origRight >= colRight - justifyTol);
|
|
|
|
|
|
2026-06-30 21:18:46 +05:30
|
|
|
bool doSiblingShift = !wasJustified && hasOrigBounds;
|
2026-06-29 10:51:04 +05:30
|
|
|
|
|
|
|
|
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;
|
2026-06-30 21:18:46 +05:30
|
|
|
bool reconFullyCovered = false;
|
2026-06-29 10:51:04 +05:30
|
|
|
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) {
|
2026-06-30 21:18:46 +05:30
|
|
|
reconFullyCovered = true;
|
2026-06-29 10:51:04 +05:30
|
|
|
for (uint32_t cp : unicodeCodepoints)
|
2026-06-30 21:18:46 +05:30
|
|
|
if (cp >= 0x20 && !reconRf->coveredUnicode.count(cp)) { reconFullyCovered = false; break; }
|
2026-06-29 10:51:04 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-30 21:18:46 +05:30
|
|
|
if (!font && reconFont && reconFullyCovered) {
|
|
|
|
|
font = reconFont;
|
|
|
|
|
spdlog::info("Tier-2: replace_text reconstructed fallback '{}'", matchedFontInfo->internalFontId);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 10:51:04 +05:30
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-30 21:18:46 +05:30
|
|
|
|
|
|
|
|
if (font && useEmbedded) reconFont = nullptr;
|
2026-06-29 10:51:04 +05:30
|
|
|
|
2026-06-30 21:18:46 +05:30
|
|
|
std::vector<FPDF_PAGEOBJECT> emittedObjs;
|
2026-06-29 10:51:04 +05:30
|
|
|
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);
|
2026-06-30 21:18:46 +05:30
|
|
|
emittedObjs.push_back(wobj);
|
2026-06-29 10:51:04 +05:30
|
|
|
}
|
|
|
|
|
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);
|
2026-06-30 21:18:46 +05:30
|
|
|
emittedObjs.push_back(obj);
|
2026-06-29 10:51:04 +05:30
|
|
|
}
|
|
|
|
|
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);
|
2026-06-30 21:18:46 +05:30
|
|
|
emittedObjs.push_back(newTextObj);
|
2026-06-29 10:51:04 +05:30
|
|
|
} else {
|
|
|
|
|
spdlog::error("Failed to create new text object");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-30 21:18:46 +05:30
|
|
|
if (doSiblingShift) {
|
|
|
|
|
double shift = totalWidth - origWidth;
|
|
|
|
|
if (std::abs(shift) > 0.05) {
|
|
|
|
|
double rowTol = (std::max)(5.0, fontSize * 0.5);
|
|
|
|
|
int nObj = FPDFPage_CountObjects(page);
|
|
|
|
|
int moved = 0;
|
|
|
|
|
for (int k = 0; k < nObj; ++k) {
|
|
|
|
|
FPDF_PAGEOBJECT o = FPDFPage_GetObject(page, k);
|
|
|
|
|
if (!o || FPDFPageObj_GetType(o) != FPDF_PAGEOBJ_TEXT) continue;
|
|
|
|
|
if (std::find(emittedObjs.begin(), emittedObjs.end(), o) != emittedObjs.end()) 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 - 2.0f) {
|
|
|
|
|
FPDFPageObj_Transform(o, 1.0, 0.0, 0.0, 1.0, shift, 0.0);
|
|
|
|
|
moved++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
spdlog::info("replace_text: sibling shift {} obj by {:.2f} (deltaX = newW {:.2f} - origW {:.2f})",
|
|
|
|
|
moved, shift, totalWidth, origWidth);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 10:51:04 +05:30
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|