Skip to content

Commit 3b40fdd

Browse files
committed
feat: advanced image compressor (batch, URL, metadata, lossless)
1 parent db9e815 commit 3b40fdd

File tree

2 files changed

+146
-78
lines changed

2 files changed

+146
-78
lines changed
Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,56 @@
1-
# [How to Compress Images in Python](https://www.thepythoncode.com/article/compress-images-in-python)
2-
To run this:
3-
- `pip3 install -r requirements.txt`
4-
- `python compress_image.py --help`
1+
# Compress Image
2+
3+
Advanced Image Compressor with Batch Processing
4+
5+
This script provides advanced image compression and resizing features using Python and Pillow.
6+
7+
## Features
8+
9+
- Batch processing of multiple images or directories
10+
- Lossy and lossless compression (PNG/WebP)
11+
- Optional JPEG conversion
12+
- Resize by ratio or explicit dimensions
13+
- Preserve or strip metadata (EXIF)
14+
- Custom output directory
15+
- Progress bar using `tqdm`
16+
- Detailed logging
17+
18+
## Requirements
19+
20+
- Python 3.6+
21+
- [Pillow](https://pypi.org/project/Pillow/)
22+
- [tqdm](https://pypi.org/project/tqdm/)
23+
24+
Install dependencies:
25+
26+
```bash
27+
pip install pillow tqdm
28+
```
29+
30+
## Usage
31+
32+
```bash
33+
python compress_image.py [options] <input> [<input> ...]
34+
```
35+
36+
## Options
37+
- `-o`, `--output-dir`: Output directory (default: same as input)
38+
- `-q`, `--quality`: Compression quality (0-100, default: 85)
39+
- `-r`, `--resize-ratio`: Resize ratio (0-1, default: 1.0)
40+
- `-w`, `--width`: Output width (requires `--height`)
41+
- `-hh`, `--height`: Output height (requires `--width`)
42+
- `-j`, `--to-jpg`: Convert output to JPEG
43+
- `-m`, `--no-metadata`: Strip metadata (default: preserve)
44+
- `-l`, `--lossless`: Use lossless compression (PNG/WEBP)
45+
46+
## Examples
47+
48+
```bash
49+
python compress_image.py image.jpg -r 0.5 -q 80 -j
50+
python compress_image.py images/ -o output/ -m
51+
python compress_image.py image.png -l
52+
```
53+
54+
## License
55+
56+
MIT License.
Lines changed: 90 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,104 @@
11
import os
22
from PIL import Image
3+
import argparse
4+
import logging
5+
from tqdm import tqdm
36

7+
# Configure logging
8+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
9+
logger = logging.getLogger(__name__)
410

511
def get_size_format(b, factor=1024, suffix="B"):
6-
"""
7-
Scale bytes to its proper byte format
8-
e.g:
9-
1253656 => '1.20MB'
10-
1253656678 => '1.17GB'
11-
"""
12+
"""Scale bytes to its proper byte format."""
1213
for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]:
1314
if b < factor:
1415
return f"{b:.2f}{unit}{suffix}"
1516
b /= factor
1617
return f"{b:.2f}Y{suffix}"
17-
1818

19-
20-
def compress_img(image_name, new_size_ratio=0.9, quality=90, width=None, height=None, to_jpg=True):
21-
# load the image to memory
22-
img = Image.open(image_name)
23-
# print the original image shape
24-
print("[*] Image shape:", img.size)
25-
# get the original image size in bytes
26-
image_size = os.path.getsize(image_name)
27-
# print the size before compression/resizing
28-
print("[*] Size before compression:", get_size_format(image_size))
29-
if new_size_ratio < 1.0:
30-
# if resizing ratio is below 1.0, then multiply width & height with this ratio to reduce image size
31-
img = img.resize((int(img.size[0] * new_size_ratio), int(img.size[1] * new_size_ratio)), Image.LANCZOS)
32-
# print new image shape
33-
print("[+] New Image shape:", img.size)
34-
elif width and height:
35-
# if width and height are set, resize with them instead
36-
img = img.resize((width, height), Image.LANCZOS)
37-
# print new image shape
38-
print("[+] New Image shape:", img.size)
39-
# split the filename and extension
40-
filename, ext = os.path.splitext(image_name)
41-
# make new filename appending _compressed to the original file name
42-
if to_jpg:
43-
# change the extension to JPEG
44-
new_filename = f"{filename}_compressed.jpg"
45-
else:
46-
# retain the same extension of the original image
47-
new_filename = f"{filename}_compressed{ext}"
19+
def compress_image(
20+
input_path,
21+
output_dir=None,
22+
quality=85,
23+
resize_ratio=1.0,
24+
width=None,
25+
height=None,
26+
to_jpg=False,
27+
preserve_metadata=True,
28+
lossless=False,
29+
):
30+
"""Compress an image with advanced options."""
4831
try:
49-
# save the image with the corresponding quality and optimize set to True
50-
img.save(new_filename, quality=quality, optimize=True)
51-
except OSError:
52-
# convert the image to RGB mode first
53-
img = img.convert("RGB")
54-
# save the image with the corresponding quality and optimize set to True
55-
img.save(new_filename, quality=quality, optimize=True)
56-
print("[+] New file saved:", new_filename)
57-
# get the new image size in bytes
58-
new_image_size = os.path.getsize(new_filename)
59-
# print the new size in a good format
60-
print("[+] Size after compression:", get_size_format(new_image_size))
61-
# calculate the saving bytes
62-
saving_diff = new_image_size - image_size
63-
# print the saving percentage
64-
print(f"[+] Image size change: {saving_diff/image_size*100:.2f}% of the original image size.")
65-
66-
32+
img = Image.open(input_path)
33+
logger.info(f"[*] Processing: {os.path.basename(input_path)}")
34+
logger.info(f"[*] Original size: {get_size_format(os.path.getsize(input_path))}")
35+
36+
# Resize if needed
37+
if resize_ratio < 1.0:
38+
new_size = (int(img.size[0] * resize_ratio), int(img.size[1] * resize_ratio))
39+
img = img.resize(new_size, Image.LANCZOS)
40+
logger.info(f"[+] Resized to: {new_size}")
41+
elif width and height:
42+
img = img.resize((width, height), Image.LANCZOS)
43+
logger.info(f"[+] Resized to: {width}x{height}")
44+
45+
# Prepare output path
46+
filename, ext = os.path.splitext(os.path.basename(input_path))
47+
output_ext = ".jpg" if to_jpg else ext
48+
output_filename = f"{filename}_compressed{output_ext}"
49+
output_path = os.path.join(output_dir or os.path.dirname(input_path), output_filename)
50+
51+
# Save with options
52+
save_kwargs = {"quality": quality, "optimize": True}
53+
if not preserve_metadata:
54+
save_kwargs["exif"] = b"" # Strip metadata
55+
if lossless and ext.lower() in (".png", ".webp"):
56+
save_kwargs["lossless"] = True
57+
58+
try:
59+
img.save(output_path, **save_kwargs)
60+
except OSError:
61+
img = img.convert("RGB")
62+
img.save(output_path, **save_kwargs)
63+
64+
logger.info(f"[+] Saved to: {output_path}")
65+
logger.info(f"[+] New size: {get_size_format(os.path.getsize(output_path))}")
66+
except Exception as e:
67+
logger.error(f"[!] Error processing {input_path}: {e}")
68+
69+
def batch_compress(
70+
input_paths,
71+
output_dir=None,
72+
quality=85,
73+
resize_ratio=1.0,
74+
width=None,
75+
height=None,
76+
to_jpg=False,
77+
preserve_metadata=True,
78+
lossless=False,
79+
):
80+
"""Compress multiple images."""
81+
if output_dir and not os.path.exists(output_dir):
82+
os.makedirs(output_dir, exist_ok=True)
83+
for path in tqdm(input_paths, desc="Compressing images"):
84+
compress_image(path, output_dir, quality, resize_ratio, width, height, to_jpg, preserve_metadata, lossless)
85+
6786
if __name__ == "__main__":
68-
import argparse
69-
parser = argparse.ArgumentParser(description="Simple Python script for compressing and resizing images")
70-
parser.add_argument("image", help="Target image to compress and/or resize")
71-
parser.add_argument("-j", "--to-jpg", action="store_true", help="Whether to convert the image to the JPEG format")
72-
parser.add_argument("-q", "--quality", type=int, help="Quality ranging from a minimum of 0 (worst) to a maximum of 95 (best). Default is 90", default=90)
73-
parser.add_argument("-r", "--resize-ratio", type=float, help="Resizing ratio from 0 to 1, setting to 0.5 will multiply width & height of the image by 0.5. Default is 1.0", default=1.0)
74-
parser.add_argument("-w", "--width", type=int, help="The new width image, make sure to set it with the `height` parameter")
75-
parser.add_argument("-hh", "--height", type=int, help="The new height for the image, make sure to set it with the `width` parameter")
87+
parser = argparse.ArgumentParser(description="Advanced Image Compressor with Batch Processing")
88+
parser.add_argument("input", nargs='+', help="Input image(s) or directory")
89+
parser.add_argument("-o", "--output-dir", help="Output directory (default: same as input)")
90+
parser.add_argument("-q", "--quality", type=int, default=85, help="Compression quality (0-100)")
91+
parser.add_argument("-r", "--resize-ratio", type=float, default=1.0, help="Resize ratio (0-1)")
92+
parser.add_argument("-w", "--width", type=int, help="Output width (requires --height)")
93+
parser.add_argument("-hh", "--height", type=int, help="Output height (requires --width)")
94+
parser.add_argument("-j", "--to-jpg", action="store_true", help="Convert output to JPEG")
95+
parser.add_argument("-m", "--no-metadata", action="store_false", help="Strip metadata")
96+
parser.add_argument("-l", "--lossless", action="store_true", help="Use lossless compression (PNG/WEBP)")
97+
7698
args = parser.parse_args()
77-
# print the passed arguments
78-
print("="*50)
79-
print("[*] Image:", args.image)
80-
print("[*] To JPEG:", args.to_jpg)
81-
print("[*] Quality:", args.quality)
82-
print("[*] Resizing ratio:", args.resize_ratio)
83-
if args.width and args.height:
84-
print("[*] Width:", args.width)
85-
print("[*] Height:", args.height)
86-
print("="*50)
87-
# compress the image
88-
compress_img(args.image, args.resize_ratio, args.quality, args.width, args.height, args.to_jpg)
99+
input_paths = []
100+
for path in args.input:
101+
if os.path.isdir(path): input_paths.extend(os.path.join(path, f) for f in os.listdir(path) if f.lower().endswith((".jpg",".jpeg",".png",".webp")))
102+
else: input_paths.append(path)
103+
if not input_paths: logger.error("No valid images found!"); exit(1)
104+
batch_compress(input_paths, args.output_dir, args.quality, args.resize_ratio, args.width, args.height, args.to_jpg, args.no_metadata, args.lossless)

0 commit comments

Comments
 (0)