Files
pdf/tests/edits/test_decorations.py
T

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())