imagex 0.2.1__tar.gz → 0.2.2__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.
- {imagex-0.2.1 → imagex-0.2.2}/PKG-INFO +2 -1
- {imagex-0.2.1 → imagex-0.2.2}/README.md +1 -0
- imagex-0.2.2/imagex/__init__.py +1 -0
- {imagex-0.2.1 → imagex-0.2.2}/imagex/cli.py +20 -0
- imagex-0.2.2/imagex/features/flip.py +38 -0
- {imagex-0.2.1 → imagex-0.2.2}/imagex.egg-info/PKG-INFO +2 -1
- {imagex-0.2.1 → imagex-0.2.2}/imagex.egg-info/SOURCES.txt +2 -0
- {imagex-0.2.1 → imagex-0.2.2}/pyproject.toml +1 -1
- imagex-0.2.2/tests/test_flip.py +72 -0
- imagex-0.2.1/imagex/__init__.py +0 -1
- {imagex-0.2.1 → imagex-0.2.2}/LICENSE +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/imagex/__main__.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/imagex/config.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/imagex/features/__init__.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/imagex/features/add_noise.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/imagex/features/compress.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/imagex/features/convert.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/imagex/features/remove_metadata.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/imagex/features/rename_batch.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/imagex/features/resize.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/imagex/features/rotate.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/imagex/features/watermark.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/imagex/utils/__init__.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/imagex/utils/file_ops.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/imagex/utils/progress.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/imagex.egg-info/dependency_links.txt +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/imagex.egg-info/entry_points.txt +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/imagex.egg-info/requires.txt +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/imagex.egg-info/top_level.txt +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/setup.cfg +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/tests/test_add_noise.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/tests/test_cli.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/tests/test_compress.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/tests/test_convert.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/tests/test_file_ops.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/tests/test_remove_metadata.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/tests/test_rename_batch.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/tests/test_resize.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/tests/test_rotate.py +0 -0
- {imagex-0.2.1 → imagex-0.2.2}/tests/test_watermark.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: imagex
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: All-in-one image processing CLI — resize, convert, watermark, compress, and more
|
|
5
5
|
Author-email: kushal1o1 <kushal1o1@users.noreply.github.com>
|
|
6
6
|
License: MIT License
|
|
@@ -81,6 +81,7 @@ Navigate to any folder with images and run `imagex`.
|
|
|
81
81
|
| Feature | Description |
|
|
82
82
|
|---|---|
|
|
83
83
|
| Rotate | Rotate 90° Left, 90° Right, or 180° |
|
|
84
|
+
| Flip | Mirror horizontally or vertically |
|
|
84
85
|
| Remove Metadata | Strip EXIF/XMP/IPTC (incl. AI generation markers) |
|
|
85
86
|
| Convert Format | JPG ↔ PNG ↔ WEBP ↔ TIFF ↔ BMP ↔ GIF ↔ HEIC |
|
|
86
87
|
| Compress / Optimize | Reduce file size with quality slider |
|
|
@@ -21,6 +21,7 @@ Navigate to any folder with images and run `imagex`.
|
|
|
21
21
|
| Feature | Description |
|
|
22
22
|
|---|---|
|
|
23
23
|
| Rotate | Rotate 90° Left, 90° Right, or 180° |
|
|
24
|
+
| Flip | Mirror horizontally or vertically |
|
|
24
25
|
| Remove Metadata | Strip EXIF/XMP/IPTC (incl. AI generation markers) |
|
|
25
26
|
| Convert Format | JPG ↔ PNG ↔ WEBP ↔ TIFF ↔ BMP ↔ GIF ↔ HEIC |
|
|
26
27
|
| Compress / Optimize | Reduce file size with quality slider |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.2"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
import json
|
|
2
3
|
import sys
|
|
4
|
+
import urllib.request
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
from typing import Optional
|
|
5
7
|
|
|
@@ -163,6 +165,23 @@ def select_files(all_files: list[Path]) -> Optional[list[Path]]:
|
|
|
163
165
|
return [Path(f) for f in selected]
|
|
164
166
|
|
|
165
167
|
|
|
168
|
+
PYPI_URL = "https://pypi.org/pypi/imagex/json"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _check_version():
|
|
172
|
+
try:
|
|
173
|
+
req = urllib.request.Request(PYPI_URL, headers={"Accept": "application/json"})
|
|
174
|
+
resp = urllib.request.urlopen(req, timeout=2)
|
|
175
|
+
latest = json.loads(resp.read())["info"]["version"]
|
|
176
|
+
if latest != __version__:
|
|
177
|
+
console.print(
|
|
178
|
+
f" [dim] New version available :) : {latest} → "
|
|
179
|
+
f"pip install --upgrade imagex[/dim]"
|
|
180
|
+
)
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
|
|
166
185
|
def main():
|
|
167
186
|
_parse_args()
|
|
168
187
|
|
|
@@ -214,6 +233,7 @@ def main():
|
|
|
214
233
|
)
|
|
215
234
|
|
|
216
235
|
console.print("\n[bold green]✓ All done![/bold green]")
|
|
236
|
+
_check_version()
|
|
217
237
|
|
|
218
238
|
except KeyboardInterrupt:
|
|
219
239
|
console.print("\n[yellow]Interrupted. Exiting.[/yellow]")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
import questionary
|
|
5
|
+
from PIL import Image
|
|
6
|
+
|
|
7
|
+
NAME = "Flip"
|
|
8
|
+
DESCRIPTION = "Mirror images horizontally or vertically"
|
|
9
|
+
|
|
10
|
+
OPTIONS = {
|
|
11
|
+
"Horizontal (left ↔ right)": Image.FLIP_LEFT_RIGHT,
|
|
12
|
+
"Vertical (top ↔ bottom)": Image.FLIP_TOP_BOTTOM,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def ask_args(files: list[Path]) -> dict[str, Any]:
|
|
17
|
+
direction = questionary.select(
|
|
18
|
+
"Flip direction:",
|
|
19
|
+
choices=list(OPTIONS.keys()),
|
|
20
|
+
).ask()
|
|
21
|
+
|
|
22
|
+
return {"method": OPTIONS[direction]}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run(file: Path, output_path: Path, args: Optional[dict[str, Any]] = None) -> bool:
|
|
26
|
+
if args is None:
|
|
27
|
+
msg = "args required for flip"
|
|
28
|
+
raise ValueError(msg)
|
|
29
|
+
|
|
30
|
+
img = Image.open(file)
|
|
31
|
+
flipped = img.transpose(args["method"])
|
|
32
|
+
kw = {"format": img.format or "JPEG"}
|
|
33
|
+
if exif := img.info.get("exif"):
|
|
34
|
+
kw["exif"] = exif
|
|
35
|
+
if icc := img.info.get("icc_profile"):
|
|
36
|
+
kw["icc_profile"] = icc
|
|
37
|
+
flipped.save(str(output_path), **kw)
|
|
38
|
+
return True
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: imagex
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: All-in-one image processing CLI — resize, convert, watermark, compress, and more
|
|
5
5
|
Author-email: kushal1o1 <kushal1o1@users.noreply.github.com>
|
|
6
6
|
License: MIT License
|
|
@@ -81,6 +81,7 @@ Navigate to any folder with images and run `imagex`.
|
|
|
81
81
|
| Feature | Description |
|
|
82
82
|
|---|---|
|
|
83
83
|
| Rotate | Rotate 90° Left, 90° Right, or 180° |
|
|
84
|
+
| Flip | Mirror horizontally or vertically |
|
|
84
85
|
| Remove Metadata | Strip EXIF/XMP/IPTC (incl. AI generation markers) |
|
|
85
86
|
| Convert Format | JPG ↔ PNG ↔ WEBP ↔ TIFF ↔ BMP ↔ GIF ↔ HEIC |
|
|
86
87
|
| Compress / Optimize | Reduce file size with quality slider |
|
|
@@ -15,6 +15,7 @@ imagex/features/__init__.py
|
|
|
15
15
|
imagex/features/add_noise.py
|
|
16
16
|
imagex/features/compress.py
|
|
17
17
|
imagex/features/convert.py
|
|
18
|
+
imagex/features/flip.py
|
|
18
19
|
imagex/features/remove_metadata.py
|
|
19
20
|
imagex/features/rename_batch.py
|
|
20
21
|
imagex/features/resize.py
|
|
@@ -28,6 +29,7 @@ tests/test_cli.py
|
|
|
28
29
|
tests/test_compress.py
|
|
29
30
|
tests/test_convert.py
|
|
30
31
|
tests/test_file_ops.py
|
|
32
|
+
tests/test_flip.py
|
|
31
33
|
tests/test_remove_metadata.py
|
|
32
34
|
tests/test_rename_batch.py
|
|
33
35
|
tests/test_resize.py
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from PIL import Image
|
|
2
|
+
|
|
3
|
+
from imagex.features.flip import run
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _marked_image(path, size=(4, 4)):
|
|
7
|
+
"""An all-black image with a single white pixel in the top-left corner."""
|
|
8
|
+
img = Image.new("RGB", size, color="black")
|
|
9
|
+
img.putpixel((0, 0), (255, 255, 255))
|
|
10
|
+
img.save(str(path))
|
|
11
|
+
return img
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestFlipRun:
|
|
15
|
+
def test_flip_horizontal_moves_pixel_left_to_right(self, tmp_path):
|
|
16
|
+
src = tmp_path / "marked.png"
|
|
17
|
+
_marked_image(src)
|
|
18
|
+
out = tmp_path / "h.png"
|
|
19
|
+
assert run(src, out, {"method": Image.FLIP_LEFT_RIGHT})
|
|
20
|
+
|
|
21
|
+
flipped = Image.open(out)
|
|
22
|
+
# top-left white pixel mirrors to the top-right
|
|
23
|
+
assert flipped.getpixel((3, 0)) == (255, 255, 255)
|
|
24
|
+
assert flipped.getpixel((0, 0)) == (0, 0, 0)
|
|
25
|
+
|
|
26
|
+
def test_flip_vertical_moves_pixel_top_to_bottom(self, tmp_path):
|
|
27
|
+
src = tmp_path / "marked.png"
|
|
28
|
+
_marked_image(src)
|
|
29
|
+
out = tmp_path / "v.png"
|
|
30
|
+
assert run(src, out, {"method": Image.FLIP_TOP_BOTTOM})
|
|
31
|
+
|
|
32
|
+
flipped = Image.open(out)
|
|
33
|
+
# top-left white pixel mirrors to the bottom-left
|
|
34
|
+
assert flipped.getpixel((0, 3)) == (255, 255, 255)
|
|
35
|
+
assert flipped.getpixel((0, 0)) == (0, 0, 0)
|
|
36
|
+
|
|
37
|
+
def test_preserves_dimensions(self, tmp_images, tmp_path):
|
|
38
|
+
src = tmp_images / "test.jpg"
|
|
39
|
+
original = Image.open(src)
|
|
40
|
+
out = tmp_path / "out.jpg"
|
|
41
|
+
assert run(src, out, {"method": Image.FLIP_LEFT_RIGHT})
|
|
42
|
+
|
|
43
|
+
flipped = Image.open(out)
|
|
44
|
+
assert flipped.width == original.width
|
|
45
|
+
assert flipped.height == original.height
|
|
46
|
+
|
|
47
|
+
def test_double_flip_restores_original(self, tmp_path):
|
|
48
|
+
src = tmp_path / "marked.png"
|
|
49
|
+
_marked_image(src)
|
|
50
|
+
once = tmp_path / "once.png"
|
|
51
|
+
twice = tmp_path / "twice.png"
|
|
52
|
+
run(src, once, {"method": Image.FLIP_LEFT_RIGHT})
|
|
53
|
+
run(once, twice, {"method": Image.FLIP_LEFT_RIGHT})
|
|
54
|
+
|
|
55
|
+
assert Image.open(twice).getpixel((0, 0)) == (255, 255, 255)
|
|
56
|
+
|
|
57
|
+
def test_preserves_format(self, tmp_images, tmp_path):
|
|
58
|
+
src = tmp_images / "image.webp"
|
|
59
|
+
out = tmp_path / "out.webp"
|
|
60
|
+
assert run(src, out, {"method": Image.FLIP_TOP_BOTTOM})
|
|
61
|
+
|
|
62
|
+
flipped = Image.open(out)
|
|
63
|
+
assert flipped.format == "WEBP"
|
|
64
|
+
|
|
65
|
+
def test_run_without_args_raises(self, tmp_images, tmp_path):
|
|
66
|
+
src = tmp_images / "test.jpg"
|
|
67
|
+
out = tmp_path / "out.jpg"
|
|
68
|
+
try:
|
|
69
|
+
run(src, out, None)
|
|
70
|
+
assert False, "should have raised"
|
|
71
|
+
except ValueError as e:
|
|
72
|
+
assert "args required" in str(e).lower()
|
imagex-0.2.1/imagex/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|