imgcmprs 0.1.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.
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: imgcmprs
3
+ Version: 0.1.0
4
+ Summary: A CLI tool for compressing images (JPEG, PNG)
5
+ Author: Your Name
6
+ Requires-Python: >=3.7
7
+ Requires-Dist: Pillow
8
+ Dynamic: author
9
+ Dynamic: requires-dist
10
+ Dynamic: requires-python
11
+ Dynamic: summary
@@ -0,0 +1,95 @@
1
+ # img
2
+
3
+ A fast, easy-to-use Python CLI tool for compressing JPEG and PNG images with both lossless and lossy modes.
4
+
5
+ ---
6
+
7
+ ## Features
8
+ - Compresses images individually or in bulk (folders)
9
+ - Supports JPEG and PNG
10
+ - **Lossless mode** (`-l`): Optimizes files without visible quality loss
11
+ - **Lossy mode** (`-q`): Reduce file size by lowering image quality
12
+ - Recursive directory support
13
+ - Custom output folder
14
+ - Prompts to delete originals after compression
15
+ - Skips replacement if compressed file is larger
16
+ - Cross-platform, requires Python 3.7+
17
+
18
+ ---
19
+
20
+ ## Installation
21
+
22
+ 1. Install Python 3.7+
23
+ 2. Clone this repo and install:
24
+ ```bash
25
+ pip install -e .
26
+ # Or for user-local install:
27
+ pip install --user .
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Usage
33
+
34
+ ### Compress a single image (lossy, 60% quality):
35
+
36
+ ```bash
37
+ img -i myphoto.jpg -q 60
38
+ ```
39
+
40
+ ### Compress a folder (lossless, best for PNG):
41
+
42
+ ```bash
43
+ img -i images/ -o optimized/ -l -r
44
+ ```
45
+
46
+ ### Options (Single-letter flags)
47
+ | Flag | Meaning | Example |
48
+ |------|--------------------------------|---------------------------|
49
+ | -i | Input file or folder (required)| `-i mypic.jpg` |
50
+ | -o | Output file or folder (optional)| `-o compressed/` |
51
+ | -q | JPEG quality (default 60; ignored in lossless mode)| `-q 80` |
52
+ | -l | Use lossless compression | `-l` |
53
+ | -r | Recursively process folders | `-r` |
54
+
55
+ - All flags are **single-letter** for speed and ease: `-i`, `-o`, `-q`, `-l`, `-r`.
56
+ - After compressing, you will be **prompted to delete the original files**.
57
+ - If the compressed file is larger, the original is kept and a warning is printed.
58
+
59
+ ---
60
+
61
+ ## Requirements
62
+ - Python 3.7+
63
+ - Pillow (`pip install Pillow`)
64
+
65
+ ---
66
+
67
+ ## Notes
68
+ - Lossless for PNG is truly lossless; for JPEG, uses `quality=100` with optimizations (minor effect but no further visual loss).
69
+ - Output defaults to `_compressed` directory if not specified for folders.
70
+ - Re-run with `-l` to optimize previously compressed images further (if possible).
71
+
72
+ ---
73
+
74
+ ## Publishing to PyPI
75
+
76
+ 1. **Ensure you have an account on [PyPI](https://pypi.org/).**
77
+ 2. **Install required tools:**
78
+ ```bash
79
+ pip install build twine
80
+ ```
81
+ 3. **Build your package:**
82
+ ```bash
83
+ python -m build
84
+ ```
85
+ This creates a `dist/` folder with your distributable files (tar.gz and .whl).
86
+ 4. **Upload to PyPI:**
87
+ ```bash
88
+ python -m twine upload dist/*
89
+ ```
90
+ 5. **Enter your PyPI credentials when prompted.**
91
+
92
+ ---
93
+
94
+ ## License
95
+ MIT
File without changes
@@ -0,0 +1,4 @@
1
+ from .img_compress import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,167 @@
1
+ import argparse
2
+ import os
3
+ from PIL import Image
4
+ import sys
5
+ import traceback
6
+ import shutil
7
+
8
+ def get_save_format(ext):
9
+ ext = ext.lstrip('.')
10
+ if ext in ('jpg', 'jpeg'):
11
+ return 'JPEG'
12
+ if ext == 'png':
13
+ return 'PNG'
14
+ return None
15
+
16
+ def compress_image(input_path, output_path, quality, lossless, debug, force):
17
+ try:
18
+ img = Image.open(input_path)
19
+ ext = os.path.splitext(input_path)[1].lower()
20
+ in_place = os.path.abspath(input_path) == os.path.abspath(output_path)
21
+ save_format = get_save_format(ext)
22
+ tmp_path = None
23
+ if in_place:
24
+ base, ext_base = os.path.splitext(input_path)
25
+ tmp_path = f"{base}.imgcmprs_tmp{ext_base}"
26
+ real_output = tmp_path
27
+ else:
28
+ real_output = output_path
29
+
30
+ if debug:
31
+ print(f"[DEBUG] Processing: {input_path}")
32
+ print(f"[DEBUG] Format: {ext}, Output: {output_path}")
33
+ print(f"[DEBUG] Options: quality={quality}, lossless={lossless}, in_place={in_place}, force={force}")
34
+ print(f"[DEBUG] Temp file: {tmp_path if in_place else 'N/A'} for in-place")
35
+ save_kwargs = {'format': save_format}
36
+ if lossless:
37
+ if ext in (".png",):
38
+ img.save(real_output, optimize=True, **save_kwargs)
39
+ elif ext in (".jpg", ".jpeg"):
40
+ img.save(real_output, quality=100, optimize=True, progressive=True, **save_kwargs)
41
+ else:
42
+ img.save(real_output, **save_kwargs)
43
+ else:
44
+ if ext in (".jpg", ".jpeg"):
45
+ img.save(real_output, optimize=True, quality=quality, **save_kwargs)
46
+ elif ext == ".png":
47
+ img.save(real_output, optimize=True, **save_kwargs)
48
+ else:
49
+ img.save(real_output, **save_kwargs)
50
+ original_size = os.path.getsize(input_path)
51
+ compressed_size = os.path.getsize(real_output)
52
+ if debug:
53
+ print(f"[DEBUG] Input size: {original_size} bytes ; Output size: {compressed_size} bytes")
54
+ if compressed_size < original_size or force:
55
+ if in_place:
56
+ shutil.move(tmp_path, input_path)
57
+ print(f"Compressed: {input_path} -> {output_path} [{original_size//1024}KB → {compressed_size//1024}KB]")
58
+ return True
59
+ else:
60
+ if in_place:
61
+ if tmp_path and os.path.exists(tmp_path):
62
+ os.remove(tmp_path)
63
+ print(f"Warning: Compression would not reduce size; in-place file left unchanged!")
64
+ else:
65
+ print(f"Warning: {output_path} is larger than or equal to original! Keeping original. [{original_size//1024}KB → {compressed_size//1024}KB]")
66
+ if os.path.exists(output_path):
67
+ os.remove(output_path)
68
+ return False
69
+ except Exception as e:
70
+ print(f"Failed to compress {input_path}: {e}")
71
+ if debug:
72
+ traceback.print_exc()
73
+ if 'tmp_path' in locals() and tmp_path and os.path.exists(tmp_path):
74
+ os.remove(tmp_path)
75
+ return False
76
+
77
+ def process_folder(input_folder, output_folder, quality, recursive, lossless, debug, force):
78
+ changed_files = []
79
+ for root, dirs, files in os.walk(input_folder):
80
+ for file in files:
81
+ if file.lower().endswith((".jpg", ".jpeg", ".png")):
82
+ in_path = os.path.join(root, file)
83
+ rel_path = os.path.relpath(in_path, input_folder)
84
+ out_path = os.path.join(output_folder, rel_path)
85
+ os.makedirs(os.path.dirname(out_path), exist_ok=True)
86
+ success = compress_image(in_path, out_path, quality, lossless, debug, force)
87
+ if success:
88
+ changed_files.append((in_path, out_path))
89
+ if not recursive:
90
+ break
91
+ return changed_files
92
+
93
+ def ask_delete_or_keep_copy(targets, force, debug):
94
+ # Only triggers meaningful copy if single file in-place
95
+ if len(targets) == 1:
96
+ original, compressed = targets[0]
97
+ in_place = os.path.abspath(original) == os.path.abspath(compressed)
98
+ answer = input(f"Do you want to delete the original file after compression? [y/N] ").strip().lower()
99
+ if answer == 'y':
100
+ # Overwrite already occurred if compressed, so nothing to do
101
+ print(f'Original file deleted (overwritten).')
102
+ else:
103
+ # In-place: keep original and save comp output to _comp.ext
104
+ if in_place:
105
+ base, ext = os.path.splitext(original)
106
+ comp_name = base + '_comp' + ext
107
+ if debug:
108
+ print(f'[DEBUG] Saving compressed copy as: {comp_name}')
109
+ # Move current compressed result to new comp file
110
+ shutil.copy2(original, comp_name)
111
+ print(f"Compressed copy saved as: {comp_name} (original preserved)")
112
+ else:
113
+ print('Original file(s) kept.')
114
+ else:
115
+ # Folders or batch: just use original logic
116
+ answer = input(f"Do you want to delete the original file(s) after compression? [y/N] ").strip().lower()
117
+ if answer == 'y':
118
+ for original, compressed in targets:
119
+ try:
120
+ os.remove(original)
121
+ print(f"Deleted: {original}")
122
+ except Exception as e:
123
+ print(f"Could not delete {original}: {e}")
124
+ else:
125
+ print('Original file(s) kept (batch mode).')
126
+
127
+ def main():
128
+ parser = argparse.ArgumentParser(
129
+ description="Image Compressor CLI Tool: Lossless/lossy JPEG and PNG compression.\n\nFlags:\n -i Input file or folder (required)\n -o Output file or folder (optional)\n -q JPEG quality, 1-95 (default 60, ignored in lossless mode)\n -l Use lossless compression for PNG/JPEG\n -r Recursively process folders\n -d Enable debug output\n -f Force overwrite even if output is bigger",
130
+ formatter_class=argparse.RawTextHelpFormatter
131
+ )
132
+ parser.add_argument('-i', required=True, metavar='PATH', help='Input file or folder (required)')
133
+ parser.add_argument('-o', metavar='PATH', help='Output file or folder (optional)')
134
+ parser.add_argument('-q', type=int, default=60, metavar='N', help='JPEG quality, 1-95 (default 60, ignored with -l)')
135
+ parser.add_argument('-l', action='store_true', help='Lossless compression for PNG/JPEG (flag)')
136
+ parser.add_argument('-r', action='store_true', help='Recursively process folders (flag)')
137
+ parser.add_argument('-d', action='store_true', help='Enable debug output (flag)')
138
+ parser.add_argument('-f', action='store_true', help='Force overwrite even if output is bigger (flag)')
139
+ args = parser.parse_args()
140
+
141
+ input_path = args.i
142
+ output_path = args.o
143
+ quality = args.q
144
+ recursive = args.r
145
+ lossless = args.l
146
+ debug = args.d
147
+ force = args.f
148
+
149
+ changed_files = []
150
+ if os.path.isfile(input_path):
151
+ out = output_path if output_path else input_path
152
+ success = compress_image(input_path, out, quality, lossless, debug, force)
153
+ if success:
154
+ changed_files.append((input_path, out))
155
+ elif os.path.isdir(input_path):
156
+ out = output_path if output_path else input_path + "_compressed"
157
+ os.makedirs(out, exist_ok=True)
158
+ changed_files = process_folder(input_path, out, quality, recursive, lossless, debug, force)
159
+ else:
160
+ print("Input path is not a valid file or directory.")
161
+ sys.exit(1)
162
+
163
+ if changed_files:
164
+ ask_delete_or_keep_copy(changed_files, force, debug)
165
+
166
+ if __name__ == "__main__":
167
+ main()
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: imgcmprs
3
+ Version: 0.1.0
4
+ Summary: A CLI tool for compressing images (JPEG, PNG)
5
+ Author: Your Name
6
+ Requires-Python: >=3.7
7
+ Requires-Dist: Pillow
8
+ Dynamic: author
9
+ Dynamic: requires-dist
10
+ Dynamic: requires-python
11
+ Dynamic: summary
@@ -0,0 +1,11 @@
1
+ README.md
2
+ setup.py
3
+ imgcmprs/__init__.py
4
+ imgcmprs/__main__.py
5
+ imgcmprs/img_compress.py
6
+ imgcmprs.egg-info/PKG-INFO
7
+ imgcmprs.egg-info/SOURCES.txt
8
+ imgcmprs.egg-info/dependency_links.txt
9
+ imgcmprs.egg-info/entry_points.txt
10
+ imgcmprs.egg-info/requires.txt
11
+ imgcmprs.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ img = imgcmprs.img_compress:main
@@ -0,0 +1 @@
1
+ Pillow
@@ -0,0 +1 @@
1
+ imgcmprs
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,18 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name='imgcmprs',
5
+ version='0.1.0',
6
+ description='A CLI tool for compressing images (JPEG, PNG)',
7
+ author='Your Name',
8
+ packages=find_packages(),
9
+ install_requires=[
10
+ 'Pillow'
11
+ ],
12
+ entry_points={
13
+ 'console_scripts': [
14
+ 'img=imgcmprs.img_compress:main',
15
+ ],
16
+ },
17
+ python_requires='>=3.7',
18
+ )