imagex 0.2.0__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.0 → imagex-0.2.2}/PKG-INFO +40 -6
  2. {imagex-0.2.0 → imagex-0.2.2}/README.md +3 -2
  3. imagex-0.2.2/imagex/__init__.py +1 -0
  4. {imagex-0.2.0 → imagex-0.2.2}/imagex/cli.py +20 -0
  5. {imagex-0.2.0 → imagex-0.2.2}/imagex/features/add_noise.py +6 -1
  6. {imagex-0.2.0 → imagex-0.2.2}/imagex/features/compress.py +20 -13
  7. {imagex-0.2.0 → imagex-0.2.2}/imagex/features/convert.py +15 -10
  8. imagex-0.2.2/imagex/features/flip.py +38 -0
  9. {imagex-0.2.0 → imagex-0.2.2}/imagex/features/resize.py +6 -1
  10. {imagex-0.2.0 → imagex-0.2.2}/imagex/features/rotate.py +6 -1
  11. {imagex-0.2.0 → imagex-0.2.2}/imagex/features/watermark.py +8 -2
  12. {imagex-0.2.0 → imagex-0.2.2}/imagex.egg-info/PKG-INFO +40 -6
  13. {imagex-0.2.0 → imagex-0.2.2}/imagex.egg-info/SOURCES.txt +2 -0
  14. {imagex-0.2.0 → imagex-0.2.2}/pyproject.toml +29 -5
  15. imagex-0.2.2/tests/test_flip.py +72 -0
  16. imagex-0.2.0/imagex/__init__.py +0 -1
  17. {imagex-0.2.0 → imagex-0.2.2}/LICENSE +0 -0
  18. {imagex-0.2.0 → imagex-0.2.2}/imagex/__main__.py +0 -0
  19. {imagex-0.2.0 → imagex-0.2.2}/imagex/config.py +0 -0
  20. {imagex-0.2.0 → imagex-0.2.2}/imagex/features/__init__.py +0 -0
  21. {imagex-0.2.0 → imagex-0.2.2}/imagex/features/remove_metadata.py +0 -0
  22. {imagex-0.2.0 → imagex-0.2.2}/imagex/features/rename_batch.py +0 -0
  23. {imagex-0.2.0 → imagex-0.2.2}/imagex/utils/__init__.py +0 -0
  24. {imagex-0.2.0 → imagex-0.2.2}/imagex/utils/file_ops.py +0 -0
  25. {imagex-0.2.0 → imagex-0.2.2}/imagex/utils/progress.py +0 -0
  26. {imagex-0.2.0 → imagex-0.2.2}/imagex.egg-info/dependency_links.txt +0 -0
  27. {imagex-0.2.0 → imagex-0.2.2}/imagex.egg-info/entry_points.txt +0 -0
  28. {imagex-0.2.0 → imagex-0.2.2}/imagex.egg-info/requires.txt +0 -0
  29. {imagex-0.2.0 → imagex-0.2.2}/imagex.egg-info/top_level.txt +0 -0
  30. {imagex-0.2.0 → imagex-0.2.2}/setup.cfg +0 -0
  31. {imagex-0.2.0 → imagex-0.2.2}/tests/test_add_noise.py +0 -0
  32. {imagex-0.2.0 → imagex-0.2.2}/tests/test_cli.py +0 -0
  33. {imagex-0.2.0 → imagex-0.2.2}/tests/test_compress.py +0 -0
  34. {imagex-0.2.0 → imagex-0.2.2}/tests/test_convert.py +0 -0
  35. {imagex-0.2.0 → imagex-0.2.2}/tests/test_file_ops.py +0 -0
  36. {imagex-0.2.0 → imagex-0.2.2}/tests/test_remove_metadata.py +0 -0
  37. {imagex-0.2.0 → imagex-0.2.2}/tests/test_rename_batch.py +0 -0
  38. {imagex-0.2.0 → imagex-0.2.2}/tests/test_resize.py +0 -0
  39. {imagex-0.2.0 → imagex-0.2.2}/tests/test_rotate.py +0 -0
  40. {imagex-0.2.0 → imagex-0.2.2}/tests/test_watermark.py +0 -0
@@ -1,16 +1,49 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imagex
3
- Version: 0.2.0
4
- Summary: Image processing CLI tool - right in your terminal
5
- Author: kushal1o1
6
- License: MIT
3
+ Version: 0.2.2
4
+ Summary: All-in-one image processing CLI resize, convert, watermark, compress, and more
5
+ Author-email: kushal1o1 <kushal1o1@users.noreply.github.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Kushal Baral
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/kushal1o1/ImageX
29
+ Project-URL: Repository, https://github.com/kushal1o1/ImageX
30
+ Project-URL: Documentation, https://github.com/kushal1o1/ImageX/blob/main/OPERATIONS.md
31
+ Project-URL: Changelog, https://github.com/kushal1o1/ImageX/releases
32
+ Project-URL: Issues, https://github.com/kushal1o1/ImageX/issues
33
+ Keywords: image-processing,cli,pillow,resize,convert,watermark,compress,metadata,batch,rename
7
34
  Classifier: Development Status :: 3 - Alpha
8
35
  Classifier: Environment :: Console
36
+ Classifier: Intended Audience :: End Users/Desktop
37
+ Classifier: License :: OSI Approved :: MIT License
38
+ Classifier: Natural Language :: English
9
39
  Classifier: Operating System :: OS Independent
10
40
  Classifier: Programming Language :: Python :: 3
11
41
  Classifier: Programming Language :: Python :: 3.10
12
42
  Classifier: Programming Language :: Python :: 3.11
13
43
  Classifier: Programming Language :: Python :: 3.12
44
+ Classifier: Topic :: Multimedia :: Graphics
45
+ Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
46
+ Classifier: Topic :: Utilities
14
47
  Requires-Python: >=3.10
15
48
  Description-Content-Type: text/markdown
16
49
  License-File: LICENSE
@@ -48,6 +81,7 @@ Navigate to any folder with images and run `imagex`.
48
81
  | Feature | Description |
49
82
  |---|---|
50
83
  | Rotate | Rotate 90° Left, 90° Right, or 180° |
84
+ | Flip | Mirror horizontally or vertically |
51
85
  | Remove Metadata | Strip EXIF/XMP/IPTC (incl. AI generation markers) |
52
86
  | Convert Format | JPG ↔ PNG ↔ WEBP ↔ TIFF ↔ BMP ↔ GIF ↔ HEIC |
53
87
  | Compress / Optimize | Reduce file size with quality slider |
@@ -56,7 +90,7 @@ Navigate to any folder with images and run `imagex`.
56
90
  | Add Noise | Gaussian or salt & pepper (bypass AI detection) |
57
91
  | Watermark | Add text/image or remove existing |
58
92
 
59
- Full details in [OPERATIONS.md](OPERATIONS.md).
93
+ Full details in [OPERATIONS.md](https://github.com/kushal1o1/ImageX/blob/main/OPERATIONS.md).
60
94
 
61
95
  ## Install
62
96
 
@@ -72,7 +106,7 @@ Then run `imagex` from any folder.
72
106
 
73
107
  ## Adding Features
74
108
 
75
- See [CONTRIBUTION.md](CONTRIBUTION.md) for the contribution guide.
109
+ See [CONTRIBUTION.md](https://github.com/kushal1o1/ImageX/blob/main/CONTRIBUTION.md) for the contribution guide.
76
110
 
77
111
  ## License
78
112
 
@@ -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 |
@@ -29,7 +30,7 @@ Navigate to any folder with images and run `imagex`.
29
30
  | Add Noise | Gaussian or salt & pepper (bypass AI detection) |
30
31
  | Watermark | Add text/image or remove existing |
31
32
 
32
- Full details in [OPERATIONS.md](OPERATIONS.md).
33
+ Full details in [OPERATIONS.md](https://github.com/kushal1o1/ImageX/blob/main/OPERATIONS.md).
33
34
 
34
35
  ## Install
35
36
 
@@ -45,7 +46,7 @@ Then run `imagex` from any folder.
45
46
 
46
47
  ## Adding Features
47
48
 
48
- See [CONTRIBUTION.md](CONTRIBUTION.md) for the contribution guide.
49
+ See [CONTRIBUTION.md](https://github.com/kushal1o1/ImageX/blob/main/CONTRIBUTION.md) for the contribution guide.
49
50
 
50
51
  ## License
51
52
 
@@ -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]")
@@ -44,7 +44,12 @@ def run(file: Path, output_path: Path, args: Optional[dict[str, Any]] = None) ->
44
44
  else:
45
45
  _add_salt_pepper(img, intensity)
46
46
 
47
- img.save(str(output_path), format=img.format or "JPEG")
47
+ kw = {"format": img.format or "JPEG"}
48
+ if exif := img.info.get("exif"):
49
+ kw["exif"] = exif
50
+ if icc := img.info.get("icc_profile"):
51
+ kw["icc_profile"] = icc
52
+ img.save(str(output_path), **kw)
48
53
  return True
49
54
 
50
55
 
@@ -28,17 +28,22 @@ def run(file: Path, output_path: Path, args: Optional[dict[str, Any]] = None) ->
28
28
 
29
29
  quality = args.get("quality", 80)
30
30
  img = Image.open(file)
31
+ meta = {}
32
+ if exif := img.info.get("exif"):
33
+ meta["exif"] = exif
34
+ if icc := img.info.get("icc_profile"):
35
+ meta["icc_profile"] = icc
31
36
  fmt = img.format
32
37
  original_size = file.stat().st_size
33
38
 
34
39
  if fmt == "JPEG":
35
- _compress_jpeg(img, output_path, quality)
40
+ _compress_jpeg(img, output_path, quality, meta)
36
41
  elif fmt == "PNG":
37
- _compress_png(img, output_path, quality)
42
+ _compress_png(img, output_path, quality, meta)
38
43
  elif fmt == "WEBP":
39
- _compress_webp(img, output_path, quality)
44
+ _compress_webp(img, output_path, quality, meta)
40
45
  else:
41
- _compress_other(img, output_path, fmt, quality)
46
+ _compress_other(img, output_path, fmt, quality, meta)
42
47
 
43
48
  new_size = output_path.stat().st_size
44
49
  saved = original_size - new_size
@@ -51,17 +56,17 @@ def run(file: Path, output_path: Path, args: Optional[dict[str, Any]] = None) ->
51
56
  return True
52
57
 
53
58
 
54
- def _compress_jpeg(img: Image.Image, output_path: Path, quality: int):
59
+ def _compress_jpeg(img: Image.Image, output_path: Path, quality: int, meta: dict):
55
60
  if img.mode == "RGBA":
56
61
  bg = Image.new("RGB", img.size, (255, 255, 255))
57
62
  bg.paste(img, mask=img.split()[3])
58
63
  img = bg
59
64
  elif img.mode != "RGB":
60
65
  img = img.convert("RGB")
61
- img.save(str(output_path), format="JPEG", quality=quality, optimize=True)
66
+ img.save(str(output_path), format="JPEG", quality=quality, optimize=True, **meta)
62
67
 
63
68
 
64
- def _compress_png(img: Image.Image, output_path: Path, quality: int):
69
+ def _compress_png(img: Image.Image, output_path: Path, quality: int, meta: dict):
65
70
  if quality < 50:
66
71
  if img.mode == "RGBA":
67
72
  bg = Image.new("RGB", img.size, (255, 255, 255))
@@ -71,17 +76,19 @@ def _compress_png(img: Image.Image, output_path: Path, quality: int):
71
76
  img = img.convert("RGB")
72
77
  colors = max(quality * 2, 16)
73
78
  img = img.quantize(colors=colors, method=Image.Quantize.MEDIANCUT)
74
- img.save(str(output_path), format="PNG", optimize=True)
79
+ img.save(str(output_path), format="PNG", optimize=True, **meta)
75
80
  else:
76
- img.save(str(output_path), format="PNG", optimize=True)
81
+ img.save(str(output_path), format="PNG", optimize=True, **meta)
77
82
 
78
83
 
79
- def _compress_webp(img: Image.Image, output_path: Path, quality: int):
80
- img.save(str(output_path), format="WEBP", quality=quality)
84
+ def _compress_webp(img: Image.Image, output_path: Path, quality: int, meta: dict):
85
+ img.save(str(output_path), format="WEBP", quality=quality, **meta)
81
86
 
82
87
 
83
- def _compress_other(img: Image.Image, output_path: Path, fmt: str, quality: int):
84
- params = {"format": fmt}
88
+ def _compress_other(
89
+ img: Image.Image, output_path: Path, fmt: str, quality: int, meta: dict,
90
+ ):
91
+ params = {"format": fmt, **meta}
85
92
  if fmt == "GIF":
86
93
  params["save_all"] = True
87
94
  img.save(str(output_path), **params)
@@ -48,16 +48,21 @@ def run(file: Path, output_path: Path, args: Optional[dict[str, Any]] = None) ->
48
48
  target_fmt = args["target_format"]
49
49
 
50
50
  img = Image.open(file)
51
+ meta = {}
52
+ if exif := img.info.get("exif"):
53
+ meta["exif"] = exif
54
+ if icc := img.info.get("icc_profile"):
55
+ meta["icc_profile"] = icc
51
56
  actual_output = output_path.with_suffix(args["target_ext"])
52
57
 
53
58
  if target_fmt == "JPEG":
54
- _save_as_jpeg(img, actual_output)
59
+ _save_as_jpeg(img, actual_output, meta)
55
60
  elif target_fmt == "PNG":
56
- _save_as_png(img, actual_output)
61
+ _save_as_png(img, actual_output, meta)
57
62
  elif target_fmt == "WEBP":
58
- _save_as_webp(img, actual_output)
63
+ _save_as_webp(img, actual_output, meta)
59
64
  elif target_fmt == "TIFF":
60
- img.save(str(actual_output), format="TIFF")
65
+ img.save(str(actual_output), format="TIFF", **meta)
61
66
  elif target_fmt == "BMP":
62
67
  img.save(str(actual_output), format="BMP")
63
68
  elif target_fmt == "GIF":
@@ -68,22 +73,22 @@ def run(file: Path, output_path: Path, args: Optional[dict[str, Any]] = None) ->
68
73
  return True
69
74
 
70
75
 
71
- def _save_as_jpeg(img: Image.Image, output_path: Path):
76
+ def _save_as_jpeg(img: Image.Image, output_path: Path, meta: dict):
72
77
  if img.mode == "RGBA":
73
78
  bg = Image.new("RGB", img.size, (255, 255, 255))
74
79
  bg.paste(img, mask=img.split()[3])
75
80
  img = bg
76
81
  elif img.mode != "RGB":
77
82
  img = img.convert("RGB")
78
- img.save(str(output_path), format="JPEG")
83
+ img.save(str(output_path), format="JPEG", **meta)
79
84
 
80
85
 
81
- def _save_as_png(img: Image.Image, output_path: Path):
82
- img.save(str(output_path), format="PNG")
86
+ def _save_as_png(img: Image.Image, output_path: Path, meta: dict):
87
+ img.save(str(output_path), format="PNG", **meta)
83
88
 
84
89
 
85
- def _save_as_webp(img: Image.Image, output_path: Path):
86
- img.save(str(output_path), format="WEBP")
90
+ def _save_as_webp(img: Image.Image, output_path: Path, meta: dict):
91
+ img.save(str(output_path), format="WEBP", **meta)
87
92
 
88
93
 
89
94
  def _save_as_heic(img: Image.Image, output_path: Path):
@@ -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
@@ -78,7 +78,12 @@ def run(file: Path, output_path: Path, args: Optional[dict[str, Any]] = None) ->
78
78
  raise ValueError(msg)
79
79
 
80
80
  resized = img.resize((new_w, new_h), Image.LANCZOS)
81
- resized.save(str(output_path), format=img.format or "JPEG")
81
+ kw = {"format": img.format or "JPEG"}
82
+ if exif := img.info.get("exif"):
83
+ kw["exif"] = exif
84
+ if icc := img.info.get("icc_profile"):
85
+ kw["icc_profile"] = icc
86
+ resized.save(str(output_path), **kw)
82
87
 
83
88
  return True
84
89
 
@@ -30,5 +30,10 @@ def run(file: Path, output_path: Path, args: Optional[dict[str, Any]] = None) ->
30
30
 
31
31
  img = Image.open(file)
32
32
  rotated = img.transpose(args["method"])
33
- rotated.save(str(output_path), format=img.format or "JPEG")
33
+ kw = {"format": img.format or "JPEG"}
34
+ if exif := img.info.get("exif"):
35
+ kw["exif"] = exif
36
+ if icc := img.info.get("icc_profile"):
37
+ kw["icc_profile"] = icc
38
+ rotated.save(str(output_path), **kw)
34
39
  return True
@@ -123,7 +123,13 @@ def run(file: Path, output_path: Path, args: Optional[dict[str, Any]] = None) ->
123
123
  msg = "args required for watermark"
124
124
  raise ValueError(msg)
125
125
 
126
- img = Image.open(file).convert("RGBA")
126
+ src = Image.open(file)
127
+ meta = {}
128
+ if exif := src.info.get("exif"):
129
+ meta["exif"] = exif
130
+ if icc := src.info.get("icc_profile"):
131
+ meta["icc_profile"] = icc
132
+ img = src.convert("RGBA")
127
133
  action = args["action"]
128
134
 
129
135
  if action == "add":
@@ -135,7 +141,7 @@ def run(file: Path, output_path: Path, args: Optional[dict[str, Any]] = None) ->
135
141
  raise ValueError(msg)
136
142
 
137
143
  out = img.convert("RGB") if img.mode == "RGBA" else img
138
- out.save(str(output_path))
144
+ out.save(str(output_path), **meta)
139
145
  return True
140
146
 
141
147
 
@@ -1,16 +1,49 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imagex
3
- Version: 0.2.0
4
- Summary: Image processing CLI tool - right in your terminal
5
- Author: kushal1o1
6
- License: MIT
3
+ Version: 0.2.2
4
+ Summary: All-in-one image processing CLI resize, convert, watermark, compress, and more
5
+ Author-email: kushal1o1 <kushal1o1@users.noreply.github.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Kushal Baral
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/kushal1o1/ImageX
29
+ Project-URL: Repository, https://github.com/kushal1o1/ImageX
30
+ Project-URL: Documentation, https://github.com/kushal1o1/ImageX/blob/main/OPERATIONS.md
31
+ Project-URL: Changelog, https://github.com/kushal1o1/ImageX/releases
32
+ Project-URL: Issues, https://github.com/kushal1o1/ImageX/issues
33
+ Keywords: image-processing,cli,pillow,resize,convert,watermark,compress,metadata,batch,rename
7
34
  Classifier: Development Status :: 3 - Alpha
8
35
  Classifier: Environment :: Console
36
+ Classifier: Intended Audience :: End Users/Desktop
37
+ Classifier: License :: OSI Approved :: MIT License
38
+ Classifier: Natural Language :: English
9
39
  Classifier: Operating System :: OS Independent
10
40
  Classifier: Programming Language :: Python :: 3
11
41
  Classifier: Programming Language :: Python :: 3.10
12
42
  Classifier: Programming Language :: Python :: 3.11
13
43
  Classifier: Programming Language :: Python :: 3.12
44
+ Classifier: Topic :: Multimedia :: Graphics
45
+ Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
46
+ Classifier: Topic :: Utilities
14
47
  Requires-Python: >=3.10
15
48
  Description-Content-Type: text/markdown
16
49
  License-File: LICENSE
@@ -48,6 +81,7 @@ Navigate to any folder with images and run `imagex`.
48
81
  | Feature | Description |
49
82
  |---|---|
50
83
  | Rotate | Rotate 90° Left, 90° Right, or 180° |
84
+ | Flip | Mirror horizontally or vertically |
51
85
  | Remove Metadata | Strip EXIF/XMP/IPTC (incl. AI generation markers) |
52
86
  | Convert Format | JPG ↔ PNG ↔ WEBP ↔ TIFF ↔ BMP ↔ GIF ↔ HEIC |
53
87
  | Compress / Optimize | Reduce file size with quality slider |
@@ -56,7 +90,7 @@ Navigate to any folder with images and run `imagex`.
56
90
  | Add Noise | Gaussian or salt & pepper (bypass AI detection) |
57
91
  | Watermark | Add text/image or remove existing |
58
92
 
59
- Full details in [OPERATIONS.md](OPERATIONS.md).
93
+ Full details in [OPERATIONS.md](https://github.com/kushal1o1/ImageX/blob/main/OPERATIONS.md).
60
94
 
61
95
  ## Install
62
96
 
@@ -72,7 +106,7 @@ Then run `imagex` from any folder.
72
106
 
73
107
  ## Adding Features
74
108
 
75
- See [CONTRIBUTION.md](CONTRIBUTION.md) for the contribution guide.
109
+ See [CONTRIBUTION.md](https://github.com/kushal1o1/ImageX/blob/main/CONTRIBUTION.md) for the contribution guide.
76
110
 
77
111
  ## License
78
112
 
@@ -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,23 +4,40 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "imagex"
7
- version = "0.2.0"
8
- description = "Image processing CLI tool - right in your terminal"
7
+ version = "0.2.2"
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"
11
- license = {text = "MIT"}
11
+ license = {file = "LICENSE"}
12
12
  authors = [
13
- {name = "kushal1o1"},
13
+ {name = "kushal1o1", email = "kushal1o1@users.noreply.github.com"},
14
+ ]
15
+ keywords = [
16
+ "image-processing",
17
+ "cli",
18
+ "pillow",
19
+ "resize",
20
+ "convert",
21
+ "watermark",
22
+ "compress",
23
+ "metadata",
24
+ "batch",
25
+ "rename",
14
26
  ]
15
27
  classifiers = [
16
28
  "Development Status :: 3 - Alpha",
17
29
  "Environment :: Console",
30
+ "Intended Audience :: End Users/Desktop",
31
+ "License :: OSI Approved :: MIT License",
32
+ "Natural Language :: English",
18
33
  "Operating System :: OS Independent",
19
34
  "Programming Language :: Python :: 3",
20
-
21
35
  "Programming Language :: Python :: 3.10",
22
36
  "Programming Language :: Python :: 3.11",
23
37
  "Programming Language :: Python :: 3.12",
38
+ "Topic :: Multimedia :: Graphics",
39
+ "Topic :: Multimedia :: Graphics :: Graphics Conversion",
40
+ "Topic :: Utilities",
24
41
  ]
25
42
  dependencies = [
26
43
  "rich>=13.0",
@@ -28,6 +45,13 @@ dependencies = [
28
45
  "Pillow>=10.0",
29
46
  ]
30
47
 
48
+ [project.urls]
49
+ Homepage = "https://github.com/kushal1o1/ImageX"
50
+ Repository = "https://github.com/kushal1o1/ImageX"
51
+ Documentation = "https://github.com/kushal1o1/ImageX/blob/main/OPERATIONS.md"
52
+ Changelog = "https://github.com/kushal1o1/ImageX/releases"
53
+ Issues = "https://github.com/kushal1o1/ImageX/issues"
54
+
31
55
  [project.optional-dependencies]
32
56
  heif = [
33
57
  "pillow-heif>=0.16",
@@ -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.0"
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