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.
Files changed (40) hide show
  1. {imagex-0.2.1 → imagex-0.2.2}/PKG-INFO +2 -1
  2. {imagex-0.2.1 → imagex-0.2.2}/README.md +1 -0
  3. imagex-0.2.2/imagex/__init__.py +1 -0
  4. {imagex-0.2.1 → imagex-0.2.2}/imagex/cli.py +20 -0
  5. imagex-0.2.2/imagex/features/flip.py +38 -0
  6. {imagex-0.2.1 → imagex-0.2.2}/imagex.egg-info/PKG-INFO +2 -1
  7. {imagex-0.2.1 → imagex-0.2.2}/imagex.egg-info/SOURCES.txt +2 -0
  8. {imagex-0.2.1 → imagex-0.2.2}/pyproject.toml +1 -1
  9. imagex-0.2.2/tests/test_flip.py +72 -0
  10. imagex-0.2.1/imagex/__init__.py +0 -1
  11. {imagex-0.2.1 → imagex-0.2.2}/LICENSE +0 -0
  12. {imagex-0.2.1 → imagex-0.2.2}/imagex/__main__.py +0 -0
  13. {imagex-0.2.1 → imagex-0.2.2}/imagex/config.py +0 -0
  14. {imagex-0.2.1 → imagex-0.2.2}/imagex/features/__init__.py +0 -0
  15. {imagex-0.2.1 → imagex-0.2.2}/imagex/features/add_noise.py +0 -0
  16. {imagex-0.2.1 → imagex-0.2.2}/imagex/features/compress.py +0 -0
  17. {imagex-0.2.1 → imagex-0.2.2}/imagex/features/convert.py +0 -0
  18. {imagex-0.2.1 → imagex-0.2.2}/imagex/features/remove_metadata.py +0 -0
  19. {imagex-0.2.1 → imagex-0.2.2}/imagex/features/rename_batch.py +0 -0
  20. {imagex-0.2.1 → imagex-0.2.2}/imagex/features/resize.py +0 -0
  21. {imagex-0.2.1 → imagex-0.2.2}/imagex/features/rotate.py +0 -0
  22. {imagex-0.2.1 → imagex-0.2.2}/imagex/features/watermark.py +0 -0
  23. {imagex-0.2.1 → imagex-0.2.2}/imagex/utils/__init__.py +0 -0
  24. {imagex-0.2.1 → imagex-0.2.2}/imagex/utils/file_ops.py +0 -0
  25. {imagex-0.2.1 → imagex-0.2.2}/imagex/utils/progress.py +0 -0
  26. {imagex-0.2.1 → imagex-0.2.2}/imagex.egg-info/dependency_links.txt +0 -0
  27. {imagex-0.2.1 → imagex-0.2.2}/imagex.egg-info/entry_points.txt +0 -0
  28. {imagex-0.2.1 → imagex-0.2.2}/imagex.egg-info/requires.txt +0 -0
  29. {imagex-0.2.1 → imagex-0.2.2}/imagex.egg-info/top_level.txt +0 -0
  30. {imagex-0.2.1 → imagex-0.2.2}/setup.cfg +0 -0
  31. {imagex-0.2.1 → imagex-0.2.2}/tests/test_add_noise.py +0 -0
  32. {imagex-0.2.1 → imagex-0.2.2}/tests/test_cli.py +0 -0
  33. {imagex-0.2.1 → imagex-0.2.2}/tests/test_compress.py +0 -0
  34. {imagex-0.2.1 → imagex-0.2.2}/tests/test_convert.py +0 -0
  35. {imagex-0.2.1 → imagex-0.2.2}/tests/test_file_ops.py +0 -0
  36. {imagex-0.2.1 → imagex-0.2.2}/tests/test_remove_metadata.py +0 -0
  37. {imagex-0.2.1 → imagex-0.2.2}/tests/test_rename_batch.py +0 -0
  38. {imagex-0.2.1 → imagex-0.2.2}/tests/test_resize.py +0 -0
  39. {imagex-0.2.1 → imagex-0.2.2}/tests/test_rotate.py +0 -0
  40. {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.1
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.1
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "imagex"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "All-in-one image processing CLI — resize, convert, watermark, compress, and more"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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()
@@ -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