Add SVG compression via npx svgo
Add vector extension support for .svg and route SVG files through npx svgo before raster compression. Keep behavior fail-safe: missing npx/svgo or non-zero svgo exit returns None and preserves existing flow. Extend tests for SVG discovery, SVG routing priority, and missing npx handling. Co-Authored-By: Abacus.AI CLI <agent@abacus.ai>
This commit is contained in:
+45
-1
@@ -44,7 +44,9 @@ from typing import Callable, List, Optional
|
|||||||
|
|
||||||
__version__ = "1.1.7"
|
__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
|
PROGRESS_BAR_LEN = 40
|
||||||
TEMP_PREFIX = "pptx_compress_"
|
TEMP_PREFIX = "pptx_compress_"
|
||||||
DEFAULT_MIN_SAVINGS = "2%"
|
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(
|
def compress_image_with_routing(
|
||||||
compressor: Callable[..., Path | None],
|
compressor: Callable[..., Path | None],
|
||||||
original: Path,
|
original: Path,
|
||||||
@@ -244,6 +285,9 @@ def compress_image_with_routing(
|
|||||||
quality: int,
|
quality: int,
|
||||||
min_savings: str,
|
min_savings: str,
|
||||||
) -> Path | None:
|
) -> 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(
|
return compress_raster_image(
|
||||||
compressor=compressor,
|
compressor=compressor,
|
||||||
original=original,
|
original=original,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import tempfile
|
|||||||
import unittest
|
import unittest
|
||||||
import zipfile
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
import pptx_image_compress as pic
|
import pptx_image_compress as pic
|
||||||
|
|
||||||
@@ -14,8 +15,9 @@ class TestPptxImageCompress(unittest.TestCase):
|
|||||||
(media_dir / "b.png").write_bytes(b"1")
|
(media_dir / "b.png").write_bytes(b"1")
|
||||||
(media_dir / "c.txt").write_bytes(b"1")
|
(media_dir / "c.txt").write_bytes(b"1")
|
||||||
(media_dir / "d.GIF").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)
|
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):
|
def test_image_result_to_log_line(self):
|
||||||
image_result = pic.ImageProcessResult(
|
image_result = pic.ImageProcessResult(
|
||||||
@@ -271,6 +273,42 @@ class TestPptxImageCompress(unittest.TestCase):
|
|||||||
self.fail("Output should not be None")
|
self.fail("Output should not be None")
|
||||||
self.assertEqual(out.name, "image1.png")
|
self.assertEqual(out.name, "image1.png")
|
||||||
self.assertEqual(out.stat().st_size, 80)
|
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("<svg></svg>", 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("<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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user