fix: additional font handling, with operators font issues

This commit is contained in:
Furqan-14
2026-06-22 10:48:00 +05:30
parent c4f5012351
commit c0bcfe47a2
21 changed files with 248 additions and 1096 deletions
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"