Merge pull request 'furqan' (#65) from furqan into dev

Reviewed-on: https://gitea.maskantech.in/gitea_admin/pdf/pulls/65
This commit is contained in:
furqan
2026-06-22 05:18:42 +00:00
29 changed files with 257 additions and 1096 deletions
+9
View File
@@ -63,6 +63,15 @@ if(PDFENGINE_WITH_PDFIUM)
target_compile_definitions(pdfengine PRIVATE PDFENGINE_WITH_PDFIUM)
endif()
# Directory of the bundled fallback fonts (Carlito/Tinos). Under WASM they're embedded into the
# module's in-memory FS at /fonts (see wasm/CMakeLists.txt --embed-file); natively the engine reads
# them straight from the source assets dir. font_fallback.cpp prefers these over OS fonts.
if(EMSCRIPTEN)
target_compile_definitions(pdfengine PRIVATE PDFENGINE_FONT_DIR="/fonts")
else()
target_compile_definitions(pdfengine PRIVATE PDFENGINE_FONT_DIR="${CMAKE_SOURCE_DIR}/engine/assets/fonts")
endif()
if(PDFENGINE_WITH_SKIA)
target_link_libraries(pdfengine PRIVATE skia::skia)
target_compile_definitions(pdfengine PUBLIC PDFENGINE_WITH_SKIA)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+44 -10
View File
@@ -27,8 +27,30 @@ FontFallback::FontFallback() {
void FontFallback::initializeDefaults() {
default_rules_.clear();
#ifdef PDFENGINE_FONT_DIR
{
const std::string fd = PDFENGINE_FONT_DIR;
auto sans = [&](const char* s) { return std::vector<std::string>{ fd + "/Carlito-" + s + ".ttf" }; };
auto serif = [&](const char* s) { return std::vector<std::string>{ fd + "/Tinos-" + s + ".ttf" }; };
for (const char* fam : {"calibri", "carlito"}) {
const std::string f = fam;
default_rules_.push_back({f + "-bolditalic", sans("BoldItalic")});
default_rules_.push_back({f + "-bold", sans("Bold")});
default_rules_.push_back({f + "-italic", sans("Italic")});
default_rules_.push_back({f + "-oblique", sans("Italic")});
default_rules_.push_back({f, sans("Regular")});
}
for (const char* fam : {"times", "tinos", "serif"}) {
const std::string f = fam;
default_rules_.push_back({f + "-bolditalic", serif("BoldItalic")});
default_rules_.push_back({f + "-bold", serif("Bold")});
default_rules_.push_back({f + "-italic", serif("Italic")});
default_rules_.push_back({f, serif("Regular")});
}
}
#endif
#if defined(_WIN32)
// Helvetica / Arial fallback rules
default_rules_.push_back({"helvetica-bolditalic", {"C:\\Windows\\Fonts\\LiberationSans-BoldItalic.ttf", "C:\\Windows\\Fonts\\arialbi.ttf"}});
default_rules_.push_back({"helvetica-bold", {"C:\\Windows\\Fonts\\LiberationSans-Bold.ttf", "C:\\Windows\\Fonts\\arialbd.ttf"}});
default_rules_.push_back({"helvetica-oblique", {"C:\\Windows\\Fonts\\LiberationSans-Italic.ttf", "C:\\Windows\\Fonts\\ariali.ttf"}});
@@ -38,13 +60,11 @@ void FontFallback::initializeDefaults() {
default_rules_.push_back({"arial-italic", {"C:\\Windows\\Fonts\\LiberationSans-Italic.ttf", "C:\\Windows\\Fonts\\ariali.ttf"}});
default_rules_.push_back({"arial", {"C:\\Windows\\Fonts\\LiberationSans-Regular.ttf", "C:\\Windows\\Fonts\\arial.ttf"}});
// Times fallback rules
default_rules_.push_back({"times-bolditalic", {"C:\\Windows\\Fonts\\LiberationSerif-BoldItalic.ttf", "C:\\Windows\\Fonts\\timesbi.ttf"}});
default_rules_.push_back({"times-bold", {"C:\\Windows\\Fonts\\LiberationSerif-Bold.ttf", "C:\\Windows\\Fonts\\timesbd.ttf"}});
default_rules_.push_back({"times-italic", {"C:\\Windows\\Fonts\\LiberationSerif-Italic.ttf", "C:\\Windows\\Fonts\\timesi.ttf"}});
default_rules_.push_back({"times", {"C:\\Windows\\Fonts\\LiberationSerif-Regular.ttf", "C:\\Windows\\Fonts\\times.ttf"}});
// Courier fallback rules
default_rules_.push_back({"courier-bolditalic", {"C:\\Windows\\Fonts\\LiberationMono-BoldItalic.ttf", "C:\\Windows\\Fonts\\courbi.ttf"}});
default_rules_.push_back({"courier-bold", {"C:\\Windows\\Fonts\\LiberationMono-Bold.ttf", "C:\\Windows\\Fonts\\courbd.ttf"}});
default_rules_.push_back({"courier-oblique", {"C:\\Windows\\Fonts\\LiberationMono-Italic.ttf", "C:\\Windows\\Fonts\\couri.ttf"}});
@@ -63,7 +83,6 @@ void FontFallback::initializeDefaults() {
default_rules_.push_back({"japanese", {"C:\\Windows\\Fonts\\msgothic.ttc"}});
default_rules_.push_back({"korean", {"C:\\Windows\\Fonts\\malgun.ttf"}});
#elif defined(__APPLE__)
// Helvetica / Arial fallback rules
default_rules_.push_back({"helvetica-bolditalic", {"/Library/Fonts/LiberationSans-BoldItalic.ttf", "/System/Library/Fonts/Supplemental/Arial Bold Italic.ttf", "/Library/Fonts/Arial Bold Italic.ttf"}});
default_rules_.push_back({"helvetica-bold", {"/Library/Fonts/LiberationSans-Bold.ttf", "/System/Library/Fonts/Supplemental/Arial Bold.ttf", "/Library/Fonts/Arial Bold.ttf"}});
default_rules_.push_back({"helvetica-oblique", {"/Library/Fonts/LiberationSans-Italic.ttf", "/System/Library/Fonts/Supplemental/Arial Italic.ttf", "/Library/Fonts/Arial Italic.ttf"}});
@@ -73,19 +92,16 @@ void FontFallback::initializeDefaults() {
default_rules_.push_back({"arial-italic", {"/Library/Fonts/LiberationSans-Italic.ttf", "/System/Library/Fonts/Supplemental/Arial Italic.ttf", "/Library/Fonts/Arial Italic.ttf"}});
default_rules_.push_back({"arial", {"/Library/Fonts/LiberationSans-Regular.ttf", "/Library/Fonts/Arial.ttf"}});
// Times fallback rules
default_rules_.push_back({"times-bolditalic", {"/Library/Fonts/LiberationSerif-BoldItalic.ttf", "/System/Library/Fonts/Supplemental/Times New Roman Bold Italic.ttf", "/Library/Fonts/Times New Roman Bold Italic.ttf"}});
default_rules_.push_back({"times-bold", {"/Library/Fonts/LiberationSerif-Bold.ttf", "/System/Library/Fonts/Supplemental/Times New Roman Bold.ttf", "/Library/Fonts/Times New Roman Bold.ttf"}});
default_rules_.push_back({"times-italic", {"/Library/Fonts/LiberationSerif-Italic.ttf", "/System/Library/Fonts/Supplemental/Times New Roman Italic.ttf", "/Library/Fonts/Times New Roman Italic.ttf"}});
default_rules_.push_back({"times", {"/Library/Fonts/LiberationSerif-Regular.ttf", "/Library/Fonts/Times New Roman.ttf", "/System/Library/Fonts/Times.ttc"}});
// Courier fallback rules
default_rules_.push_back({"courier-bolditalic", {"/Library/Fonts/LiberationMono-BoldItalic.ttf", "/System/Library/Fonts/Supplemental/Courier New Bold Italic.ttf", "/Library/Fonts/Courier New Bold Italic.ttf"}});
default_rules_.push_back({"courier-bold", {"/Library/Fonts/LiberationMono-Bold.ttf", "/System/Library/Fonts/Supplemental/Courier New Bold.ttf", "/Library/Fonts/Courier New Bold.ttf"}});
default_rules_.push_back({"courier-oblique", {"/Library/Fonts/LiberationMono-Italic.ttf", "/System/Library/Fonts/Supplemental/Courier New Italic.ttf", "/Library/Fonts/Courier New Italic.ttf"}});
default_rules_.push_back({"courier", {"/Library/Fonts/LiberationMono-Regular.ttf", "/Library/Fonts/Courier New.ttf", "/System/Library/Fonts/Courier.dfont"}});
#else
// Helvetica / Arial fallback rules
default_rules_.push_back({"helvetica-bolditalic", {"/usr/share/fonts/truetype/liberation/LiberationSans-BoldItalic.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans-BoldOblique.ttf"}});
default_rules_.push_back({"helvetica-bold", {"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"}});
default_rules_.push_back({"helvetica-oblique", {"/usr/share/fonts/truetype/liberation/LiberationSans-Italic.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Oblique.ttf"}});
@@ -95,13 +111,11 @@ void FontFallback::initializeDefaults() {
default_rules_.push_back({"arial-italic", {"/usr/share/fonts/truetype/liberation/LiberationSans-Italic.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Oblique.ttf"}});
default_rules_.push_back({"arial", {"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"}});
// Times fallback rules
default_rules_.push_back({"times-bolditalic", {"/usr/share/fonts/truetype/liberation/LiberationSerif-BoldItalic.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSerif-BoldItalic.ttf"}});
default_rules_.push_back({"times-bold", {"/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf"}});
default_rules_.push_back({"times-italic", {"/usr/share/fonts/truetype/liberation/LiberationSerif-Italic.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSerif-Italic.ttf"}});
default_rules_.push_back({"times", {"/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf"}});
// Courier fallback rules
default_rules_.push_back({"courier-bolditalic", {"/usr/share/fonts/truetype/liberation/LiberationMono-BoldItalic.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-BoldOblique.ttf"}});
default_rules_.push_back({"courier-bold", {"/usr/share/fonts/truetype/liberation/LiberationMono-Bold.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf"}});
default_rules_.push_back({"courier-oblique", {"/usr/share/fonts/truetype/liberation/LiberationMono-Italic.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Oblique.ttf"}});
@@ -143,6 +157,26 @@ std::string FontFallback::getFallbackFontPath(const std::string& fontName, bool
}
}
// Generic bundled catch-all: any font that matched NO specific rule above (an exotic/embedded
// family we don't recognise) still gets a real, present-on-both-engines fallback instead of OS
// Arial (native only) or nothing (WASM -> garble). Pick serif (Tinos) vs sans (Carlito) from the
// name; default sans. Recognised families (helvetica/arial/times/calibri/courier) already
// returned above, so this never overrides them. Keeps WASM == native for every font.
#ifdef PDFENGINE_FONT_DIR
{
auto has = [&](const char* s) { return lowerName.find(s) != std::string::npos; };
const bool serif = has("times") || has("serif") || has("roman") || has("georgia") ||
has("garamond") || has("minion") || has("cambria") || has("tinos") ||
has("book antiqua") || has("palatino");
const std::string base = serif ? "Tinos" : "Carlito";
const std::string style = (bold && italic) ? "BoldItalic" : bold ? "Bold" : italic ? "Italic" : "Regular";
const std::string p = std::string(PDFENGINE_FONT_DIR) + "/" + base + "-" + style + ".ttf";
if (std::filesystem::exists(p)) {
return p;
}
}
#endif
#if defined(_WIN32)
std::vector<std::string> lastResort;
if (bold && italic) lastResort = {"C:\\Windows\\Fonts\\arialbi.ttf", "C:\\Windows\\Fonts\\timesbi.ttf"};
@@ -190,4 +224,4 @@ void FontFallback::resetToDefaults() {
custom_rules_.clear();
}
} // namespace pdfengine::fonts::pdf_fonts
}
File diff suppressed because one or more lines are too long
Binary file not shown.
+1 -1
View File
@@ -16,7 +16,7 @@ function getModule(): Promise<PdfiumModule | null> {
if (!modulePromise) {
modulePromise = (async () => {
try {
const V = '20260618c';
const V = '20260618f-allfonts';
const resp = await fetch(`/pdfium-engine.mjs?v=${V}`, { cache: 'no-store' });
if (!resp.ok) throw new Error(`pdfium-engine.mjs ${resp.status}`);
const blobUrl = URL.createObjectURL(new Blob([await resp.text()], { type: 'text/javascript' }));
-12
View File
@@ -1,12 +0,0 @@
editing bullet: paragraph 5, line 2, x=66.6
scoped item lines [2..4): • Architecture & APIs: Microservices, | Design Patterns
op columnLeft(text indent)=78.1 pushColumnLeft(marker)=66.6 objs=16
--- AFTER (edited bullet lines) ---
--- ADJACENT bullets (must be unchanged) ---
[OK ] 'Languages:': '• Languages: Core Java, Java 8+, OOPs, Collections, Streams, L'
[OK ] 'Frameworks': '• Frameworks & Libraries: Spring Boot, Spring MVC, Spring Data'
[OK ] 'Databases': '• Databases & Caching: MySQL (Indexing, Query Tuning, Transact'
[OK ] 'Messaging': '• Messaging & Streaming: Apache Kafka (Producer/Consumer, Part'
[OK ] 'Cloud': '• Cloud & DevOps: AWS (EC2, S3, Lambda, RDS, CloudWatch), Dock'
Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

-143
View File
@@ -1,143 +0,0 @@
#!/usr/bin/env python3
"""Reproduce the bullet-item reflow corruption against the CURRENT build .pyd: edit one bullet
("Architecture & APIs") and check whether (a) the edited item keeps its hanging indent and (b)
ADJACENT bullets are left intact. Mimics the frontend buildBulletItem scoping.
Run: gateway/.venv/Scripts/python.exe tests/edits/_bullet_repro.py
"""
from __future__ import annotations
import io, json, sys
from pathlib import Path
sys.path.insert(0, r"C:\Users\furqa\pdfeng-build\win-local-pdfium\lib") # fresh build
import pdfengine # noqa: E402
ROOT = Path(__file__).resolve().parents[2]
PDF = ROOT / "Mo-Faishal-Qureshi.pdf"
OUT = Path(__file__).resolve().parent / "_bullet_repro.out.txt"
rep = io.StringIO()
def log(*a): rep.write(" ".join(str(x) for x in a) + "\n")
def is_marker(t):
t = (t or "").strip()
return t == "" or (len(t) <= 2 and not t.isalnum()) # bullet glyph / symbol, not a word
def line_text(l):
return "".join(r.text for r in l.runs)
def para_text(p):
return "".join(line_text(l) for l in p.lines)
def find_line(model, needle):
for pi, p in enumerate(model.paragraphs):
for li, l in enumerate(p.lines):
if needle in line_text(l):
return pi, p, li
return -1, None, -1
def build_bullet_op(p, click_line, page_index, edit_fn):
lines = p.lines
col_left = min(l.x for l in lines)
col_right = max(l.x + l.w for l in lines)
def is_start(idx):
rs = list(lines[idx].runs)
return idx == 0 or (bool(rs) and is_marker(rs[0].text))
start = click_line
while start > 0 and not is_start(start):
start -= 1
end = click_line + 1
while end < len(lines) and not is_start(end):
end += 1
item_lines = lines[start:end]
log(f" scoped item lines [{start}..{end}): " + " | ".join(line_text(l)[:38] for l in item_lines))
text_indent = item_lines[0].x
obj_idx, runs = [], []
for idx, l in enumerate(item_lines):
rs = list(l.runs)
if idx == 0 and rs and is_marker(rs[0].text):
ti = 1
while ti < len(rs) and not (rs[ti].text or "").strip():
ti += 1
if ti < len(rs):
text_indent = rs[ti].x
rs = rs[ti:]
for r in rs:
obj_idx.extend(list(r.object_indices))
if r.text:
runs.append({"text": r.text, "internalFontId": r.internal_font_id,
"fontSize": r.font_size, "color": "#000000"})
runs = edit_fn(runs)
baselines = [l.baseline_y for l in lines]
leading = abs(baselines[start] - baselines[start + 1]) if start + 1 < len(baselines) else 14.0
return {
"id": "rf", "type": "reflow_paragraph", "pageIndex": page_index,
"data": {
"objectIndices": sorted(set(obj_idx)),
"runs": runs,
"columnLeft": text_indent, "columnRight": col_right,
"pushColumnLeft": col_left,
"firstBaselineY": baselines[start], "leading": leading,
"oldLineCount": len(item_lines), "align": "left",
},
}
WATCH = ("Languages:", "Frameworks", "Databases", "Messaging", "Cloud")
def run():
doc = pdfengine.PdfDocument.load_from_memory(PDF.read_bytes(), "")
m = doc.get_page(0).extract_document_model()
pi, p, li = find_line(m, "Architecture & APIs")
if p is None:
log("Architecture bullet not found"); return
log(f"editing bullet: paragraph {pi}, line {li}, x={p.lines[li].x:.1f}")
before = {}
for n in WATCH:
_, pp, ll = find_line(m, n)
before[n] = line_text(pp.lines[ll]) if pp else "<none>"
def edit(runs):
out = []
for r in runs:
out.append(r)
if "Architecture," in r["text"]:
out.append({"text": " in the main app", "internalFontId": r["internalFontId"],
"fontSize": r["fontSize"], "color": "#000000"})
return out
op = build_bullet_op(p, li, 0, edit)
log(f" op columnLeft(text indent)={op['data']['columnLeft']:.1f} pushColumnLeft(marker)={op['data']['pushColumnLeft']:.1f} objs={len(op['data']['objectIndices'])}")
doc.apply_edits(json.dumps({"version": "1.0", "operations": [op]}))
m2 = pdfengine.PdfDocument.load_from_memory(doc.save_full(), "").get_page(0).extract_document_model()
log("\n--- AFTER (edited bullet lines) ---")
_, pa, la = find_line(m2, "in the main app")
if pa is None:
_, pa, la = find_line(m2, "API Gateway")
if pa:
for l in pa.lines:
t = line_text(l)
if any(k in t for k in ("Architecture", "Design", "app", "SOLID")):
fonts = sorted({r.internal_font_id for r in l.runs if r.text.strip()})
log(f" x={l.x:.1f} fonts={fonts} | {t[:64]!r}")
log("\n--- ADJACENT bullets (must be unchanged) ---")
for n in WATCH:
_, pp, ll = find_line(m2, n)
after = line_text(pp.lines[ll]) if pp else "<MISSING>"
same = "OK " if after == before[n] else "CHANGED"
log(f" [{same}] {n!r}: {after[:62]!r}")
if __name__ == "__main__":
run()
OUT.write_text(rep.getvalue(), encoding="utf-8")
print(rep.getvalue())
+72
View File
@@ -0,0 +1,72 @@
"""Phase 1 check: editing a styled run with characters NOT in its embedded subset now falls back to
the bundled Carlito (metric-compatible with Calibri) instead of OS Arial/Helvetica, and the new
glyphs survive.
Run: gateway/.venv/Scripts/python.exe tests/edits/_font_fallback_check.py
"""
from __future__ import annotations
import json, sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, r"C:\Users\furqa\pdfeng-build\win-local-pdfium\lib")
sys.path.insert(1, str(ROOT / "gateway"))
import pdfengine
PDF = ROOT / "Mo-Faishal-Qureshi.pdf"
def ptext(p):
return "".join(r.text for ln in p.lines for r in ln.runs)
def main():
doc = pdfengine.PdfDocument.load_from_memory(PDF.read_bytes(), "")
for pageno in range(doc.page_count):
m = doc.get_page(pageno).extract_document_model()
para = next((p for p in m.paragraphs if "Present" in ptext(p)), None)
if para:
break
if not para:
print("FAIL: couldn't find the 'Present' italic paragraph"); return
fonts_before = sorted({r.internal_font_id for ln in para.lines for r in ln.runs if r.text.strip()})
print("para text:", ptext(para)[:70])
print("fonts before:", fonts_before)
oi = [i for ln in para.lines for r in ln.runs for i in r.object_indices]
cl = min(ln.x for ln in para.lines); cr = max(ln.x + ln.w for ln in para.lines)
fb = max(ln.baseline_y for ln in para.lines)
bls = sorted({round(ln.baseline_y, 1) for ln in para.lines}, reverse=True)
lead = abs(bls[0]-bls[1]) if len(bls) > 1 else 13.0
fid = para.lines[0].runs[0].internal_font_id
new_text = ptext(para).strip() + " Zephyr xkcd | the quux"
op = {"version": "1.0", "operations": [{"id": "f", "type": "reflow_paragraph", "pageIndex": pageno, "data": {
"objectIndices": oi, "runs": [{"text": new_text, "internalFontId": fid, "fontSize": 11.0, "color": "#1a5276"}],
"columnLeft": cl, "columnRight": cr, "firstBaselineY": fb, "leading": lead,
"oldLineCount": len(para.lines), "align": "left", "paraId": "FONTCHK"}}]}
doc.apply_edits(json.dumps(op))
r = pdfengine.PdfDocument.load_from_memory(doc.save_full(), "")
edited = None
for pageno2 in range(r.page_count):
mm = r.get_page(pageno2).extract_document_model()
for p in mm.paragraphs:
if "Zephyr" in ptext(p):
edited = p; break
if edited:
break
if not edited:
print("FAIL: edited paragraph not found after save"); return
names = sorted({(r.font_name or "") for ln in edited.lines for r in ln.runs if r.text.strip()})
txt = ptext(edited)
print("\nedited fonts:", names)
print("glyphs survived: Zephyr=%s '|'=%s the=%s xkcd=%s" %
("Zephyr" in txt, "|" in txt, "the" in txt.replace(" ", ""), "xkcd" in txt.replace(" ", "")))
carlito = any("carlito" in n.lower() for n in names)
arial = any(("arial" in n.lower() or "helvetica" in n.lower() or "liberation" in n.lower()) for n in names)
print(f"\n=> fallback uses Carlito: {carlito} (uses Arial/Helvetica: {arial})")
print("PHASE1 OK" if carlito and not arial else "PHASE1: check fonts above")
if __name__ == "__main__":
main()
-74
View File
@@ -1,74 +0,0 @@
"""Does cross-page MIGRATION preserve hyphens/dashes? Grow the page-0 Technologies paragraph so it
pushes the 'Architected ... high-scale e-commerce' paragraph onto page 2 (migration), then check the
migrated text still has its hyphens.
Run: gateway/.venv/Scripts/python.exe tests/edits/_hyphen_check.py
"""
from __future__ import annotations
import json, sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, r"C:\Users\furqa\pdfeng-build\win-local-pdfium\lib")
sys.path.insert(1, str(ROOT / "gateway"))
import pdfengine
PDF = ROOT / "Mo-Faishal-Qureshi.pdf"
def ptext(p):
return "".join(r.text for ln in p.lines for r in ln.runs)
def find(doc, page, needle):
for p in doc.get_page(page).extract_document_model().paragraphs:
if needle in ptext(p):
return p
return None
def alltext(doc):
out = []
for i in range(doc.page_count):
for p in doc.get_page(i).extract_document_model().paragraphs:
out.append(ptext(p))
return " ".join(out)
def main():
doc = pdfengine.PdfDocument.load_from_memory(PDF.read_bytes(), "")
before = alltext(doc)
print("BEFORE migration:")
print(" 'high' + U+002D + 'scale' :", ("high-scale" in before))
print(" 'high' + U+2011 + 'scale' :", ("highscale" in before))
print(" 'e' + U+002D + 'commerce' :", ("e-commerce" in before))
print(" 'e' + U+2011 + 'commerce' :", ("ecommerce" in before))
tech = find(doc, 0, "Technologies")
oi = [i for ln in tech.lines for r in ln.runs for i in r.object_indices]
cl = min(ln.x for ln in tech.lines); cr = max(ln.x + ln.w for ln in tech.lines)
fb = max(ln.baseline_y for ln in tech.lines)
bls = sorted({round(ln.baseline_y, 1) for ln in tech.lines}, reverse=True)
lead = abs(bls[0]-bls[1]) if len(bls) > 1 else 14.0
big = ptext(tech).strip() + " " + ("filler " * 60)
op = {"version": "1.0", "operations": [{"id": "g", "type": "reflow_paragraph", "pageIndex": 0, "data": {
"objectIndices": oi, "runs": [{"text": big, "internalFontId": tech.lines[0].runs[0].internal_font_id,
"fontSize": 11.0, "color": "#000000"}], "columnLeft": cl, "columnRight": cr, "firstBaselineY": fb,
"leading": lead, "oldLineCount": len(tech.lines), "align": "left", "paraId": "TECHPARA"}}]}
doc.apply_edits(json.dumps(op))
r = pdfengine.PdfDocument.load_from_memory(doc.save_full(), "")
after = alltext(r)
print(f"\nAFTER migration (pages={r.page_count}):")
print(" 'high' + U+002D + 'scale' :", ("high-scale" in after))
print(" 'high' + U+2011 + 'scale' :", ("highscale" in after))
print(" 'e' + U+002D + 'commerce' :", ("e-commerce" in after))
print(" 'e' + U+2011 + 'commerce' :", ("ecommerce" in after))
for i in range(r.page_count):
a = find(r, i, "Architected")
if a:
safe = ptext(a)[:120].encode("ascii", "replace").decode("ascii")
print(f"\n Architected (page {i}): {safe!r}")
break
if __name__ == "__main__":
main()
+61
View File
@@ -0,0 +1,61 @@
"""Diagnose: when the ITALIC run's fid is used, does the engine emit an ITALIC fallback (Carlito-
Italic)? Rules out an engine style-derivation bug vs. a frontend mis-tagging issue.
Run: gateway/.venv/Scripts/python.exe tests/edits/_italic_diag.py
"""
from __future__ import annotations
import json, sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, r"C:\Users\furqa\pdfeng-build\win-local-pdfium\lib")
sys.path.insert(1, str(ROOT / "gateway"))
import pdfengine
PDF = ROOT / "Mo-Faishal-Qureshi.pdf"
def ptext(p):
return "".join(r.text for ln in p.lines for r in ln.runs)
def main():
doc = pdfengine.PdfDocument.load_from_memory(PDF.read_bytes(), "")
italic_fid = None; para = None; pageno = 0
for pg in range(doc.page_count):
m = doc.get_page(pg).extract_document_model()
for p in m.paragraphs:
for ln in p.lines:
for r in ln.runs:
if "italic" in (r.internal_font_id or "").lower() and r.text.strip():
italic_fid = r.internal_font_id; para = p; pageno = pg; break
if italic_fid: break
if italic_fid: break
if italic_fid: break
if not italic_fid:
print("no italic run found"); return
print("italic fid:", italic_fid)
print("para:", ptext(para)[:80])
oi = [i for ln in para.lines for r in ln.runs for i in r.object_indices]
cl = min(ln.x for ln in para.lines); cr = max(ln.x + ln.w for ln in para.lines)
fb = max(ln.baseline_y for ln in para.lines)
op = {"version": "1.0", "operations": [{"id": "i", "type": "reflow_paragraph", "pageIndex": pageno, "data": {
"objectIndices": oi, "runs": [{"text": "Mar 2023 Present Zephyr xyzk | the quux",
"internalFontId": italic_fid, "fontSize": 11.0, "color": "#1a5276"}],
"columnLeft": cl, "columnRight": cr, "firstBaselineY": fb, "leading": 13.0,
"oldLineCount": len(para.lines), "align": "left", "paraId": "ITALCHK"}}]}
doc.apply_edits(json.dumps(op))
r = pdfengine.PdfDocument.load_from_memory(doc.save_full(), "")
names = set()
for pg in range(r.page_count):
for p in r.get_page(pg).extract_document_model().paragraphs:
if "Zephyr" in ptext(p):
for ln in p.lines:
for rr in ln.runs:
if rr.text.strip(): names.add(rr.font_name or "")
print("emitted fonts:", sorted(names))
print("=> italic fallback used:", any("italic" in n.lower() for n in names))
if __name__ == "__main__":
main()
-62
View File
@@ -1,62 +0,0 @@
"""Diagnose early-overflow: edit the real Technologies paragraph with a long single-run text and
report where each resulting line lands (page + baseline) vs the page-bottom limit.
Run: gateway/.venv/Scripts/python.exe tests/edits/_overflow_diag.py
"""
from __future__ import annotations
import json, sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, r"C:\Users\furqa\pdfeng-build\win-local-pdfium\lib")
sys.path.insert(1, str(ROOT / "gateway"))
import pdfengine
PDF = ROOT / "Mo-Faishal-Qureshi.pdf"
def ptext(p):
return "".join(r.text for ln in p.lines for r in ln.runs)
def main():
doc = pdfengine.PdfDocument.load_from_memory(PDF.read_bytes(), "")
m = doc.get_page(0).extract_document_model()
tech = next(p for p in m.paragraphs if "Technologies" in ptext(p))
oi = [i for ln in tech.lines for r in ln.runs for i in r.object_indices]
cl = min(ln.x for ln in tech.lines); cr = max(ln.x + ln.w for ln in tech.lines)
fb = max(ln.baseline_y for ln in tech.lines)
bls = sorted({round(ln.baseline_y, 1) for ln in tech.lines}, reverse=True)
lead = abs(bls[0]-bls[1]) if len(bls) > 1 else 14.0
page_h = doc.get_page(0).height
tops = []
for p in m.paragraphs:
for ln in p.lines:
if (ln.x + ln.w) > cl and ln.x < cr:
tops.append(ln.y + ln.h)
max_top = max(tops) if tops else 0
mirror = page_h - max_top
bottom_limit = max(18.0, min(mirror, page_h * 0.25))
print(f"page_h={page_h:.0f} tech firstBaselineY={fb:.1f} leading={lead:.1f} cols=[{cl:.0f},{cr:.0f}]")
print(f"max_top(in-col)={max_top:.1f} mirror={mirror:.1f} bottomLimitY(approx)={bottom_limit:.1f}")
room_lines = int((fb - bottom_limit) / lead)
print(f"=> room for ~{room_lines} lines on page 0 before hitting the bottom limit")
new_text = ptext(tech).strip() + " " + " ".join(f"w{n}" for n in range(40))
op = {"version": "1.0", "operations": [{"id": "g", "type": "reflow_paragraph", "pageIndex": 0, "data": {
"objectIndices": oi, "runs": [{"text": new_text, "internalFontId": tech.lines[0].runs[0].internal_font_id,
"fontSize": 11.0, "color": "#000000"}], "columnLeft": cl, "columnRight": cr, "firstBaselineY": fb,
"leading": lead, "oldLineCount": len(tech.lines), "align": "left", "paraId": "DIAG"}}]}
doc.apply_edits(json.dumps(op))
r = pdfengine.PdfDocument.load_from_memory(doc.save_full(), "")
print(f"\nAFTER edit: pages={r.page_count}")
for i in range(r.page_count):
mm = r.get_page(i).extract_document_model()
diag_lines = [ln for p in mm.paragraphs for ln in p.lines
if any(getattr(rr, "para_id", "") == "DIAG" for rr in ln.runs)]
ys = sorted((ln.baseline_y for ln in diag_lines), reverse=True)
print(f" page {i}: {len(diag_lines)} DIAG line(s) baselineY range "
f"{ys[0]:.0f}..{ys[-1]:.0f}" if ys else f" page {i}: 0 DIAG lines")
if __name__ == "__main__":
main()
-95
View File
@@ -1,95 +0,0 @@
"""Stage 1 verification: PDFPARA object marks survive (a) save/reload and (b) cross-page migration.
Run: gateway/.venv/Scripts/python.exe tests/edits/_paraid_probe.py
"""
from __future__ import annotations
import json, sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, r"C:\Users\furqa\pdfeng-build\win-local-pdfium\lib")
sys.path.insert(1, str(ROOT / "gateway"))
import pdfengine
def build_pdf() -> bytes:
content = (
b"BT /F1 14 Tf 72 720 Td (HEADING) Tj ET\n"
b"BT /F1 11 Tf 72 150 Td (The quick brown fox jumps over the lazy) Tj ET\n"
b"BT /F1 11 Tf 72 136 Td (dog near the river bank on a sunny) Tj ET\n"
b"BT /F1 11 Tf 72 122 Td (afternoon in early spring.) Tj ET\n"
)
objs = [
b"<< /Type /Catalog /Pages 2 0 R >>",
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
b"<< /Length %d >>\nstream\n" % len(content) + content + b"endstream",
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
]
pdf = b"%PDF-1.7\n"; offs = []
for i, o in enumerate(objs, 1):
offs.append(len(pdf)); pdf += b"%d 0 obj\n" % i + o + b"\nendobj\n"
xref = len(pdf); pdf += b"xref\n0 %d\n0000000000 65535 f \n" % (len(objs) + 1)
for o in offs: pdf += b"%010d 00000 n \n" % o
pdf += b"trailer\n<< /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF\n" % (len(objs) + 1, xref)
return pdf
def body_para(doc, page=0):
m = doc.get_page(page).extract_document_model()
return next(p for p in m.paragraphs
if "quick brown fox" in " ".join("".join(r.text for r in ln.runs) for ln in p.lines))
def reflow(doc, page, para, text, para_id):
oi = [i for ln in para.lines for r in ln.runs for i in r.object_indices]
cl = min(ln.x for ln in para.lines); cr = max(ln.x + ln.w for ln in para.lines)
fb = max(ln.baseline_y for ln in para.lines)
bls = sorted({round(ln.baseline_y, 1) for ln in para.lines}, reverse=True)
lead = abs(bls[0]-bls[1]) if len(bls) > 1 else 14.0
op = {"version": "1.0", "operations": [{"id": "p", "type": "reflow_paragraph", "pageIndex": page, "data": {
"objectIndices": oi, "runs": [{"text": text, "internalFontId": para.lines[0].runs[0].internal_font_id,
"fontSize": 11.0, "color": "#000000"}], "columnLeft": cl, "columnRight": cr, "firstBaselineY": fb,
"leading": lead, "oldLineCount": len(para.lines), "align": "left", "paraId": para_id}}]}
doc.apply_edits(json.dumps(op))
return pdfengine.PdfDocument.load_from_memory(doc.save_full(), "")
def all_para_ids(doc):
ids = {}
for i in range(doc.page_count):
m = doc.get_page(i).extract_document_model()
for p in m.paragraphs:
for ln in p.lines:
for r in ln.runs:
if getattr(r, "para_id", ""):
ids.setdefault(r.para_id, []).append(i)
return ids
def main() -> int:
ok = True
doc = pdfengine.PdfDocument.load_from_memory(build_pdf(), "")
doc = reflow(doc, 0, body_para(doc), "Short edited text here.", "PARA_TEST_42")
ids = all_para_ids(doc)
if "PARA_TEST_42" in ids:
print(f" ok mark survived save/reload: PARA_TEST_42 on pages {sorted(set(ids['PARA_TEST_42']))}")
else:
print(f" FAIL no paraId after save/reload. ids={ids}"); ok = False
doc2 = pdfengine.PdfDocument.load_from_memory(build_pdf(), "")
big = "Edited overflow text " + " ".join(f"m{n:03d}" for n in range(80))
doc2 = reflow(doc2, 0, body_para(doc2), big, "SPAN_ID_7")
ids2 = all_para_ids(doc2)
pages = sorted(set(ids2.get("SPAN_ID_7", [])))
if len(pages) >= 2:
print(f" ok mark survived cross-page migration: SPAN_ID_7 on pages {pages}")
else:
print(f" FAIL paraId not on both pages after overflow. pages={pages} all={ids2}"); ok = False
print("\nPASS" if ok else "\nFAIL")
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())
-41
View File
@@ -1,41 +0,0 @@
PDF: Mo-Faishal-Qureshi.pdf pages=2
=== PAGE 0 (612x792) paragraphs=10 ===
[0.0] lines=1 fonts=['Calibri-Bold_TrueType_32']
text='MO FAISHAL QURESHI'
[0.1] lines=1 fonts=['Calibri-Bold_TrueType_32']
text='Software Engineer | 3.2 Years Experience'
[0.2] lines=1 fonts=['Calibri-Bold_TrueType_32']
text='Professional Summary'
[0.3] lines=5 fonts=['Calibri_TrueType_32']
text='Java Backend Developer with 3.2 years of experience in designing, developing, and deployin'
[0.4] lines=1 fonts=['Calibri-Bold_TrueType_32']
text='Technical Skills'
[0.5] lines=11 fonts=['BCDGEE+TimesNewRomanPSMT', 'Calibri-Bold_TrueType_32', 'Calibri_TrueType_32']
text='• Languages: Core Java, Java 8+, OOPs, Collections, Streams, Lambda • '
[0.6] lines=1 fonts=['Calibri-Bold_TrueType_32']
text='Professional Experience'
[0.7] lines=19 fonts=['BCDGEE+TimesNewRomanPSMT', 'BCDJEE+Calibri-Italic', 'BCDKEE+Calibri', 'Calibri-Bold_TrueType_32', 'Calibri-Italic_TrueType_96', 'Calibri_TrueType_32']
text='Software Engineer | TapQwik Software Pvt. Ltd. Mar 2023 Present | '
[0.8] lines=1 fonts=['Calibri-Bold_TrueType_32']
text='Projects'
[0.9] lines=3 fonts=['BCDLEE+Calibri-Bold', 'Calibri-Bold_TrueType_32', 'Calibri_TrueType_32']
text='Project: LG E-Commerce Platform Technologies: Java 8/17, Spring Boot, '
=== PAGE 1 (612x792) paragraphs=8 ===
[1.0] lines=13 fonts=['BCDGEE+TimesNewRomanPSMT', 'BCDKEE+Calibri', 'Calibri-Bold_TrueType_32', 'Calibri_TrueType_32']
text='Architected and developed a high-scale e-commerce backend for LG Electro'
[1.1] lines=16 fonts=['BCDGEE+TimesNewRomanPSMT', 'BCDKEE+Calibri', 'BCDLEE+Calibri-Bold', 'Calibri-Bold_TrueType_32', 'Calibri_TrueType_32']
text='Project: Wego Hotel Booking Platform Technologies: Java 8/17, Spring B'
[1.2] lines=1 fonts=['Calibri-Bold_TrueType_32']
text='Education'
[1.3] lines=2 fonts=['Calibri-Bold_TrueType_32', 'Calibri-Italic_TrueType_96', 'Calibri_TrueType_32']
text='Bachelor of Computer Applications 2023 HKBK Degree College | Bangalore, '
[1.4] lines=1 fonts=['Calibri-Bold_TrueType_32']
text='Courses & Certifications'
[1.5] lines=3 fonts=['BCDGEE+TimesNewRomanPSMT', 'BCDKEE+Calibri', 'Calibri_TrueType_32']
text='• Java Development Certification Course • Apache Kafka for Java Develope'
[1.6] lines=1 fonts=['Calibri-Bold_TrueType_32']
text='Soft Skills'
[1.7] lines=2 fonts=['BCDKEE+Calibri', 'Calibri_TrueType_32']
text='Problem-Solving • Team Collaboration • Adaptability • Ownership & Accoun'
-380
View File
@@ -1,380 +0,0 @@
#!/usr/bin/env python3
"""Scratch harness: reproduce the multi-edit font-collision scramble, and (after the
fix) verify it's gone. Reads engine output through pybind (NOT HTTP/stdin) to avoid the
Windows cp1252 mojibake trap. Writes a UTF-8 report to _reflow_repro.out.txt.
Run: gateway/.venv/Scripts/python.exe tests/edits/_reflow_repro.py
"""
from __future__ import annotations
import io
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "gateway"))
import pdfengine # noqa: E402
PDF = ROOT / "Mo-Faishal-Qureshi.pdf"
OUT = Path(__file__).resolve().parent / "_reflow_repro.out.txt"
report = io.StringIO()
def log(*a):
report.write(" ".join(str(x) for x in a) + "\n")
def para_text(p):
return " ".join(r.text for l in p.lines for r in l.runs)
def para_fonts(p):
return sorted({r.internal_font_id for l in p.lines for r in l.runs if r.text.strip()})
def line_advances(line):
"""Per-character ORIGINAL advances for every run in a line, computed ACROSS the line so each
glyph's advance = the next glyph's origin minus this one (captures inter-run gaps exactly; a
per-run computation would overshoot by each run's left-side bearing and smear cumulatively).
Returns {run_index_in_line: [adv,...]} only for runs whose glyphs align 1:1 with their chars."""
seq = [] # (run_idx_in_line, char_idx, origin_x) in reading order
aligned = {}
for ri, r in enumerate(line.runs):
gs = list(r.glyphs)
ok = bool(r.text) and len(gs) == len(r.text)
aligned[ri] = ok
if ok:
for ci in range(len(r.text)):
seq.append((ri, ci, gs[ci].origin_x))
out = {ri: [0.0] * len(line.runs[ri].text) for ri in aligned if aligned[ri]}
anchor_x = seq[0][2] if seq else line.x # the line's FIRST glyph origin (true left anchor)
for k, (ri, ci, ox) in enumerate(seq):
if k + 1 < len(seq):
out[ri][ci] = seq[k + 1][2] - ox # delta to next glyph (incl. inter-run gap)
else:
out[ri][ci] = (line.x + line.w) - ox # last glyph of line: to the line's right edge
# Guard: drop any run that produced a non-positive advance (out-of-order glyphs) -> measure it.
for ri in list(out.keys()):
if any(a <= 0 for a in out[ri]):
del out[ri]
return out, anchor_x
def build_reflow_op(p, page_index, new_runs=None, use_lines=False):
"""Build a reflow_paragraph op for paragraph p. If new_runs is None, re-emit the
paragraph's own runs unchanged (so a correct engine is a no-op on text).
use_lines=True sends the paragraph's ORIGINAL visual line breaks (the no-change-on-click
path) so the engine emits them verbatim instead of greedy-wrapping."""
runs = []
obj_idx = []
op_lines = []
op_line_x = []
op_line_base = []
for l in p.lines:
line_frags = []
advs, anchor_x = line_advances(l)
for ri, r in enumerate(l.runs):
obj_idx.extend(list(r.object_indices))
if r.text == "":
continue
frag = {"text": r.text, "internalFontId": r.internal_font_id,
"fontSize": r.font_size, "color": "#000000"}
if ri in advs:
frag["advances"] = advs[ri]
runs.append(frag)
line_frags.append(frag)
if line_frags:
op_lines.append(line_frags)
op_line_x.append(anchor_x)
op_line_base.append(l.baseline_y)
if new_runs is not None:
runs = new_runs
baselines = [l.baseline_y for l in p.lines]
first_baseline = baselines[0] if baselines else (p.y + p.h)
if len(baselines) >= 2:
leading = abs(baselines[0] - baselines[1])
else:
leading = p.lines[0].h if p.lines else 14.0
if leading <= 0:
leading = 14.0
return {
"id": "rf", "type": "reflow_paragraph", "pageIndex": page_index,
"data": {
"objectIndices": sorted(set(obj_idx)),
"runs": runs,
"columnLeft": p.x,
"columnRight": p.x + p.w,
"firstBaselineY": first_baseline,
"leading": leading,
"oldLineCount": len(p.lines),
"align": "left",
**({"lines": op_lines, "lineBaselineY": op_line_base, "lineX": op_line_x}
if use_lines and new_runs is None else {}),
},
}
def dump_structure():
doc = pdfengine.PdfDocument.load_from_memory(PDF.read_bytes(), "")
npages = 0
while True:
try:
doc.get_page(npages)
npages += 1
except Exception:
break
log(f"PDF: {PDF.name} pages={npages}")
for pi in range(npages):
m = doc.get_page(pi).extract_document_model()
log(f"\n=== PAGE {pi} ({m.width:.0f}x{m.height:.0f}) paragraphs={len(m.paragraphs)} ===")
for i, p in enumerate(m.paragraphs):
t = para_text(p)
log(f" [{pi}.{i}] lines={len(p.lines)} fonts={para_fonts(p)}")
log(f" text={t[:90]!r}")
def find_para(model, needle):
for i, p in enumerate(model.paragraphs):
if needle in para_text(p):
return i, p
return -1, None
def repro(page_index=0, a_needle="Java Backend Developer", b_needle="Core Java",
survive=("Core", "Java")):
"""Faithful app flow: edit paragraph A (modified), re-extract, then re-emit
paragraph B UNCHANGED. B's text must survive intact in the committed PDF."""
# ---- CONTROL: B-only on a fresh doc (proves B alone is fine) ----
doc = pdfengine.PdfDocument.load_from_memory(PDF.read_bytes(), "")
m0 = doc.get_page(page_index).extract_document_model()
bi, pb = find_para(m0, b_needle)
log(f"\n--- CONTROL: re-emit B [{page_index}.{bi}] unchanged (no prior edit) ---")
log(f" B text(before) = {para_text(pb)[:80]!r}")
doc.apply_edits(json.dumps({"version": "1.0", "operations": [build_reflow_op(pb, page_index)]}))
m_ctrl = pdfengine.PdfDocument.load_from_memory(doc.save_full(), "").get_page(page_index).extract_document_model()
_, pb_ctrl = find_para(m_ctrl, survive[0])
ctrl_text = para_text(pb_ctrl) if pb_ctrl else "<B PARA NOT FOUND>"
log(f" B text(after) = {ctrl_text[:80]!r}")
ctrl_ok = all(s in ctrl_text for s in survive)
log(f" CONTROL survive={survive} -> {'OK' if ctrl_ok else 'LOST'}")
# ---- TEST: edit A first, re-extract, then re-emit B unchanged ----
doc = pdfengine.PdfDocument.load_from_memory(PDF.read_bytes(), "")
m0 = doc.get_page(page_index).extract_document_model()
ai, pa = find_para(m0, a_needle)
log(f"\n--- TEST: edit A [{page_index}.{ai}] THEN re-emit B unchanged (same doc) ---")
# Modify A: prepend a word to its first run.
a_runs = []
first = True
for l in pa.lines:
for r in l.runs:
if r.text == "":
continue
txt = ("EDITED " + r.text) if first else r.text
first = False
a_runs.append({"text": txt, "internalFontId": r.internal_font_id,
"fontSize": r.font_size, "color": "#000000"})
doc.apply_edits(json.dumps({"version": "1.0",
"operations": [build_reflow_op(pa, page_index, new_runs=a_runs)]}))
# Re-extract (frontend re-fetches the model after each edit) and re-locate B.
m1 = doc.get_page(page_index).extract_document_model()
bi2, pb2 = find_para(m1, b_needle)
if pb2 is None:
log(f" !! B not found after edit A (searched {b_needle!r})")
else:
log(f" B text(before 2nd edit) = {para_text(pb2)[:80]!r}")
doc.apply_edits(json.dumps({"version": "1.0", "operations": [build_reflow_op(pb2, page_index)]}))
# In-memory (pre-save) B text: localizes corruption to emission vs save-merge.
m_inmem = doc.get_page(page_index).extract_document_model()
_, pb_inmem = find_para(m_inmem, survive[0])
log(f" B text(in-memory, pre-save) = {(para_text(pb_inmem) if pb_inmem else '<NOT FOUND>')[:80]!r}")
m2 = pdfengine.PdfDocument.load_from_memory(doc.save_full(), "").get_page(page_index).extract_document_model()
_, pb_test = find_para(m2, survive[0])
test_text = para_text(pb_test) if pb_test else "<B PARA NOT FOUND>"
log(f" B text(after) = {test_text[:80]!r}")
test_ok = all(s in test_text for s in survive)
log(f" TEST survive={survive} -> {'OK' if test_ok else 'SCRAMBLED/LOST'}")
if not test_ok:
# Show the corrupted bullets paragraph (locate by leftover 'Languages'/'ang' or dump near B).
for i, p in enumerate(m2.paragraphs):
t = para_text(p)
if "ang" in t or "OOP" in t or "Collec" in t or "Stream" in t or (b_needle[:3] in t):
log(f" >> scrambled B candidate [{page_index}.{i}] = {t[:90]!r}")
log(f"\n==> CONTROL={'OK' if ctrl_ok else 'FAIL'} TEST={'OK' if test_ok else 'FAIL'} "
f"(bug reproduced if CONTROL=OK and TEST=FAIL)")
def stress():
"""Sequentially reflow EVERY paragraph on both pages (re-extracting between each, like the
real app), then assert distinctive substrings from each survive — catches any font scramble
across non-subset AND subset (BCDKEE+Calibri...) fonts under heavy multi-edit."""
# Distinctive substrings to verify survive on each page after all edits.
checks = {
0: ["Professional", "Backend", "Core", "Java", "Frameworks", "Microservices",
"TapQwik", "Present", "LG", "Commerce"],
1: ["Architected", "Wego", "Booking", "Education", "Bachelor", "Certification",
"Kafka", "Problem", "Ownership"],
}
# Edit (reflow-in-place) the paragraph CONTAINING each anchor, sequentially, re-extracting
# between edits — exactly how a user edits N paragraphs in one session.
anchors = {
0: ["Backend", "Core Java", "Mar 2023", "LG"],
1: ["Architected", "Wego", "Java Development", "Problem"],
}
failures = []
for pi in (0, 1):
doc = pdfengine.PdfDocument.load_from_memory(PDF.read_bytes(), "")
log(f"\n=== STRESS page {pi}: sequentially reflowing {len(anchors[pi])} targeted paragraphs ===")
for anchor in anchors[pi]:
m = doc.get_page(pi).extract_document_model()
_, p = find_para(m, anchor)
if p is None:
log(f" anchor {anchor!r}: paragraph not found (possibly merged by a prior edit)")
continue
try:
doc.apply_edits(json.dumps({"version": "1.0", "operations": [build_reflow_op(p, pi)]}))
except Exception as exc:
log(f" anchor {anchor!r}: apply failed: {exc}")
final = para_textall(doc.get_page(pi).extract_document_model())
for needle in checks[pi]:
if needle not in final:
failures.append((pi, needle))
present = [n for n in checks[pi] if n in final]
log(f" page {pi}: {len(present)}/{len(checks[pi])} substrings survived: "
f"missing={[n for n in checks[pi] if n not in final]}")
log(f"\n==> STRESS {'PASS' if not failures else 'FAIL ' + str(failures)}")
def para_textall(model):
return " ".join(r.text for p in model.paragraphs for l in p.lines for r in l.runs)
def _png_to_img(pageimg):
from PIL import Image # noqa: PLC0415
data = pageimg.data
if len(data) >= 8 and data[:8] == b"\x89PNG\r\n\x1a\n":
return Image.open(io.BytesIO(data)).convert("RGB")
n = pageimg.width * pageimg.height
mode = "RGBA" if len(data) == n * 4 else "RGB"
return Image.frombytes(mode, (pageimg.width, pageimg.height), data).convert("RGB")
def _diff_stats(base_img, edited_img):
"""Return (total_nonzero_px, bbox_of_diff) between two RGB images."""
from PIL import ImageChops # noqa: PLC0415
diff = ImageChops.difference(base_img, edited_img)
bbox = diff.getbbox() # None if identical
nz = 0
for px in diff.getdata():
if px != (0, 0, 0):
nz += 1
return nz, bbox
def overlay(page_index=0, anchor="Java Backend Developer", edit=None, dpi=150, use_lines=False):
"""Objective pixel gate. Render the page, then reflow the paragraph containing `anchor`
(UNCHANGED if edit is None, else `edit(runs)->runs`), render again, and report the pixel
diff. A no-op reflow should ideally diff ~0 everywhere (the drift baseline); a real edit
should diff ONLY inside the edited region."""
base = _png_to_img(pdfengine.PdfDocument.load_from_memory(PDF.read_bytes(), "").get_page(page_index).render(dpi))
doc = pdfengine.PdfDocument.load_from_memory(PDF.read_bytes(), "")
m = doc.get_page(page_index).extract_document_model()
i, p = find_para(m, anchor)
if p is None:
log(f"overlay: anchor {anchor!r} not found"); return
new_runs = None
if edit is not None:
base_runs = [{"text": r.text, "internalFontId": r.internal_font_id, "fontSize": r.font_size, "color": "#000000"}
for l in p.lines for r in l.runs if r.text]
new_runs = edit(base_runs)
op = build_reflow_op(p, page_index, new_runs=new_runs, use_lines=use_lines)
doc.apply_edits(json.dumps({"version": "1.0", "operations": [op]}))
edited = _png_to_img(doc.get_page(page_index).render(dpi))
nz, bbox = _diff_stats(base, edited)
total = base.width * base.height
log(f"overlay [{page_index}] anchor={anchor!r} edit={'no-op' if edit is None else 'changed'} "
f"path={'lines' if use_lines else 'greedy'} dpi={dpi}")
log(f" diff pixels = {nz} / {total} ({100.0*nz/total:.3f}%) bbox={bbox}")
return nz, bbox
def render_after_edits():
"""Apply A (Summary) then B (bullets) on one doc, save, reload, render page 0 to PNG —
GROUND TRUTH: is the committed PDF visually corrupt, or only text extraction?"""
from PIL import Image # noqa: PLC0415
doc = pdfengine.PdfDocument.load_from_memory(PDF.read_bytes(), "")
m0 = doc.get_page(0).extract_document_model()
_, pa = find_para(m0, "Java Backend Developer")
a_runs = []
first = True
for l in pa.lines:
for r in l.runs:
if r.text == "":
continue
a_runs.append({"text": ("EDITED " + r.text) if first else r.text,
"internalFontId": r.internal_font_id, "fontSize": r.font_size, "color": "#000000"})
first = False
doc.apply_edits(json.dumps({"version": "1.0", "operations": [build_reflow_op(pa, 0, new_runs=a_runs)]}))
m1 = doc.get_page(0).extract_document_model()
_, pb = find_para(m1, "Core Java")
doc.apply_edits(json.dumps({"version": "1.0", "operations": [build_reflow_op(pb, 0)]}))
out = doc.save_full()
img = pdfengine.PdfDocument.load_from_memory(out, "").get_page(0).render(150)
data = img.data
png = Path(__file__).resolve().parent / "_reflow_render.png"
n = img.width * img.height
if len(data) >= 8 and data[:8] == b"\x89PNG\r\n\x1a\n":
png.write_bytes(data) # already PNG-encoded
else:
mode = "RGBA" if len(data) == n * 4 else ("RGB" if len(data) == n * 3 else None)
from PIL import Image # noqa: PLC0415
Image.frombytes(mode, (img.width, img.height), data).convert("RGB").save(png)
log(f"rendered committed page0 -> {png} ({img.width}x{img.height}, {len(data)} bytes)")
def probe_fonts():
doc = pdfengine.PdfDocument.load_from_memory(PDF.read_bytes(), "")
for pi in (0, 1):
fonts = doc.get_page(pi).get_fonts()
log(f"\n=== PAGE {pi} fonts ({len(fonts)}) ===")
for f in fonts:
log(f" font_name={f.font_name!r:34} internal_id={f.internal_font_id!r:28} "
f"type={f.type!r:12} flags={f.flags:<4} subset={f.is_subset} tag={f.subset_tag!r} "
f"embedded={f.is_embedded}")
if __name__ == "__main__":
mode = sys.argv[1] if len(sys.argv) > 1 else "dump"
if mode == "dump":
dump_structure()
elif mode == "repro":
repro()
elif mode == "repro_diff":
# A = Calibri body (Summary); B = a Calibri-Bold-only heading ("Projects").
# Different base fonts -> if B survives, the scramble is same-/BaseFont aliasing.
repro(a_needle="Java Backend Developer", b_needle="Projects", survive=("Projects",))
elif mode == "probe":
probe_fonts()
elif mode == "render":
render_after_edits()
elif mode == "stress":
stress()
elif mode == "overlay":
# Baseline drift: no-op reflow, GREEDY path (post-edit behavior).
log("== GREEDY path (re-wrap from scratch; the 'while typing' path) ==")
overlay(0, "Java Backend Developer")
overlay(0, "Core Java")
overlay(1, "Architected and")
# no-op reflow, LINES path (the no-change-on-click path).
log("\n== LINES path (original breaks emitted verbatim; the 'on open' path) ==")
overlay(0, "Java Backend Developer", use_lines=True)
overlay(0, "Core Java", use_lines=True)
overlay(1, "Architected and", use_lines=True)
# noise floor for reference
base = _png_to_img(pdfengine.PdfDocument.load_from_memory(PDF.read_bytes(), "").get_page(0).render(150))
b2 = _png_to_img(pdfengine.PdfDocument.load_from_memory(PDF.read_bytes(), "").get_page(0).render(150))
nz, _ = _diff_stats(base, b2)
log(f"\n(noise floor, same pdf twice = {nz} px)")
OUT.write_text(report.getvalue(), encoding="utf-8")
print(f"wrote {OUT}")
+64
View File
@@ -0,0 +1,64 @@
"""Diagnose the 'star broke the font + overlap' report. Edit a Calibri run adding * and the unicode
star, then check: which font emitted, whether the glyphs are present, and whether consecutive glyphs
OVERLAP (x positions go backwards / a glyph has ~0 advance).
Run: gateway/.venv/Scripts/python.exe tests/edits/_symbol_diag.py
"""
from __future__ import annotations
import json, sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, r"C:\Users\furqa\pdfeng-build\win-local-pdfium\lib")
sys.path.insert(1, str(ROOT / "gateway"))
import pdfengine
PDF = ROOT / "Mo-Faishal-Qureshi.pdf"
def ptext(p):
return "".join(r.text for ln in p.lines for r in ln.runs)
def main():
doc = pdfengine.PdfDocument.load_from_memory(PDF.read_bytes(), "")
m = doc.get_page(1).extract_document_model()
para = next(p for p in m.paragraphs if "Designed high-performance" in ptext(p))
oi = [i for ln in para.lines for r in ln.runs for i in r.object_indices]
cl = min(ln.x for ln in para.lines); cr = max(ln.x + ln.w for ln in para.lines)
fb = max(ln.baseline_y for ln in para.lines)
bls = sorted({round(ln.baseline_y, 1) for ln in para.lines}, reverse=True)
lead = abs(bls[0]-bls[1]) if len(bls) > 1 else 14.0
fid = para.lines[0].runs[0].internal_font_id
print("fid:", fid)
new_text = "Star test asterisk * and blackstar ★ end"
op = {"version": "1.0", "operations": [{"id": "s", "type": "reflow_paragraph", "pageIndex": 1, "data": {
"objectIndices": oi, "runs": [{"text": new_text, "internalFontId": fid, "fontSize": 11.0, "color": "#000000"}],
"columnLeft": cl, "columnRight": cr, "firstBaselineY": fb, "leading": lead,
"oldLineCount": len(para.lines), "align": "left", "paraId": "SYMCHK"}}]}
doc.apply_edits(json.dumps(op))
r = pdfengine.PdfDocument.load_from_memory(doc.save_full(), "")
for pg in range(r.page_count):
for p in r.get_page(pg).extract_document_model().paragraphs:
if "asterisk" in ptext(p):
fonts = sorted({rr.font_name for ln in p.lines for rr in ln.runs if rr.text.strip()})
txt = ptext(p)
print("emitted fonts:", fonts)
print("has '*':", "*" in txt, " has U+2605:", "" in txt)
gl = [(g.text, g.origin_x) for ln in p.lines for rr in ln.runs for g in rr.glyphs]
print("All glyphs:")
for text, x in gl:
print(f" {repr(text)}: x={x:.2f}")
overlaps = 0
for a, b in zip(gl, gl[1:]):
if b[1] <= a[1] - 0.5 and b[1] > 0:
overlaps += 1
print(f"glyphs={len(gl)} backward/overlap steps={overlaps}")
s = txt.find("blackstar")
print("around star:", repr(txt[max(0,s-6):s+20]))
return
if __name__ == "__main__":
main()
-24
View File
@@ -1,24 +0,0 @@
########## REFLOW stress (multi-edit survival) ##########
=== STRESS page 0: sequentially reflowing 4 targeted paragraphs ===
page 0: 10/10 substrings survived: missing=[]
=== STRESS page 1: sequentially reflowing 4 targeted paragraphs ===
page 1: 9/9 substrings survived: missing=[]
==> STRESS PASS
########## REFLOW no-op overlay (regression: left + justify paragraphs must stay ~baseline) ##########
overlay [0] anchor='Java Backend Developer' edit=no-op path=greedy dpi=150
diff pixels = 35821 / 2103750 (1.703%) bbox=(112, 263, 1150, 1435)
overlay [0] anchor='Java Backend Developer' edit=no-op path=lines dpi=150
diff pixels = 35441 / 2103750 (1.685%) bbox=(112, 263, 1150, 1435)
########## CENTER align smoke ##########
'Software Engineer' heading not found (skip center smoke)
########## RAW TEXT StreamEditor smoke (P1a) ##########
extract_text_objects(0) -> 727 objects
replace obj#2 'FAISHAL' -> 'FAISHAX' : success=True
re-extract obj#2 = 'FAISHAX' (expected 'FAISHAX', match=True)
-95
View File
@@ -1,95 +0,0 @@
#!/usr/bin/env python3
"""Verify the FRESH build-dir .pyd (the gateway copy is locked by the running gateway) without
touching the gateway. Importing pdfengine from the build lib FIRST caches it in sys.modules, so when
_reflow_repro imports pdfengine it reuses this fresh one. Runs the reflow regression gate + a Raw Text
(StreamEditor) smoke for the new TJ-positioning path.
Run: gateway/.venv/Scripts/python.exe tests/edits/_verify_build.py
"""
from __future__ import annotations
import sys
from pathlib import Path
BUILD_LIB = r"C:\Users\furqa\pdfeng-build\win-local-pdfium\lib"
sys.path.insert(0, BUILD_LIB)
import pdfengine # noqa: E402 -- FRESH build, cached in sys.modules before the harness imports it
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(Path(__file__).resolve().parent))
import _reflow_repro as H # noqa: E402 -- reuses the cached fresh pdfengine
print("pdfengine loaded from:", pdfengine.__file__)
assert BUILD_LIB.lower() in pdfengine.__file__.lower(), "NOT the fresh build .pyd!"
PDF = ROOT / "Mo-Faishal-Qureshi.pdf"
def section(t):
H.report.write(f"\n########## {t} ##########\n")
# ---- 1. Reflow regression: multi-edit survival + no-op overlay diff (left/justify) ----
section("REFLOW stress (multi-edit survival)")
H.stress()
section("REFLOW no-op overlay (regression: left + justify paragraphs must stay ~baseline)")
H.overlay(0, "Java Backend Developer") # flowing left/justify body
H.overlay(0, "Java Backend Developer", use_lines=True)
# ---- 2. Center alignment: reflow a centered heading with align=center; assert no crash + survives ----
section("CENTER align smoke")
try:
doc = pdfengine.PdfDocument.load_from_memory(PDF.read_bytes(), "")
m = doc.get_page(0).extract_document_model()
i, p = H.find_para(m, "Software Engineer")
if p is None:
H.log(" 'Software Engineer' heading not found (skip center smoke)")
else:
op = H.build_reflow_op(p, 0)
op["data"]["align"] = "center"
# widen the column to the page content box so centering is visible
op["data"]["columnLeft"] = 40.0
op["data"]["columnRight"] = m.width - 40.0
doc.apply_edits(__import__("json").dumps({"version": "1.0", "operations": [op]}))
m2 = pdfengine.PdfDocument.load_from_memory(doc.save_full(), "").get_page(0).extract_document_model()
_, pc = H.find_para(m2, "Software")
txt = H.para_text(pc) if pc else "<NOT FOUND>"
H.log(f" center reflow OK; text survived = {('Software' in txt and 'Engineer' in txt)} -> {txt[:60]!r}")
except Exception as exc:
H.log(f" CENTER smoke FAILED: {exc}")
# ---- 3. Raw Text StreamEditor: extract + same-length replace (P1a TJ preservation) ----
section("RAW TEXT StreamEditor smoke (P1a)")
try:
import tempfile, os
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf"); tmp.write(PDF.read_bytes()); tmp.close()
ed = pdfengine.StreamEditor(tmp.name)
objs = ed.extract_text_objects(0)
H.log(f" extract_text_objects(0) -> {len(objs)} objects")
# pick the first object with >=3 ASCII chars; do a SAME-LENGTH edit (exercises TJ redistribution)
target = None
for idx, o in enumerate(objs):
t = o["text"].decode("latin-1") if isinstance(o["text"], bytes) else o["text"]
if len(t) >= 3 and t.strip() and all(32 <= ord(c) < 127 for c in t):
target = (idx, t); break
if target is None:
H.log(" no suitable ASCII object found (skip)")
else:
idx, t = target
# same-length swap: reverse-safe -> replace each alnum char's case-insensitive 'a'->'e' won't change len
new = t[:-1] + ("X" if t[-1] != "X" else "Y") # same length, last char changed
out = tmp.name + ".out.pdf"
ok = ed.replace_text_object(0, idx, new.encode("latin-1"), out)
H.log(f" replace obj#{idx} {t!r} -> {new!r} : success={ok}")
if ok and os.path.exists(out):
ed2 = pdfengine.StreamEditor(out)
objs2 = ed2.extract_text_objects(0)
t2 = objs2[idx]["text"]; t2 = t2.decode("latin-1") if isinstance(t2, bytes) else t2
H.log(f" re-extract obj#{idx} = {t2!r} (expected {new!r}, match={t2 == new})")
os.remove(out)
os.remove(tmp.name)
except Exception as exc:
H.log(f" RAW TEXT smoke FAILED: {exc}")
OUT = Path(__file__).resolve().parent / "_verify_build.out.txt"
OUT.write_text(H.report.getvalue(), encoding="utf-8")
print(f"wrote {OUT}")
-82
View File
@@ -1,82 +0,0 @@
"""Cross-page reflow on the REAL resume (embedded Calibri/Times subset fonts + cascade).
Verifies that migrating EMBEDDED subset-font text objects across pages keeps glyphs intact.
Run: gateway/.venv/Scripts/python.exe tests/edits/_xpage_real.py
"""
from __future__ import annotations
import io, json, sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "gateway"))
import pdfengine
PDF = ROOT / "Mo-Faishal-Qureshi.pdf"
def ptext(p):
return " ".join("".join(r.text for r in ln.runs) for ln in p.lines)
def page_text(doc, i):
m = doc.get_page(i).extract_document_model()
return " ".join(ptext(p) for p in m.paragraphs)
def main():
doc = pdfengine.PdfDocument.load_from_memory(PDF.read_bytes(), "")
print(f"loaded: pages={doc.page_count}")
m0 = doc.get_page(0).extract_document_model()
body = [p for p in m0.paragraphs if len(p.lines) >= 1]
para = min(body, key=lambda p: min(ln.baseline_y for ln in p.lines))
print(f"editing page-0 paragraph (lowest): {ptext(para)[:60]!r} lines={len(para.lines)}")
obj_idxs = [oi for ln in para.lines for r in ln.runs for oi in r.object_indices]
col_left = min(ln.x for ln in para.lines)
col_right = max(ln.x + ln.w for ln in para.lines)
first_baseline = max(ln.baseline_y for ln in para.lines)
bls = sorted({round(ln.baseline_y, 1) for ln in para.lines}, reverse=True)
leading = abs(bls[0] - bls[1]) if len(bls) > 1 else 14.0
fid = para.lines[0].runs[0].internal_font_id
marker = " ".join(f"X{n:03d}" for n in range(60))
new_text = ptext(para).strip() + " " + marker + " ZZEND"
op = {"version": "1.0", "operations": [{
"id": "rg", "type": "reflow_paragraph", "pageIndex": 0, "data": {
"objectIndices": obj_idxs,
"runs": [{"text": new_text, "internalFontId": fid, "fontSize": 11.0, "color": "#000000"}],
"columnLeft": col_left, "columnRight": col_right, "firstBaselineY": first_baseline,
"leading": leading, "oldLineCount": len(para.lines), "align": "left"}}]}
doc.apply_edits(json.dumps(op))
out = doc.save_full()
r = pdfengine.PdfDocument.load_from_memory(out, "")
print(f"after grow: pages={r.page_count}")
for i in range(r.page_count):
t = page_text(r, i)
has_marker = any(f"X{n:03d}" in t for n in range(60))
print(f" page {i}: ZZEND={'ZZEND' in t} markers={has_marker} len={len(t)}")
allt = " ".join(page_text(r, i) for i in range(r.page_count))
print("Architected preserved:", "Architected" in allt)
print("Education preserved:", "Education" in allt)
nospace = allt.replace(" ", "")
miss_join = [f"X{n:03d}" for n in range(60) if f"X{n:03d}" not in allt]
miss_nospace = [f"X{n:03d}" for n in range(60) if f"X{n:03d}" not in nospace]
print(f"markers intact (space-join): {60-len(miss_join)}/60 missing: {miss_join}")
print(f"markers intact (no-space): {60-len(miss_nospace)}/60 missing: {miss_nospace}")
p0 = page_text(r, 0); p1 = page_text(r, 1)
print("\n--- page0 tail ---\n", p0[-220:])
print("\n--- page1 head ---\n", p1[:220])
orig = pdfengine.PdfDocument.load_from_memory(PDF.read_bytes(), "")
orig_p1 = page_text(orig, 1)
for needle in ["Architected", "Education", "Soft", "Problem-Solving", "Ownership", "Certifications", "Bachelor"]:
print(f" page1-orig token {needle!r}: present_before={needle in orig_p1} present_after={needle in allt}")
for i in range(r.page_count):
mm = r.get_page(i).extract_document_model()
ys = [ln.baseline_y for p in mm.paragraphs for ln in p.lines]
print(f" page {i}: lowest_baseline_y={min(ys):.1f} (page height 792; <0 means off-page)")
if __name__ == "__main__":
main()
-76
View File
@@ -1,76 +0,0 @@
"""Reproduce the user's bug: grow a page-0 paragraph so it overflows to page 1, save, THEN edit a
paragraph that now lives on page 1 -> is the result scrambled (leftover glyphs / wrong fonts)?
Run: gateway/.venv/Scripts/python.exe tests/edits/_xpage_reedit.py
"""
from __future__ import annotations
import json, sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "gateway"))
import pdfengine
PDF = ROOT / "Mo-Faishal-Qureshi.pdf"
def ptext(p):
return "".join(r.text for ln in p.lines for r in ln.runs)
def find_para(doc, page, needle):
m = doc.get_page(page).extract_document_model()
for p in m.paragraphs:
if needle in ptext(p):
return p
return None
def reflow_para(doc, page, para, new_text, align="left"):
obj_idxs = [oi for ln in para.lines for r in ln.runs for oi in r.object_indices]
col_left = min(ln.x for ln in para.lines)
col_right = max(ln.x + ln.w for ln in para.lines)
first_baseline = max(ln.baseline_y for ln in para.lines)
bls = sorted({round(ln.baseline_y, 1) for ln in para.lines}, reverse=True)
leading = abs(bls[0] - bls[1]) if len(bls) > 1 else 14.0
fid = para.lines[0].runs[0].internal_font_id
op = {"version": "1.0", "operations": [{
"id": "e", "type": "reflow_paragraph", "pageIndex": page, "data": {
"objectIndices": obj_idxs,
"runs": [{"text": new_text, "internalFontId": fid, "fontSize": 11.0, "color": "#000000"}],
"columnLeft": col_left, "columnRight": col_right, "firstBaselineY": first_baseline,
"leading": leading, "oldLineCount": len(para.lines), "align": align}}]}
doc.apply_edits(json.dumps(op))
return pdfengine.PdfDocument.load_from_memory(doc.save_full(), "")
def main():
doc = pdfengine.PdfDocument.load_from_memory(PDF.read_bytes(), "")
tech = find_para(doc, 0, "Technologies")
print("step1: growing page-0 para:", ptext(tech)[:50], "...")
big = ptext(tech).strip() + " " + ("g" * 30 + " ") * 25
doc = reflow_para(doc, 0, tech, big)
print(" pages after grow:", doc.page_count)
arch = find_para(doc, 1, "Architected")
if not arch:
print(" !! 'Architected' not found on page 1; pages dump:")
for i in range(doc.page_count):
print(f" page {i}:", " | ".join(ptext(p)[:30] for p in doc.get_page(i).extract_document_model().paragraphs)[:200])
return
print("step2: editing page-1 para:", ptext(arch)[:50], "...")
before = ptext(arch)
doc2 = reflow_para(doc, 1, arch, "Architected and developed a high-scale ecommerce backend NEWTEXT123 for testing.")
after = ptext(find_para(doc2, 1, "Architected") or find_para(doc2, 0, "Architected"))
print("\n BEFORE:", before[:90])
print(" AFTER :", after[:120])
print(" contains NEWTEXT123:", "NEWTEXT123" in after)
clean = "Architected and developed a high-scale ecommerce backend NEWTEXT123 for testing."
print(" clean match:", after.strip() == clean)
for w in ["product", "catalog", "dynamic", "pricing", "transactions"]:
if w in after:
print(f" !! LEFTOVER old word in edited paragraph: {w!r}")
if __name__ == "__main__":
main()
+5
View File
@@ -43,7 +43,12 @@ target_include_directories(pdfengine_wasm PRIVATE
target_link_libraries(pdfengine_wasm PRIVATE pdfengine::pdfengine)
set(_wasm_font_src "${CMAKE_SOURCE_DIR}/engine/assets/fonts")
set(_wasm_font_dir "${CMAKE_BINARY_DIR}/embed_fonts")
file(COPY "${_wasm_font_src}/" DESTINATION "${_wasm_font_dir}")
target_link_options(pdfengine_wasm PRIVATE
"--embed-file" "${_wasm_font_dir}@/fonts"
"-sMODULARIZE=1"
"-sEXPORT_ES6=1"
"-sENVIRONMENT=node,web"