kellerlab-pre-commit-hooks 0.2.2__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kellerlab_pre_commit_hooks-0.2.2/src/kellerlab_pre_commit_hooks.egg-info → kellerlab_pre_commit_hooks-0.3.0}/PKG-INFO +4 -4
- {kellerlab_pre_commit_hooks-0.2.2 → kellerlab_pre_commit_hooks-0.3.0}/README.md +3 -3
- {kellerlab_pre_commit_hooks-0.2.2 → kellerlab_pre_commit_hooks-0.3.0}/pyproject.toml +1 -1
- {kellerlab_pre_commit_hooks-0.2.2 → kellerlab_pre_commit_hooks-0.3.0/src/kellerlab_pre_commit_hooks.egg-info}/PKG-INFO +4 -4
- {kellerlab_pre_commit_hooks-0.2.2 → kellerlab_pre_commit_hooks-0.3.0}/src/pre_commit_hooks/optimize_images.py +30 -11
- {kellerlab_pre_commit_hooks-0.2.2 → kellerlab_pre_commit_hooks-0.3.0}/tests/test_optimize_images.py +131 -0
- {kellerlab_pre_commit_hooks-0.2.2 → kellerlab_pre_commit_hooks-0.3.0}/LICENSE +0 -0
- {kellerlab_pre_commit_hooks-0.2.2 → kellerlab_pre_commit_hooks-0.3.0}/setup.cfg +0 -0
- {kellerlab_pre_commit_hooks-0.2.2 → kellerlab_pre_commit_hooks-0.3.0}/src/kellerlab_pre_commit_hooks.egg-info/SOURCES.txt +0 -0
- {kellerlab_pre_commit_hooks-0.2.2 → kellerlab_pre_commit_hooks-0.3.0}/src/kellerlab_pre_commit_hooks.egg-info/dependency_links.txt +0 -0
- {kellerlab_pre_commit_hooks-0.2.2 → kellerlab_pre_commit_hooks-0.3.0}/src/kellerlab_pre_commit_hooks.egg-info/entry_points.txt +0 -0
- {kellerlab_pre_commit_hooks-0.2.2 → kellerlab_pre_commit_hooks-0.3.0}/src/kellerlab_pre_commit_hooks.egg-info/requires.txt +0 -0
- {kellerlab_pre_commit_hooks-0.2.2 → kellerlab_pre_commit_hooks-0.3.0}/src/kellerlab_pre_commit_hooks.egg-info/top_level.txt +0 -0
- {kellerlab_pre_commit_hooks-0.2.2 → kellerlab_pre_commit_hooks-0.3.0}/src/pre_commit_hooks/__init__.py +0 -0
- {kellerlab_pre_commit_hooks-0.2.2 → kellerlab_pre_commit_hooks-0.3.0}/src/pre_commit_hooks/flatten_validate.py +0 -0
- {kellerlab_pre_commit_hooks-0.2.2 → kellerlab_pre_commit_hooks-0.3.0}/tests/test_flatten_validate.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kellerlab-pre-commit-hooks
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Reusable pre-commit hooks for OpenSCAD projects
|
|
5
5
|
Author-email: Patrick Pötz <kellervater@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -39,7 +39,7 @@ Two hooks for OpenSCAD-based 3D printing projects:
|
|
|
39
39
|
| Hook | Description |
|
|
40
40
|
|------|-------------|
|
|
41
41
|
| `flatten-validate` | Runs `scadm flatten --all` and fails if flattened files have uncommitted changes |
|
|
42
|
-
| `optimize-images` | Resizes oversized JPEGs, compresses them, and strips
|
|
42
|
+
| `optimize-images` | Resizes oversized JPEGs/PNGs, compresses them, and strips metadata (privacy) |
|
|
43
43
|
|
|
44
44
|
## 🔧 Usage
|
|
45
45
|
|
|
@@ -72,7 +72,7 @@ Flattens OpenSCAD files via [scadm](https://pypi.org/project/scadm/) and validat
|
|
|
72
72
|
|
|
73
73
|
### `optimize-images`
|
|
74
74
|
|
|
75
|
-
Resizes JPEGs exceeding max width, compresses
|
|
75
|
+
Resizes JPEGs and PNGs exceeding max width, compresses them, and strips metadata. EXIF data (including GPS coordinates) is removed from JPEGs; text metadata chunks are removed from PNGs.
|
|
76
76
|
|
|
77
77
|
```yaml
|
|
78
78
|
- id: optimize-images
|
|
@@ -84,7 +84,7 @@ Resizes JPEGs exceeding max width, compresses to target quality, and strips EXIF
|
|
|
84
84
|
| Arg | Default | Description |
|
|
85
85
|
|-----|---------|-------------|
|
|
86
86
|
| `--max-width` | `1920` | Maximum image width in pixels |
|
|
87
|
-
| `--quality` | `85` | JPEG compression quality (1-95) |
|
|
87
|
+
| `--quality` | `85` | JPEG compression quality (1-95, ignored for PNG) |
|
|
88
88
|
|
|
89
89
|
Both hooks **fail when files are modified**, printing instructions to `git add` the changes. Re-run `git commit` after staging.
|
|
90
90
|
|
|
@@ -11,7 +11,7 @@ Two hooks for OpenSCAD-based 3D printing projects:
|
|
|
11
11
|
| Hook | Description |
|
|
12
12
|
|------|-------------|
|
|
13
13
|
| `flatten-validate` | Runs `scadm flatten --all` and fails if flattened files have uncommitted changes |
|
|
14
|
-
| `optimize-images` | Resizes oversized JPEGs, compresses them, and strips
|
|
14
|
+
| `optimize-images` | Resizes oversized JPEGs/PNGs, compresses them, and strips metadata (privacy) |
|
|
15
15
|
|
|
16
16
|
## 🔧 Usage
|
|
17
17
|
|
|
@@ -44,7 +44,7 @@ Flattens OpenSCAD files via [scadm](https://pypi.org/project/scadm/) and validat
|
|
|
44
44
|
|
|
45
45
|
### `optimize-images`
|
|
46
46
|
|
|
47
|
-
Resizes JPEGs exceeding max width, compresses
|
|
47
|
+
Resizes JPEGs and PNGs exceeding max width, compresses them, and strips metadata. EXIF data (including GPS coordinates) is removed from JPEGs; text metadata chunks are removed from PNGs.
|
|
48
48
|
|
|
49
49
|
```yaml
|
|
50
50
|
- id: optimize-images
|
|
@@ -56,7 +56,7 @@ Resizes JPEGs exceeding max width, compresses to target quality, and strips EXIF
|
|
|
56
56
|
| Arg | Default | Description |
|
|
57
57
|
|-----|---------|-------------|
|
|
58
58
|
| `--max-width` | `1920` | Maximum image width in pixels |
|
|
59
|
-
| `--quality` | `85` | JPEG compression quality (1-95) |
|
|
59
|
+
| `--quality` | `85` | JPEG compression quality (1-95, ignored for PNG) |
|
|
60
60
|
|
|
61
61
|
Both hooks **fail when files are modified**, printing instructions to `git add` the changes. Re-run `git commit` after staging.
|
|
62
62
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kellerlab-pre-commit-hooks
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Reusable pre-commit hooks for OpenSCAD projects
|
|
5
5
|
Author-email: Patrick Pötz <kellervater@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -39,7 +39,7 @@ Two hooks for OpenSCAD-based 3D printing projects:
|
|
|
39
39
|
| Hook | Description |
|
|
40
40
|
|------|-------------|
|
|
41
41
|
| `flatten-validate` | Runs `scadm flatten --all` and fails if flattened files have uncommitted changes |
|
|
42
|
-
| `optimize-images` | Resizes oversized JPEGs, compresses them, and strips
|
|
42
|
+
| `optimize-images` | Resizes oversized JPEGs/PNGs, compresses them, and strips metadata (privacy) |
|
|
43
43
|
|
|
44
44
|
## 🔧 Usage
|
|
45
45
|
|
|
@@ -72,7 +72,7 @@ Flattens OpenSCAD files via [scadm](https://pypi.org/project/scadm/) and validat
|
|
|
72
72
|
|
|
73
73
|
### `optimize-images`
|
|
74
74
|
|
|
75
|
-
Resizes JPEGs exceeding max width, compresses
|
|
75
|
+
Resizes JPEGs and PNGs exceeding max width, compresses them, and strips metadata. EXIF data (including GPS coordinates) is removed from JPEGs; text metadata chunks are removed from PNGs.
|
|
76
76
|
|
|
77
77
|
```yaml
|
|
78
78
|
- id: optimize-images
|
|
@@ -84,7 +84,7 @@ Resizes JPEGs exceeding max width, compresses to target quality, and strips EXIF
|
|
|
84
84
|
| Arg | Default | Description |
|
|
85
85
|
|-----|---------|-------------|
|
|
86
86
|
| `--max-width` | `1920` | Maximum image width in pixels |
|
|
87
|
-
| `--quality` | `85` | JPEG compression quality (1-95) |
|
|
87
|
+
| `--quality` | `85` | JPEG compression quality (1-95, ignored for PNG) |
|
|
88
88
|
|
|
89
89
|
Both hooks **fail when files are modified**, printing instructions to `git add` the changes. Re-run `git commit` after staging.
|
|
90
90
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Pre-commit hook: resize and compress JPEG images."""
|
|
1
|
+
"""Pre-commit hook: resize and compress JPEG and PNG images."""
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import shlex
|
|
@@ -8,24 +8,32 @@ from PIL import Image, ImageOps
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def _optimize_file(filepath, max_width, quality):
|
|
11
|
-
"""Resize, compress, and strip
|
|
11
|
+
"""Resize, compress, and strip metadata from a single image file.
|
|
12
|
+
|
|
13
|
+
Supports JPEG and PNG formats. JPEGs are compressed with the given quality
|
|
14
|
+
and have EXIF data stripped. PNGs are saved with Pillow's optimize flag
|
|
15
|
+
and have text metadata stripped.
|
|
12
16
|
|
|
13
17
|
Args:
|
|
14
|
-
filepath: Path to the
|
|
18
|
+
filepath: Path to the image file.
|
|
15
19
|
max_width: Maximum allowed width in pixels.
|
|
16
|
-
quality: JPEG compression quality (1-95).
|
|
20
|
+
quality: JPEG compression quality (1-95). Ignored for PNG.
|
|
17
21
|
|
|
18
22
|
Returns:
|
|
19
23
|
True if the file was modified, False otherwise.
|
|
20
24
|
"""
|
|
21
25
|
with Image.open(filepath) as img:
|
|
22
|
-
|
|
26
|
+
img_format = img.format # "JPEG" or "PNG"
|
|
27
|
+
if img_format not in ("JPEG", "PNG"):
|
|
28
|
+
msg = f"Unsupported format '{img_format}' for {filepath}"
|
|
29
|
+
raise ValueError(msg)
|
|
30
|
+
has_png_text = img_format == "PNG" and bool(getattr(img, "text", None))
|
|
23
31
|
img = ImageOps.exif_transpose(img)
|
|
24
32
|
width, height = img.size
|
|
25
33
|
needs_resize = width > max_width
|
|
26
34
|
has_exif = bool(img.info.get("exif"))
|
|
27
35
|
|
|
28
|
-
if not needs_resize and not has_exif:
|
|
36
|
+
if not needs_resize and not has_exif and not has_png_text:
|
|
29
37
|
return False
|
|
30
38
|
|
|
31
39
|
if needs_resize:
|
|
@@ -34,17 +42,28 @@ def _optimize_file(filepath, max_width, quality):
|
|
|
34
42
|
new_size = (max_width, new_height)
|
|
35
43
|
img = img.resize(new_size, Image.LANCZOS)
|
|
36
44
|
|
|
37
|
-
if
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
if img_format == "PNG":
|
|
46
|
+
# Strip metadata by creating a clean image without materializing
|
|
47
|
+
# the pixel stream as a Python list.
|
|
48
|
+
clean = Image.new(img.mode, img.size)
|
|
49
|
+
if img.mode == "P":
|
|
50
|
+
palette = img.getpalette()
|
|
51
|
+
if palette is not None:
|
|
52
|
+
clean.putpalette(palette)
|
|
53
|
+
clean.frombytes(img.tobytes())
|
|
54
|
+
clean.save(filepath, "PNG", optimize=True)
|
|
55
|
+
else:
|
|
56
|
+
if img.mode in ("RGBA", "P"):
|
|
57
|
+
img = img.convert("RGB")
|
|
58
|
+
img.save(filepath, "JPEG", quality=quality, optimize=True)
|
|
40
59
|
|
|
41
60
|
return True
|
|
42
61
|
|
|
43
62
|
|
|
44
63
|
def main():
|
|
45
64
|
"""Entry point for the optimize-images pre-commit hook."""
|
|
46
|
-
parser = argparse.ArgumentParser(description="Resize and compress
|
|
47
|
-
parser.add_argument("filenames", nargs="*", help="
|
|
65
|
+
parser = argparse.ArgumentParser(description="Resize and compress images.")
|
|
66
|
+
parser.add_argument("filenames", nargs="*", help="Image files to check.")
|
|
48
67
|
parser.add_argument(
|
|
49
68
|
"--max-width",
|
|
50
69
|
type=int,
|
{kellerlab_pre_commit_hooks-0.2.2 → kellerlab_pre_commit_hooks-0.3.0}/tests/test_optimize_images.py
RENAMED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from unittest import mock
|
|
4
4
|
|
|
5
5
|
from PIL import Image
|
|
6
|
+
from PIL.PngImagePlugin import PngInfo
|
|
6
7
|
|
|
7
8
|
from pre_commit_hooks.optimize_images import main
|
|
8
9
|
|
|
@@ -27,6 +28,23 @@ def _create_jpeg(path, width, height, exif=False):
|
|
|
27
28
|
img.save(str(path), "JPEG", quality=95)
|
|
28
29
|
|
|
29
30
|
|
|
31
|
+
def _create_png(path, width, height, text_meta=False):
|
|
32
|
+
"""Create a test PNG file.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
path: File path to write.
|
|
36
|
+
width: Image width in pixels.
|
|
37
|
+
height: Image height in pixels.
|
|
38
|
+
text_meta: If True, embed text metadata chunks.
|
|
39
|
+
"""
|
|
40
|
+
img = Image.new("RGBA", (width, height), color="blue")
|
|
41
|
+
pnginfo = PngInfo()
|
|
42
|
+
if text_meta:
|
|
43
|
+
pnginfo.add_text("Software", "TestSuite")
|
|
44
|
+
pnginfo.add_text("Comment", "test metadata")
|
|
45
|
+
img.save(str(path), "PNG", pnginfo=pnginfo)
|
|
46
|
+
|
|
47
|
+
|
|
30
48
|
class TestOptimizeImages:
|
|
31
49
|
"""Tests for the optimize-images hook."""
|
|
32
50
|
|
|
@@ -130,3 +148,116 @@ class TestOptimizeImages:
|
|
|
130
148
|
result = main()
|
|
131
149
|
|
|
132
150
|
assert result == 1
|
|
151
|
+
|
|
152
|
+
# --- PNG tests ---
|
|
153
|
+
|
|
154
|
+
def test_large_png_resized(self, tmp_path):
|
|
155
|
+
"""PNGs wider than max-width are resized."""
|
|
156
|
+
img_path = tmp_path / "large.png"
|
|
157
|
+
_create_png(img_path, 3000, 2000)
|
|
158
|
+
|
|
159
|
+
with mock.patch("sys.argv", ["optimize-images", "--max-width=1920", str(img_path)]):
|
|
160
|
+
result = main()
|
|
161
|
+
|
|
162
|
+
assert result == 1
|
|
163
|
+
with Image.open(img_path) as img:
|
|
164
|
+
assert img.size[0] == 1920
|
|
165
|
+
assert img.size[1] == 1280
|
|
166
|
+
|
|
167
|
+
def test_small_png_untouched(self, tmp_path):
|
|
168
|
+
"""Small PNGs without metadata are not modified."""
|
|
169
|
+
img_path = tmp_path / "small.png"
|
|
170
|
+
_create_png(img_path, 800, 600)
|
|
171
|
+
original_size = img_path.stat().st_size
|
|
172
|
+
|
|
173
|
+
with mock.patch("sys.argv", ["optimize-images", "--max-width=1920", str(img_path)]):
|
|
174
|
+
result = main()
|
|
175
|
+
|
|
176
|
+
assert result == 0
|
|
177
|
+
assert img_path.stat().st_size == original_size
|
|
178
|
+
|
|
179
|
+
def test_png_metadata_stripped(self, tmp_path):
|
|
180
|
+
"""Text metadata is stripped from PNGs."""
|
|
181
|
+
img_path = tmp_path / "meta.png"
|
|
182
|
+
_create_png(img_path, 800, 600, text_meta=True)
|
|
183
|
+
|
|
184
|
+
with mock.patch("sys.argv", ["optimize-images", "--max-width=1920", str(img_path)]):
|
|
185
|
+
result = main()
|
|
186
|
+
|
|
187
|
+
assert result == 1
|
|
188
|
+
with Image.open(img_path) as img:
|
|
189
|
+
assert not getattr(img, "text", None)
|
|
190
|
+
|
|
191
|
+
def test_png_preserves_transparency(self, tmp_path):
|
|
192
|
+
"""PNGs retain their alpha channel after optimization."""
|
|
193
|
+
img_path = tmp_path / "alpha.png"
|
|
194
|
+
img = Image.new("RGBA", (3000, 2000), color=(0, 0, 255, 128))
|
|
195
|
+
img.save(str(img_path), "PNG")
|
|
196
|
+
|
|
197
|
+
with mock.patch("sys.argv", ["optimize-images", "--max-width=1920", str(img_path)]):
|
|
198
|
+
result = main()
|
|
199
|
+
|
|
200
|
+
assert result == 1
|
|
201
|
+
with Image.open(img_path) as img:
|
|
202
|
+
assert img.mode == "RGBA"
|
|
203
|
+
assert img.size[0] == 1920
|
|
204
|
+
|
|
205
|
+
def test_mixed_jpeg_and_png(self, tmp_path):
|
|
206
|
+
"""Both JPEG and PNG files are handled in a single run."""
|
|
207
|
+
jpg_path = tmp_path / "big.jpg"
|
|
208
|
+
png_path = tmp_path / "big.png"
|
|
209
|
+
_create_jpeg(jpg_path, 4000, 3000)
|
|
210
|
+
_create_png(png_path, 4000, 3000)
|
|
211
|
+
|
|
212
|
+
with mock.patch(
|
|
213
|
+
"sys.argv",
|
|
214
|
+
["optimize-images", "--max-width=1920", str(jpg_path), str(png_path)],
|
|
215
|
+
):
|
|
216
|
+
result = main()
|
|
217
|
+
|
|
218
|
+
assert result == 1
|
|
219
|
+
with Image.open(jpg_path) as img:
|
|
220
|
+
assert img.size[0] == 1920
|
|
221
|
+
with Image.open(png_path) as img:
|
|
222
|
+
assert img.size[0] == 1920
|
|
223
|
+
|
|
224
|
+
def test_paletted_png_resized(self, tmp_path):
|
|
225
|
+
"""Paletted PNGs are resized and palette is preserved."""
|
|
226
|
+
img_path = tmp_path / "palette.png"
|
|
227
|
+
img = Image.new("P", (3000, 2000))
|
|
228
|
+
img.putpalette([i % 256 for i in range(768)])
|
|
229
|
+
img.save(str(img_path), "PNG")
|
|
230
|
+
|
|
231
|
+
with mock.patch("sys.argv", ["optimize-images", "--max-width=1920", str(img_path)]):
|
|
232
|
+
result = main()
|
|
233
|
+
|
|
234
|
+
assert result == 1
|
|
235
|
+
with Image.open(img_path) as img:
|
|
236
|
+
assert img.mode == "P"
|
|
237
|
+
assert img.size[0] == 1920
|
|
238
|
+
assert img.getpalette() is not None
|
|
239
|
+
|
|
240
|
+
def test_paletted_png_with_transparency(self, tmp_path):
|
|
241
|
+
"""Paletted PNGs with transparency are handled correctly."""
|
|
242
|
+
img_path = tmp_path / "palette_alpha.png"
|
|
243
|
+
img = Image.new("RGBA", (3000, 2000), color=(255, 0, 0, 128))
|
|
244
|
+
img = img.convert("P")
|
|
245
|
+
img.save(str(img_path), "PNG")
|
|
246
|
+
|
|
247
|
+
with mock.patch("sys.argv", ["optimize-images", "--max-width=1920", str(img_path)]):
|
|
248
|
+
result = main()
|
|
249
|
+
|
|
250
|
+
assert result == 1
|
|
251
|
+
with Image.open(img_path) as img:
|
|
252
|
+
assert img.size[0] == 1920
|
|
253
|
+
|
|
254
|
+
def test_unsupported_format_raises(self, tmp_path):
|
|
255
|
+
"""Unsupported image formats cause a failure."""
|
|
256
|
+
img_path = tmp_path / "image.bmp"
|
|
257
|
+
img = Image.new("RGB", (800, 600), color="green")
|
|
258
|
+
img.save(str(img_path), "BMP")
|
|
259
|
+
|
|
260
|
+
with mock.patch("sys.argv", ["optimize-images", str(img_path)]):
|
|
261
|
+
result = main()
|
|
262
|
+
|
|
263
|
+
assert result == 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kellerlab_pre_commit_hooks-0.2.2 → kellerlab_pre_commit_hooks-0.3.0}/tests/test_flatten_validate.py
RENAMED
|
File without changes
|