74 lines
2.7 KiB
Python
74 lines
2.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Text-markup decoration tests (underline / strikeout / squiggly).
|
|
|
|
Decorations are real PDF text-markup annotations (not baked paths): they round-trip,
|
|
are extractable with the correct subtype + geometry, and are deletable like any
|
|
annotation. PDFium positions them within the text quad.
|
|
|
|
Run: gateway/.venv/Scripts/python.exe tests/edits/test_decorations.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
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
|
|
|
|
CORPUS = ROOT / "corpus" / "fonts" / "utf-8.pdf"
|
|
|
|
|
|
def _load():
|
|
return pdfengine.PdfDocument.load_from_memory(CORPUS.read_bytes(), "")
|
|
|
|
|
|
def _quad(run, page_height):
|
|
top, bot = page_height - (run.y + run.h), page_height - run.y
|
|
return {"x1": run.x, "y1": top, "x2": run.x + run.w, "y2": top,
|
|
"x3": run.x + run.w, "y3": bot, "x4": run.x, "y4": bot}
|
|
|
|
|
|
def _op(deco, run, page_height):
|
|
return {"version": "1.0", "operations": [{
|
|
"id": deco, "type": deco, "pageIndex": 0,
|
|
"data": {"quadPoints": [_quad(run, page_height)], "color": "#ff0000", "author": "Me"}}]}
|
|
|
|
|
|
def test_decorations_are_annotations_and_deletable():
|
|
for deco in ("underline", "strikeout", "squiggly"):
|
|
d = _load()
|
|
m = d.get_page(0).extract_document_model()
|
|
run = m.paragraphs[0].lines[0].runs[0]
|
|
d.apply_edits(json.dumps(_op(deco, run, m.height)))
|
|
doc2 = pdfengine.PdfDocument.load_from_memory(d.save_full(), "")
|
|
annots = doc2.get_page(0).extract_annotations()
|
|
match = [a for a in annots if a.type == deco]
|
|
assert match, f"{deco}: expected a '{deco}' annotation, got {[a.type for a in annots]}"
|
|
a = match[0]
|
|
# geometry roughly covers the run (top-left frame)
|
|
assert abs(a.x - run.x) < 3 and a.width > 10, f"{deco}: bbox off ({a.x},{a.width})"
|
|
|
|
# deletable like any annotation
|
|
doc2.apply_edits(json.dumps({"version": "1.0", "operations": [
|
|
{"id": "del", "type": "delete_annotation", "pageIndex": 0, "data": {"annotationId": a.id}}]}))
|
|
after = [x.type for x in pdfengine.PdfDocument.load_from_memory(doc2.save_full(), "").get_page(0).extract_annotations()]
|
|
assert deco not in after, f"{deco}: still present after delete ({after})"
|
|
print(f" ok {deco}: round-trips as annotation (bbox x={a.x:.0f} w={a.width:.0f}) and deletes cleanly")
|
|
|
|
|
|
def main() -> int:
|
|
try:
|
|
test_decorations_are_annotations_and_deletable()
|
|
except AssertionError as exc:
|
|
print(f" FAIL: {exc}")
|
|
return 1
|
|
print("\n1/1 passed.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|