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.
- {imagex-0.2.2 → imagex-0.3.0}/PKG-INFO +3 -1
- {imagex-0.2.2 → imagex-0.3.0}/README.md +2 -0
- imagex-0.3.0/imagex/__init__.py +1 -0
- imagex-0.3.0/imagex/features/grayscale.py +57 -0
- imagex-0.3.0/imagex/features/invert.py +30 -0
- {imagex-0.2.2 → imagex-0.3.0}/imagex.egg-info/PKG-INFO +3 -1
- {imagex-0.2.2 → imagex-0.3.0}/imagex.egg-info/SOURCES.txt +4 -0
- {imagex-0.2.2 → imagex-0.3.0}/pyproject.toml +1 -1
- imagex-0.3.0/tests/test_grayscale.py +53 -0
- imagex-0.3.0/tests/test_invert.py +57 -0
- imagex-0.2.2/imagex/__init__.py +0 -1
- {imagex-0.2.2 → imagex-0.3.0}/LICENSE +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/imagex/__main__.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/imagex/cli.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/imagex/config.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/imagex/features/__init__.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/imagex/features/add_noise.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/imagex/features/compress.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/imagex/features/convert.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/imagex/features/flip.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/imagex/features/remove_metadata.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/imagex/features/rename_batch.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/imagex/features/resize.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/imagex/features/rotate.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/imagex/features/watermark.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/imagex/utils/__init__.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/imagex/utils/file_ops.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/imagex/utils/progress.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/imagex.egg-info/dependency_links.txt +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/imagex.egg-info/entry_points.txt +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/imagex.egg-info/requires.txt +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/imagex.egg-info/top_level.txt +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/setup.cfg +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/tests/test_add_noise.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/tests/test_cli.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/tests/test_compress.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/tests/test_convert.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/tests/test_file_ops.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/tests/test_flip.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/tests/test_remove_metadata.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/tests/test_rename_batch.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/tests/test_resize.py +0 -0
- {imagex-0.2.2 → imagex-0.3.0}/tests/test_rotate.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
@@ -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)
|
imagex-0.2.2/imagex/__init__.py
DELETED
|
@@ -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
|
|
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
|