#!/usr/bin/env python3 """Deterministic, self-authored PDF annotation geometry; no external content.""" from pathlib import Path import hashlib import json root = Path(__file__).parent / "annotation-flags" root.mkdir(exist_ok=True) objects = ["", ""] def add(value): objects.append(value.encode("ascii") if isinstance(value, str) else value) return len(objects) def stream(content, extra): data = content.encode("ascii") return add(f"<< /Length {len(data)} {extra} >>\nstream\n".encode() + data + b"\nendstream") # Non-identity AP matrix, named graphics state, and selected appearance state # must survive rectangle compensation. Normalization still yields red left / # blue right in the annotation's 32x16 rectangle; Off is deliberately green. normal = stream("q /Opaque gs 1 0 0 rg 0 0 16 16 re f 0 0 1 rg 16 0 16 16 re f Q", "/Type /XObject /Subtype /Form /BBox [0 0 32 16] /Matrix [2 0 0 3 7 11] " "/Resources << /ExtGState << /Opaque << /Type /ExtGState /ca 1 /CA 1 >> >> >>") green = stream("0 1 0 rg 0 0 32 16 re f", "/Type /XObject /Subtype /Form /BBox [0 0 32 16] /Resources << >>") pages, cases = [], [] def page(annots, rotate, case): pid = add(f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 128 128] /CropBox [8 12 120 116] /Rotate {rotate} " f"/Resources << >> /Annots [{' '.join(f'{x} 0 R' for x in annots)}] >>") pages.append(pid) cases.append(dict(page=len(pages)-1, intrinsicRotation=rotate, **case)) for kind in ["generated-link", "stored-link", "stored-comment"]: for flags in [8, 16, 24]: for rotate in [0, 90, 180, 270]: extra = ("/C [1 0 0] /BS << /W 2 /S /S >>" if kind == "generated-link" else f"/AP << /N << /On {normal} 0 R /Off {green} 0 R >> >> /AS /On") subtype = "Stamp" if kind == "stored-comment" else "Link" annot = add(f"<< /Type /Annot /Subtype /{subtype} /Rect [42 48 74 64] /F {flags} " f"/Contents ({kind} flags {flags}) /A << /S /URI /URI (https://example.org/flags) >> {extra} >>") page([annot], rotate, dict(kind=kind, flags=flags, rect=[42,48,74,64])) # Annots order is retained: the later green appearance covers the generated # border; Hidden / NoView appearances at distinct rectangles remain invisible. annots = [add("<< /Type /Annot /Subtype /Link /Rect [42 48 74 64] /F 24 /C [1 0 0] /BS << /W 2 >> >>"), add(f"<< /Type /Annot /Subtype /Link /Rect [41 47 75 65] /F 24 /AP << /N {green} 0 R >> >>")] for flag, rect in [(26, "20 80 36 96"), (56, "80 80 96 96")]: annots.append(add(f"<< /Type /Annot /Subtype /Link /Rect [{rect}] /F {flag} /Contents (Invisible comment) /AP << /N {normal} 0 R >> >>")) page(annots, 0, dict(kind="visibility-zorder", flags=24)) # Right-edge clipping of an upright unscaled AP remains clipped to the CropBox. edge = add(f"<< /Type /Annot /Subtype /Link /Rect [108 48 140 64] /F 24 /AP << /N {normal} 0 R >> >>") page([edge], 0, dict(kind="crop-clipping", flags=24, rect=[108,48,140,64])) objects[0] = b"<< /Type /Catalog /Pages 2 0 R >>" objects[1] = f"<< /Type /Pages /Count {len(pages)} /Kids [{' '.join(f'{p} 0 R' for p in pages)}] >>".encode() data = bytearray(b"%PDF-1.7\n%\xe2\xe3\xcf\xd3\n") offsets = [0] for index, value in enumerate(objects, 1): offsets.append(len(data)) data += f"{index} 0 obj\n".encode() + value + b"\nendobj\n" xref = len(data) data += f"xref\n0 {len(offsets)}\n0000000000 65535 f \n".encode() for offset in offsets[1:]: data += f"{offset:010d} 00000 n \n".encode() data += f"trailer\n<< /Size {len(offsets)} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n".encode() (root / "flags.pdf").write_bytes(data) (root / "manifest.json").write_text(json.dumps(dict(generator="generate_annotation_flags.py", license="Self-created test content; same license as DocView.", sha256=hashlib.sha256(data).hexdigest(), cropBox=[8,12,120,116], cases=cases, expected="At 100% one PDF point is one logical pixel. " "NoZoom preserves logical 32x16 size across zoom and scales only with DPR. NoRotate preserves upright " "red-left/blue-right appearance around the original Rect upper-left through intrinsic plus user rotation. " "Generated red border extends one point beyond Rect. This is synthetic geometry, not a general PDF compatibility golden."), indent=2)+"\n") # Native Text icons have no supplied AP. Shared annotation objects exercise # capture-once behavior across rotated pages without accumulating AP streams. text_objects = [b"<< /Type /Catalog /Pages 2 0 R >>", b""] text_content = b"BT /F1 10 Tf 12 100 Td (Native icon with unembedded body text) Tj ET" text_objects.append(f"<< /Length {len(text_content)} >>\nstream\n".encode() + text_content + b"\nendstream") text_objects.append(b"<< /Type /Annot /Subtype /Ink /Rect [80 24 100 34] /F 8 /Contents (Native ink) /BS << /W 4 >> /InkList [[80 24 90 34 100 24]] >>") text_pages = [] for flags in [8,16,24]: aid = len(text_objects) + 1 text_objects.append(f"<< /Type /Annot /Subtype /Text /Rect [42 48 74 64] /F {flags} /Contents (Native text icon) /C [1 0 0] >>".encode()) for rotation in [0,90,180,270]: text_pages.append(len(text_objects)+1) text_objects.append(f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 128 128] /CropBox [8 12 120 116] /Rotate {rotation} " f"/Resources << /Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Times-Roman >> >> >> /Contents 3 0 R /Annots [{aid} 0 R 4 0 R 4 0 R] >>".encode()) text_objects[1] = f"<< /Type /Pages /Count 12 /Kids [{' '.join(f'{p} 0 R' for p in text_pages)}] >>".encode() data = bytearray(b"%PDF-1.7\n") offsets = [0] for index, value in enumerate(text_objects, 1): offsets.append(len(data)) data += f"{index} 0 obj\n".encode() + value + b"\nendobj\n" xref = len(data) data += f"xref\n0 {len(offsets)}\n0000000000 65535 f \n".encode() for offset in offsets[1:]: data += f"{offset:010d} 00000 n \n".encode() data += f"trailer\n<< /Size {len(offsets)} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n".encode() (root / "text-icons.pdf").write_bytes(data) (root / "text-icons-manifest.json").write_text(json.dumps(dict(license="Self-created test content; same license as DocView.", sha256=hashlib.sha256(data).hexdigest(), originalRect=[42,48,74,64], nativeIconRect=[42,48,62,68], expected="Twelve pages: flags 8/16/24, each with intrinsic rotations 0/90/180/270. Four pages share each annotation. " "Capture the native generated AP once, retain source Rect, use the captured 20x20 icon Rect for drawing and hit testing."), indent=2)+"\n")