py-bulk-image-compressor 1.0.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 George Khananaev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,184 @@
1
+ Metadata-Version: 2.2
2
+ Name: py-bulk--image-compressor
3
+ Version: 1.0.0
4
+ Summary: A Python bulk image compressor and resizer
5
+ Home-page: https://github.com/georgekhananaev/py-image-compressor
6
+ Author: George Khananaev
7
+ Author-email: george.khananaev@gmail.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: Pillow>=11.1.0
13
+ Requires-Dist: tqdm>=4.67.1
14
+ Requires-Dist: pillow-heif>=0.21.0
15
+ Requires-Dist: pillow-avif-plugin>=1.4.6
16
+ Dynamic: author
17
+ Dynamic: author-email
18
+ Dynamic: classifier
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: home-page
22
+ Dynamic: requires-dist
23
+ Dynamic: summary
24
+
25
+ # Python Bulk Image Compressor + Resizer
26
+
27
+ This Python script allows you to compress and resize images in bulk. It can optionally preserve EXIF metadata and file timestamps or remove them entirely if the `-rm y` flag is used.
28
+
29
+ ### Features
30
+ - **Supports Multiple Formats:**
31
+ - **Full Metadata Preservation:** JPEG, JPG, TIFF.
32
+ - **Partial/No Metadata:** PNG, WEBP, BMP, etc., do not reliably store EXIF data.
33
+ - **Metadata Removal:** If `-rm y` is specified, all metadata (e.g., GPS, date/time, camera info) is stripped, and timestamps are reset instead of being copied from the source.
34
+ - **Logging:** Logs each compression operation in a `logs.txt` file. You can configure the maximum row limit for the log file in the `[logs]` section of `configurations.ini`. When the log file exceeds this limit, older rows are removed.
35
+
36
+ ![Animation](https://github.com/georgekhananaev/py-image-compressor/blob/main/screenshots/animation.gif?raw=true)
37
+
38
+ ---
39
+
40
+ ### Installation
41
+ ![Python Version](https://img.shields.io/badge/Python_3.11-Supported-green.svg)
42
+
43
+ 1. Install [Python 3+](https://www.python.org/downloads/).
44
+ 2. Install [Git](https://git-scm.com/).
45
+ 3. Clone this repository:
46
+ ```bash
47
+ git clone https://github.com/georgekhananaev/py-image-compressor.git
48
+ ```
49
+ 4. Install dependencies:
50
+ ```bash
51
+ cd py-image-compressor
52
+ pip install -r requirements.txt
53
+ ```
54
+
55
+ ---
56
+
57
+ ### Usage
58
+
59
+ | Command | Required | Default Value | Description | Details | Example |
60
+ |---------|----------|-----------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------|------------------------------|
61
+ | **-l** | Yes | N/A | Input location | Path to the folder containing original images. | `-l "C:/Original Images/"` |
62
+ | **-d** | No | `/data/output/` | Output location | Path where compressed images will be saved. | `-d "C:/Compressed Images/"` |
63
+ | **-w** | No | `1920` | Max width | Resize images to this maximum width while maintaining aspect ratio. | `-w 1920` |
64
+ | **-q** | No | `80` | Quality | Image quality (percentage). Lower values save more space. | `-q 80` |
65
+ | **-f** | No | `jpeg` | Output format | Supported formats: PNG, JPEG, JPG, PPM, GIF, TIFF, BMP, WEBP, HEIC, HEIF, AVIF. Refer to [Pillow Documentation](https://pillow.readthedocs.io/). | `-f jpeg` |
66
+ | **-r** | No | `no` | Remove larger files | If set to `y`, deletes compressed images if they are larger than the original. | `-r y` |
67
+ | **-rm** | No | `no` | Remove metadata | If set to `y`, removes all metadata (EXIF, GPS, etc.) and resets timestamps. | `-rm y` |
68
+ | **-h** | No | - | Help | Displays help information about commands. | `-h` or `--help` |
69
+
70
+ > **Note:** Use quotation marks for paths with spaces (e.g., `"C:/My Folder/"`).
71
+
72
+ ---
73
+
74
+ ### Logging
75
+
76
+ - A `logs.txt` file is created/appended in the script's directory.
77
+ - Maximum rows are configured in `[logs] max_rows`. Older entries are pruned when the limit is exceeded.
78
+ - Log levels:
79
+ - `log_success`
80
+ - `log_errors`
81
+ - `log_warnings`
82
+ - Enable or disable logging levels in `configurations.ini`.
83
+
84
+ ---
85
+
86
+ ### Examples
87
+
88
+ ```bash
89
+ python main.py -l "D:/Images/Originals/" -d "D:/Images/Compressed/" -f webp -w 500 -q 80 -rm y
90
+ ```
91
+
92
+ Example output:
93
+
94
+ ![Terminal Screenshot](https://github.com/georgekhananaev/py-image-compressor/blob/main/screenshots/screenshot.jpg?raw=true)
95
+
96
+ ---
97
+
98
+ ## Updates
99
+
100
+ #### **26/01/2025**
101
+ 1. EXIF metadata is preserved if supported by the output format (e.g., JPEG).
102
+ 2. Added support for `.avif` format (requires `pillow-avif-plugin`).
103
+ 3. Minor speed optimizations for faster compression.
104
+ 4. Updated `requirements.txt` with the latest package versions.
105
+ 5. Added logs, which is exported to logs.txt
106
+ 6. Added tests, use
107
+ ```shell
108
+ pytest --maxfail=1 --disable-warnings -v
109
+ ```
110
+
111
+ #### **02/12/2023**
112
+ 1. Automatic detection and handling of RGB/RGBA channels.
113
+ 2. Thanks to "ZenithVal" for the suggestion.
114
+
115
+ #### **06/04/2023**
116
+ 1. Updated `README.md`.
117
+ 2. Updated `requirements.txt` with the latest package versions.
118
+
119
+ #### **12/12/2022**
120
+ 1. Introduced a `resources` folder containing `configurations.ini` for storing program settings.
121
+ 2. Set up future GUI plans using `customtkinter`.
122
+ 3. Removed PySimpleGUI from dependencies.
123
+
124
+ #### **06/12/2022**
125
+ 1. Default values added for `-d`, `-f`, and `-q` options. You can run:
126
+ ```bash
127
+ python main.py -l "C:/Your Folder/"
128
+ ```
129
+ Defaults: JPEG format, 80% quality, 1920px max width, `/data/output/` as the destination folder.
130
+ 2. Simplified the code using `*args` and `**kwargs`.
131
+
132
+ #### **05/12/2022**
133
+ 1. Added support for `.heif` and `.heic` formats (e.g., iPhone photos).
134
+ 2. Updated `requirements.txt` to include `pillow-heif`.
135
+
136
+ #### **04/12/2022**
137
+ 1. Improved the output to display the actual size saved for each image, with full multithreading conversion support.
138
+ > ![terminal](https://github.com/georgekhananaev/py-image-compressor/blob/main/screenshots/multicore.gif?raw=true)
139
+ 2. Enhanced terminal output for better visualization of size savings.
140
+ 3. Began GUI development for Windows and Ubuntu.
141
+ 4. Added `-r y` to skip retaining larger compressed images.
142
+
143
+ ---
144
+
145
+ ### Future Plans
146
+ - A GUI interface will be added if this project receives 50+ stars on GitHub.
147
+ - Integration with cloud storage services like AWS S3 and Google Drive for automatic uploads.
148
+ - Support for more advanced image editing options, such as cropping and watermarking.
149
+
150
+ > **Personal Note:** I use this script for compressing images for React-based websites and converting entire folders to WebP format with a single command.
151
+
152
+ ---
153
+
154
+ ## Contributing 🤝
155
+
156
+ We’d love your help to make py-image-compressor even better! To contribute:
157
+
158
+ 1. Fork the repository.
159
+ 2. Create a new branch for your feature: `git checkout -b feature-name`.
160
+ 3. Commit your changes: `git commit -m "Add awesome feature"`.
161
+ 4. Push your branch: `git push origin feature-name`.
162
+ 5. Open a pull request and tell us about your changes.
163
+
164
+ ---
165
+
166
+ ## License
167
+
168
+ py-image-compressor is licensed under the MIT License. See the `LICENSE` file for details.
169
+
170
+ ---
171
+
172
+ ## Need Help?
173
+
174
+ If you run into any issues or have questions, feel free to [open an issue](https://github.com/georgekhananaev/py-image-compressor/issues).
175
+
176
+
177
+ ## Support Me
178
+
179
+ If you find my work helpful, consider supporting me by buying me a coffee at [Buy Me A Coffee](https://www.buymeacoffee.com/georgekhananaev).
180
+ Your support helps me continue to create and maintain useful projects.
181
+
182
+ [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/georgekhananaev)
183
+
184
+ Thank you!
@@ -0,0 +1,160 @@
1
+ # Python Bulk Image Compressor + Resizer
2
+
3
+ This Python script allows you to compress and resize images in bulk. It can optionally preserve EXIF metadata and file timestamps or remove them entirely if the `-rm y` flag is used.
4
+
5
+ ### Features
6
+ - **Supports Multiple Formats:**
7
+ - **Full Metadata Preservation:** JPEG, JPG, TIFF.
8
+ - **Partial/No Metadata:** PNG, WEBP, BMP, etc., do not reliably store EXIF data.
9
+ - **Metadata Removal:** If `-rm y` is specified, all metadata (e.g., GPS, date/time, camera info) is stripped, and timestamps are reset instead of being copied from the source.
10
+ - **Logging:** Logs each compression operation in a `logs.txt` file. You can configure the maximum row limit for the log file in the `[logs]` section of `configurations.ini`. When the log file exceeds this limit, older rows are removed.
11
+
12
+ ![Animation](https://github.com/georgekhananaev/py-image-compressor/blob/main/screenshots/animation.gif?raw=true)
13
+
14
+ ---
15
+
16
+ ### Installation
17
+ ![Python Version](https://img.shields.io/badge/Python_3.11-Supported-green.svg)
18
+
19
+ 1. Install [Python 3+](https://www.python.org/downloads/).
20
+ 2. Install [Git](https://git-scm.com/).
21
+ 3. Clone this repository:
22
+ ```bash
23
+ git clone https://github.com/georgekhananaev/py-image-compressor.git
24
+ ```
25
+ 4. Install dependencies:
26
+ ```bash
27
+ cd py-image-compressor
28
+ pip install -r requirements.txt
29
+ ```
30
+
31
+ ---
32
+
33
+ ### Usage
34
+
35
+ | Command | Required | Default Value | Description | Details | Example |
36
+ |---------|----------|-----------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------|------------------------------|
37
+ | **-l** | Yes | N/A | Input location | Path to the folder containing original images. | `-l "C:/Original Images/"` |
38
+ | **-d** | No | `/data/output/` | Output location | Path where compressed images will be saved. | `-d "C:/Compressed Images/"` |
39
+ | **-w** | No | `1920` | Max width | Resize images to this maximum width while maintaining aspect ratio. | `-w 1920` |
40
+ | **-q** | No | `80` | Quality | Image quality (percentage). Lower values save more space. | `-q 80` |
41
+ | **-f** | No | `jpeg` | Output format | Supported formats: PNG, JPEG, JPG, PPM, GIF, TIFF, BMP, WEBP, HEIC, HEIF, AVIF. Refer to [Pillow Documentation](https://pillow.readthedocs.io/). | `-f jpeg` |
42
+ | **-r** | No | `no` | Remove larger files | If set to `y`, deletes compressed images if they are larger than the original. | `-r y` |
43
+ | **-rm** | No | `no` | Remove metadata | If set to `y`, removes all metadata (EXIF, GPS, etc.) and resets timestamps. | `-rm y` |
44
+ | **-h** | No | - | Help | Displays help information about commands. | `-h` or `--help` |
45
+
46
+ > **Note:** Use quotation marks for paths with spaces (e.g., `"C:/My Folder/"`).
47
+
48
+ ---
49
+
50
+ ### Logging
51
+
52
+ - A `logs.txt` file is created/appended in the script's directory.
53
+ - Maximum rows are configured in `[logs] max_rows`. Older entries are pruned when the limit is exceeded.
54
+ - Log levels:
55
+ - `log_success`
56
+ - `log_errors`
57
+ - `log_warnings`
58
+ - Enable or disable logging levels in `configurations.ini`.
59
+
60
+ ---
61
+
62
+ ### Examples
63
+
64
+ ```bash
65
+ python main.py -l "D:/Images/Originals/" -d "D:/Images/Compressed/" -f webp -w 500 -q 80 -rm y
66
+ ```
67
+
68
+ Example output:
69
+
70
+ ![Terminal Screenshot](https://github.com/georgekhananaev/py-image-compressor/blob/main/screenshots/screenshot.jpg?raw=true)
71
+
72
+ ---
73
+
74
+ ## Updates
75
+
76
+ #### **26/01/2025**
77
+ 1. EXIF metadata is preserved if supported by the output format (e.g., JPEG).
78
+ 2. Added support for `.avif` format (requires `pillow-avif-plugin`).
79
+ 3. Minor speed optimizations for faster compression.
80
+ 4. Updated `requirements.txt` with the latest package versions.
81
+ 5. Added logs, which is exported to logs.txt
82
+ 6. Added tests, use
83
+ ```shell
84
+ pytest --maxfail=1 --disable-warnings -v
85
+ ```
86
+
87
+ #### **02/12/2023**
88
+ 1. Automatic detection and handling of RGB/RGBA channels.
89
+ 2. Thanks to "ZenithVal" for the suggestion.
90
+
91
+ #### **06/04/2023**
92
+ 1. Updated `README.md`.
93
+ 2. Updated `requirements.txt` with the latest package versions.
94
+
95
+ #### **12/12/2022**
96
+ 1. Introduced a `resources` folder containing `configurations.ini` for storing program settings.
97
+ 2. Set up future GUI plans using `customtkinter`.
98
+ 3. Removed PySimpleGUI from dependencies.
99
+
100
+ #### **06/12/2022**
101
+ 1. Default values added for `-d`, `-f`, and `-q` options. You can run:
102
+ ```bash
103
+ python main.py -l "C:/Your Folder/"
104
+ ```
105
+ Defaults: JPEG format, 80% quality, 1920px max width, `/data/output/` as the destination folder.
106
+ 2. Simplified the code using `*args` and `**kwargs`.
107
+
108
+ #### **05/12/2022**
109
+ 1. Added support for `.heif` and `.heic` formats (e.g., iPhone photos).
110
+ 2. Updated `requirements.txt` to include `pillow-heif`.
111
+
112
+ #### **04/12/2022**
113
+ 1. Improved the output to display the actual size saved for each image, with full multithreading conversion support.
114
+ > ![terminal](https://github.com/georgekhananaev/py-image-compressor/blob/main/screenshots/multicore.gif?raw=true)
115
+ 2. Enhanced terminal output for better visualization of size savings.
116
+ 3. Began GUI development for Windows and Ubuntu.
117
+ 4. Added `-r y` to skip retaining larger compressed images.
118
+
119
+ ---
120
+
121
+ ### Future Plans
122
+ - A GUI interface will be added if this project receives 50+ stars on GitHub.
123
+ - Integration with cloud storage services like AWS S3 and Google Drive for automatic uploads.
124
+ - Support for more advanced image editing options, such as cropping and watermarking.
125
+
126
+ > **Personal Note:** I use this script for compressing images for React-based websites and converting entire folders to WebP format with a single command.
127
+
128
+ ---
129
+
130
+ ## Contributing 🤝
131
+
132
+ We’d love your help to make py-image-compressor even better! To contribute:
133
+
134
+ 1. Fork the repository.
135
+ 2. Create a new branch for your feature: `git checkout -b feature-name`.
136
+ 3. Commit your changes: `git commit -m "Add awesome feature"`.
137
+ 4. Push your branch: `git push origin feature-name`.
138
+ 5. Open a pull request and tell us about your changes.
139
+
140
+ ---
141
+
142
+ ## License
143
+
144
+ py-image-compressor is licensed under the MIT License. See the `LICENSE` file for details.
145
+
146
+ ---
147
+
148
+ ## Need Help?
149
+
150
+ If you run into any issues or have questions, feel free to [open an issue](https://github.com/georgekhananaev/py-image-compressor/issues).
151
+
152
+
153
+ ## Support Me
154
+
155
+ If you find my work helpful, consider supporting me by buying me a coffee at [Buy Me A Coffee](https://www.buymeacoffee.com/georgekhananaev).
156
+ Your support helps me continue to create and maintain useful projects.
157
+
158
+ [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/georgekhananaev)
159
+
160
+ Thank you!
File without changes
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/python
2
+ import sys
3
+ from PIL import Image, ImageFile
4
+ from pillow_heif import register_heif_opener
5
+
6
+ # Mitigate issues with truncated images
7
+ ImageFile.LOAD_TRUNCATED_IMAGES = True
8
+
9
+ # Add HEIC/HEIF
10
+ register_heif_opener()
11
+
12
+ avif_supported = True
13
+ try:
14
+ import avif # noqa
15
+ except ImportError:
16
+ avif_supported = False
17
+
18
+
19
+ def compress_resize_image(**kwargs) -> None:
20
+ """
21
+ Compress/resize. Remove metadata if remove_meta=True.
22
+ If format == avif but 'pillow-avif-plugin' is not installed, skip & raise.
23
+ """
24
+ remove_meta = kwargs.get('remove_meta', False)
25
+ set_format = kwargs['set_format'].lower()
26
+
27
+ # If user wants AVIF but plugin is not installed, raise an exception
28
+ if set_format == 'avif' and not avif_supported:
29
+ raise ValueError("AVIF plugin (pillow-avif-plugin) not installed. Cannot save as AVIF.")
30
+
31
+ try:
32
+ with Image.open(kwargs['image_location']) as im:
33
+ # If removing meta, skip exif
34
+ if not remove_meta:
35
+ exif_data = im.info.get('exif')
36
+ else:
37
+ exif_data = None
38
+
39
+ # Convert mode
40
+ if im.mode in ("RGBA", "LA") or (im.mode == "P" and "transparency" in im.info):
41
+ im = im.convert("RGBA")
42
+ else:
43
+ im = im.convert("RGB")
44
+
45
+ # Calculate new dimensions
46
+ if kwargs['max_width'] is None or im.width <= kwargs['max_width']:
47
+ w, h = im.width, im.height
48
+ else:
49
+ ratio = im.width / im.height
50
+ w = kwargs['max_width']
51
+ h = round(w / ratio)
52
+
53
+ im_resized = im.resize((w, h), resample=Image.LANCZOS)
54
+
55
+ # If not removing meta and set_format supports EXIF
56
+ if exif_data and set_format in ['jpeg', 'jpg', 'tiff']:
57
+ im_resized.save(
58
+ kwargs['image_destination'],
59
+ set_format,
60
+ optimize=True,
61
+ quality=kwargs['quality'],
62
+ exif=exif_data
63
+ )
64
+ else:
65
+ im_resized.save(
66
+ kwargs['image_destination'],
67
+ set_format,
68
+ optimize=True,
69
+ quality=kwargs['quality']
70
+ )
71
+
72
+ except Exception as err:
73
+ raise err
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/python
2
+
3
+ # Simple color utility for terminal output
4
+ class select:
5
+ HEADER = '\033[95m'
6
+ OKBLUE = '\033[94m'
7
+ OKCYAN = '\033[96m'
8
+ OKGREEN = '\033[92m'
9
+ WARNING = '\033[93m'
10
+ FAIL = '\033[91m'
11
+ ENDC = '\033[0m'
12
+ BOLD = '\033[1m'
13
+ UNDERLINE = '\033[4m'
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/python
2
+
3
+ class set_default_values:
4
+ """
5
+ Assigns a default value if the provided argument is None.
6
+ Usage:
7
+ set_default_values(default, provided)
8
+ """
9
+ def __init__(self, *args):
10
+ # args[0] -> default, args[1] -> provided
11
+ if args[1] is None:
12
+ self.set = args[0]
13
+ else:
14
+ self.set = args[1]
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/python
2
+ import os
3
+ import shutil
4
+ from pathlib import Path
5
+ from datetime import datetime
6
+
7
+ from components import imageCompressor as iC, setConfigurations
8
+
9
+ config = setConfigurations.get_resources()
10
+
11
+ LOG_FILE = "logs.txt"
12
+
13
+
14
+ def build_file_list(your_folder) -> dir:
15
+ """
16
+ Builds a generator of files from 'your_folder' that match any of the
17
+ supported formats in 'configurations.ini'.
18
+ """
19
+ for root, dirs, files in os.walk(your_folder):
20
+ for file_bfl in files:
21
+ if os.path.splitext(file_bfl)[1].lower() in config['supported_formats'].values():
22
+ yield os.path.join(root, file_bfl)
23
+
24
+
25
+ def create_folder(your_path) -> None:
26
+ """Creates a folder if it does not exist."""
27
+ if not os.path.exists(your_path):
28
+ os.makedirs(your_path)
29
+
30
+
31
+ def folder_size(your_folder) -> str:
32
+ """Returns a string representing total folder size in KB."""
33
+ if not os.path.exists(your_folder):
34
+ return "Total folder size: 0 KB"
35
+
36
+ total_size_kb = sum(
37
+ file.stat().st_size for file in Path(your_folder).rglob('*') if file.is_file()
38
+ ) / 1024
39
+ return f"Total folder size: {round(total_size_kb, 2)} KB"
40
+
41
+
42
+ def get_percentage_difference(num_a, num_b) -> float:
43
+ """
44
+ Positive if compressed is smaller, negative if compressed is larger.
45
+ """
46
+ if num_b == 0:
47
+ return 0.0
48
+ if num_b >= num_a:
49
+ return -round((abs(num_b - num_a) / num_b) * 100, 2)
50
+ else:
51
+ return round((abs(num_a - num_b) / num_b) * 100, 2)
52
+
53
+
54
+ def start_command(**kwargs) -> None:
55
+ """
56
+ 1) Creates destination folder.
57
+ 2) Compresses/resizes.
58
+ 3) Copies timestamps (unless remove_meta == True).
59
+ """
60
+ src_image = kwargs['source_image']
61
+ before, sep, after = src_image.partition(kwargs['img_path'])
62
+
63
+ try:
64
+ create_folder(kwargs['img_destination'] + os.path.dirname(after))
65
+ except Exception: # noqa
66
+ pass
67
+
68
+ new_file_path = (
69
+ kwargs['img_destination']
70
+ + after.replace(os.path.splitext(after)[1], '')
71
+ + f".{kwargs['set_format']}"
72
+ )
73
+
74
+ # Compress
75
+ iC.compress_resize_image(
76
+ image_location=kwargs['source_image'],
77
+ image_destination=new_file_path,
78
+ set_format=kwargs['set_format'],
79
+ max_width=kwargs['max_width'],
80
+ quality=kwargs['quality'],
81
+ remove_meta=kwargs.get('remove_meta', False)
82
+ )
83
+
84
+ # If removing metadata, skip copying timestamps
85
+ if not kwargs.get('remove_meta', False):
86
+ if os.path.exists(new_file_path):
87
+ try:
88
+ shutil.copystat(src_image, new_file_path)
89
+ except Exception as e:
90
+ log_message(
91
+ config,
92
+ "error",
93
+ f"[{datetime.now()}] ERROR copying timestamps {new_file_path}: {e}\n"
94
+ )
95
+
96
+
97
+ def log_message(config, log_type: str, message: str): # noqa
98
+ """
99
+ Logs a message if the corresponding log_<type> is True in config.
100
+ The log types are "success", "warning", "error".
101
+ """
102
+ # read booleans from config
103
+ max_rows = int(config['logs']['max_rows'])
104
+
105
+ # For safety, using getboolean
106
+ log_success = config['logs'].getboolean('log_success')
107
+ log_errors = config['logs'].getboolean('log_errors')
108
+ log_warnings = config['logs'].getboolean('log_warnings')
109
+
110
+ if log_type == "success" and not log_success:
111
+ return
112
+ if log_type == "warning" and not log_warnings:
113
+ return
114
+ if log_type == "error" and not log_errors:
115
+ return
116
+
117
+ log_path = os.path.join(os.getcwd(), LOG_FILE)
118
+
119
+ # Append log
120
+ try:
121
+ with open(log_path, 'a', encoding='utf-8') as file:
122
+ file.write(message)
123
+ except Exception as e:
124
+ print(f"Cannot write to log file: {e}")
125
+ return
126
+
127
+ # Now enforce max_rows limit
128
+ try:
129
+ with open(log_path, 'r+', encoding='utf-8') as file:
130
+ lines = file.readlines()
131
+ if len(lines) > max_rows:
132
+ new_lines = lines[-max_rows:]
133
+ with open(log_path, 'w', encoding='utf-8') as file:
134
+ file.writelines(new_lines)
135
+ except Exception as e:
136
+ print(f"Error trimming log file: {e}")
@@ -0,0 +1,9 @@
1
+ import configparser
2
+
3
+ def get_resources() -> configparser:
4
+ """
5
+ Loads and returns the settings from 'resources/configurations.ini'.
6
+ """
7
+ my_config = configparser.ConfigParser()
8
+ my_config.read('resources/configurations.ini')
9
+ return my_config
File without changes
@@ -0,0 +1,184 @@
1
+ Metadata-Version: 2.2
2
+ Name: py-bulk--image-compressor
3
+ Version: 1.0.0
4
+ Summary: A Python bulk image compressor and resizer
5
+ Home-page: https://github.com/georgekhananaev/py-image-compressor
6
+ Author: George Khananaev
7
+ Author-email: george.khananaev@gmail.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: Pillow>=11.1.0
13
+ Requires-Dist: tqdm>=4.67.1
14
+ Requires-Dist: pillow-heif>=0.21.0
15
+ Requires-Dist: pillow-avif-plugin>=1.4.6
16
+ Dynamic: author
17
+ Dynamic: author-email
18
+ Dynamic: classifier
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: home-page
22
+ Dynamic: requires-dist
23
+ Dynamic: summary
24
+
25
+ # Python Bulk Image Compressor + Resizer
26
+
27
+ This Python script allows you to compress and resize images in bulk. It can optionally preserve EXIF metadata and file timestamps or remove them entirely if the `-rm y` flag is used.
28
+
29
+ ### Features
30
+ - **Supports Multiple Formats:**
31
+ - **Full Metadata Preservation:** JPEG, JPG, TIFF.
32
+ - **Partial/No Metadata:** PNG, WEBP, BMP, etc., do not reliably store EXIF data.
33
+ - **Metadata Removal:** If `-rm y` is specified, all metadata (e.g., GPS, date/time, camera info) is stripped, and timestamps are reset instead of being copied from the source.
34
+ - **Logging:** Logs each compression operation in a `logs.txt` file. You can configure the maximum row limit for the log file in the `[logs]` section of `configurations.ini`. When the log file exceeds this limit, older rows are removed.
35
+
36
+ ![Animation](https://github.com/georgekhananaev/py-image-compressor/blob/main/screenshots/animation.gif?raw=true)
37
+
38
+ ---
39
+
40
+ ### Installation
41
+ ![Python Version](https://img.shields.io/badge/Python_3.11-Supported-green.svg)
42
+
43
+ 1. Install [Python 3+](https://www.python.org/downloads/).
44
+ 2. Install [Git](https://git-scm.com/).
45
+ 3. Clone this repository:
46
+ ```bash
47
+ git clone https://github.com/georgekhananaev/py-image-compressor.git
48
+ ```
49
+ 4. Install dependencies:
50
+ ```bash
51
+ cd py-image-compressor
52
+ pip install -r requirements.txt
53
+ ```
54
+
55
+ ---
56
+
57
+ ### Usage
58
+
59
+ | Command | Required | Default Value | Description | Details | Example |
60
+ |---------|----------|-----------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------|------------------------------|
61
+ | **-l** | Yes | N/A | Input location | Path to the folder containing original images. | `-l "C:/Original Images/"` |
62
+ | **-d** | No | `/data/output/` | Output location | Path where compressed images will be saved. | `-d "C:/Compressed Images/"` |
63
+ | **-w** | No | `1920` | Max width | Resize images to this maximum width while maintaining aspect ratio. | `-w 1920` |
64
+ | **-q** | No | `80` | Quality | Image quality (percentage). Lower values save more space. | `-q 80` |
65
+ | **-f** | No | `jpeg` | Output format | Supported formats: PNG, JPEG, JPG, PPM, GIF, TIFF, BMP, WEBP, HEIC, HEIF, AVIF. Refer to [Pillow Documentation](https://pillow.readthedocs.io/). | `-f jpeg` |
66
+ | **-r** | No | `no` | Remove larger files | If set to `y`, deletes compressed images if they are larger than the original. | `-r y` |
67
+ | **-rm** | No | `no` | Remove metadata | If set to `y`, removes all metadata (EXIF, GPS, etc.) and resets timestamps. | `-rm y` |
68
+ | **-h** | No | - | Help | Displays help information about commands. | `-h` or `--help` |
69
+
70
+ > **Note:** Use quotation marks for paths with spaces (e.g., `"C:/My Folder/"`).
71
+
72
+ ---
73
+
74
+ ### Logging
75
+
76
+ - A `logs.txt` file is created/appended in the script's directory.
77
+ - Maximum rows are configured in `[logs] max_rows`. Older entries are pruned when the limit is exceeded.
78
+ - Log levels:
79
+ - `log_success`
80
+ - `log_errors`
81
+ - `log_warnings`
82
+ - Enable or disable logging levels in `configurations.ini`.
83
+
84
+ ---
85
+
86
+ ### Examples
87
+
88
+ ```bash
89
+ python main.py -l "D:/Images/Originals/" -d "D:/Images/Compressed/" -f webp -w 500 -q 80 -rm y
90
+ ```
91
+
92
+ Example output:
93
+
94
+ ![Terminal Screenshot](https://github.com/georgekhananaev/py-image-compressor/blob/main/screenshots/screenshot.jpg?raw=true)
95
+
96
+ ---
97
+
98
+ ## Updates
99
+
100
+ #### **26/01/2025**
101
+ 1. EXIF metadata is preserved if supported by the output format (e.g., JPEG).
102
+ 2. Added support for `.avif` format (requires `pillow-avif-plugin`).
103
+ 3. Minor speed optimizations for faster compression.
104
+ 4. Updated `requirements.txt` with the latest package versions.
105
+ 5. Added logs, which is exported to logs.txt
106
+ 6. Added tests, use
107
+ ```shell
108
+ pytest --maxfail=1 --disable-warnings -v
109
+ ```
110
+
111
+ #### **02/12/2023**
112
+ 1. Automatic detection and handling of RGB/RGBA channels.
113
+ 2. Thanks to "ZenithVal" for the suggestion.
114
+
115
+ #### **06/04/2023**
116
+ 1. Updated `README.md`.
117
+ 2. Updated `requirements.txt` with the latest package versions.
118
+
119
+ #### **12/12/2022**
120
+ 1. Introduced a `resources` folder containing `configurations.ini` for storing program settings.
121
+ 2. Set up future GUI plans using `customtkinter`.
122
+ 3. Removed PySimpleGUI from dependencies.
123
+
124
+ #### **06/12/2022**
125
+ 1. Default values added for `-d`, `-f`, and `-q` options. You can run:
126
+ ```bash
127
+ python main.py -l "C:/Your Folder/"
128
+ ```
129
+ Defaults: JPEG format, 80% quality, 1920px max width, `/data/output/` as the destination folder.
130
+ 2. Simplified the code using `*args` and `**kwargs`.
131
+
132
+ #### **05/12/2022**
133
+ 1. Added support for `.heif` and `.heic` formats (e.g., iPhone photos).
134
+ 2. Updated `requirements.txt` to include `pillow-heif`.
135
+
136
+ #### **04/12/2022**
137
+ 1. Improved the output to display the actual size saved for each image, with full multithreading conversion support.
138
+ > ![terminal](https://github.com/georgekhananaev/py-image-compressor/blob/main/screenshots/multicore.gif?raw=true)
139
+ 2. Enhanced terminal output for better visualization of size savings.
140
+ 3. Began GUI development for Windows and Ubuntu.
141
+ 4. Added `-r y` to skip retaining larger compressed images.
142
+
143
+ ---
144
+
145
+ ### Future Plans
146
+ - A GUI interface will be added if this project receives 50+ stars on GitHub.
147
+ - Integration with cloud storage services like AWS S3 and Google Drive for automatic uploads.
148
+ - Support for more advanced image editing options, such as cropping and watermarking.
149
+
150
+ > **Personal Note:** I use this script for compressing images for React-based websites and converting entire folders to WebP format with a single command.
151
+
152
+ ---
153
+
154
+ ## Contributing 🤝
155
+
156
+ We’d love your help to make py-image-compressor even better! To contribute:
157
+
158
+ 1. Fork the repository.
159
+ 2. Create a new branch for your feature: `git checkout -b feature-name`.
160
+ 3. Commit your changes: `git commit -m "Add awesome feature"`.
161
+ 4. Push your branch: `git push origin feature-name`.
162
+ 5. Open a pull request and tell us about your changes.
163
+
164
+ ---
165
+
166
+ ## License
167
+
168
+ py-image-compressor is licensed under the MIT License. See the `LICENSE` file for details.
169
+
170
+ ---
171
+
172
+ ## Need Help?
173
+
174
+ If you run into any issues or have questions, feel free to [open an issue](https://github.com/georgekhananaev/py-image-compressor/issues).
175
+
176
+
177
+ ## Support Me
178
+
179
+ If you find my work helpful, consider supporting me by buying me a coffee at [Buy Me A Coffee](https://www.buymeacoffee.com/georgekhananaev).
180
+ Your support helps me continue to create and maintain useful projects.
181
+
182
+ [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/georgekhananaev)
183
+
184
+ Thank you!
@@ -0,0 +1,20 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ components/__init__.py
5
+ components/imageCompressor.py
6
+ components/localColors.py
7
+ components/mainClasses.py
8
+ components/mainFunctions.py
9
+ components/setConfigurations.py
10
+ data/__init__.py
11
+ py_bulk__image_compressor.egg-info/PKG-INFO
12
+ py_bulk__image_compressor.egg-info/SOURCES.txt
13
+ py_bulk__image_compressor.egg-info/dependency_links.txt
14
+ py_bulk__image_compressor.egg-info/requires.txt
15
+ py_bulk__image_compressor.egg-info/top_level.txt
16
+ tests/__init__.py
17
+ tests/test_imageCompressor.py
18
+ tests/test_main.py
19
+ tests/test_mainFunctions.py
20
+ tests/test_setConfigurations.py
@@ -0,0 +1,4 @@
1
+ Pillow>=11.1.0
2
+ tqdm>=4.67.1
3
+ pillow-heif>=0.21.0
4
+ pillow-avif-plugin>=1.4.6
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,24 @@
1
+ # setup.py
2
+ from setuptools import setup, find_packages
3
+
4
+ setup(
5
+ name="py-bulk--image-compressor",
6
+ version="1.0.0",
7
+ description="A Python bulk image compressor and resizer",
8
+ long_description=open("README.md").read(),
9
+ long_description_content_type="text/markdown",
10
+ author="George Khananaev",
11
+ author_email="george.khananaev@gmail.com",
12
+ url="https://github.com/georgekhananaev/py-image-compressor",
13
+ packages=find_packages(),
14
+ install_requires=[
15
+ "Pillow>=11.1.0",
16
+ "tqdm>=4.67.1",
17
+ "pillow-heif>=0.21.0",
18
+ "pillow-avif-plugin>=1.4.6",
19
+ ],
20
+ classifiers=[
21
+ "Programming Language :: Python :: 3",
22
+ "License :: OSI Approved :: MIT License",
23
+ ],
24
+ )
File without changes
@@ -0,0 +1,34 @@
1
+ import os
2
+ import pytest
3
+ from PIL import Image
4
+ from components import imageCompressor as ic
5
+
6
+ @pytest.mark.parametrize("img_format", ["jpeg", "png", "webp"])
7
+ def test_compress_resize_image(tmp_path, img_format):
8
+ """
9
+ Using a small sample image in tests/sample_images,
10
+ compress/resize it and ensure output file is created.
11
+ """
12
+ # Prepare sample image path
13
+ sample_dir = os.path.join(os.path.dirname(__file__), "sample_images")
14
+ sample_image = os.path.join(sample_dir, "sample.jpeg")
15
+
16
+ # Output path
17
+ output_image = tmp_path / f"compressed.{img_format}"
18
+
19
+ # Call compressor
20
+ ic.compress_resize_image(
21
+ image_location=sample_image,
22
+ image_destination=str(output_image),
23
+ set_format=img_format,
24
+ max_width=500,
25
+ quality=80,
26
+ remove_meta=False
27
+ )
28
+
29
+ # Check if file is created
30
+ assert output_image.exists()
31
+
32
+ # Check if it can be opened by Pillow
33
+ with Image.open(output_image) as img:
34
+ assert img.width <= 500 # we used max_width=500
@@ -0,0 +1,26 @@
1
+ import pytest
2
+ import subprocess
3
+ import sys
4
+ import os
5
+
6
+ def test_help_flag():
7
+ """
8
+ Ensure the script runs with -h and does not crash.
9
+ We'll capture stdout to verify it contains 'usage' or 'help'.
10
+ """
11
+ cmd = [sys.executable, "main.py", "-h"]
12
+ result = subprocess.run(cmd, capture_output=True, text=True)
13
+ assert result.returncode == 0
14
+ assert "usage" in result.stdout.lower() or "help" in result.stdout.lower()
15
+
16
+ @pytest.mark.parametrize("flag", ["-l", "-d"])
17
+ def test_missing_required_args(flag):
18
+ """
19
+ Test that missing required argument -l triggers an error.
20
+ We'll check for returncode != 0.
21
+ """
22
+ # When we run the script without -l or incomplete arguments, it should fail
23
+ cmd = [sys.executable, "main.py"]
24
+ result = subprocess.run(cmd, capture_output=True, text=True)
25
+ assert result.returncode != 0
26
+ assert "error" in result.stderr.lower()
@@ -0,0 +1,35 @@
1
+ import os
2
+ import pytest
3
+ from pathlib import Path
4
+ from components import mainFunctions as mf
5
+
6
+ def test_create_folder(tmp_path):
7
+ """Test that create_folder() actually creates the given folder."""
8
+ test_dir = tmp_path / "test_folder"
9
+ assert not test_dir.exists()
10
+ mf.create_folder(str(test_dir))
11
+ assert test_dir.exists()
12
+
13
+ def test_folder_size(tmp_path):
14
+ """Test folder_size() returns correct size in KB."""
15
+ test_dir = tmp_path / "test_folder2"
16
+ mf.create_folder(str(test_dir))
17
+
18
+ # Create a small file (1KB of data)
19
+ file_path = test_dir / "dummy.txt"
20
+ with open(file_path, "wb") as f:
21
+ f.write(b'X' * 1024)
22
+
23
+ result = mf.folder_size(str(test_dir))
24
+ # Should be around 1KB. We'll just check if the substring is in the output
25
+ assert "1.0 KB" in result or "1.0" in result
26
+
27
+ def test_get_percentage_difference():
28
+ """Check the logic for get_percentage_difference."""
29
+ # original_size=100, new_size=50 => saves 100%
30
+ diff = mf.get_percentage_difference(num_a=100, num_b=50)
31
+ assert diff == 100.0
32
+
33
+ # original=50, new=100 => -50% (i.e., the new is bigger by 50%)
34
+ diff = mf.get_percentage_difference(num_a=50, num_b=100)
35
+ assert diff == -50.0
@@ -0,0 +1,13 @@
1
+ import os
2
+ from components import setConfigurations
3
+
4
+ def test_get_resources():
5
+ """Check if configurations.ini is loaded properly."""
6
+ config = setConfigurations.get_resources()
7
+ # We expect certain sections to exist
8
+ assert 'default_parameters' in config
9
+ assert 'supported_formats' in config
10
+ assert 'logs' in config
11
+
12
+ # Check a known key
13
+ assert 'max_width' in config['default_parameters']