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()