Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
60 lines
2.0 KiB
Python
60 lines
2.0 KiB
Python
"""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))
|