import tempfile import unittest import zipfile from pathlib import Path from unittest import mock import pptx_image_compress as pic class TestPptxImageCompress(unittest.TestCase): def test_discover_images_filters_extensions(self): with tempfile.TemporaryDirectory() as td: media_dir = Path(td) (media_dir / "a.jpg").write_bytes(b"1") (media_dir / "b.png").write_bytes(b"1") (media_dir / "c.txt").write_bytes(b"1") (media_dir / "d.GIF").write_bytes(b"1") (media_dir / "e.svg").write_bytes(b"") images = pic.discover_images(media_dir) self.assertEqual([p.name for p in images], ["a.jpg", "b.png", "d.GIF", "e.svg"]) def test_image_result_to_log_line(self): image_result = pic.ImageProcessResult( image_name="image1.png", orig_size=1000, chosen_size=800, slide_nr="[1, 2]", image_type_changed="", ) line = pic.image_result_to_log_line(image_result) self.assertIn("image1.png", line) self.assertIn("[1, 2]", line) self.assertIn("20.0", line) self.assertTrue(line.endswith(";\n")) def test_process_image_file_replaces_when_smaller(self): with tempfile.TemporaryDirectory() as td: root = Path(td) img = root / "image1.png" img.write_bytes(b"A" * 100) scratch = root / "scratch" def fake_compressor(original: Path, out_dir: Path, caesium_threads: int | None, quality: int, min_savings: str): out_dir.mkdir(parents=True, exist_ok=True) out = out_dir / original.name out.write_bytes(b"B" * 40) return out result = pic.process_image_file( idx=1, img_path=img, scratch_dir=scratch, image_to_slides={"image1.png": [1]}, caesium_threads=1, quality=90, min_savings="2%", compressor=fake_compressor, ) self.assertEqual(result.chosen_size, 40) self.assertEqual(img.stat().st_size, 40) self.assertEqual(result.slide_nr, "[1]") def test_process_image_file_converts_large_png_to_jpg(self): with tempfile.TemporaryDirectory() as td: root = Path(td) img = root / "image1.png" img.write_bytes(b"A" * 800000) scratch = root / "scratch" def fake_compressor(original: Path, out_dir: Path, caesium_threads: int | None, quality: int, min_savings: str, output_format: str = "original"): out_dir.mkdir(parents=True, exist_ok=True) if output_format == "jpeg": out = out_dir / "image1.jpg" out.write_bytes(b"C" * 200000) return out out = out_dir / original.name out.write_bytes(b"B" * 700000) return out result = pic.process_image_file( idx=1, img_path=img, scratch_dir=scratch, image_to_slides={"image1.png": [2]}, caesium_threads=1, quality=90, min_savings="2%", compressor=fake_compressor, ) self.assertEqual(result.image_name, "image1.jpg") self.assertEqual(result.chosen_size, 200000) self.assertEqual(result.image_type_changed, "png_jpg") self.assertFalse(img.exists()) self.assertTrue((root / "image1.jpg").exists()) def test_process_image_file_keeps_original_when_bigger(self): with tempfile.TemporaryDirectory() as td: root = Path(td) img = root / "image1.png" img.write_bytes(b"A" * 100) scratch = root / "scratch" def fake_compressor(original: Path, out_dir: Path, caesium_threads: int | None, quality: int, min_savings: str): out_dir.mkdir(parents=True, exist_ok=True) out = out_dir / original.name out.write_bytes(b"B" * 120) return out result = pic.process_image_file( idx=1, img_path=img, scratch_dir=scratch, image_to_slides={}, caesium_threads=1, quality=90, min_savings="2%", compressor=fake_compressor, ) self.assertEqual(result.chosen_size, 100) self.assertEqual(img.stat().st_size, 100) self.assertEqual(result.slide_nr, "NOT_USED") self.assertEqual(result.image_type_changed, "") def test_process_single_deck_with_injected_compressor(self): with tempfile.TemporaryDirectory() as td: root = Path(td) input_pptx = root / "input.pptx" output_pptx = root / "output.pptx" source_tree = root / "src" rels_dir = source_tree / "ppt" / "slides" / "_rels" media_dir = source_tree / "ppt" / "media" rels_dir.mkdir(parents=True, exist_ok=True) media_dir.mkdir(parents=True, exist_ok=True) rels_xml = ( "" "" "" "" ) (rels_dir / "slide1.xml.rels").write_text(rels_xml, encoding="utf-8") (media_dir / "image1.png").write_bytes(b"A" * 100) with zipfile.ZipFile(input_pptx, "w", compression=zipfile.ZIP_DEFLATED) as z: for p in source_tree.rglob("*"): if p.is_file(): z.write(p, arcname=str(p.relative_to(source_tree))) def fake_compressor(original: Path, out_dir: Path, caesium_threads: int | None, quality: int, min_savings: str): out_dir.mkdir(parents=True, exist_ok=True) out = out_dir / original.name out.write_bytes(b"B" * 50) return out result = pic.process_single_deck( input_pptx=input_pptx, output_pptx=output_pptx, threads=2, quality=90, min_savings="2%", compressor=fake_compressor, ) self.assertTrue(result.ok) self.assertEqual(result.error, None) self.assertTrue(output_pptx.exists()) self.assertIsNotNone(result.log_file) with zipfile.ZipFile(output_pptx, "r") as z: out_image = z.read("ppt/media/image1.png") self.assertEqual(len(out_image), 50) log_file = result.log_file if log_file is None: self.fail("log_file should not be None") log_text = Path(log_file).read_text(encoding="utf-8") self.assertIn("image_name;size_before(kb);size_after(kb);saving(kb);saving_percent(%);in_slide_number;image_type_changed", log_text) self.assertIn("image1.png", log_text) self.assertIn("[1]", log_text) def test_process_single_deck_updates_rels_on_png_to_jpg(self): with tempfile.TemporaryDirectory() as td: root = Path(td) input_pptx = root / "input_convert.pptx" output_pptx = root / "output_convert.pptx" source_tree = root / "src_convert" rels_dir = source_tree / "ppt" / "slides" / "_rels" media_dir = source_tree / "ppt" / "media" rels_dir.mkdir(parents=True, exist_ok=True) media_dir.mkdir(parents=True, exist_ok=True) rels_xml = ( "" "" "" "" ) content_types_xml = ( "" "" "" "" "" "" ) (source_tree / "[Content_Types].xml").write_text(content_types_xml, encoding="utf-8") (rels_dir / "slide1.xml.rels").write_text(rels_xml, encoding="utf-8") (media_dir / "image1.png").write_bytes(b"A" * 800000) with zipfile.ZipFile(input_pptx, "w", compression=zipfile.ZIP_DEFLATED) as z: for p in source_tree.rglob("*"): if p.is_file(): z.write(p, arcname=str(p.relative_to(source_tree))) def fake_compressor(original: Path, out_dir: Path, caesium_threads: int | None, quality: int, min_savings: str, output_format: str = "original"): out_dir.mkdir(parents=True, exist_ok=True) if output_format == "jpeg": out = out_dir / "image1.jpg" out.write_bytes(b"C" * 200000) return out out = out_dir / original.name out.write_bytes(b"B" * 700000) return out result = pic.process_single_deck( input_pptx=input_pptx, output_pptx=output_pptx, threads=1, quality=90, min_savings="2%", compressor=fake_compressor, ) self.assertTrue(result.ok) with zipfile.ZipFile(output_pptx, "r") as z: self.assertIn("ppt/media/image1.jpg", z.namelist()) self.assertNotIn("ppt/media/image1.png", z.namelist()) rels_out = z.read("ppt/slides/_rels/slide1.xml.rels").decode("utf-8") content_types_out = z.read("[Content_Types].xml").decode("utf-8") self.assertIn("image1.jpg", rels_out) self.assertIn("Extension=\"jpg\"", content_types_out) log_file = result.log_file if log_file is None: self.fail("log_file should not be None") log_text = Path(log_file).read_text(encoding="utf-8") self.assertIn("png_jpg", log_text) def test_compress_image_with_routing_delegates_to_raster(self): with tempfile.TemporaryDirectory() as td: root = Path(td) original = root / "image1.png" original.write_bytes(b"A" * 100) out_dir = root / "out" def fake_compressor(original_path: Path, out_subdir: Path, caesium_threads: int | None, quality: int, min_savings: str): out_subdir.mkdir(parents=True, exist_ok=True) out = out_subdir / original_path.name out.write_bytes(b"B" * 80) return out out = pic.compress_image_with_routing( compressor=fake_compressor, original=original, out_dir=out_dir, caesium_threads=1, quality=90, min_savings="2%", ) self.assertIsNotNone(out) if out is None: self.fail("Output should not be None") self.assertEqual(out.name, "image1.png") self.assertEqual(out.stat().st_size, 80) def test_compress_with_caesium_ignores_svg(self): with tempfile.TemporaryDirectory() as td: root = Path(td) svg = root / "vector.svg" svg.write_text("", encoding="utf-8") out_dir = root / "out" with mock.patch("pptx_image_compress.which") as mocked_which: out = pic.compress_with_caesium( original=svg, out_dir=out_dir, caesium_threads=1, quality=90, min_savings="2%", ) self.assertIsNone(out) mocked_which.assert_not_called() def test_optimize_svg_content_with_module_uses_optimize_path_with_options(self): with tempfile.TemporaryDirectory() as td: root = Path(td) svg = root / "vector.svg" svg.write_text("", encoding="utf-8") captured_options = {} class FakeOptions: def __init__(self, **kwargs): captured_options.update(kwargs) fake_module = mock.Mock() fake_module.OptimizeOptions = FakeOptions fake_module.optimize_path = mock.Mock(return_value="") fake_module.optimize_file = None fake_module.optimize = None fake_module.optimize_string = None fake_module.polish = None result = pic.optimize_svg_content_with_module(fake_module, svg) self.assertEqual(result, "") self.assertEqual(captured_options["digits"], 2) self.assertEqual(captured_options["indent_type"], "none") self.assertEqual(captured_options["newlines"], False) self.assertEqual(captured_options["strip_xml_prolog"], True) self.assertEqual(captured_options["strip_comments"], True) self.assertEqual(captured_options["strip_ids"], True) self.assertEqual(captured_options["renderer_workaround"], False) self.assertEqual(fake_module.optimize_path.call_count, 1) def test_build_svg_polish_options_balanced_profile(self): captured_options = {} class FakeOptions: def __init__(self, **kwargs): captured_options.update(kwargs) fake_module = mock.Mock() fake_module.OptimizeOptions = FakeOptions options = pic.build_svg_polish_options(fake_module, pic.SVG_PROFILE_BALANCED) self.assertIsNotNone(options) self.assertEqual(captured_options["digits"], 3) self.assertEqual(captured_options["group_collapse"], True) self.assertNotIn("strip_ids", captured_options) def test_compress_svg_with_svg_polish_returns_none_when_module_missing(self): with tempfile.TemporaryDirectory() as td: root = Path(td) svg = root / "vector.svg" svg.write_text("", encoding="utf-8") out_dir = root / "out" with mock.patch("pptx_image_compress.import_svg_polish_module", return_value=None): out = pic.compress_svg_with_svg_polish(svg, out_dir) self.assertEqual(out, None) def test_compress_svg_with_svg_polish_uses_python_module(self): with tempfile.TemporaryDirectory() as td: root = Path(td) svg = root / "vector.svg" svg.write_text(" ", encoding="utf-8") out_dir = root / "out" fake_module = mock.Mock() fake_module.optimize = mock.Mock(return_value="") fake_module.optimize_path = None fake_module.optimize_file = None fake_module.optimize_string = None fake_module.polish = None fake_module.OptimizeOptions = None with mock.patch("pptx_image_compress.import_svg_polish_module", return_value=fake_module): out = pic.compress_svg_with_svg_polish(svg, out_dir) self.assertEqual(out, out_dir / "vector.svg") self.assertTrue((out_dir / "vector.svg").exists()) self.assertEqual((out_dir / "vector.svg").read_text(encoding="utf-8"), "") fake_module.optimize.assert_called_once_with(" ") def test_compress_svg_with_svg_polish_uses_polish_fallback(self): with tempfile.TemporaryDirectory() as td: root = Path(td) svg = root / "vector.svg" svg.write_text(" ", encoding="utf-8") out_dir = root / "out" fake_module = mock.Mock() fake_module.optimize = None fake_module.polish = mock.Mock(return_value="") with mock.patch("pptx_image_compress.import_svg_polish_module", return_value=fake_module): out = pic.compress_svg_with_svg_polish(svg, out_dir) self.assertEqual(out, out_dir / "vector.svg") fake_module.polish.assert_called_once_with(" ") def test_compress_image_with_routing_does_not_fallback_to_raster_for_svg(self): with tempfile.TemporaryDirectory() as td: root = Path(td) original = root / "vector.svg" original.write_text("", encoding="utf-8") out_dir = root / "out" def fake_compressor(original_path: Path, out_subdir: Path, caesium_threads: int | None, quality: int, min_savings: str): raise AssertionError("Raster compressor must not run for svg") with mock.patch("pptx_image_compress.compress_vector_image", return_value=None): out = pic.compress_image_with_routing( compressor=fake_compressor, original=original, out_dir=out_dir, caesium_threads=1, quality=90, min_savings="2%", ) self.assertIsNone(out) if __name__ == "__main__": unittest.main()