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.
This commit is contained in:
+16
-1
@@ -90,13 +90,28 @@ def page_raster(source: Source, index: int) -> np.ndarray:
|
|||||||
# Pixmap(doc, xref) rather than decoding extract_image() bytes:
|
# Pixmap(doc, xref) rather than decoding extract_image() bytes:
|
||||||
# MuPDF handles JBIG2 and CCITT, which no image library will.
|
# MuPDF handles JBIG2 and CCITT, which no image library will.
|
||||||
pix = pymupdf.Pixmap(source.doc, xref)
|
pix = pymupdf.Pixmap(source.doc, xref)
|
||||||
return _to_gray(pix)
|
# The embedded image is in its own orientation, not the page's: a
|
||||||
|
# scanner that fed the sheet sideways stores it landscape and the
|
||||||
|
# PDF sets /Rotate so viewers turn it upright. Extracting by xref
|
||||||
|
# bypasses that, so apply it here — otherwise every system runs
|
||||||
|
# down the page and detection finds nothing.
|
||||||
|
return _rotate(_to_gray(pix), page.rotation)
|
||||||
# A scanned PDF whose page has no embedded image (a blank, or a
|
# A scanned PDF whose page has no embedded image (a blank, or a
|
||||||
# cover typeset in vector). Rendering is the only option left.
|
# cover typeset in vector). Rendering is the only option left.
|
||||||
|
|
||||||
return _to_gray(page.get_pixmap(dpi=source.render_dpi, colorspace=pymupdf.csGRAY))
|
return _to_gray(page.get_pixmap(dpi=source.render_dpi, colorspace=pymupdf.csGRAY))
|
||||||
|
|
||||||
|
|
||||||
|
def _rotate(gray: np.ndarray, degrees: int) -> np.ndarray:
|
||||||
|
"""Turn a page raster clockwise by a multiple of 90°, as /Rotate means it.
|
||||||
|
|
||||||
|
ponytail: quarter turns only. A page rotated by anything else would need
|
||||||
|
resampling, and no scanner produces one.
|
||||||
|
"""
|
||||||
|
turns = round(degrees / 90) % 4
|
||||||
|
return np.ascontiguousarray(np.rot90(gray, -turns)) if turns else gray
|
||||||
|
|
||||||
|
|
||||||
def _to_gray(pix: pymupdf.Pixmap) -> np.ndarray:
|
def _to_gray(pix: pymupdf.Pixmap) -> np.ndarray:
|
||||||
if pix.alpha or pix.colorspace is None or pix.colorspace.n != 1:
|
if pix.alpha or pix.colorspace is None or pix.colorspace.n != 1:
|
||||||
pix = pymupdf.Pixmap(pymupdf.csGRAY, pix)
|
pix = pymupdf.Pixmap(pymupdf.csGRAY, pix)
|
||||||
|
|||||||
+26
-4
@@ -28,15 +28,21 @@ def _vector_pdf(path: Path, pages: int = 2) -> None:
|
|||||||
doc.save(path)
|
doc.save(path)
|
||||||
|
|
||||||
|
|
||||||
def _scan_pdf(path: Path, pages: int = 2, w: int = 1653, h: int = 2332) -> None:
|
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."""
|
"""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 = np.full((h, w), 255, np.uint8)
|
||||||
art[500:505, 100 : w - 100] = 0 # a staff line, so it isn't uniform
|
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)
|
pix = pymupdf.Pixmap(pymupdf.csGRAY, w, h, bytearray(art.tobytes()), False)
|
||||||
doc = pymupdf.open()
|
doc = pymupdf.open()
|
||||||
for _ in range(pages):
|
for _ in range(pages):
|
||||||
page = doc.new_page(width=A4.width, height=A4.height)
|
page = doc.new_page(width=A4.width, height=A4.width * h / w)
|
||||||
page.insert_image(page.rect, pixmap=pix)
|
page.insert_image(page.rect, pixmap=pix)
|
||||||
|
page.set_rotation(rotation)
|
||||||
doc.save(path)
|
doc.save(path)
|
||||||
|
|
||||||
|
|
||||||
@@ -64,6 +70,22 @@ def main() -> int:
|
|||||||
assert page.min() == 0 and page.max() == 255, (page.min(), page.max())
|
assert page.min() == 0 and page.max() == 255, (page.min(), page.max())
|
||||||
src.close()
|
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.
|
# An override must win over detection, and say so.
|
||||||
src = open_source(scan, SourceType.VECTOR)
|
src = open_source(scan, SourceType.VECTOR)
|
||||||
assert src.type is SourceType.VECTOR and src.detected is SourceType.RASTER
|
assert src.type is SourceType.VECTOR and src.detected is SourceType.RASTER
|
||||||
@@ -71,7 +93,7 @@ def main() -> int:
|
|||||||
assert page_raster(src, 0).shape[1] > 4000, "override must force a render"
|
assert page_raster(src, 0).shape[1] > 4000, "override must force a render"
|
||||||
src.close()
|
src.close()
|
||||||
|
|
||||||
for f in (vec, scan):
|
for f in (vec, scan, sideways):
|
||||||
f.unlink()
|
f.unlink()
|
||||||
tmp.rmdir()
|
tmp.rmdir()
|
||||||
print("ok")
|
print("ok")
|
||||||
|
|||||||
Reference in New Issue
Block a user