feat(pagesetup): page_setup_list/apply on both engines — the PDF's own /MediaBox is the evidence

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-16 21:13:48 +03:00
co-authored by Claude Opus 5
parent 49fc6ac6a5
commit f46fda10cb
8 changed files with 1345 additions and 2 deletions
+59
View File
@@ -0,0 +1,59 @@
"""Read a PDF's page size back from the file — no dependency, no rendering.
The evidence rule for page setup (spec §8.3): the sheet size a plot produced is
what the PDF says it is, not what the layout was asked for. ``/MediaBox`` is the
one PDF object that carries it. matplotlib and AutoCAD's DWG To PDF driver both
write it in clear text; a PDF 1.5+ writer may put the page dictionary inside a
FlateDecode object stream, so when the plain scan finds nothing every stream is
inflated and searched. Anything else raises rather than guessing.
"""
from __future__ import annotations
import re
import zlib
from pathlib import Path
__all__ = ["PT_TO_MM", "read_mediabox"]
#: PostScript points to millimetres.
PT_TO_MM = 25.4 / 72.0
_NUMBER = rb"([-+]?\d*\.?\d+)"
_MEDIABOX_RE = re.compile(
rb"/MediaBox\s*\[\s*"
+ _NUMBER
+ rb"\s+"
+ _NUMBER
+ rb"\s+"
+ _NUMBER
+ rb"\s+"
+ _NUMBER
+ rb"\s*\]"
)
_STREAM_RE = re.compile(rb"stream\r?\n(.*?)\r?\nendstream", re.DOTALL)
def read_mediabox(path: str) -> tuple[float, float]:
"""Return the first page ``/MediaBox`` as ``(width_mm, height_mm)``.
Raises ``ValueError`` when the file is not a PDF or carries no MediaBox.
``/Rotate`` is not applied: the box is reported as written.
"""
data = Path(path).read_bytes()
if not data.startswith(b"%PDF"):
raise ValueError(f"read_mediabox: {path} is not a PDF (no %PDF header)")
match = _MEDIABOX_RE.search(data)
if match is None:
for stream in _STREAM_RE.finditer(data):
try:
inflated = zlib.decompress(stream.group(1))
except zlib.error:
continue
match = _MEDIABOX_RE.search(inflated)
if match is not None:
break
if match is None:
raise ValueError(f"read_mediabox: no /MediaBox found in {path}")
x0, y0, x1, y1 = (float(value) for value in match.groups())
return (round(abs(x1 - x0) * PT_TO_MM, 3), round(abs(y1 - y0) * PT_TO_MM, 3))