imgzip 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.
- imgzip-0.1.0/LICENSE +0 -0
- imgzip-0.1.0/PKG-INFO +48 -0
- imgzip-0.1.0/README.md +20 -0
- imgzip-0.1.0/imgzip/__init__.py +8 -0
- imgzip-0.1.0/imgzip/compressor.py +137 -0
- imgzip-0.1.0/imgzip.egg-info/PKG-INFO +48 -0
- imgzip-0.1.0/imgzip.egg-info/SOURCES.txt +12 -0
- imgzip-0.1.0/imgzip.egg-info/dependency_links.txt +1 -0
- imgzip-0.1.0/imgzip.egg-info/requires.txt +1 -0
- imgzip-0.1.0/imgzip.egg-info/top_level.txt +2 -0
- imgzip-0.1.0/setup.cfg +4 -0
- imgzip-0.1.0/setup.py +28 -0
- imgzip-0.1.0/tests/__init__.py +1 -0
- imgzip-0.1.0/tests/test_compressor.py +100 -0
imgzip-0.1.0/LICENSE
ADDED
|
File without changes
|
imgzip-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: imgzip
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight Python library for compressing images with ease
|
|
5
|
+
Home-page: https://github.com/RKSAHOO4414/imgzip
|
|
6
|
+
Author: RKSAHOO4414
|
|
7
|
+
Author-email: your_email@example.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: Pillow>=9.0.0
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: author-email
|
|
20
|
+
Dynamic: classifier
|
|
21
|
+
Dynamic: description
|
|
22
|
+
Dynamic: description-content-type
|
|
23
|
+
Dynamic: home-page
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
Dynamic: requires-dist
|
|
26
|
+
Dynamic: requires-python
|
|
27
|
+
Dynamic: summary
|
|
28
|
+
|
|
29
|
+
# imgzip
|
|
30
|
+
|
|
31
|
+
A lightweight Python library for compressing images with ease.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- ✅ Simple and intuitive API
|
|
36
|
+
- ✅ Support for JPG, PNG, BMP, GIF formats
|
|
37
|
+
- ✅ Batch compression for multiple images
|
|
38
|
+
- ✅ Customizable compression quality
|
|
39
|
+
- ✅ Optional image resizing
|
|
40
|
+
- ✅ Automatic format conversion
|
|
41
|
+
- ✅ Detailed compression statistics
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
Install `imgzip` from PyPI:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install imgzip
|
imgzip-0.1.0/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# imgzip
|
|
2
|
+
|
|
3
|
+
A lightweight Python library for compressing images with ease.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ Simple and intuitive API
|
|
8
|
+
- ✅ Support for JPG, PNG, BMP, GIF formats
|
|
9
|
+
- ✅ Batch compression for multiple images
|
|
10
|
+
- ✅ Customizable compression quality
|
|
11
|
+
- ✅ Optional image resizing
|
|
12
|
+
- ✅ Automatic format conversion
|
|
13
|
+
- ✅ Detailed compression statistics
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Install `imgzip` from PyPI:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install imgzip
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core image compression module
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from PIL import Image
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ImageCompressor:
|
|
11
|
+
"""
|
|
12
|
+
A simple and efficient image compressor for common image formats.
|
|
13
|
+
|
|
14
|
+
Supported formats: JPG, PNG, BMP, GIF
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, quality=85, max_width=None, max_height=None):
|
|
18
|
+
"""
|
|
19
|
+
Initialize the ImageCompressor.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
quality (int): JPEG quality level (1-100). Default is 85.
|
|
23
|
+
Higher = better quality, larger file size.
|
|
24
|
+
max_width (int): Maximum width in pixels. Maintains aspect ratio if set.
|
|
25
|
+
max_height (int): Maximum height in pixels. Maintains aspect ratio if set.
|
|
26
|
+
"""
|
|
27
|
+
if not 1 <= quality <= 100:
|
|
28
|
+
raise ValueError("Quality must be between 1 and 100")
|
|
29
|
+
|
|
30
|
+
self.quality = quality
|
|
31
|
+
self.max_width = max_width
|
|
32
|
+
self.max_height = max_height
|
|
33
|
+
|
|
34
|
+
def compress(self, input_path, output_path=None, format=None):
|
|
35
|
+
"""
|
|
36
|
+
Compress a single image file.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
input_path (str): Path to the input image file.
|
|
40
|
+
output_path (str): Path to save the compressed image.
|
|
41
|
+
If None, saves as 'compressed_<original_name>'.
|
|
42
|
+
format (str): Image format for output (JPG, PNG). If None, uses input format.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
dict: Dictionary with compression details (original size, compressed size, ratio).
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
FileNotFoundError: If input file doesn't exist.
|
|
49
|
+
ValueError: If file format is not supported.
|
|
50
|
+
"""
|
|
51
|
+
# Validate input file
|
|
52
|
+
if not os.path.exists(input_path):
|
|
53
|
+
raise FileNotFoundError(f"Input file not found: {input_path}")
|
|
54
|
+
|
|
55
|
+
# Open image
|
|
56
|
+
try:
|
|
57
|
+
img = Image.open(input_path)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
raise ValueError(f"Cannot open image file: {e}")
|
|
60
|
+
|
|
61
|
+
# Get original file size
|
|
62
|
+
original_size = os.path.getsize(input_path)
|
|
63
|
+
|
|
64
|
+
# Determine output path
|
|
65
|
+
if output_path is None:
|
|
66
|
+
input_name = Path(input_path).stem
|
|
67
|
+
input_ext = Path(input_path).suffix
|
|
68
|
+
output_path = f"compressed_{input_name}{input_ext}"
|
|
69
|
+
|
|
70
|
+
# Determine output format
|
|
71
|
+
if format is None:
|
|
72
|
+
format = img.format or "JPEG"
|
|
73
|
+
format = format.upper()
|
|
74
|
+
|
|
75
|
+
# Resize if max dimensions are set
|
|
76
|
+
if self.max_width or self.max_height:
|
|
77
|
+
img.thumbnail((self.max_width or img.width, self.max_height or img.height), Image.Resampling.LANCZOS)
|
|
78
|
+
|
|
79
|
+
# Convert RGBA to RGB if saving as JPEG
|
|
80
|
+
if format == "JPEG" and img.mode in ("RGBA", "LA", "P"):
|
|
81
|
+
rgb_img = Image.new("RGB", img.size, (255, 255, 255))
|
|
82
|
+
rgb_img.paste(img, mask=img.split()[-1] if img.mode == "RGBA" else None)
|
|
83
|
+
img = rgb_img
|
|
84
|
+
|
|
85
|
+
# Save compressed image
|
|
86
|
+
img.save(output_path, format=format, quality=self.quality, optimize=True)
|
|
87
|
+
|
|
88
|
+
# Get compressed file size
|
|
89
|
+
compressed_size = os.path.getsize(output_path)
|
|
90
|
+
compression_ratio = (1 - (compressed_size / original_size)) * 100
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
"input_path": input_path,
|
|
94
|
+
"output_path": output_path,
|
|
95
|
+
"original_size_kb": round(original_size / 1024, 2),
|
|
96
|
+
"compressed_size_kb": round(compressed_size / 1024, 2),
|
|
97
|
+
"compression_ratio_percent": round(compression_ratio, 2),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
def compress_batch(self, input_folder, output_folder=None):
|
|
101
|
+
"""
|
|
102
|
+
Compress all images in a folder.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
input_folder (str): Path to folder containing images.
|
|
106
|
+
output_folder (str): Path to save compressed images.
|
|
107
|
+
If None, creates 'compressed' folder in input folder.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
list: List of dictionaries with compression details for each image.
|
|
111
|
+
"""
|
|
112
|
+
if not os.path.isdir(input_folder):
|
|
113
|
+
raise FileNotFoundError(f"Input folder not found: {input_folder}")
|
|
114
|
+
|
|
115
|
+
if output_folder is None:
|
|
116
|
+
output_folder = os.path.join(input_folder, "compressed")
|
|
117
|
+
|
|
118
|
+
os.makedirs(output_folder, exist_ok=True)
|
|
119
|
+
|
|
120
|
+
results = []
|
|
121
|
+
supported_formats = {".jpg", ".jpeg", ".png", ".bmp", ".gif"}
|
|
122
|
+
|
|
123
|
+
for filename in os.listdir(input_folder):
|
|
124
|
+
if Path(filename).suffix.lower() in supported_formats:
|
|
125
|
+
input_path = os.path.join(input_folder, filename)
|
|
126
|
+
output_path = os.path.join(output_folder, f"compressed_{filename}")
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
result = self.compress(input_path, output_path)
|
|
130
|
+
results.append(result)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
results.append({
|
|
133
|
+
"input_path": input_path,
|
|
134
|
+
"error": str(e)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
return results
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: imgzip
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight Python library for compressing images with ease
|
|
5
|
+
Home-page: https://github.com/RKSAHOO4414/imgzip
|
|
6
|
+
Author: RKSAHOO4414
|
|
7
|
+
Author-email: your_email@example.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: Pillow>=9.0.0
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: author-email
|
|
20
|
+
Dynamic: classifier
|
|
21
|
+
Dynamic: description
|
|
22
|
+
Dynamic: description-content-type
|
|
23
|
+
Dynamic: home-page
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
Dynamic: requires-dist
|
|
26
|
+
Dynamic: requires-python
|
|
27
|
+
Dynamic: summary
|
|
28
|
+
|
|
29
|
+
# imgzip
|
|
30
|
+
|
|
31
|
+
A lightweight Python library for compressing images with ease.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- ✅ Simple and intuitive API
|
|
36
|
+
- ✅ Support for JPG, PNG, BMP, GIF formats
|
|
37
|
+
- ✅ Batch compression for multiple images
|
|
38
|
+
- ✅ Customizable compression quality
|
|
39
|
+
- ✅ Optional image resizing
|
|
40
|
+
- ✅ Automatic format conversion
|
|
41
|
+
- ✅ Detailed compression statistics
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
Install `imgzip` from PyPI:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install imgzip
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
imgzip/__init__.py
|
|
5
|
+
imgzip/compressor.py
|
|
6
|
+
imgzip.egg-info/PKG-INFO
|
|
7
|
+
imgzip.egg-info/SOURCES.txt
|
|
8
|
+
imgzip.egg-info/dependency_links.txt
|
|
9
|
+
imgzip.egg-info/requires.txt
|
|
10
|
+
imgzip.egg-info/top_level.txt
|
|
11
|
+
tests/__init__.py
|
|
12
|
+
tests/test_compressor.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Pillow>=9.0.0
|
imgzip-0.1.0/setup.cfg
ADDED
imgzip-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
with open("README.md", "r", encoding="utf-8") as fh:
|
|
4
|
+
long_description = fh.read()
|
|
5
|
+
|
|
6
|
+
setup(
|
|
7
|
+
name="imgzip",
|
|
8
|
+
version="0.1.0",
|
|
9
|
+
author="RKSAHOO4414",
|
|
10
|
+
author_email="your_email@example.com",
|
|
11
|
+
description="A lightweight Python library for compressing images with ease",
|
|
12
|
+
long_description=long_description,
|
|
13
|
+
long_description_content_type="text/markdown",
|
|
14
|
+
url="https://github.com/RKSAHOO4414/imgzip",
|
|
15
|
+
packages=find_packages(),
|
|
16
|
+
classifiers=[
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.8",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
],
|
|
24
|
+
python_requires=">=3.8",
|
|
25
|
+
install_requires=[
|
|
26
|
+
"Pillow>=9.0.0",
|
|
27
|
+
],
|
|
28
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Tests package
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for imgzip compressor
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import unittest
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from PIL import Image
|
|
10
|
+
from imgzip.compressor import ImageCompressor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestImageCompressor(unittest.TestCase):
|
|
14
|
+
"""Test cases for ImageCompressor class"""
|
|
15
|
+
|
|
16
|
+
def setUp(self):
|
|
17
|
+
"""Set up test fixtures"""
|
|
18
|
+
self.temp_dir = tempfile.mkdtemp()
|
|
19
|
+
self.compressor = ImageCompressor(quality=85)
|
|
20
|
+
|
|
21
|
+
# Create a test image
|
|
22
|
+
self.test_image_path = os.path.join(self.temp_dir, "test_image.png")
|
|
23
|
+
test_image = Image.new("RGB", (1000, 1000), color="red")
|
|
24
|
+
test_image.save(self.test_image_path)
|
|
25
|
+
|
|
26
|
+
def tearDown(self):
|
|
27
|
+
"""Clean up test files"""
|
|
28
|
+
for file in os.listdir(self.temp_dir):
|
|
29
|
+
file_path = os.path.join(self.temp_dir, file)
|
|
30
|
+
if os.path.isfile(file_path):
|
|
31
|
+
os.remove(file_path)
|
|
32
|
+
os.rmdir(self.temp_dir)
|
|
33
|
+
|
|
34
|
+
def test_quality_validation(self):
|
|
35
|
+
"""Test that quality parameter is validated"""
|
|
36
|
+
with self.assertRaises(ValueError):
|
|
37
|
+
ImageCompressor(quality=0)
|
|
38
|
+
|
|
39
|
+
with self.assertRaises(ValueError):
|
|
40
|
+
ImageCompressor(quality=101)
|
|
41
|
+
|
|
42
|
+
def test_compress_file_exists(self):
|
|
43
|
+
"""Test compression of an existing file"""
|
|
44
|
+
output_path = os.path.join(self.temp_dir, "compressed_test.png")
|
|
45
|
+
result = self.compressor.compress(self.test_image_path, output_path)
|
|
46
|
+
|
|
47
|
+
self.assertTrue(os.path.exists(output_path))
|
|
48
|
+
self.assertIn("compression_ratio_percent", result)
|
|
49
|
+
self.assertGreater(result["compression_ratio_percent"], 0)
|
|
50
|
+
|
|
51
|
+
def test_compress_nonexistent_file(self):
|
|
52
|
+
"""Test that compression fails for nonexistent file"""
|
|
53
|
+
with self.assertRaises(FileNotFoundError):
|
|
54
|
+
self.compressor.compress("/nonexistent/path/image.png")
|
|
55
|
+
|
|
56
|
+
def test_auto_output_path(self):
|
|
57
|
+
"""Test automatic output path generation"""
|
|
58
|
+
original_dir = os.getcwd()
|
|
59
|
+
try:
|
|
60
|
+
os.chdir(self.temp_dir)
|
|
61
|
+
result = self.compressor.compress(self.test_image_path)
|
|
62
|
+
|
|
63
|
+
self.assertTrue(os.path.exists(result["output_path"]))
|
|
64
|
+
self.assertIn("compressed_", result["output_path"])
|
|
65
|
+
finally:
|
|
66
|
+
os.chdir(original_dir)
|
|
67
|
+
|
|
68
|
+
def test_resize_functionality(self):
|
|
69
|
+
"""Test image resizing during compression"""
|
|
70
|
+
compressor_with_resize = ImageCompressor(quality=85, max_width=500, max_height=500)
|
|
71
|
+
output_path = os.path.join(self.temp_dir, "resized.png")
|
|
72
|
+
|
|
73
|
+
compressor_with_resize.compress(self.test_image_path, output_path)
|
|
74
|
+
|
|
75
|
+
# Verify resized image
|
|
76
|
+
compressed_img = Image.open(output_path)
|
|
77
|
+
self.assertLessEqual(compressed_img.width, 500)
|
|
78
|
+
self.assertLessEqual(compressed_img.height, 500)
|
|
79
|
+
|
|
80
|
+
def test_compression_ratio(self):
|
|
81
|
+
"""Test that compression actually reduces file size"""
|
|
82
|
+
# Create a more complex image that compresses better
|
|
83
|
+
import random
|
|
84
|
+
test_image = Image.new("RGB", (1000, 1000))
|
|
85
|
+
pixels = test_image.load()
|
|
86
|
+
for i in range(1000):
|
|
87
|
+
for j in range(1000):
|
|
88
|
+
pixels[i, j] = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
|
|
89
|
+
|
|
90
|
+
complex_image_path = os.path.join(self.temp_dir, "complex_image.png")
|
|
91
|
+
test_image.save(complex_image_path)
|
|
92
|
+
|
|
93
|
+
output_path = os.path.join(self.temp_dir, "compressed_test.jpg")
|
|
94
|
+
result = self.compressor.compress(complex_image_path, output_path, format="JPEG")
|
|
95
|
+
|
|
96
|
+
self.assertLess(result["compressed_size_kb"], result["original_size_kb"])
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if __name__ == "__main__":
|
|
100
|
+
unittest.main()
|