"""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 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] assert abs(a.x - run.x) < 3 and a.width > 10, f"{deco}: bbox off ({a.x},{a.width})" 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())