Files
zipbundler/test_pdf_workflow.py

240 lines
12 KiB
Python

import tempfile
import unittest
import warnings
import zipfile
from pathlib import Path
from unittest.mock import Mock, patch
import fitz
import zipbundler as zb
class CompressionWorkflowTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.source = self.root / "source.pdf"
with fitz.open() as doc:
for number in range(2):
page = doc.new_page(width=300, height=400)
page.insert_text((30, 40), f"Page {number + 1}: readable text")
doc.save(self.source)
self.file = zb.BundleFile(self.source, Path("source.pdf"))
self.out = self.root / "output"
def report(self, size=5000, kind="pdf", dpi=72):
return zb.OptimizedPdf(self.file, self.file, 10000, size, kind, "test", dpi, 45,
zip_size=size, max_zip_size=1000)
@staticmethod
def close_test_app(app):
for callback in app.tk.call("after", "info"):
app.after_cancel(callback)
app.destroy()
def test_standard_fitting_result_never_asks_permission(self):
for kind in ("pdf", "image", "office"):
with self.subTest(kind=kind):
preview = Mock(side_effect=AssertionError("No dialog expected"))
expected = self.report(size=1000, kind=kind)
self.assertIs(zb.negotiate_compression(expected, [], 1000, preview), expected)
def test_pdf_standard_success_is_automatic(self):
expected = self.report(size=500)
preview = Mock()
with patch.object(zb, "try_optimize_pdf_for_limit", return_value=expected):
report, separate = zb.choose_pdf_for_limit(
self.file, self.out, 1000, confirm_optimization=preview,
)
self.assertIs(report, expected)
self.assertFalse(separate)
preview.assert_not_called()
def test_oversized_standard_can_be_explicitly_accepted(self):
retry = Mock()
preview = Mock(return_value=True)
report = zb.negotiate_compression(self.report(), [("stronger", retry)], 1000, preview)
self.assertEqual(report.zip_size, 5000)
preview.assert_called_once()
retry.assert_not_called()
def test_each_rejected_preview_advances_even_when_candidate_fits(self):
attempts = [("first", Mock(return_value=self.report(800))),
("second", Mock(return_value=self.report(600)))]
preview = Mock(side_effect=[False, False, True])
with patch.object(zb, "measured_report", side_effect=lambda r, limit: r):
result = zb.negotiate_compression(self.report(), attempts, 1000, preview)
self.assertEqual(result.zip_size, 600)
self.assertEqual([call.args[0].zip_size for call in preview.call_args_list], [5000, 800, 600])
self.assertFalse(preview.call_args.args[0].can_retry)
for _, attempt in attempts:
attempt.assert_called_once()
def test_cancelling_never_starts_another_conversion(self):
retry = Mock()
with self.assertRaises(zb.BundleCancelled):
zb.negotiate_compression(self.report(), [("stronger", retry)], 1000, lambda r: None)
retry.assert_not_called()
def test_rejecting_last_profile_never_falls_back_to_original_or_200_dpi(self):
with patch.object(zb, "pdf_at_dpi") as convert, self.assertRaises(zb.BundleCancelled):
zb.negotiate_compression(self.report(), [], 1000, lambda r: False)
convert.assert_not_called()
def test_noninteractive_oversized_result_is_not_silently_accepted(self):
with self.assertRaisesRegex(ValueError, "GUI"):
zb.negotiate_compression(self.report(), [], 1000, None)
def test_best_standard_file_is_preserved_when_later_attempt_overwrites_it(self):
destination = self.root / "reduced.pdf"
expected_bytes = b"first and best"
def attempt(content, size):
destination.write_bytes(content)
return zb.replace(self.report(size), optimized=zb.BundleFile(destination, Path("reduced.pdf")))
with patch.object(zb, "measured_report", side_effect=lambda r, limit: zb.replace(
r, zip_size=r.optimized_size, max_zip_size=limit)):
result = zb.select_standard_result(self.file, "pdf", [
lambda: attempt(expected_bytes, 500), lambda: attempt(b"worse", 900),
], 1)
self.assertEqual(result.zip_size, 500)
self.assertEqual(result.optimized.path.read_bytes(), expected_bytes)
def test_jpeg_that_fits_is_bundled_without_preview(self):
source_folder = self.root / "photos"
source_folder.mkdir()
jpeg = source_folder / "large.jpg"
image = zb.Image.effect_noise((1000, 1400), 80)
image.save(jpeg, quality=98)
original = jpeg.read_bytes()
preview = Mock(side_effect=AssertionError("Unexpected preview"))
results = zb.bundle_folder(source_folder, self.out, 0.6, confirm_optimization=preview)
self.assertGreater(len(original), zb.mb_to_bytes(0.6))
self.assertTrue(results)
for archive, _, _, size in results:
self.assertLessEqual(size, zb.mb_to_bytes(0.6))
with zipfile.ZipFile(archive) as zipped:
self.assertTrue(all(name.endswith(".jpg") for name in zipped.namelist()))
self.assertNotEqual(zipped.read(zipped.namelist()[0]), original)
preview.assert_not_called()
self.assertEqual(jpeg.read_bytes(), original)
def test_aggressive_images_preserve_format_and_require_preview(self):
noise = zb.Image.effect_noise((700, 900), 80)
for suffix, fmt in (("png", "PNG"), ("tif", "TIFF"), ("bmp", "BMP"), ("webp", "WEBP")):
with self.subTest(format=fmt):
path = self.root / f"image.{suffix}"
noise.save(path, format=fmt)
source = zb.BundleFile(path, Path(path.name))
standard = zb.measured_report(zb.OptimizedPdf(
source, source, path.stat().st_size, path.stat().st_size,
"image", "originale", None, None,
), 100)
preview = Mock(side_effect=[False, True])
with patch.object(zb, "try_optimize_image_for_limit", return_value=standard):
report = zb.choose_image_for_limit(source, self.out, 100, confirm_optimization=preview)
self.assertEqual(preview.call_count, 2)
self.assertEqual(report.optimized.path.suffix, "." + suffix)
with zb.Image.open(report.optimized.path) as image:
self.assertEqual(image.format, fmt)
def test_explicitly_accepted_oversized_result_is_alone_in_zip(self):
replacement = zb.optimize_pdf(self.file, self.out, "raster_72", 72, 45)
replacement = zb.measured_report(replacement, 150)
with patch.object(zb, "choose_pdf_for_limit", return_value=(replacement, True)):
groups, reports = zb.split_into_groups([self.file], self.out, 150)
self.assertEqual(groups, [[replacement.optimized]])
archive = self.root / "accepted.zip"
zb.create_zip(archive, groups[0])
with zipfile.ZipFile(archive) as zipped:
self.assertEqual(zipped.read(zipped.namelist()[0]), replacement.optimized.path.read_bytes())
def test_cancelled_bundle_creates_no_zip(self):
folder = self.root / "docs"
folder.mkdir()
zb.shutil.copyfile(self.source, folder / "source.pdf")
with self.assertRaises(zb.BundleCancelled):
zb.bundle_folder(folder, self.out, 0.0001, confirm_optimization=lambda r: None)
self.assertEqual(list(self.out.glob("*.zip")), [])
def test_office_pdf_within_limit_is_automatic(self):
preview = Mock()
with patch.object(zb, "convert_office_to_pdf", return_value=self.report(kind="office")), \
patch.object(zb, "compressed_zip_size", return_value=500):
report = zb.try_convert_office_for_limit(self.file, self.out, 1000, confirm_optimization=preview)
self.assertEqual(report.zip_size, 500)
preview.assert_not_called()
def test_office_pdf_outside_limit_enters_pdf_workflow(self):
converted = self.report(kind="office")
preview = Mock()
with patch.object(zb, "convert_office_to_pdf", return_value=converted), \
patch.object(zb, "compressed_zip_size", return_value=5000), \
patch.object(zb, "choose_pdf_for_limit", return_value=(self.report(800), False)) as choose:
result = zb.try_convert_office_for_limit(self.file, self.out, 1000, confirm_optimization=preview)
self.assertEqual(result.zip_size, 800)
self.assertEqual(choose.call_args.args[0], converted.optimized)
self.assertIs(choose.call_args.kwargs["confirm_optimization"], preview)
def test_large_image_warning_is_logged_but_hard_limit_remains(self):
path = self.root / "pixels.png"
zb.Image.new("RGB", (100, 100)).save(path)
log = Mock()
with patch.object(zb.Image, "MAX_IMAGE_PIXELS", 6000), warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
with zb.open_image(path, log=log) as image:
self.assertEqual(image.size, (100, 100))
self.assertEqual(caught, [])
log.assert_called_once()
with patch.object(zb.Image, "MAX_IMAGE_PIXELS", 4000):
with self.assertRaises(zb.Image.DecompressionBombError):
with zb.open_image(path):
pass
def test_raster_page_dimensions_and_zoom(self):
report = zb.optimize_pdf(self.file, self.out, "test", 100, 54)
with fitz.open(report.optimized.path) as doc:
self.assertEqual(doc.page_count, 2)
self.assertEqual(doc[0].rect, fitz.Rect(0, 0, 300, 400))
self.assertEqual(zb.render_pdf_preview(report.optimized.path, zoom=1).width, 400)
self.assertEqual(zb.render_pdf_preview(report.optimized.path, zoom=4).width, 1600)
@unittest.skipUnless(zb.find_ghostscript(), "Ghostscript unavailable")
def test_real_ghostscript_preserves_text(self):
report = zb.pdf_at_dpi(self.file, self.out, 48, 30)
self.assertEqual(report.profile, "dpi_48")
with fitz.open(report.optimized.path) as doc:
self.assertIn("readable text", doc[0].get_text())
self.assertEqual(doc.page_count, 2)
def test_gui_zoom_navigation_retry_and_cancel(self):
app = zb.ctk.CTk()
dialog = None
try:
dialog = zb.OptimizationDialog(app, zb.replace(self.report(), can_retry=False))
dialog.update()
dialog.set_zoom("400%")
self.assertEqual(dialog.original_image.width(), 1600)
self.assertEqual(dialog.optimized_image.width(), 1600)
dialog.scroll_previews("y", "moveto", 0.5)
self.assertGreater(dialog.canvases[0].yview()[0], 0)
self.assertEqual(dialog.canvases[0].yview(), dialog.canvases[1].yview())
dialog.next_page()
self.assertEqual(dialog.page_label.cget("text"), "Pagina 2 / 2")
dialog.set_zoom("Adatta")
self.assertEqual(dialog.more_button.cget("state"), "disabled")
dialog.cancel()
self.assertIsNone(dialog.result)
dialog = None
finally:
if dialog is not None:
dialog.destroy()
self.close_test_app(app)
if __name__ == "__main__":
unittest.main()