Files
pdf/engine/src/parser/pdfium_edit_replace.cpp
T

631 lines
35 KiB
C++

#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;
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;
}
}
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;
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<double>(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<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;
bool reconFullyCovered = false;
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) {
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<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 && 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<std::mutex> lock(loadedFontsMutex_);
loadedFontsCache_[cacheKey] = font;
}
}
if (font && useEmbedded) reconFont = nullptr;
std::vector<FPDF_PAGEOBJECT> emittedObjs;
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);
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<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);
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<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);
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
}
}