#include "parser/pdfium_internal.hpp" namespace pdfengine::parser { std::expected PdfiumDocument::applyOp_replaceText(const nlohmann::json& op, int pageIndex) { #ifdef PDFENGINE_WITH_PDFIUM std::vector 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()); } } newText = data.value("text", ""); internalFontId = data.value("internalFontId", ""); if (data.contains("fontSize")) { fontSize = data["fontSize"].get(); } disableJustify = data.value("disableJustify", false); } else { if (op.contains("objectIndices") && op["objectIndices"].is_array()) { for (auto& idx : op["objectIndices"]) { objectIndices.push_back(idx.get()); } } newText = op.value("text", ""); internalFontId = op.value("internalFontId", ""); if (op.contains("fontSize")) { fontSize = op["fontSize"].get(); } } 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 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(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 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(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(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 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 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; std::shared_ptr measureFace; { std::vector 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(); if (mf->loadFromMemory(mb)) measureFace = mf; } } auto utf16 = utf8_to_utf16le(newText); std::vector 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; constexpr unsigned int kRefMeasure = 1000; fonts::FontFace* mface = measureFace ? measureFace.get() : (resolvedFont ? &resolvedFont->getFontFace() : nullptr); if (mface) { try { fonts::HbShaper shaper; auto shapedGlyphs = shaper.shapeRun(newText, *mface, kRefMeasure); if (!shapedGlyphs.empty()) { double sum = 0.0; for (const auto& sg : shapedGlyphs) sum += sg.advanceX; totalWidth = sum * (fontSize > 0.0 ? fontSize : 12.0) / static_cast(kRefMeasure); shapedSuccessful = true; spdlog::info("Font Engine: measured {} chars -> width {:.2f} ({} face)", newText.size(), totalWidth, measureFace ? "embedded" : "resolved"); } } catch (const std::exception& e) { spdlog::warn("Font Engine: shaping failed: {}", e.what()); } catch (...) { spdlog::warn("Font Engine: shaping failed (unknown)"); } } 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; bool hasFollowingText = false; double rowTolFollow = (std::max)(4.0, (origTop - origBottom) * 0.6); 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; } if (std::abs((bo + tt) / 2.0 - origCenterY) <= rowTolFollow && l > origRight + 1.0) hasFollowingText = true; } } double justifyTol = (std::max)(4.0, (colRight - origLeft) * 0.02); bool wasJustified = !disableJustify && axisAligned && resolvedFont && hasOrigBounds && sawSibling && !hasFollowingText && (colRight - origLeft) > 20.0 && (origRight >= colRight - justifyTol); bool doSiblingShift = !wasJustified && hasOrigBounds; 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 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; bool reconFullyCovered = false; if (matchedFontInfo && matchedFontInfo->isEmbedded && classifyFontFidelity(*matchedFontInfo) != "exact") { reconRf = lookupReconFont(matchedFontInfo->internalFontId); if (reconRf && reconRf->ok) { std::lock_guard 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(bytes.size()), FPDF_FONT_TRUETYPE, true); if (reconFont) loadedFontsCache_[rkey] = reconFont; } if (reconFont) { reconFullyCovered = true; for (uint32_t cp : unicodeCodepoints) if (cp >= 0x20 && !reconRf->coveredUnicode.count(cp)) { reconFullyCovered = false; break; } } } } if (!font) { if (useEmbedded) { auto fontDataRes = getFontData(matchedFontInfo->internalFontId); if (fontDataRes.has_value()) { std::lock_guard lock(loadedFontsMutex_); loadedFontDataBuffers_[cacheKey] = fontDataRes.value(); const auto& bytes = loadedFontDataBuffers_[cacheKey]; font = FPDFText_LoadFont(doc_, bytes.data(), static_cast(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 fileBytes((std::istreambuf_iterator(fs)), std::istreambuf_iterator()); if (!fileBytes.empty()) { const size_t fullSize = fileBytes.size(); std::lock_guard lock(loadedFontsMutex_); loadedFontDataBuffers_[cacheKey] = std::move(fileBytes); const auto& bytes = loadedFontDataBuffers_[cacheKey]; font = FPDFText_LoadFont(doc_, bytes.data(), static_cast(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 && reconFont && reconFullyCovered) { font = reconFont; spdlog::info("Tier-2: replace_text reconstructed fallback '{}'", matchedFontInfo->internalFontId); } 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 lock(loadedFontsMutex_); loadedFontsCache_[cacheKey] = font; } } if (font && useEmbedded) reconFont = nullptr; std::vector emittedObjs; if (font) { constexpr unsigned int kRef = 1000; double emToPage = (fontSize > 0.0 ? fontSize : 1.0) * a / static_cast(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 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& u16le) -> double { FPDF_PAGEOBJECT m = FPDFPageObj_CreateTextObj(doc_, font, static_cast(fontSize)); if (!m) return 0.0; FPDFText_SetText(m, reinterpret_cast(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 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(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(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(wu.data())); FPDFPageObj_Transform(wobj, a, b, c, d, penX, f); FPDFPage_InsertObjectAtIndex(page, wobj, minIndex); emittedObjs.push_back(wobj); } 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 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& dst){ if (cp <= 0xFFFF) dst.push_back(static_cast(cp)); else { cp -= 0x10000; dst.push_back(static_cast(0xD800 + (cp >> 10))); dst.push_back(static_cast(0xDC00 + (cp & 0x3FF))); } }; auto cpToU8 = [](uint32_t cp, std::string& d){ if (cp < 0x80) d += static_cast(cp); else if (cp < 0x800) { d += static_cast(0xC0 | (cp >> 6)); d += static_cast(0x80 | (cp & 0x3F)); } else if (cp < 0x10000) { d += static_cast(0xE0 | (cp >> 12)); d += static_cast(0x80 | ((cp >> 6) & 0x3F)); d += static_cast(0x80 | (cp & 0x3F)); } else { d += static_cast(0xF0 | (cp >> 18)); d += static_cast(0x80 | ((cp >> 12) & 0x3F)); d += static_cast(0x80 | ((cp >> 6) & 0x3F)); d += static_cast(0x80 | (cp & 0x3F)); } }; double penX = e; size_t i = 0; while (i < unicodeCodepoints.size()) { bool cov = isCov(unicodeCodepoints[i]); std::vector 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(fontSize)); if (obj) { FPDFPageObj_SetFillColor(obj, r, g, b_color, a_color); FPDFTextObj_SetTextRenderMode(obj, renderMode); FPDFText_SetText(obj, reinterpret_cast(seg.data())); FPDFPageObj_Transform(obj, a, b, c, d, penX, f); FPDFPage_InsertObjectAtIndex(page, obj, minIndex); emittedObjs.push_back(obj); } 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(fontSize)); if (newTextObj) { FPDFPageObj_SetFillColor(newTextObj, r, g, b_color, a_color); FPDFTextObj_SetTextRenderMode(newTextObj, renderMode); FPDFText_SetText(newTextObj, reinterpret_cast(utf16.data())); FPDFPageObj_Transform(newTextObj, aScale, b, c, d, e, f); FPDFPage_InsertObjectAtIndex(page, newTextObj, minIndex); emittedObjs.push_back(newTextObj); } else { spdlog::error("Failed to create new text object"); } } } } 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); } } 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 } }