Files
pptx-image-compress/test_pptx_image_compress.py
T
frank.conrads 6c5a5256c7 Use local svgo.cmd wrapper binary
Switch SVG optimizer resolution from bin/svgo-cli.exe to bin/svgo.cmd.

Update unit tests to validate the new local binary path behavior.

Co-Authored-By: Abacus.AI CLI <agent@abacus.ai>
2026-06-08 14:51:40 +02:00

341 lines
15 KiB
Python

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"<svg/>")
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 = (
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
"<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">"
"<Relationship Id=\"rId2\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\" Target=\"../media/image1.png\"/>"
"</Relationships>"
)
(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 = (
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
"<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">"
"<Relationship Id=\"rId2\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\" Target=\"../media/image1.png\"/>"
"</Relationships>"
)
content_types_xml = (
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
"<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">"
"<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>"
"<Default Extension=\"xml\" ContentType=\"application/xml\"/>"
"<Default Extension=\"png\" ContentType=\"image/png\"/>"
"</Types>"
)
(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_svg_with_svgo_returns_none_when_binary_missing(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
svg = root / "vector.svg"
svg.write_text("<svg></svg>", encoding="utf-8")
out_dir = root / "out"
with mock.patch("pptx_image_compress.get_svgo_executable_path", return_value=root / "bin" / "svgo.cmd"):
out = pic.compress_svg_with_svgo(svg, out_dir)
self.assertEqual(out, None)
def test_compress_svg_with_svgo_uses_local_binary(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
svg = root / "vector.svg"
svg.write_text("<svg></svg>", encoding="utf-8")
out_dir = root / "out"
fake_exe = root / "bin" / "svgo.cmd"
fake_exe.parent.mkdir(parents=True, exist_ok=True)
fake_exe.write_bytes(b"exe")
def fake_run(cmd, capture_output, text):
self.assertEqual(cmd[0], str(fake_exe))
self.assertEqual(cmd[1], str(svg))
self.assertEqual(cmd[2], "-o")
self.assertEqual(cmd[3], str(out_dir / "vector.svg"))
out_dir.mkdir(parents=True, exist_ok=True)
(out_dir / "vector.svg").write_text("<svg/>", encoding="utf-8")
return mock.Mock(returncode=0, stderr="")
with mock.patch("pptx_image_compress.get_svgo_executable_path", return_value=fake_exe):
with mock.patch("pptx_image_compress.subprocess.run", side_effect=fake_run):
out = pic.compress_svg_with_svgo(svg, out_dir)
self.assertEqual(out, out_dir / "vector.svg")
def test_compress_image_with_routing_uses_svg_backend(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
original = root / "vector.svg"
original.write_text("<svg></svg>", encoding="utf-8")
out_dir = root / "out"
out_dir.mkdir(parents=True, exist_ok=True)
vector_out = out_dir / "vector.svg"
vector_out.write_text("<svg/>", encoding="utf-8")
def fake_compressor(original_path: Path, out_subdir: Path, caesium_threads: int | None, quality: int, min_savings: str):
raise AssertionError("Raster compressor should not be called for svg")
with mock.patch("pptx_image_compress.compress_vector_image", return_value=vector_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.assertEqual(out, vector_out)
if __name__ == "__main__":
unittest.main()