From 0cec37eecd6df9b0fbd1393a9dabf4cc3bfb36e9 Mon Sep 17 00:00:00 2001 From: Frank Conrads Date: Wed, 10 Jun 2026 10:17:36 +0200 Subject: [PATCH] =?UTF-8?q?SVG-Compress=20hinzugef=C3=BCgt=20via=20svg-pol?= =?UTF-8?q?ish=20python=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 15 ++-- install_and_run.bat | 77 +++++++++++++++-- pptx_image_compress.py | 162 ++++++++++++++++++++++++++++-------- test_pptx_image_compress.py | 131 +++++++++++++++++++++++------ 4 files changed, 310 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 02d3149..2901581 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # PPTX Image Compressor (CaesiumCLT only) -**Version 1.1.7** +**Version 1.1.8** Dieses Paket enthält: @@ -27,26 +27,28 @@ install_and_run.bat --input-dir "C:\Slides" --recursive -O "C:\Slides\out" -q 80 ``` Die Batch lädt bei Bedarf automatisch das **Windows Embeddable Python Package**, entpackt es lokal und führt das Tool aus. +Zusätzlich wird **pip** installiert, damit das **svg-polish** Modul installiert werden kann. + ## Was das Tool macht - Entpackt die PPTX in einen Temp‑Ordner - Komprimiert **JPG/JPEG, PNG, WebP, GIF** mit **CaesiumCLT** (Default `-q 90`, `-O bigger`) +- Komprimiert **SVG** mit **svg-polish** (Default-Modus: `agressive`) - Ersetzt Bilder nur, wenn die komprimierte Datei kleiner ist - Versucht bei PNG zusätzlich einen PNG->JPG Wechsel, wenn das Bild nach Kompression noch größer als 500 KB ist - Ersetzt Bilder nur, wenn sei mindestens 2% kleiner sind (verhindert *doppelte Komprimierung*) - Schreibt ein CSV‑Log (`.log` neben der Output‑PPTX) - Baut eine neue PPTX und zeigt eine Summary (Name, Größe vorher/nachher, Ersparnis %, Zeit) -- Räumt alle temporären Dateien auf (keine Caesium‑Tempfiles in der finalen PPTX) +- Baut eine neue PPTX und zeigt eine Summary (Name, Größe vorher/nachher, Ersparnis %, Zeit) -## Änderungen in 1.1.7 -- PNG->JPG Fallback für große PNGs (> 500 KB nach Kompression) hinzugefügt -- CSV-Logging um `image_type_changed` erweitert (`png_jpg` bei Typwechsel) +## Änderungen in 1.1.8 +- SVG Files werden bei Vorhandensein von svg-polish anhand von 2 Profilen optimiert: balanced|agressive ## Hinweise - `-t` steuert die Parallelität der Python‑Threads; intern wird `caesiumclt --threads 1` gesetzt, sobald `-t > 1`, um Oversubscription zu vermeiden. Default ist 16 - `-q` steuert das Qualitätslevel; intern wird `caesiumclt -q` mit diesem Wert von `0..100` benutzt, Default ist 90 - `--min-savings` steuert das Mindestmass an Komprimierung zur Verhinderung von doppelter Komprimierunt, Default ist 2% - Die Batch **verwendet bevorzugt das Embeddable Python** neben der BAT; ansonsten sucht sie echte `python.exe`/`py.exe` im PATH, **ignoriert** aber die Microsoft‑Store‑Alias‑Pfade (`WindowsApps`). +- `--svg-profile` steuert das Vector-Optimierungsprofil `balanced|agressive` ## Manuelle Nutzung des .py (falls Python vorhanden) ```bat @@ -55,4 +57,5 @@ python pptx_image_compress.py -i "C:\Pfad\input.pptx" -t 8 ## Quellen & Tools - CaesiumCLT – Projekt/Downloads: https://github.com/Lymphatus/caesium-clt +- SVG Polish - https://github.com/g-battaglia/svg_polish - Windows Embeddable Python Package – Doku/Downloads: https://docs.python.org/3/using/windows.html \ No newline at end of file diff --git a/install_and_run.bat b/install_and_run.bat index 97ff0f6..51afa91 100644 --- a/install_and_run.bat +++ b/install_and_run.bat @@ -44,13 +44,22 @@ rem ---- Python discovery (avoid MS Store alias) ---- set "PY_CMD=" set "USE_PY_LAUNCHER=" -rem 1) Prefer local embeddable first -if exist "%PY_EXE%" ( - set "PY_CMD=%PY_EXE%" +if defined VIRTUAL_ENV ( + if exist "%VIRTUAL_ENV%\Scripts\python.exe" ( + set "PY_CMD=%VIRTUAL_ENV%\Scripts\python.exe" + goto :have_python + ) +) + +if exist "%SELF_DIR%.venv\Scripts\python.exe" ( + set "PY_CMD=%SELF_DIR%.venv\Scripts\python.exe" + goto :have_python +) +if exist "%SELF_DIR%venv\Scripts\python.exe" ( + set "PY_CMD=%SELF_DIR%venv\Scripts\python.exe" goto :have_python ) -rem 2) Real python.exe in PATH (exclude WindowsApps alias) for /f "delims=" %%P in ('where python.exe 2^>nul') do ( echo %%P | find /I "WindowsApps" >nul if errorlevel 1 ( @@ -66,7 +75,6 @@ for /f "delims=" %%P in ('where python3.exe 2^>nul') do ( ) ) -rem 3) Python launcher py.exe (exclude WindowsApps) for /f "delims=" %%P in ('where py.exe 2^>nul') do ( echo %%P | find /I "WindowsApps" >nul if errorlevel 1 ( @@ -76,7 +84,11 @@ for /f "delims=" %%P in ('where py.exe 2^>nul') do ( ) ) -rem 4) Download embeddable locally +if exist "%PY_EXE%" ( + set "PY_CMD=%PY_EXE%" + goto :have_python +) + if not exist "%SELF_DIR%%PY_EMBED_ZIP%" ( echo [INFO] Kein Python gefunden. Lade Embeddable Python %PY_EMBED_VERSION% ... powershell -NoLogo -NoProfile -Command ^ @@ -113,6 +125,59 @@ if not exist "%SCRIPT%" ( set "RUN_ARGS=%*" if "%~1"=="" set "RUN_ARGS=-h" +echo [INFO] Pruefe und installiere Python-Abhaengigkeit: svg-polish ... +set "PIP_OK=0" +if defined USE_PY_LAUNCHER ( + "%PY_CMD%" -3 -m pip --version >nul 2>&1 +) else ( + "%PY_CMD%" -m pip --version >nul 2>&1 +) +if not errorlevel 1 set "PIP_OK=1" + +if "%PIP_OK%"=="0" ( + echo [INFO] pip nicht verfuegbar. Versuche ensurepip ... + if defined USE_PY_LAUNCHER ( + "%PY_CMD%" -3 -m ensurepip --upgrade >nul 2>&1 + ) else ( + "%PY_CMD%" -m ensurepip --upgrade >nul 2>&1 + ) + if defined USE_PY_LAUNCHER ( + "%PY_CMD%" -3 -m pip --version >nul 2>&1 + ) else ( + "%PY_CMD%" -m pip --version >nul 2>&1 + ) + if not errorlevel 1 set "PIP_OK=1" +) + +if "%PIP_OK%"=="0" ( + if /I "%PY_CMD%"=="%PY_EXE%" ( + echo [INFO] ensurepip nicht verfuegbar. Lade get-pip.py ... + powershell -NoLogo -NoProfile -Command ^ + "try { Invoke-WebRequest -Uri 'https://bootstrap.pypa.io/get-pip.py' -OutFile '%SELF_DIR%get-pip.py' -UseBasicParsing; exit 0 } catch { Write-Error $_; exit 1 }" + if exist "%SELF_DIR%get-pip.py" ( + "%PY_CMD%" "%SELF_DIR%get-pip.py" >nul 2>&1 + del "%SELF_DIR%get-pip.py" >nul 2>&1 + ) + "%PY_CMD%" -m pip --version >nul 2>&1 + if not errorlevel 1 set "PIP_OK=1" + ) +) + +if "%PIP_OK%"=="1" ( + if defined USE_PY_LAUNCHER ( + "%PY_CMD%" -3 -m pip install --disable-pip-version-check --quiet svg-polish + ) else ( + "%PY_CMD%" -m pip install --disable-pip-version-check --quiet svg-polish + ) + if errorlevel 1 ( + echo [WARN] 'svg-polish' konnte nicht installiert werden. SVG-Dateien werden nicht komprimiert. + ) else ( + echo [OK] 'svg-polish' ist verfuegbar. + ) +) else ( + echo [WARN] pip konnte nicht eingerichtet werden. SVG-Dateien werden nicht komprimiert. +) + echo. echo [%APP_NAME%] Starte ... echo Command: "%PY_CMD%" "%SCRIPT%" %RUN_ARGS% diff --git a/pptx_image_compress.py b/pptx_image_compress.py index bd5d489..e96efb9 100644 --- a/pptx_image_compress.py +++ b/pptx_image_compress.py @@ -1,16 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -PPTX Grafik-Komprimier-Tool (nur CaesiumCLT, Multi-Thread, Batch, sauberes Cleanup) -Version: 1.1.7 +PPTX Raster & Vector Komprimier-Tool (Raster-Iamges: via CaesiumCLT, Vector-Images: via python Module svg_polish) +Version: 1.1.8 - -Highlights: -- Caesium-Scratch außerhalb des PPTX-Arbeitsverzeichnisses -> keine Tempfiles in finaler PPTX -- Safety-Cleanup: entfernt 'caesium*' Ordner und '*.tmp' in ppt/media, bevor gezippt wird -- Overwrite Policy: -O bigger -- Log: image_name,size_before,size_after,saving,saving_percent,in_slide_number,image_type_changed -- Summary inkl. Zeit benötigt +Änderungen in 1.1.8: +- SVG Files werden bei Vorhandensein von svg_polish anhand von 2 Profilen optimiert: balanced|agressive Änderungen in 1.1.7: - PNG->JPG Fallback für große PNGs hinzugefügt (wenn nach Kompression weiterhin > 500 KB) @@ -21,6 +16,7 @@ Highlights: """ import argparse +import importlib import inspect import os import re @@ -42,7 +38,7 @@ from typing import Callable, List, Optional -__version__ = "1.1.7" +__version__ = "1.1.8" RASTER_EXT = {".jpg", ".jpeg", ".png", ".webp", ".gif"} VECTOR_EXT = {".svg"} @@ -51,8 +47,11 @@ PROGRESS_BAR_LEN = 40 TEMP_PREFIX = "pptx_compress_" DEFAULT_MIN_SAVINGS = "2%" PNG_TO_JPEG_THRESHOLD_BYTES = 500 * 1024 -SVGO_CLI_RELATIVE_PATH = Path("bin") / "svgo-client" / "svgo.cmd" - +SVG_POLISH_MODULE_NAME = "svg_polish" +SVG_PROFILE_BALANCED = "balanced" +SVG_PROFILE_AGGRESSIVE = "aggressive" +SVG_PROFILE_DEFAULT = SVG_PROFILE_AGGRESSIVE + @dataclass class DeckResult: @@ -151,13 +150,13 @@ def compress_with_caesium( min_savings: str, output_format: str = "original", ) -> Path | None: + ext = original.suffix.lower() + if ext not in RASTER_EXT: + return None exe = which("caesiumclt") if not exe: raise RuntimeError("[ERROR] 'caesiumclt' wurde nicht gefunden. Bitte CaesiumCLT installieren und in PATH verfügbar machen.") out_dir.mkdir(parents=True, exist_ok=True) - ext = original.suffix.lower() - if ext not in ALLOWED_EXT: - return None cmd = [ exe, "-q", @@ -239,44 +238,126 @@ def compress_raster_image( ) -def get_svgo_executable_path() -> Path: - return Path(__file__).resolve().parent / SVGO_CLI_RELATIVE_PATH +def import_svg_polish_module() -> object | None: + try: + return importlib.import_module(SVG_POLISH_MODULE_NAME) + except Exception: + return None -def compress_svg_with_svgo( +def build_svg_polish_options(svg_polish_module: object, profile: str = SVG_PROFILE_DEFAULT) -> object | None: + options_type = getattr(svg_polish_module, "OptimizeOptions", None) + if not callable(options_type): + return None + try: + if profile == SVG_PROFILE_BALANCED: + return options_type( + digits=3, + style_to_xml=True, + group_collapse=True, + simple_colors=True, + indent_type="none", + newlines=False, + strip_xml_prolog=True, + strip_comments=True, + remove_metadata=True, + remove_titles=True, + remove_descriptions=True, + ) + return options_type( + digits=2, + style_to_xml=True, + group_collapse=True, + simple_colors=True, + indent_type="none", + newlines=False, + strip_xml_prolog=True, + strip_comments=True, + remove_metadata=True, + remove_titles=True, + remove_descriptions=True, + remove_descriptive_elements=True, + strip_ids=True, + shorten_ids=True, + renderer_workaround=False, + ) + except Exception: + return None + + +def call_svg_polish_callable(fn: Callable[..., object], options: object | None, *args: object) -> object: + if options is not None: + try: + return fn(*args, options=options) + except TypeError: + return fn(*args) + return fn(*args) + + +def optimize_svg_content_with_module(svg_polish_module: object, original: Path, svg_profile: str = SVG_PROFILE_DEFAULT) -> str | None: + options = build_svg_polish_options(svg_polish_module, svg_profile) + optimize_path = getattr(svg_polish_module, "optimize_path", None) + if callable(optimize_path): + result = call_svg_polish_callable(optimize_path, options, original) + if isinstance(result, str): + return result + optimize_file = getattr(svg_polish_module, "optimize_file", None) + if callable(optimize_file): + result = call_svg_polish_callable(optimize_file, options, str(original)) + if isinstance(result, str): + return result + svg_text = original.read_text(encoding="utf-8") + optimize = getattr(svg_polish_module, "optimize", None) + if callable(optimize): + result = call_svg_polish_callable(optimize, options, svg_text) + if isinstance(result, str): + return result + optimize_string = getattr(svg_polish_module, "optimize_string", None) + if callable(optimize_string): + result = call_svg_polish_callable(optimize_string, options, svg_text) + if isinstance(result, str): + return result + polish = getattr(svg_polish_module, "polish", None) + if callable(polish): + result = polish(svg_text) + if isinstance(result, str): + return result + return None + + +def compress_svg_with_svg_polish( original: Path, out_dir: Path, + svg_profile: str = SVG_PROFILE_DEFAULT, ) -> Path | None: if original.suffix.lower() not in VECTOR_EXT: return None - svgo_exe = get_svgo_executable_path() - if not svgo_exe.exists() or not svgo_exe.is_file(): + svg_polish_module = import_svg_polish_module() + if svg_polish_module is None: + sys.stderr.write(f"[svg_polish] Modul '{SVG_POLISH_MODULE_NAME}' nicht verfügbar für {original.name}\n") return None out_dir.mkdir(parents=True, exist_ok=True) out_file = out_dir / original.name - cmd = [ - str(svgo_exe), - str(original), - "-o", - str(out_file), - ] try: - r = subprocess.run(cmd, capture_output=True, text=True) - if r.returncode != 0: - sys.stderr.write(f"[svgo] Fehler bei {original.name}:{r.stderr}") + optimized_svg = optimize_svg_content_with_module(svg_polish_module, original, svg_profile) + if not isinstance(optimized_svg, str): return None - return out_file if out_file.exists() else None + out_file.write_text(optimized_svg, encoding="utf-8") + if out_file.stat().st_size >= original.stat().st_size: + return None + return out_file except Exception as ex: - sys.stderr.write(f"[svgo] Ausnahme bei {original.name}: {ex}") + sys.stderr.write(f"[svg_polish] Ausnahme bei {original.name}: {ex}") return None def compress_vector_image( original: Path, out_dir: Path, + svg_profile: str = SVG_PROFILE_DEFAULT, ) -> Path | None: if original.suffix.lower() == ".svg": - return compress_svg_with_svgo(original=original, out_dir=out_dir) + return compress_svg_with_svg_polish(original=original, out_dir=out_dir, svg_profile=svg_profile) return None @@ -287,10 +368,10 @@ def compress_image_with_routing( caesium_threads: int | None, quality: int, min_savings: str, + svg_profile: str = SVG_PROFILE_DEFAULT, ) -> Path | None: - vector_out = compress_vector_image(original=original, out_dir=out_dir) - if vector_out is not None: - return vector_out + if original.suffix.lower() in VECTOR_EXT: + return compress_vector_image(original=original, out_dir=out_dir, svg_profile=svg_profile) return compress_raster_image( compressor=compressor, original=original, @@ -394,6 +475,7 @@ def process_image_file( quality: int, min_savings: str, compressor: Callable[..., Path | None], + svg_profile: str = SVG_PROFILE_DEFAULT, ) -> ImageProcessResult: orig_size = img_path.stat().st_size chosen_size = orig_size @@ -411,6 +493,7 @@ def process_image_file( caesium_threads=caesium_threads, quality=quality, min_savings=min_savings, + svg_profile=svg_profile, ) if caesium_out and caesium_out.exists(): compressed_size = caesium_out.stat().st_size @@ -463,6 +546,7 @@ def process_single_deck( threads: int, quality: int, min_savings: str, + svg_profile: str = SVG_PROFILE_DEFAULT, compressor: Callable[..., Path | None] = compress_with_caesium, ) -> DeckResult: start_time = time.perf_counter() @@ -522,6 +606,7 @@ def process_single_deck( quality=quality, min_savings=min_savings, compressor=compressor, + svg_profile=svg_profile, ) with lock: @@ -660,6 +745,9 @@ def main(): print("[ERROR] 'caesiumclt' nicht gefunden. Bitte installieren und in PATH verfügbar machen.") sys.exit(3) + if import_svg_polish_module() is None: + print("[WARN] 'svg-polish' nicht gefunden. SVG-Dateien werden nicht komprimiert. Installation: python -m pip install svg-polish") + overall_before = 0 overall_after = 0 successes = 0 @@ -675,7 +763,7 @@ def main(): failures += 1 continue dst = out_dir / f"{src.stem}_compressed.pptx" - res = process_single_deck(src, dst, args.threads, args.quality, args.min_savings) + res = process_single_deck(src, dst, args.threads, args.quality, args.min_savings, args.svg_profile) if res.ok: successes += 1 overall_before += res.size_before @@ -748,9 +836,11 @@ def extractParserArguments(): parser.add_argument('-t','--threads', type=int, default=16, help='Anzahl paralleler Threads pro Datei') parser.add_argument('-q','--quality', type=int, default=90, help='Qualität für caesiumclt (0..100), höher = bessere Qualität / größere Datei') parser.add_argument('--min-savings', default=DEFAULT_MIN_SAVINGS, help="Mindestersparnis für caesiumclt (z. B. 2%%, 100KB, 1MB oder Bytes als Zahl)") + parser.add_argument('--svg-profile', choices=[SVG_PROFILE_BALANCED, SVG_PROFILE_AGGRESSIVE], default=SVG_PROFILE_DEFAULT, help='Optimierungsprofil für SVG-Kompression') parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}', help="Zeigt die Versionsnummer an" ) args = parser.parse_args() + return parser,args diff --git a/test_pptx_image_compress.py b/test_pptx_image_compress.py index cc13915..74b561c 100644 --- a/test_pptx_image_compress.py +++ b/test_pptx_image_compress.py @@ -273,57 +273,134 @@ class TestPptxImageCompress(unittest.TestCase): 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): + 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.get_svgo_executable_path", return_value=root / "bin" / "svgo.cmd"): - out = pic.compress_svg_with_svgo(svg, out_dir) + 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_svgo_uses_local_binary(self): + 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") + svg.write_text(" ", 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") + 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 - 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("", 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) + 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_image_with_routing_uses_svg_backend(self): + 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" - out_dir.mkdir(parents=True, exist_ok=True) - vector_out = out_dir / "vector.svg" - vector_out.write_text("", 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") + raise AssertionError("Raster compressor must not run for svg") - with mock.patch("pptx_image_compress.compress_vector_image", return_value=vector_out): + with mock.patch("pptx_image_compress.compress_vector_image", return_value=None): out = pic.compress_image_with_routing( compressor=fake_compressor, original=original, @@ -333,7 +410,7 @@ class TestPptxImageCompress(unittest.TestCase): min_savings="2%", ) - self.assertEqual(out, vector_out) + self.assertIsNone(out) if __name__ == "__main__":