Scanned scores are black ink on white paper stored as 8-bit greyscale or RGB, which costs several times what the same page costs as a bilevel image. Across an 11-song corpus this is 27.6 MB to 7.7 MB; Engel's bundle goes from 5997 KB to 2092 KB with byte-identical slices, since only the archived copy changes. Three approaches were measured and discarded first, which is worth recording because two of them are the obvious ones. Converting RGB to greyscale and re-encoding makes these files 20-86% LARGER: the source JPEGs are already near 0.7 bits per pixel, so re-encoding adds generation loss and spends more bits than the original did, and dropping chroma recovers nothing because JPEG already subsamples it. Lossless structural optimisation gains 0.1%, because images are 99% of every file and there are no duplicates. Downsampling works but 300 DPI is print resolution, and the PDF exists to be printed. Two failure modes were found by looking at output rather than at byte counts, and both are now refused: - A scan at ~115 DPI came back with broken staff lines. Guarded on resolution as the image is *placed on the page*, so a tiled scan with 126 small images still qualifies where a pixel count would reject it. - Cover artwork was flattened to grey. Guarded on chroma: artwork measures 44% off-grey against 3% for sensor tint on a greyscale scan. The first threshold of 2% was a false positive that cost 685 KB on one song for nothing; 10% sits in the gap with room either side. Exposed as a button rather than a checkbox. It reports what it skipped and why, and shows a before/after crop, because the failure it can produce is obvious at a glance and invisible in a size figure. Off by default: this is lossy on the copy kept for printing.
97 lines
3.4 KiB
Python
97 lines
3.4 KiB
Python
"""Runnable check for optional PDF shrinking, including what it refuses to do."""
|
|
|
|
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.pdfopt import MIN_DPI, optimise # noqa: E402
|
|
|
|
A4_PT = (595, 842)
|
|
|
|
|
|
def _pdf(path: Path, width: int, height: int, *, colour: bool = False, bilevel: bool = False):
|
|
"""One full-page image of ruled lines, at the given pixel size."""
|
|
art = np.full((height, width, 3), 255, np.uint8)
|
|
for i in range(6):
|
|
y = int(height * (0.2 + i * 0.03))
|
|
art[y : y + max(1, height // 900), int(width * 0.1) : int(width * 0.9)] = 0
|
|
if colour:
|
|
art[: height // 3, :, 0] = 40 # a strong blue cast over the top third
|
|
art[: height // 3, :, 1] = 90
|
|
grey = art[:, :, 0] if not colour else None
|
|
|
|
doc = pymupdf.open()
|
|
page = doc.new_page(width=A4_PT[0], height=A4_PT[1])
|
|
if bilevel:
|
|
pix = pymupdf.Pixmap(pymupdf.csGRAY, width, height, bytearray(grey.tobytes()), False)
|
|
page.insert_image(page.rect, pixmap=pix)
|
|
doc.save(path, garbage=4, deflate=True)
|
|
# Re-save through a 1-bit PNG so the stored image really is bilevel.
|
|
import cv2
|
|
|
|
ok, buf = cv2.imencode(".png", (grey > 127).astype(np.uint8) * 255)
|
|
doc2 = pymupdf.open()
|
|
p2 = doc2.new_page(width=A4_PT[0], height=A4_PT[1])
|
|
p2.insert_image(p2.rect, stream=buf.tobytes())
|
|
doc2.save(path, garbage=4, deflate=True)
|
|
return
|
|
stream = art if colour else np.dstack([grey] * 3)
|
|
import cv2
|
|
|
|
ok, buf = cv2.imencode(".jpg", stream, [cv2.IMWRITE_JPEG_QUALITY, 92])
|
|
page.insert_image(page.rect, stream=buf.tobytes())
|
|
doc.save(path, garbage=4, deflate=True)
|
|
|
|
|
|
def main() -> int:
|
|
tmp = Path(__file__).with_name("_tmp")
|
|
tmp.mkdir(exist_ok=True)
|
|
|
|
# A4 is 8.26in wide, so 2480px is ~300 DPI and 800px is ~97 DPI.
|
|
fine, coarse, colour = tmp / "fine.pdf", tmp / "coarse.pdf", tmp / "colour.pdf"
|
|
_pdf(fine, 2480, 3508)
|
|
_pdf(coarse, 800, 1130)
|
|
_pdf(colour, 2480, 3508, colour=True)
|
|
|
|
data, report = optimise(pymupdf.open(fine), fine.stat().st_size)
|
|
assert report.converted == 1, report.summary()
|
|
assert data, "a greyscale scan at 300 DPI should shrink"
|
|
assert report.ratio < 0.9, report.ratio
|
|
# The result must still be a readable PDF of the same page count.
|
|
assert len(pymupdf.open(stream=data, filetype="pdf")) == 1
|
|
|
|
# Too coarse: staff lines would break, so it is left alone.
|
|
_, report = optimise(pymupdf.open(coarse), coarse.stat().st_size)
|
|
assert report.converted == 0, report.summary()
|
|
assert any("DPI" in reason for reason in report.skipped), report.skipped
|
|
|
|
# Genuine colour: artwork is not thrown away.
|
|
_, report = optimise(pymupdf.open(colour), colour.stat().st_size)
|
|
assert report.converted == 0, report.summary()
|
|
assert any("colour" in reason for reason in report.skipped), report.skipped
|
|
|
|
# A no-op run reports honestly rather than returning something bigger.
|
|
empty = pymupdf.open()
|
|
empty.new_page()
|
|
data, report = optimise(empty, 1)
|
|
assert data == b"" and report.converted == 0
|
|
assert report.ratio == 1.0
|
|
|
|
assert MIN_DPI >= 150, "the floor exists to protect thin staff lines"
|
|
|
|
for f in (fine, coarse, colour):
|
|
f.unlink(missing_ok=True)
|
|
tmp.rmdir()
|
|
print("ok")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|