#include "parser/pdfium_internal.hpp" #include "fonts/face/free_type_manager.hpp" #include "fonts/face/font_face.hpp" #include "pdfengine/text_layout_engine.hpp" namespace pdfengine::parser { std::expected 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 advances; // When set (and advances.size() == advanceSeedText.size()), advances are // metrics for advanceSeedText; merge onto text via LCP/LCS so typing // preserves kerning on the unchanged prefix/suffix. std::string advanceSeedText; }; std::vector 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(hv(hex[1]) * 16 + hv(hex[2])); g = static_cast(hv(hex[3]) * 16 + hv(hex[4])); b = static_cast(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", 0.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()); } rs.advanceSeedText = rj.value("advanceSeedText", ""); runs.push_back(std::move(rs)); }; std::vector> providedLines; bool hasProvidedLines = data.contains("lines") && data["lines"].is_array() && !data["lines"].empty(); if (hasProvidedLines) { for (const auto& lineJson : data["lines"]) { std::vector lineRunIdxs; if (lineJson.is_array()) { for (const auto& rj : lineJson) { lineRunIdxs.push_back(static_cast(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 objectIndices; if (data.contains("objectIndices") && data["objectIndices"].is_array()) { for (auto& idx : data["objectIndices"]) objectIndices.push_back(idx.get()); } 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 lineBaselineY, lineX; if (data.contains("lineBaselineY") && data["lineBaselineY"].is_array()) for (const auto& v : data["lineBaselineY"]) lineBaselineY.push_back(v.get()); if (data.contains("lineX") && data["lineX"].is_array()) for (const auto& v : data["lineX"]) lineX.push_back(v.get()); double columnWidth = columnRight - columnLeft; double pushColumnLeft = data.value("pushColumnLeft", columnLeft); double hangingIndent = data.value("hangingIndent", 0.0); std::string paraId; if (data.contains("paraId") && data["paraId"].is_string()) paraId = data["paraId"].get(); 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 anchoredCentersStart = startBand.valid ? repagAnchoredCenters(page, pushColumnLeft, columnRight, bottomLimitY) : std::vector{}; 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 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); } double exactNominal = 0.0; double exactScaleX = 1.0; double exactScaleY = 1.0; for (int idx : paragraphSet) { FPDF_PAGEOBJECT o = FPDFPage_GetObject(page, idx); if (!o || FPDFPageObj_GetType(o) != FPDF_PAGEOBJ_TEXT) continue; float nom = 0; if (FPDFTextObj_GetFontSize(o, &nom) && nom > 0.1) { FS_MATRIX mtx; if (FPDFPageObj_GetMatrix(o, &mtx)) { exactScaleX = std::sqrt(static_cast(mtx.a) * mtx.a + static_cast(mtx.b) * mtx.b); exactScaleY = std::sqrt(static_cast(mtx.c) * mtx.c + static_cast(mtx.d) * mtx.d); exactNominal = nom; break; } } } double textAspect = 1.0; if (exactNominal > 0.1 && exactScaleY > 0.001) { double trueVerticalSize = exactNominal * exactScaleY; for (auto& rs : runs) { spdlog::info("[STAGE_3_WASM_INPUT] fontSize={:.2f}, trueVerticalSize={:.2f}, exactNominal={:.2f}, exactScaleY={:.4f}", rs.fontSize, trueVerticalSize, exactNominal, exactScaleY); if (rs.fontSize <= 0.0 || rs.fontSize < trueVerticalSize * 0.85) { rs.fontSize = trueVerticalSize; } } textAspect = exactScaleX / exactScaleY; } else { for (auto& rs : runs) { if (rs.fontSize <= 0.0) { rs.fontSize = 12.0; } } } spdlog::info("[FONT_METRICS_DEBUG] exactNominal={:.2f}, exactScaleX={:.2f}, exactScaleY={:.2f}, trueVerticalSize={:.2f}, textAspect={:.2f}", exactNominal, exactScaleX, exactScaleY, exactNominal * exactScaleY, textAspect); std::vector runFonts(runs.size()); // utf8_to_utf16le appends a trailing NUL for PDFium wide-string APIs. // That terminator is not text content and must not enter coverage checks. auto toCodepoints = [](const std::string& s) { auto u16 = utf8_to_utf16le(s); std::vector 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; if (cp == 0) continue; cps.push_back(cp); } return cps; }; { std::unordered_map> 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 nb(nl); if (FPDFFont_GetBaseFontName(fo, nb.data(), nl) == 0) continue; if (std::string(nb.data()) == expected) return fo; } return nullptr; }; std::unordered_map 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 { std::vector 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 * textAspect) / static_cast(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(text.size()); for (auto& v : out) v = per; return out; } for (auto& v : out) v = size * 0.5; return out; }; // Forensic flag: force whole-run emission while keeping client advances. // Set data.forensicForceWholeRun=true to isolate whether runPerChar causes the visual change. const bool forensicForceWholeRun = data.value("forensicForceWholeRun", false); std::vector> runCharAdv(runs.size()); std::vector runPerChar(runs.size(), 0); // Merge seed advances onto edited text: keep LCP/LCS metrics, HB-fill the edit middle. // (Logic inlined in the run loop so unchanged glyphs are never passed to HarfBuzz.) for (size_t ri = 0; ri < runs.size(); ++ri) { const auto& rs = runs[ri]; bool usedClient = false; // Prefer client/seed advances for UNCHANGED glyphs. Only HarfBuzz-shape // characters that are not covered by seed metrics (the edited middle). if (rs.advances.size() == rs.text.size() && !rs.text.empty()) { runCharAdv[ri] = rs.advances; usedClient = true; } else if (!rs.advanceSeedText.empty() && rs.advances.size() == rs.advanceSeedText.size() && !rs.text.empty()) { // Shape only the middle gap; prefix/suffix keep seed advances. size_t p = 0; const auto& seed = rs.advanceSeedText; while (p < seed.size() && p < rs.text.size() && seed[p] == rs.text[p]) ++p; size_t s = 0; while (s < seed.size() - p && s < rs.text.size() - p && seed[seed.size() - 1 - s] == rs.text[rs.text.size() - 1 - s]) ++s; runCharAdv[ri].assign(rs.text.size(), 0.0); for (size_t i = 0; i < p; ++i) runCharAdv[ri][i] = rs.advances[i]; for (size_t i = 0; i < s; ++i) runCharAdv[ri][rs.text.size() - 1 - i] = rs.advances[seed.size() - 1 - i]; if (p + s < rs.text.size()) { std::string middle = rs.text.substr(p, rs.text.size() - p - s); auto midNat = perCharAdvances(ri, middle); for (size_t i = 0; i < midNat.size(); ++i) runCharAdv[ri][p + i] = midNat[i]; } usedClient = true; spdlog::info("[ADVANCE_SEED_MERGE] run={} seedLen={} textLen={} " "prefixKept={} suffixKept={} (unchanged glyphs not reshaped)", ri, seed.size(), rs.text.size(), p, s); } else if (!rs.advances.empty() && !rs.text.empty() && rs.advances.size() < rs.text.size()) { // Prefix-only advances (append without advanceSeedText). runCharAdv[ri] = perCharAdvances(ri, rs.text); for (size_t c = 0; c < rs.advances.size(); ++c) runCharAdv[ri][c] = rs.advances[c]; usedClient = true; spdlog::info("[ADVANCE_PREFIX_KEEP] run={} prefixAdv={} textLen={} " "(unchanged prefix kept; only suffix reshaped)", ri, rs.advances.size(), rs.text.size()); } else if (!rs.advances.empty() && !rs.text.empty() && rs.advances.size() > rs.text.size()) { // Truncate (end-delete without advanceSeedText). runCharAdv[ri].assign(rs.advances.begin(), rs.advances.begin() + static_cast(rs.text.size())); usedClient = true; } else { // FIRST MUTATION SITE when client advances are missing/mismatched: // HarfBuzz recomputes advances for EVERY glyph, including unchanged ones. runCharAdv[ri] = perCharAdvances(ri, rs.text); spdlog::warn("[ADVANCE_RECOMPUTE_ALL] run={} textLen={} advLen={} " "FIRST_MUTATION=perCharAdvances full reshape (no client advances)", ri, rs.text.size(), rs.advances.size()); } if (usedClient) { // Client advances (esp. PDF TJ kerning) diverge from HarfBuzz naturals. // Emit per-glyph so those advances are applied; do NOT replace them. auto natural = perCharAdvances(ri, rs.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] = (forensicForceWholeRun ? 0 : (diverges ? 1 : 0)); spdlog::info("[FORENSIC_RUNPERCHAR] run={} textLen={} advLen={} seedLen={} diverges={} forceWhole={} runPerChar={} natural0={:.4f} client0={:.4f} measureFace={}", ri, rs.text.size(), rs.advances.size(), rs.advanceSeedText.size(), diverges, forensicForceWholeRun, (int)runPerChar[ri], natural.empty() ? -1.0 : natural[0], runCharAdv[ri].empty() ? -1.0 : runCharAdv[ri][0], (bool)(runFonts[ri].measureFace)); } else { spdlog::info("[FORENSIC_RUNPERCHAR] run={} textLen={} advLen={} -> recomputed advances (no client match) runPerChar=0 forceWhole={}", ri, rs.text.size(), rs.advances.size(), forensicForceWholeRun); } } auto charAdvAt = [&](int ri, size_t off) -> double { const auto& v = runCharAdv[static_cast(ri)]; return off < v.size() ? v[off] : 0.0; }; auto isSpace = [](char ch) { return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'; }; constexpr size_t kHardBreak = static_cast(-1); struct Seg { int runIdx; std::string text; double width; size_t off; }; struct Word { std::vector segs; double width; double spaceAfter; }; std::vector words; auto tokenize = [&](const std::vector& runIdxs) -> std::vector { std::string ft; std::vector sof; std::vector 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 out; size_t i = 0; while (i < ft.size()) { if (isSpace(ft[i])) { if (ft[i] == '\n') out.push_back(kHardBreak); 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; } size_t newlines = 0; while (i < ft.size() && isSpace(ft[i])) { if (ft[i] == '\n') newlines++; else if (newlines == 0) w.spaceAfter += charAdvAt(sof[i], cof[i]); i++; } out.push_back(words.size()); words.push_back(std::move(w)); for (size_t n = 0; n < newlines; ++n) out.push_back(kHardBreak); } return out; }; std::vector> lines; std::vector lineCont; std::vector lineSegEnd; if (hasProvidedLines) { for (const auto& lineRunIdxs : providedLines) { auto wi = tokenize(lineRunIdxs); wi.erase(std::remove(wi.begin(), wi.end(), kHardBreak), wi.end()); if (!wi.empty()) { lines.push_back(std::move(wi)); lineCont.push_back(0); lineSegEnd.push_back(0); } } for (size_t li = 0; li < lineSegEnd.size(); ++li) lineSegEnd[li] = (li + 1 == lineSegEnd.size()) ? 1 : 0; } else { std::vector allRuns(runs.size()); for (size_t ri = 0; ri < runs.size(); ++ri) allRuns[ri] = static_cast(ri); auto allWords = tokenize(allRuns); std::vector cur; double curW = 0.0; size_t prevWord = kHardBreak; bool firstLineOfSeg = true; auto pushLine = [&](char segEnd) { lines.push_back(cur); lineCont.push_back(firstLineOfSeg ? 0 : 1); lineSegEnd.push_back(segEnd); }; spdlog::info("[KEYSTROKE_BACKEND_DEBUG] columnLeft={:.2f}, columnRight={:.2f}, columnWidth={:.2f}, firstBaselineY={:.2f}, oldLineCount={}, hangingIndent={:.2f}", columnLeft, columnRight, columnWidth, firstBaselineY, oldLineCount, hangingIndent); for (size_t k = 0; k < allWords.size(); ++k) { size_t wi = allWords[k]; if (wi == kHardBreak) { // forced break: end the current line (may be blank) pushLine(1); cur.clear(); curW = 0.0; prevWord = kHardBreak; firstLineOfSeg = true; continue; } double effW = columnWidth - (firstLineOfSeg ? 0.0 : hangingIndent); double gap = (cur.empty() || prevWord == kHardBreak) ? 0.0 : words[prevWord].spaceAfter; if (!cur.empty() && curW + gap + words[wi].width > effW) { spdlog::info("[KEYSTROKE_BACKEND_WRAP] Wrapped at word index {} ('{}'): curW={:.2f}, gap={:.2f}, wordW={:.2f}, sum={:.2f} > effW={:.2f} (columnWidth={:.2f})", wi, words[wi].segs.empty() ? "" : words[wi].segs[0].text, curW, gap, words[wi].width, curW + gap + words[wi].width, effW, columnWidth); pushLine(0); cur.clear(); firstLineOfSeg = false; cur.push_back(wi); curW = words[wi].width; } else { cur.push_back(wi); curW += gap + words[wi].width; } prevWord = wi; } if (!cur.empty()) pushLine(1); } if (words.empty() || lines.empty()) { FPDF_ClosePage(page); return {}; } int newLineCount = static_cast(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 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(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; } double indent = (li < lineCont.size() && lineCont[li]) ? hangingIndent : 0.0; double effCol = columnWidth - indent; bool segEnd = (li < lineSegEnd.size()) ? (lineSegEnd[li] != 0) : (li + 1 == lines.size()); bool justifyThis = (align == "justify") && !segEnd && lw.size() > 1; double extraPerGap = 0.0; if (justifyThis) { double slack = effCol - naturalW; if (slack > 0) extraPerGap = slack / static_cast(lw.size() - 1); } std::string lineText; std::vector adv; double lineFontSize = 0.0; double x = (li < lineX.size()) ? lineX[li] : (columnLeft + indent); if (li >= lineX.size()) { if (align == "right") x = columnLeft + indent + (effCol - naturalW); else if (align == "center") x = columnLeft + indent + (effCol - 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); } for (auto& seg : words[wi].segs) { double segX = x; FPDF_FONT font = runFonts[seg.runIdx].font; double emitFontSize = runs[seg.runIdx].fontSize; double mtxScaleX = textAspect; double mtxScaleY = 1.0; if (exactNominal > 0.1 && exactScaleY > 0.001) { emitFontSize = exactNominal; mtxScaleX = exactScaleX; mtxScaleY = exactScaleY; lineFontSize = exactNominal * exactScaleY; } else { lineFontSize = (std::max)(lineFontSize, runs[seg.runIdx].fontSize); } spdlog::info("[STAGE_4_REFLOW_OUTPUT] lineFontSize={:.2f}, emitFontSize={:.2f}, mtxScaleY={:.4f}", lineFontSize, emitFontSize, mtxScaleY); auto emitObj = [&](const std::string& s, double atX) { if (!font || s.empty()) return; std::string baseFontName; { size_t nl = FPDFFont_GetBaseFontName(font, nullptr, 0); if (nl > 0) { std::vector nb(nl); if (FPDFFont_GetBaseFontName(font, nb.data(), nl) > 0) baseFontName = nb.data(); } } const auto& ef = runFonts[seg.runIdx]; spdlog::info("[EMIT_FONT] text='{}' baseFont='{}' fontPtr={} measureFacePtr={} " "hasResolved={} fontSize={:.4f} atX={:.4f} baselineY={:.4f} " "mtxScaleX={:.4f} mtxScaleY={:.4f} runPerChar={} internalFontId='{}'", s, baseFontName, (void*)font, (void*)(ef.measureFace ? ef.measureFace.get() : nullptr), (bool)ef.resolved, emitFontSize, atX, baselineY, mtxScaleX, mtxScaleY, (int)runPerChar[seg.runIdx], runs[seg.runIdx].internalFontId); FPDF_PAGEOBJECT obj = FPDFPageObj_CreateTextObj(doc_, font, static_cast(emitFontSize)); 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(u16.data())); FPDFPageObj_Transform(obj, mtxScaleX, 0.0, 0.0, mtxScaleY, atX, baselineY); repagSetParaId(doc_, obj, paraId); FPDFPage_InsertObjectAtIndex(page, obj, minIndex); FS_MATRIX objMtx; FPDFPageObj_GetMatrix(obj, &objMtx); float objFS = 0; FPDFTextObj_GetFontSize(obj, &objFS); float l = 0, b = 0, r = 0, t = 0; FPDFPageObj_GetBounds(obj, &l, &b, &r, &t); FPDF_FONT objFont = FPDFTextObj_GetFont(obj); std::string objBase; if (objFont) { size_t nl = FPDFFont_GetBaseFontName(objFont, nullptr, 0); if (nl > 0) { std::vector nb(nl); if (FPDFFont_GetBaseFontName(objFont, nb.data(), nl) > 0) objBase = nb.data(); } } spdlog::info("[EDITED_TEXT_OBJECT_DEBUG] text='{}' objBaseFont='{}' fontSize={:.2f}, " "matrix=[{:.4f}, {:.4f}, {:.4f}, {:.4f}, {:.4f}, {:.4f}], " "bbox=[{:.2f}, {:.2f}, {:.2f}, {:.2f}]", s, objBase, objFS, objMtx.a, objMtx.b, objMtx.c, objMtx.d, objMtx.e, objMtx.f, l, b, r, t); }; 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(); 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(); spdlog::info("[STAGE_5_SERIALIZED_JSON] {}", lastReflowLayout_); 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 } std::string PdfiumDocument::validateLayout(int pageIndex, const std::string& jsonStr) { #ifdef PDFENGINE_WITH_PDFIUM try { auto data = nlohmann::json::parse(jsonStr); if (!data.is_object()) return "{}"; std::string text = ""; std::string internalFontId = ""; double fontSize = 12.0; if (data.contains("runs") && data["runs"].is_array() && !data["runs"].empty()) { auto run = data["runs"][0]; text = run.value("text", ""); internalFontId = run.value("internalFontId", ""); fontSize = run.value("fontSize", 12.0); } text::LayoutConstraints constraints; constraints.columnLeft = data.value("columnLeft", 0.0); constraints.columnRight = data.value("columnRight", 0.0); constraints.firstBaselineY = data.value("firstBaselineY", 0.0); constraints.leading = data.value("leading", 0.0); auto fontDataRes = getFontData(internalFontId); auto face = std::make_shared(); if (!fontDataRes || !face->loadFromMemory(*fontDataRes)) { return "{}"; } text::TextLayoutEngine engine; auto layout = engine.ComputeLayout(text, constraints, *face, fontSize); nlohmann::json out; out["lines"] = nlohmann::json::array(); for (const auto& l : layout.lines) { out["lines"].push_back({ {"rect", {{"x", l.rect.x}, {"y", l.rect.y}, {"width", l.rect.width}, {"height", l.rect.height}}} }); } out["glyphs"] = nlohmann::json::array(); for (const auto& g : layout.glyphs) { out["glyphs"].push_back({ {"x", g.x}, {"y", g.y}, {"width", g.width}, {"advance", g.advance}, {"ascent", g.ascent}, {"descent", g.descent}, {"cluster", g.cluster}, {"text", g.text} }); } return out.dump(); } catch (...) { return "{}"; } #else (void)pageIndex; (void)jsonStr; return "{}"; #endif } }