imagex 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.
Files changed (44) hide show
  1. {imagex-0.2.2 → imagex-0.3.0}/PKG-INFO +3 -1
  2. {imagex-0.2.2 → imagex-0.3.0}/README.md +2 -0
  3. imagex-0.3.0/imagex/__init__.py +1 -0
  4. imagex-0.3.0/imagex/features/grayscale.py +57 -0
  5. imagex-0.3.0/imagex/features/invert.py +30 -0
  6. {imagex-0.2.2 → imagex-0.3.0}/imagex.egg-info/PKG-INFO +3 -1
  7. {imagex-0.2.2 → imagex-0.3.0}/imagex.egg-info/SOURCES.txt +4 -0
  8. {imagex-0.2.2 → imagex-0.3.0}/pyproject.toml +1 -1
  9. imagex-0.3.0/tests/test_grayscale.py +53 -0
  10. imagex-0.3.0/tests/test_invert.py +57 -0
  11. imagex-0.2.2/imagex/__init__.py +0 -1
  12. {imagex-0.2.2 → imagex-0.3.0}/LICENSE +0 -0
  13. {imagex-0.2.2 → imagex-0.3.0}/imagex/__main__.py +0 -0
  14. {imagex-0.2.2 → imagex-0.3.0}/imagex/cli.py +0 -0
  15. {imagex-0.2.2 → imagex-0.3.0}/imagex/config.py +0 -0
  16. {imagex-0.2.2 → imagex-0.3.0}/imagex/features/__init__.py +0 -0
  17. {imagex-0.2.2 → imagex-0.3.0}/imagex/features/add_noise.py +0 -0
  18. {imagex-0.2.2 → imagex-0.3.0}/imagex/features/compress.py +0 -0
  19. {imagex-0.2.2 → imagex-0.3.0}/imagex/features/convert.py +0 -0
  20. {imagex-0.2.2 → imagex-0.3.0}/imagex/features/flip.py +0 -0
  21. {imagex-0.2.2 → imagex-0.3.0}/imagex/features/remove_metadata.py +0 -0
  22. {imagex-0.2.2 → imagex-0.3.0}/imagex/features/rename_batch.py +0 -0
  23. {imagex-0.2.2 → imagex-0.3.0}/imagex/features/resize.py +0 -0
  24. {imagex-0.2.2 → imagex-0.3.0}/imagex/features/rotate.py +0 -0
  25. {imagex-0.2.2 → imagex-0.3.0}/imagex/features/watermark.py +0 -0
  26. {imagex-0.2.2 → imagex-0.3.0}/imagex/utils/__init__.py +0 -0
  27. {imagex-0.2.2 → imagex-0.3.0}/imagex/utils/file_ops.py +0 -0
  28. {imagex-0.2.2 → imagex-0.3.0}/imagex/utils/progress.py +0 -0
  29. {imagex-0.2.2 → imagex-0.3.0}/imagex.egg-info/dependency_links.txt +0 -0
  30. {imagex-0.2.2 → imagex-0.3.0}/imagex.egg-info/entry_points.txt +0 -0
  31. {imagex-0.2.2 → imagex-0.3.0}/imagex.egg-info/requires.txt +0 -0
  32. {imagex-0.2.2 → imagex-0.3.0}/imagex.egg-info/top_level.txt +0 -0
  33. {imagex-0.2.2 → imagex-0.3.0}/setup.cfg +0 -0
  34. {imagex-0.2.2 → imagex-0.3.0}/tests/test_add_noise.py +0 -0
  35. {imagex-0.2.2 → imagex-0.3.0}/tests/test_cli.py +0 -0
  36. {imagex-0.2.2 → imagex-0.3.0}/tests/test_compress.py +0 -0
  37. {imagex-0.2.2 → imagex-0.3.0}/tests/test_convert.py +0 -0
  38. {imagex-0.2.2 → imagex-0.3.0}/tests/test_file_ops.py +0 -0
  39. {imagex-0.2.2 → imagex-0.3.0}/tests/test_flip.py +0 -0
  40. {imagex-0.2.2 → imagex-0.3.0}/tests/test_remove_metadata.py +0 -0
  41. {imagex-0.2.2 → imagex-0.3.0}/tests/test_rename_batch.py +0 -0
  42. {imagex-0.2.2 → imagex-0.3.0}/tests/test_resize.py +0 -0
  43. {imagex-0.2.2 → imagex-0.3.0}/tests/test_rotate.py +0 -0
  44. {imagex-0.2.2 → imagex-0.3.0}/tests/test_watermark.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imagex
3
- Version: 0.2.2
3
+ Version: 0.3.0
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
@@ -89,6 +89,8 @@ Navigate to any folder with images and run `imagex`.
89
89
  | Rename Batch | Pattern-based renaming (%n, %o) |
90
90
  | Add Noise | Gaussian or salt & pepper (bypass AI detection) |
91
91
  | Watermark | Add text/image or remove existing |
92
+ | Grayscale / B&W | Convert to grayscale or true black & white |
93
+ | Invert Colors | Negative effect , invert all colors |
92
94
 
93
95
  Full details in [OPERATIONS.md](https://github.com/kushal1o1/ImageX/blob/main/OPERATIONS.md).
94
96
 
@@ -29,6 +29,8 @@ Navigate to any folder with images and run `imagex`.
29
29
  | Rename Batch | Pattern-based renaming (%n, %o) |
30
30
  | Add Noise | Gaussian or salt & pepper (bypass AI detection) |
31
31
  | Watermark | Add text/image or remove existing |
32
+ | Grayscale / B&W | Convert to grayscale or true black & white |
33
+ | Invert Colors | Negative effect , invert all colors |
32
34
 
33
35
  Full details in [OPERATIONS.md](https://github.com/kushal1o1/ImageX/blob/main/OPERATIONS.md).
34
36
 
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -0,0 +1,57 @@
1
+ from pathlib import Path
2
+ from typing import Any, Optional
3
+
4
+ import questionary
5
+ from PIL import Image
6
+
7
+ NAME = "Grayscale / B&W"
8
+ DESCRIPTION = "Convert images to grayscale or true black & white"
9
+
10
+ MODES = {
11
+ "Grayscale (luminosity)": "grayscale",
12
+ "True Black & White (threshold)": "bw",
13
+ }
14
+
15
+
16
+ def ask_args(files: list[Path]) -> dict[str, Any]:
17
+ mode_label = questionary.select(
18
+ "Mode:",
19
+ choices=list(MODES.keys()),
20
+ ).ask()
21
+
22
+ args = {"mode": MODES[mode_label]}
23
+
24
+ if MODES[mode_label] == "bw":
25
+ threshold = questionary.text(
26
+ "Threshold (0-255, lower = more black):",
27
+ default="128",
28
+ validate=lambda v: v.isdigit() and 0 <= int(v) <= 255 or "Enter 0-255",
29
+ ).ask()
30
+ args["threshold"] = int(threshold)
31
+
32
+ return args
33
+
34
+
35
+ def run(file: Path, output_path: Path, args: Optional[dict[str, Any]] = None) -> bool:
36
+ if args is None:
37
+ msg = "args required for grayscale"
38
+ raise ValueError(msg)
39
+
40
+ img = Image.open(file)
41
+ kw = {"format": img.format or "JPEG"}
42
+ if exif := img.info.get("exif"):
43
+ kw["exif"] = exif
44
+ if icc := img.info.get("icc_profile"):
45
+ kw["icc_profile"] = icc
46
+
47
+ gray = img.convert("L")
48
+
49
+ if args["mode"] == "grayscale":
50
+ gray.save(str(output_path), **kw)
51
+ else:
52
+ threshold = args.get("threshold", 128)
53
+ bw = gray.point(lambda p: 255 if p > threshold else 0, mode="1")
54
+ bw = bw.convert("L")
55
+ bw.save(str(output_path), **kw)
56
+
57
+ return True
@@ -0,0 +1,30 @@
1
+ from pathlib import Path
2
+ from typing import Any, Optional
3
+
4
+ from PIL import Image, ImageOps
5
+
6
+ NAME = "Invert Colors"
7
+ DESCRIPTION = "Invert image colors (negative effect)"
8
+
9
+
10
+ def run(file: Path, output_path: Path, args: Optional[dict[str, Any]] = None) -> bool:
11
+ img = Image.open(file)
12
+ kw = {"format": img.format or "JPEG"}
13
+ if exif := img.info.get("exif"):
14
+ kw["exif"] = exif
15
+ if icc := img.info.get("icc_profile"):
16
+ kw["icc_profile"] = icc
17
+
18
+ if img.mode == "RGBA":
19
+ r, g, b, a = img.split()
20
+ rgb = Image.merge("RGB", (r, g, b))
21
+ inverted = ImageOps.invert(rgb)
22
+ r2, g2, b2 = inverted.split()
23
+ img = Image.merge("RGBA", (r2, g2, b2, a))
24
+ else:
25
+ if img.mode != "RGB":
26
+ img = img.convert("RGB")
27
+ img = ImageOps.invert(img)
28
+
29
+ img.save(str(output_path), **kw)
30
+ return True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imagex
3
- Version: 0.2.2
3
+ Version: 0.3.0
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
@@ -89,6 +89,8 @@ Navigate to any folder with images and run `imagex`.
89
89
  | Rename Batch | Pattern-based renaming (%n, %o) |
90
90
  | Add Noise | Gaussian or salt & pepper (bypass AI detection) |
91
91
  | Watermark | Add text/image or remove existing |
92
+ | Grayscale / B&W | Convert to grayscale or true black & white |
93
+ | Invert Colors | Negative effect , invert all colors |
92
94
 
93
95
  Full details in [OPERATIONS.md](https://github.com/kushal1o1/ImageX/blob/main/OPERATIONS.md).
94
96
 
@@ -16,6 +16,8 @@ imagex/features/add_noise.py
16
16
  imagex/features/compress.py
17
17
  imagex/features/convert.py
18
18
  imagex/features/flip.py
19
+ imagex/features/grayscale.py
20
+ imagex/features/invert.py
19
21
  imagex/features/remove_metadata.py
20
22
  imagex/features/rename_batch.py
21
23
  imagex/features/resize.py
@@ -30,6 +32,8 @@ tests/test_compress.py
30
32
  tests/test_convert.py
31
33
  tests/test_file_ops.py
32
34
  tests/test_flip.py
35
+ tests/test_grayscale.py
36
+ tests/test_invert.py
33
37
  tests/test_remove_metadata.py
34
38
  tests/test_rename_batch.py
35
39
  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.2"
7
+ version = "0.3.0"
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,53 @@
1
+ from PIL import Image
2
+
3
+ from imagex.features.grayscale import run
4
+
5
+
6
+ class TestGrayscaleRun:
7
+ def test_grayscale_converts_mode(self, tmp_images, tmp_path):
8
+ src = tmp_images / "test.jpg"
9
+ out = tmp_path / "gray.jpg"
10
+ assert run(src, out, {"mode": "grayscale"})
11
+
12
+ result = Image.open(out)
13
+ assert result.mode == "L"
14
+
15
+ def test_grayscale_preserves_size(self, tmp_images, tmp_path):
16
+ src = tmp_images / "test.jpg"
17
+ img = Image.open(src)
18
+ out = tmp_path / "gray.png"
19
+ assert run(src, out, {"mode": "grayscale"})
20
+
21
+ result = Image.open(out)
22
+ assert result.width == img.width
23
+ assert result.height == img.height
24
+
25
+ def test_bw_threshold_high(self, tmp_images, tmp_path):
26
+ src = tmp_images / "test.jpg"
27
+ out = tmp_path / "white.png"
28
+ assert run(src, out, {"mode": "bw", "threshold": 255})
29
+
30
+ result = Image.open(out)
31
+ assert result.mode == "L"
32
+ # threshold 255 means everything below 255 becomes black
33
+ # our red image (255,0,0) -> L ~ 76 -> 0 = black
34
+ assert result.getpixel((0, 0)) == 0
35
+
36
+ def test_bw_threshold_low(self, tmp_images, tmp_path):
37
+ src = tmp_images / "test.jpg"
38
+ out = tmp_path / "black.png"
39
+ assert run(src, out, {"mode": "bw", "threshold": 0})
40
+
41
+ result = Image.open(out)
42
+ # threshold 0 means only pure black stays black
43
+ # our red image -> L ~ 76 -> 255 = white
44
+ assert result.getpixel((0, 0)) == 255
45
+
46
+ def test_run_without_args_raises(self, tmp_images, tmp_path):
47
+ src = tmp_images / "test.jpg"
48
+ out = tmp_path / "out.jpg"
49
+ try:
50
+ run(src, out, None)
51
+ assert False, "should have raised"
52
+ except ValueError as e:
53
+ assert "args required" in str(e).lower()
@@ -0,0 +1,57 @@
1
+ from PIL import Image
2
+
3
+ from imagex.features.invert import run
4
+
5
+
6
+ class TestInvertRun:
7
+ def test_invert_rgb_changes_pixels(self, tmp_images, tmp_path):
8
+ src = tmp_images / "test.jpg"
9
+ img = Image.open(src)
10
+ original_pixel = img.getpixel((0, 0))
11
+ out = tmp_path / "inverted.png"
12
+ assert run(src, out, {})
13
+
14
+ result = Image.open(out)
15
+ inverted = result.getpixel((0, 0))
16
+ # red (255,0,0) -> cyan (0,255,255)
17
+ expected = tuple(255 - v for v in original_pixel)
18
+ assert inverted == expected
19
+
20
+ def test_invert_preserves_size(self, tmp_images, tmp_path):
21
+ src = tmp_images / "test.jpg"
22
+ img = Image.open(src)
23
+ out = tmp_path / "inv.png"
24
+ assert run(src, out, {})
25
+
26
+ result = Image.open(out)
27
+ assert result.width == img.width
28
+ assert result.height == img.height
29
+
30
+ def test_invert_rgba_handles_alpha(self, tmp_images, tmp_path):
31
+ src = tmp_images / "with_alpha.png"
32
+ img = Image.open(src)
33
+ out = tmp_path / "inv_alpha.png"
34
+ assert run(src, out, {})
35
+
36
+ result = Image.open(out)
37
+ assert result.mode == "RGBA"
38
+ assert result.width == img.width
39
+ assert result.height == img.height
40
+
41
+ def test_invert_white_to_black(self, tmp_path):
42
+ src = tmp_path / "white.png"
43
+ Image.new("RGB", (10, 10), color="white").save(str(src))
44
+ out = tmp_path / "black.png"
45
+ assert run(src, out, {})
46
+
47
+ result = Image.open(out)
48
+ assert result.getpixel((0, 0)) == (0, 0, 0)
49
+
50
+ def test_invert_black_to_white(self, tmp_path):
51
+ src = tmp_path / "black.png"
52
+ Image.new("RGB", (10, 10), color="black").save(str(src))
53
+ out = tmp_path / "white.png"
54
+ assert run(src, out, {})
55
+
56
+ result = Image.open(out)
57
+ assert result.getpixel((0, 0)) == (255, 255, 255)
@@ -1 +0,0 @@
1
- __version__ = "0.2.2"
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