diff --git a/pptx_image_compress.py b/pptx_image_compress.py
index 791f4b3..9b5ec35 100644
--- a/pptx_image_compress.py
+++ b/pptx_image_compress.py
@@ -44,7 +44,9 @@ from typing import Callable, List, Optional
__version__ = "1.1.7"
-ALLOWED_EXT = {".jpg", ".jpeg", ".png", ".webp", ".gif"}
+RASTER_EXT = {".jpg", ".jpeg", ".png", ".webp", ".gif"}
+VECTOR_EXT = {".svg"}
+ALLOWED_EXT = RASTER_EXT | VECTOR_EXT
PROGRESS_BAR_LEN = 40
TEMP_PREFIX = "pptx_compress_"
DEFAULT_MIN_SAVINGS = "2%"
@@ -236,6 +238,45 @@ def compress_raster_image(
)
+def compress_svg_with_svgo(
+ original: Path,
+ out_dir: Path,
+) -> Path | None:
+ if original.suffix.lower() not in VECTOR_EXT:
+ return None
+ npx_exe = which("npx")
+ if not npx_exe:
+ return None
+ out_dir.mkdir(parents=True, exist_ok=True)
+ out_file = out_dir / original.name
+ cmd = [
+ npx_exe,
+ "--yes",
+ "svgo",
+ 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}")
+ return None
+ return out_file if out_file.exists() else None
+ except Exception as ex:
+ sys.stderr.write(f"[svgo] Ausnahme bei {original.name}: {ex}")
+ return None
+
+
+def compress_vector_image(
+ original: Path,
+ out_dir: Path,
+) -> Path | None:
+ if original.suffix.lower() == ".svg":
+ return compress_svg_with_svgo(original=original, out_dir=out_dir)
+ return None
+
+
def compress_image_with_routing(
compressor: Callable[..., Path | None],
original: Path,
@@ -244,6 +285,9 @@ def compress_image_with_routing(
quality: int,
min_savings: str,
) -> Path | None:
+ vector_out = compress_vector_image(original=original, out_dir=out_dir)
+ if vector_out is not None:
+ return vector_out
return compress_raster_image(
compressor=compressor,
original=original,
diff --git a/test_pptx_image_compress.py b/test_pptx_image_compress.py
index 3c48a1f..2811c56 100644
--- a/test_pptx_image_compress.py
+++ b/test_pptx_image_compress.py
@@ -2,6 +2,7 @@ import tempfile
import unittest
import zipfile
from pathlib import Path
+from unittest import mock
import pptx_image_compress as pic
@@ -14,8 +15,9 @@ class TestPptxImageCompress(unittest.TestCase):
(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"])
+ 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(
@@ -271,6 +273,42 @@ 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_npx_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.which", return_value=None):
+ out = pic.compress_svg_with_svgo(svg, out_dir)
+
+ self.assertEqual(out, None)
+
+ 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("", 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")
+
+ 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__":