Files
pdf/engine/src/parser/pdfium_edit_reflow.cpp
T
2026-07-31 10:50:37 +05:30

699 lines
41 KiB
C++

#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<void, EngineError> PdfiumDocument::applyOp_reflow(const nlohmann::json& op, int pageIndex) {
#ifdef PDFENGINE_WITH_PDFIUM
if (!op.contains("data") || !op["data"].is_object()) {
spdlog::error("reflow_paragraph missing 'data'");
return std::unexpected(EngineError::InvalidFormat);
}
auto data = op["data"];
struct RunStyle {
std::string text;
std::string internalFontId;
double fontSize;
unsigned int r, g, b;
std::vector<double> advances;
// 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<RunStyle> runs;
auto parseHex = [](const std::string& hex, unsigned int& r, unsigned int& g, unsigned int& b) {
r = 0; g = 0; b = 0;
if (hex.size() >= 7 && hex[0] == '#') {
auto hv = [](char ch) -> int {
if (ch >= '0' && ch <= '9') return ch - '0';
if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10;
if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10;
return 0;
};
r = static_cast<unsigned int>(hv(hex[1]) * 16 + hv(hex[2]));
g = static_cast<unsigned int>(hv(hex[3]) * 16 + hv(hex[4]));
b = static_cast<unsigned int>(hv(hex[5]) * 16 + hv(hex[6]));
}
};
auto parseRun = [&](const nlohmann::json& rj) {
RunStyle rs;
rs.text = rj.value("text", "");
rs.internalFontId = rj.value("internalFontId", "");
rs.fontSize = rj.value("fontSize", 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<double>());
}
rs.advanceSeedText = rj.value("advanceSeedText", "");
runs.push_back(std::move(rs));
};
std::vector<std::vector<int>> providedLines;
bool hasProvidedLines = data.contains("lines") && data["lines"].is_array() && !data["lines"].empty();
if (hasProvidedLines) {
for (const auto& lineJson : data["lines"]) {
std::vector<int> lineRunIdxs;
if (lineJson.is_array()) {
for (const auto& rj : lineJson) { lineRunIdxs.push_back(static_cast<int>(runs.size())); parseRun(rj); }
}
providedLines.push_back(std::move(lineRunIdxs));
}
} else if (data.contains("runs") && data["runs"].is_array()) {
for (const auto& rj : data["runs"]) parseRun(rj);
}
std::vector<int> objectIndices;
if (data.contains("objectIndices") && data["objectIndices"].is_array()) {
for (auto& idx : data["objectIndices"]) objectIndices.push_back(idx.get<int>());
}
double columnLeft = data.value("columnLeft", 0.0);
double columnRight = data.value("columnRight", 0.0);
double firstBaselineY = data.value("firstBaselineY", 0.0);
double leading = data.value("leading", 0.0);
int oldLineCount = data.value("oldLineCount", 1);
std::string align = data.value("align", std::string("left"));
std::vector<double> lineBaselineY, lineX;
if (data.contains("lineBaselineY") && data["lineBaselineY"].is_array())
for (const auto& v : data["lineBaselineY"]) lineBaselineY.push_back(v.get<double>());
if (data.contains("lineX") && data["lineX"].is_array())
for (const auto& v : data["lineX"]) lineX.push_back(v.get<double>());
double columnWidth = columnRight - columnLeft;
double pushColumnLeft = data.value("pushColumnLeft", columnLeft);
double hangingIndent = data.value("hangingIndent", 0.0);
std::string paraId;
if (data.contains("paraId") && data["paraId"].is_string())
paraId = data["paraId"].get<std::string>();
if (runs.empty() || objectIndices.empty() || columnWidth <= 1.0 || leading <= 0.0) {
spdlog::warn("reflow_paragraph: insufficient layout data, skipping");
return {};
}
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
spdlog::error("Failed to load page {} for reflow_paragraph", pageIndex);
return std::unexpected(EngineError::Unknown);
}
PageBand startBand = repagComputeBand(page, pushColumnLeft, columnRight);
double bottomLimitY = startBand.valid ? startBand.bottomLimitY : -1e18;
std::vector<double> anchoredCentersStart =
startBand.valid ? repagAnchoredCenters(page, pushColumnLeft, columnRight, bottomLimitY)
: std::vector<double>{};
if (!paraId.empty()) {
int cont = repagRemoveContinuations(doc_, pageIndex, paraId, pushColumnLeft, columnRight);
if (cont > 0)
spdlog::info("reflow_paragraph: removed continuation of paraId={} on {} page(s)", paraId, cont);
}
std::vector<int> paragraphSet = objectIndices;
{
float ul = 0, ub = 0, ur = 0, ut = 0; bool haveUnion = false;
for (int idx : objectIndices) {
FPDF_PAGEOBJECT o = FPDFPage_GetObject(page, idx);
if (!o) continue;
float l = 0, b = 0, r = 0, t = 0;
if (!FPDFPageObj_GetBounds(o, &l, &b, &r, &t)) continue;
if (!haveUnion) { ul = l; ub = b; ur = r; ut = t; haveUnion = true; }
else {
if (l < ul) ul = l; if (b < ub) ub = b;
if (r > ur) ur = r; if (t > ut) ut = t;
}
}
if (haveUnion) {
const float eps = 0.5f;
int nObjs = FPDFPage_CountObjects(page);
int adopted = 0;
for (int k = 0; k < nObjs; ++k) {
if (std::find(objectIndices.begin(), objectIndices.end(), k) != objectIndices.end()) continue;
FPDF_PAGEOBJECT o = FPDFPage_GetObject(page, k);
if (!o || FPDFPageObj_GetType(o) != FPDF_PAGEOBJ_TEXT) continue;
float l = 0, b = 0, r = 0, t = 0;
if (!FPDFPageObj_GetBounds(o, &l, &b, &r, &t)) continue;
if (l >= ul - eps && b >= ub - eps && r <= ur + eps && t <= ut + eps) {
paragraphSet.push_back(k);
adopted++;
spdlog::debug("reflow_paragraph: adopted leftover text object idx={} inside paragraph bbox (not in objectIndices) -> prevents bulge/merge + font substitution", k);
}
}
if (adopted > 0)
spdlog::debug("reflow_paragraph: geometric backstop adopted {} object(s) the model omitted", adopted);
}
}
if (!paraId.empty()) {
int nObjs = FPDFPage_CountObjects(page);
int adoptedById = 0;
for (int k = 0; k < nObjs; ++k) {
if (std::find(paragraphSet.begin(), paragraphSet.end(), k) != paragraphSet.end()) continue;
FPDF_PAGEOBJECT o = FPDFPage_GetObject(page, k);
if (o && repagGetParaId(o) == paraId) { paragraphSet.push_back(k); adoptedById++; }
}
if (adoptedById > 0)
spdlog::debug("reflow_paragraph: adopted {} anchor-page object(s) by paraId", adoptedById);
}
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<double>(mtx.a) * mtx.a + static_cast<double>(mtx.b) * mtx.b);
exactScaleY = std::sqrt(static_cast<double>(mtx.c) * mtx.c + static_cast<double>(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<EmissionFont> 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<uint32_t> cps;
for (size_t i = 0; i < u16.size();) {
uint32_t cp = u16[i];
if (cp >= 0xD800 && cp <= 0xDBFF && i + 1 < u16.size()) {
uint32_t low = u16[i + 1];
if (low >= 0xDC00 && low <= 0xDFFF) { cp = 0x10000 + ((cp - 0xD800) << 10) + (low - 0xDC00); i += 2; }
else i += 1;
} else i += 1;
if (cp == 0) continue;
cps.push_back(cp);
}
return cps;
};
{
std::unordered_map<std::string, std::vector<uint32_t>> fontCps;
for (const auto& rs : runs) {
auto cps = toCodepoints(rs.text);
auto& dst = fontCps[rs.internalFontId];
dst.insert(dst.end(), cps.begin(), cps.end());
}
auto resolveOrigFont = [&](const std::string& fid) -> FPDF_FONT {
const std::string expected = baseNameFromInternalFontId(fid);
for (int idx : paragraphSet) {
FPDF_PAGEOBJECT o = FPDFPage_GetObject(page, idx);
if (!o || FPDFPageObj_GetType(o) != FPDF_PAGEOBJ_TEXT) continue;
FPDF_FONT fo = FPDFTextObj_GetFont(o);
if (!fo) continue;
size_t nl = FPDFFont_GetBaseFontName(fo, nullptr, 0);
if (nl == 0) continue;
std::vector<char> nb(nl);
if (FPDFFont_GetBaseFontName(fo, nb.data(), nl) == 0) continue;
if (std::string(nb.data()) == expected) return fo;
}
return nullptr;
};
std::unordered_map<std::string, EmissionFont> fontByFid;
for (const auto& rs : runs) {
if (!fontByFid.count(rs.internalFontId))
fontByFid[rs.internalFontId] = loadEmissionFont(pageIndex, rs.internalFontId, rs.fontSize, fontCps[rs.internalFontId], paragraphSet, resolveOrigFont(rs.internalFontId));
}
for (size_t ri = 0; ri < runs.size(); ++ri) runFonts[ri] = fontByFid[runs[ri].internalFontId];
for (size_t ri = 0; ri < runs.size(); ++ri) {
const fonts::FontFace* face = runFonts[ri].measureFace
? runFonts[ri].measureFace.get()
: (runFonts[ri].resolved ? &runFonts[ri].resolved->getFontFace() : nullptr);
runs[ri].text = normalizeReflowText(runs[ri].text, face);
}
}
fonts::HbShaper shaper;
constexpr unsigned int kRefSize = 1000;
auto perCharAdvances = [&](size_t runIdx, const std::string& text) -> std::vector<double> {
std::vector<double> out(text.size(), 0.0);
if (text.empty()) return out;
auto& rf = runFonts[runIdx];
double size = runs[runIdx].fontSize > 0 ? runs[runIdx].fontSize : 12.0;
double scale = (size * textAspect) / static_cast<double>(kRefSize);
fonts::FontFace* face = rf.measureFace ? rf.measureFace.get()
: (rf.resolved ? &rf.resolved->getFontFace() : nullptr);
if (face) {
auto glyphs = shaper.shapeRun(text, *face, kRefSize);
if (glyphs.size() == text.size()) {
for (size_t i = 0; i < text.size(); ++i) out[i] = glyphs[i].advanceX * scale;
return out;
}
double w = 0.0; for (auto& gph : glyphs) w += gph.advanceX;
double per = w * scale / static_cast<double>(text.size());
for (auto& v : out) v = per;
return out;
}
for (auto& v : out) v = size * 0.5;
return out;
};
// 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<std::vector<double>> runCharAdv(runs.size());
std::vector<char> 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<std::ptrdiff_t>(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<size_t>(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<size_t>(-1);
struct Seg { int runIdx; std::string text; double width; size_t off; };
struct Word { std::vector<Seg> segs; double width; double spaceAfter; };
std::vector<Word> words;
auto tokenize = [&](const std::vector<int>& runIdxs) -> std::vector<size_t> {
std::string ft; std::vector<int> sof; std::vector<size_t> cof;
for (int ri : runIdxs) {
size_t off = 0;
for (char ch : runs[ri].text) { ft.push_back(ch); sof.push_back(ri); cof.push_back(off++); }
}
std::vector<size_t> out;
size_t i = 0;
while (i < ft.size()) {
if (isSpace(ft[i])) { 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<std::vector<size_t>> lines;
std::vector<char> lineCont;
std::vector<char> 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<int> allRuns(runs.size());
for (size_t ri = 0; ri < runs.size(); ++ri) allRuns[ri] = static_cast<int>(ri);
auto allWords = tokenize(allRuns);
std::vector<size_t> cur; double curW = 0.0;
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<int>(lines.size());
double deltaH = (newLineCount - oldLineCount) * leading;
double paragraphBottomBaseline = firstBaselineY - (oldLineCount - 1) * leading;
if (std::abs(deltaH) > 0.01) {
double threshold = paragraphBottomBaseline - 0.5 * leading;
int nObjs = FPDFPage_CountObjects(page);
int pushed = 0;
for (int k = 0; k < nObjs; ++k) {
if (std::find(paragraphSet.begin(), paragraphSet.end(), k) != paragraphSet.end()) continue;
FPDF_PAGEOBJECT o = FPDFPage_GetObject(page, k);
if (!o) continue;
float l = 0, bo = 0, rr = 0, tt = 0;
if (!FPDFPageObj_GetBounds(o, &l, &bo, &rr, &tt)) continue;
double cY = (bo + tt) / 2.0;
if (cY < threshold && cY >= bottomLimitY && rr > pushColumnLeft && l < columnRight) {
FPDFPageObj_Transform(o, 1.0, 0.0, 0.0, 1.0, 0.0, -deltaH);
pushed++;
}
}
spdlog::info("reflow_paragraph: lines {}->{}, deltaH={}, pushed {} objects", oldLineCount, newLineCount, deltaH, pushed);
}
std::sort(paragraphSet.begin(), paragraphSet.end(), std::greater<int>());
int minIndex = paragraphSet.back();
for (int idx : paragraphSet) {
FPDF_PAGEOBJECT o = FPDFPage_GetObject(page, idx);
if (o) { FPDFPage_RemoveObject(page, o); FPDFPageObj_Destroy(o); }
}
nlohmann::json layoutLines = nlohmann::json::array();
for (size_t li = 0; li < lines.size(); ++li) {
double baselineY = (li < lineBaselineY.size())
? lineBaselineY[li] : firstBaselineY - static_cast<double>(li) * leading;
auto& lw = lines[li];
double naturalW = 0.0;
for (size_t k = 0; k < lw.size(); ++k) {
naturalW += words[lw[k]].width;
if (k > 0) naturalW += words[lw[k - 1]].spaceAfter;
}
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<double>(lw.size() - 1); }
std::string lineText;
std::vector<double> 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<char> 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<float>(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<FPDF_WIDESTRING>(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<char> 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<double>();
if (!flowing && startBand.valid && b < startBand.bottomLimitY - 0.01) {
flowing = true; pageOff = 1; cur = startBand.placementTopY;
}
if (flowing) {
if (cur < startBand.bottomLimitY - 0.01) { pageOff++; cur = startBand.placementTopY; }
ln["baselineY"] = cur; cur -= leading;
}
ln["pageIndex"] = pageIndex + pageOff;
}
}
lastReflowLayout_ = nlohmann::json{
{"columnLeft", columnLeft}, {"anchorPage", pageIndex}, {"lines", layoutLines}}.dump();
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<fonts::FontFace>();
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
}
}