Add optional bilevel shrinking of the archived PDF
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.
This commit is contained in:
+67
-14
@@ -34,6 +34,7 @@ from PySide6.QtWidgets import (
|
||||
QGraphicsScene,
|
||||
QGraphicsView,
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
@@ -205,20 +206,6 @@ class PageView(QGraphicsView):
|
||||
text.setPos(x, y)
|
||||
return x + box.width() * scale + pad * 3
|
||||
|
||||
for i, cut in enumerate(self.page.cuts):
|
||||
colour = _CUT_ACTIVE if i == self.selected_cut else _CUT
|
||||
pen = QPen(colour, 2)
|
||||
pen.setCosmetic(True)
|
||||
points = [QPointF(x * w, y * h) for x, y in cut.points]
|
||||
for a, b in zip(points, points[1:]):
|
||||
scene.addLine(a.x(), a.y(), b.x(), b.y(), pen)
|
||||
if i == self.selected_cut:
|
||||
r = HIT * 1.5 / max(self.transform().m11(), 1e-6)
|
||||
for p in points:
|
||||
scene.addEllipse(
|
||||
p.x() - r, p.y() - r, r * 2, r * 2, QPen(Qt.NoPen), QBrush(_VERTEX)
|
||||
)
|
||||
|
||||
def _slice_polygon(self, slot: int, w: int, h: int) -> QPolygonF:
|
||||
above, below = self.page.bounds(slot)
|
||||
top = [(0.0, 0.0), (1.0, 0.0)] if above is None else above.points
|
||||
@@ -573,6 +560,11 @@ class Editor(QMainWindow):
|
||||
edit.setFixedWidth(90)
|
||||
meta_form.addRow(f"{field.replace('_', ' ').title()}{' *' if required else ''}", edit)
|
||||
|
||||
self.optimise = QPushButton("Shrink the original PDF…")
|
||||
self.optimise.setToolTip("Convert scanned pages to bilevel in the archived PDF")
|
||||
self.optimise.clicked.connect(self._optimise_pdf)
|
||||
meta_layout.addWidget(self.optimise)
|
||||
|
||||
self.summary = QLabel()
|
||||
self.summary.setWordWrap(True)
|
||||
box.addWidget(self.summary)
|
||||
@@ -765,6 +757,67 @@ class Editor(QMainWindow):
|
||||
}
|
||||
self.autosave.start()
|
||||
|
||||
def _optimise_pdf(self) -> None:
|
||||
"""Offer to shrink the archived PDF, showing the result before agreeing.
|
||||
|
||||
A before/after crop rather than a checkbox: the failure this can produce
|
||||
— broken staff lines on a coarse scan — is obvious at a glance and
|
||||
invisible in a byte count.
|
||||
"""
|
||||
import pymupdf
|
||||
|
||||
from .pdfopt import Report, optimise, preview
|
||||
|
||||
self.statusBar().showMessage("examining the PDF…")
|
||||
QApplication.processEvents()
|
||||
source = self.project.source
|
||||
data, report = optimise(pymupdf.open(source), source.stat().st_size)
|
||||
self.statusBar().clearMessage()
|
||||
|
||||
if not data:
|
||||
QMessageBox.information(self, "Nothing to shrink", report.summary())
|
||||
self.project.optimise_pdf = False
|
||||
return
|
||||
|
||||
dialog = QDialog(self)
|
||||
dialog.setWindowTitle("Shrink the original PDF")
|
||||
layout = QVBoxLayout(dialog)
|
||||
text = QLabel(report.summary() + "\n\nThe slices are unaffected — only the archived PDF.")
|
||||
text.setWordWrap(True)
|
||||
layout.addWidget(text)
|
||||
|
||||
crop = preview(pymupdf.open(source), pymupdf.open(stream=data, filetype="pdf"))
|
||||
crop = np.ascontiguousarray(crop)
|
||||
h, w, _ = crop.shape
|
||||
image = QImage(crop.data, w, h, w * 3, QImage.Format_BGR888).copy()
|
||||
label = QLabel()
|
||||
label.setPixmap(QPixmap.fromImage(image))
|
||||
area = QScrollArea()
|
||||
area.setWidget(label)
|
||||
area.setWidgetResizable(True)
|
||||
area.setMinimumHeight(420)
|
||||
layout.addWidget(area)
|
||||
layout.addWidget(QLabel("Original above, shrunk below. Check the staff lines."))
|
||||
|
||||
buttons = QHBoxLayout()
|
||||
use = QPushButton("Use the smaller PDF")
|
||||
use.clicked.connect(dialog.accept)
|
||||
keep = QPushButton("Keep the original")
|
||||
keep.clicked.connect(dialog.reject)
|
||||
buttons.addWidget(use)
|
||||
buttons.addWidget(keep)
|
||||
layout.addLayout(buttons)
|
||||
dialog.resize(1100, 700)
|
||||
|
||||
self.project.optimise_pdf = dialog.exec() == QDialog.Accepted
|
||||
self._touched()
|
||||
self.statusBar().showMessage(
|
||||
"the bundle will carry the shrunk PDF"
|
||||
if self.project.optimise_pdf
|
||||
else "the bundle will carry the original PDF",
|
||||
4000,
|
||||
)
|
||||
|
||||
def _reset_rect(self) -> None:
|
||||
"""Back to what detection proposed for this page.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user