Files

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