imgcmprs 0.1.0__py3-none-any.whl
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.
imgcmprs/__init__.py
ADDED
File without changes
|
imgcmprs/__main__.py
ADDED
imgcmprs/img_compress.py
ADDED
@@ -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,8 @@
|
|
1
|
+
imgcmprs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
imgcmprs/__main__.py,sha256=Eisst5VBCByPJapyMDtOUk5acdHYHZCDLeX2hPYSvYA,74
|
3
|
+
imgcmprs/img_compress.py,sha256=Cf85lESgNvA_04bZRDbWnY19MRMuf9Ut1QphiDNJP_g,7840
|
4
|
+
imgcmprs-0.1.0.dist-info/METADATA,sha256=9JpKytDWbdNjerobZlTwCKjZCGlAf3cOrFDUB1j9jME,262
|
5
|
+
imgcmprs-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
6
|
+
imgcmprs-0.1.0.dist-info/entry_points.txt,sha256=YjgihZeWdWEFCxM9YNipeRN89ZQSzzX-ug2Vih-5Qi4,51
|
7
|
+
imgcmprs-0.1.0.dist-info/top_level.txt,sha256=_XIlYusRXoBCj1vxMvtaSgG9ZAVM1-7C1SQCJQ1GFKU,9
|
8
|
+
imgcmprs-0.1.0.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
imgcmprs
|