Files
noteman-slicer/tests/test_pdf.py
T
Esa Kataja 2a22fc469f Honour a page's /Rotate when extracting a scan
A scan fed sideways stores its image landscape and sets /Rotate 90 so a
viewer turns it upright. Extracting the image by xref — which is how a
raster source is read, to keep the scan's native resolution — bypasses
that, so every system ran down the page and detection found nothing.

Apply the page rotation to the extracted raster. Quarter turns only;
nothing produces anything else.
2026-07-29 12:08:45 +03:00

105 lines
3.6 KiB
Python

"""Runnable check for source classification and raster loading.
Builds its own PDFs so it needs no corpus files (scores are copyrighted and
gitignored). Run with `python tests/test_pdf.py`.
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pymupdf
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from noteman_slicer.pdf import SourceType, open_source, page_raster # noqa: E402
A4 = pymupdf.paper_rect("a4")
def _vector_pdf(path: Path, pages: int = 2) -> None:
doc = pymupdf.open()
for _ in range(pages):
page = doc.new_page(width=A4.width, height=A4.height)
page.draw_line((50, 100), (A4.width - 50, 100))
page.insert_text((50, 150), "notation", fontsize=24)
doc.save(path)
def _scan_pdf(path: Path, pages: int = 2, w: int = 1653, h: int = 2332, rotation: int = 0) -> None:
"""Each page is one full-page grayscale image — what a real scan looks like.
`rotation` reproduces a sheet fed sideways: the image is stored in its own
orientation and /Rotate turns it upright for a viewer.
"""
art = np.full((h, w), 255, np.uint8)
art[500:505, 100 : w - 100] = 0 # a staff line, so it isn't uniform
art[:60, :60] = 0 # a corner mark, so orientation is checkable
pix = pymupdf.Pixmap(pymupdf.csGRAY, w, h, bytearray(art.tobytes()), False)
doc = pymupdf.open()
for _ in range(pages):
page = doc.new_page(width=A4.width, height=A4.width * h / w)
page.insert_image(page.rect, pixmap=pix)
page.set_rotation(rotation)
doc.save(path)
def main() -> int:
tmp = Path(__file__).with_name("_tmp")
tmp.mkdir(exist_ok=True)
vec, scan = tmp / "vector.pdf", tmp / "scan.pdf"
_vector_pdf(vec)
_scan_pdf(scan)
src = open_source(vec)
assert src.type is SourceType.VECTOR, src.type
assert not src.overridden
page = page_raster(src, 0)
# Rendered at 600 DPI, so an A4 page is ~4960px wide.
assert page.ndim == 2 and page.dtype == np.uint8, (page.ndim, page.dtype)
assert 4900 < page.shape[1] < 5000, page.shape
src.close()
src = open_source(scan)
assert src.type is SourceType.RASTER, src.type
page = page_raster(src, 0)
# Native resolution of the embedded image, NOT a 600 DPI re-render.
assert page.shape == (2332, 1653), page.shape
assert page.min() == 0 and page.max() == 255, (page.min(), page.max())
src.close()
# The corner mark sits top-left in an upright scan.
assert page[:60, :60].max() == 0 and page[:60, -60:].min() == 255
# A sideways scan comes back upright: the page's /Rotate applies to the
# image extracted by xref, which bypasses it. Okular gets this right and
# the slicer used to not.
sideways = tmp / "sideways.pdf"
_scan_pdf(sideways, pages=1, w=2332, h=1653, rotation=90)
src = open_source(sideways)
assert src.type is SourceType.RASTER
turned = page_raster(src, 0)
assert turned.shape == (2332, 1653), turned.shape
# Turned clockwise, so the mark that was top-left is now top-right.
assert turned[:60, -60:].max() == 0 and turned[:60, :60].min() == 255
src.close()
# An override must win over detection, and say so.
src = open_source(scan, SourceType.VECTOR)
assert src.type is SourceType.VECTOR and src.detected is SourceType.RASTER
assert src.overridden
assert page_raster(src, 0).shape[1] > 4000, "override must force a render"
src.close()
for f in (vec, scan, sideways):
f.unlink()
tmp.rmdir()
print("ok")
return 0
if __name__ == "__main__":
sys.exit(main())