- A modern, lightweight web application for automatically cropping transparent areas from PNG, GIF, and WEBP images.
- Built with Flask and containerized with Docker for easy deployment.
+ A lightweight web app for automatically cropping images - removes transparent areas and uniform backgrounds.
+ Built with Flask and Docker.
@@ -14,11 +14,12 @@
## 🚀 Features
-- **Fast In-Memory Processing**: Images are processed in memory without saving to disk
-- **Drag & Drop Interface**: Easy to use with drag and drop or file selection
-- **Instant Preview**: See the results immediately before downloading
-- **Size Comparison**: View the original and cropped dimensions with saved space calculation
-- **Docker Ready**: Easy deployment with Docker and Docker Compose
+- **Smart Cropping**: Auto-crops transparency (PNG, GIF, WEBP) or uniform backgrounds (JPEG)
+- **Multiple Formats**: PNG, JPEG, GIF, WEBP support
+- **Fast Processing**: In-memory processing, no disk storage
+- **Drag & Drop**: Simple file upload interface
+- **Format Preservation**: JPEG stays JPEG, PNG stays PNG
+- **Docker Ready**: Easy deployment
## 📸 Screenshots (Light and Darkmode)
@@ -56,7 +57,7 @@ Use your favorite editor to create a `compose.yaml` file and paste this into it:
services:
croptransparent:
image: pianonic/croptransparent:latest # Uses the image from Docker Hub
- # image: ghcr.io/pianonic/croptransparent:latest # Uses the image from GiitHub Container Registry
+ # image: ghcr.io/pianonic/croptransparent:latest # Uses the image from GitHub Container Registry
ports:
- "5000:5000"
restart: unless-stopped
@@ -91,33 +92,22 @@ python wsgi.py
The application will be available at [http://localhost:5000](http://localhost:5000) (or the port configured in `wsgi.py`).
## 🛠️ Usage
-1. Upload a transparent image (PNG, GIF, WEBP) by dragging and dropping or using the file browser.
-2. The application automatically finds the bounding box of non-transparent pixels and crops the image.
-3. Preview the cropped result and compare its dimensions to the original.
-4. Download the optimized image with a single click.
+1. Upload an image (drag & drop or browse)
+2. App automatically detects and crops transparent areas or uniform backgrounds
+3. Preview and download the cropped result
## ⚙️ Technical Details
### Image Processing
-The application uses the Python Imaging Library (PIL/Pillow) and NumPy to:
-1. Convert images to RGBA format if needed.
-2. Extract the alpha channel.
-3. Find the bounding box of non-transparent pixels using NumPy.
-4. Crop the image to this bounding box using Pillow.
-5. Encode the result and deliver it directly to the user's browser without saving to disk.
-
-### Security
-User privacy is prioritized. Uploaded images are processed on the server **entirely in memory**. No original or cropped images are saved to the server's disk. The resulting image is streamed directly back to the user's browser. When the application is self-hosted, images are processed locally and do not leave the user's machine.
+Uses PIL/Pillow and NumPy for smart cropping:
+- **Transparent images**: Crops based on alpha channel
+- **Opaque images**: Detects uniform background from corners and crops
+- **Security**: All processing in memory, no files saved to disk
## 📋 Requirements
- Python 3.8+
-- Docker and Docker Compose (for containerized deployment)
-- Dependencies listed in `requirements.txt`:
- - Flask
- - Pillow
- - NumPy
- - Waitress (for production WSGI serving)
- - Werkzeug (Flask dependency)
+- Docker (for containerized deployment)
+- Dependencies: Flask, Pillow, NumPy, Waitress
## 📜 License
This project is licensed under the MIT License.
diff --git a/src/controllers/image_controller.py b/src/controllers/image_controller.py
index 9fb620d..433056a 100644
--- a/src/controllers/image_controller.py
+++ b/src/controllers/image_controller.py
@@ -34,23 +34,47 @@ def process_image():
try:
image_data = file.read()
- output_buffer, original_size, cropped_size, crop_method, background_info = auto_crop_image(image_data)
+ output_buffer, original_size, cropped_size, crop_method, background_info, output_format = auto_crop_image(image_data)
encoded = base64.b64encode(output_buffer.getvalue()).decode('utf-8')
output_buffer.seek(0)
- filename = file.filename
- extension = filename.rsplit('.', 1)[1].lower() if '.' in filename else 'png'
- if extension not in ['png', 'gif', 'webp']:
- extension = 'png'
+ filename = file.filename or 'image.png'
+ original_extension = filename.rsplit('.', 1)[1].lower() if '.' in filename else 'png'
+
+ # Use the output format from the service, but map it to lowercase for consistency
+ if output_format:
+ extension = output_format.lower()
+ if extension == 'jpeg':
+ extension = 'jpg'
+ else:
+ # Fallback logic
+ if original_extension not in ['png', 'gif', 'webp', 'jpg', 'jpeg']:
+ extension = 'png'
+ else:
+ extension = original_extension
+ if extension == 'jpeg':
+ extension = 'jpg'
+
+ # Set proper MIME type
+ mime_type = 'png'
+ if extension == 'jpg':
+ mime_type = 'jpeg'
+ elif extension in ['gif', 'webp']:
+ mime_type = extension
+
+ # Create output filename
+ base_name = filename.rsplit('.', 1)[0] if '.' in filename else filename
+ output_filename = f"cropped_{base_name}.{extension}"
response_data = {
"success": True,
- "image": f"data:image/{extension};base64,{encoded}",
- "filename": f"cropped_{filename}",
+ "image": f"data:image/{mime_type};base64,{encoded}",
+ "filename": output_filename,
"original_size": f"{original_size[0]}x{original_size[1]}",
"cropped_size": f"{cropped_size[0]}x{cropped_size[1]}",
- "crop_method": crop_method
+ "crop_method": crop_method,
+ "output_format": extension
}
if background_info:
@@ -73,11 +97,25 @@ def download_image():
output = io.BytesIO(image_binary)
output.seek(0)
+ # Determine MIME type from filename extension
+ filename = data['filename']
+ extension = filename.rsplit('.', 1)[1].lower() if '.' in filename else 'png'
+
+ mime_type_map = {
+ 'png': 'image/png',
+ 'jpg': 'image/jpeg',
+ 'jpeg': 'image/jpeg',
+ 'gif': 'image/gif',
+ 'webp': 'image/webp'
+ }
+
+ mime_type = mime_type_map.get(extension, 'image/png')
+
return send_file(
output,
- download_name=data['filename'],
+ download_name=filename,
as_attachment=True,
- mimetype='image/png'
+ mimetype=mime_type
)
except Exception as e:
return jsonify({"error": str(e)}), 500
\ No newline at end of file
diff --git a/src/services/image_service.py b/src/services/image_service.py
index b61da0a..5297b64 100644
--- a/src/services/image_service.py
+++ b/src/services/image_service.py
@@ -1,8 +1,9 @@
from PIL import Image
+from PIL.Image import Image as PILImage
import numpy as np
import io
-def crop_transparent_image(image_data):
+def crop_transparent_image(image_data, output_format='PNG'):
"""Crops transparent areas from an image."""
img = Image.open(io.BytesIO(image_data))
@@ -22,20 +23,22 @@ def crop_transparent_image(image_data):
original_size = img.size
cropped_size = cropped.size
+ # Ensure PNG format for transparent images
output = io.BytesIO()
- cropped.save(output, format=img.format if img.format else 'PNG')
+ cropped.save(output, format='PNG')
output.seek(0)
- return output, original_size, cropped_size
+ return output, original_size, cropped_size, 'PNG'
else:
output = io.BytesIO()
- img.save(output, format=img.format if img.format else 'PNG')
+ img.save(output, format='PNG')
output.seek(0)
- return output, img.size, img.size
+ return output, img.size, img.size, 'PNG'
-def crop_by_background_color(image_data, threshold=30, corner_offset=5):
+def crop_by_background_color(image_data, threshold=30, corner_offset=5, output_format=None):
"""Crops background areas based on corner color sampling."""
img = Image.open(io.BytesIO(image_data))
+ original_format = img.format
if img.mode not in ['RGB', 'RGBA']:
img = img.convert('RGB')
@@ -87,20 +90,52 @@ def crop_by_background_color(image_data, threshold=30, corner_offset=5):
original_size = img.size
cropped_size = cropped.size
+ # Determine output format
+ if output_format is None:
+ output_format = original_format if original_format in ['JPEG', 'PNG', 'WEBP', 'GIF'] else 'PNG'
+
+ # Convert to RGB for JPEG format
+ if output_format == 'JPEG' and cropped.mode in ['RGBA', 'LA']:
+ cropped = composite_on_white(cropped)
+
output = io.BytesIO()
- cropped.save(output, format=img.format if img.format else 'PNG')
+ save_kwargs = {'format': output_format}
+ if output_format == 'JPEG':
+ save_kwargs['quality'] = 95
+ save_kwargs['optimize'] = True
+
+ cropped.save(output, **save_kwargs)
output.seek(0)
- return output, original_size, cropped_size, background_color
+ return output, original_size, cropped_size, background_color, output_format
else:
+ # Convert to RGB for JPEG format
+ if output_format == 'JPEG' and img.mode in ['RGBA', 'LA']:
+ img = composite_on_white(img)
+
output = io.BytesIO()
- img.save(output, format=img.format if img.format else 'PNG')
+ save_kwargs = {'format': output_format if output_format else (original_format if original_format in ['JPEG', 'PNG', 'WEBP', 'GIF'] else 'PNG')}
+ if output_format == 'JPEG':
+ save_kwargs['quality'] = 95
+ save_kwargs['optimize'] = True
+
+ img.save(output, **save_kwargs)
output.seek(0)
- return output, img.size, img.size, background_color
+ return output, img.size, img.size, background_color, save_kwargs['format']
+
+def composite_on_white(img):
+ rgb_img = Image.new('RGB', img.size, 255)
+
+ if img.mode == 'RGBA':
+ rgb_img.paste(img, mask=img.split()[-1]) # Use alpha channel as mask
+ else:
+ rgb_img.paste(img)
+ return rgb_img
def auto_crop_image(image_data, threshold=30, corner_offset=5):
"""Automatically chooses between transparent or color background cropping."""
img = Image.open(io.BytesIO(image_data))
+ original_format = img.format
has_alpha = img.mode in ['RGBA', 'LA'] or 'transparency' in img.info
@@ -116,10 +151,13 @@ def auto_crop_image(image_data, threshold=30, corner_offset=5):
has_transparent_pixels = np.any(alpha == 0)
if has_meaningful_transparency or has_transparent_pixels:
- output, original_size, cropped_size = crop_transparent_image(image_data)
- return output, original_size, cropped_size, "transparent", None
+ output, original_size, cropped_size, output_format = crop_transparent_image(image_data)
+ return output, original_size, cropped_size, "transparent", None, output_format
+
+ # Determine output format for non-transparent images
+ output_format = original_format if original_format in ['JPEG', 'PNG', 'WEBP', 'GIF'] else 'PNG'
- output, original_size, cropped_size, background_color = crop_by_background_color(
- image_data, threshold, corner_offset
+ output, original_size, cropped_size, background_color, final_format = crop_by_background_color(
+ image_data, threshold, corner_offset, output_format
)
- return output, original_size, cropped_size, "color_background", background_color
\ No newline at end of file
+ return output, original_size, cropped_size, "color_background", background_color, final_format
\ No newline at end of file
diff --git a/src/templates/about.html b/src/templates/about.html
index 2971a9b..e248050 100644
--- a/src/templates/about.html
+++ b/src/templates/about.html
@@ -4,7 +4,7 @@
- About - Transparent Image Cropper
+ About - Smart Image Cropper
@@ -27,43 +27,31 @@
About CropTransparent
-
The fast and simple transparent image cropping tool
-
+
Fast and simple image cropping
What is CropTransparent?
- CropTransparent is a lightweight web application designed to automatically crop transparent
- areas from PNG, GIF, and WEBP images. It's built with a focus on speed, simplicity, and privacy.
+ A lightweight web app that automatically crops transparent areas from PNG/GIF/WEBP images and removes uniform backgrounds from JPEG images.
How It Works
-
- The application uses the Python Imaging Library (PIL/Pillow) and NumPy to:
+
+ Uses PIL/Pillow and NumPy for smart detection:
+ Transparent images: Crops based on alpha channel
+ JPEG/opaque images: Detects uniform backgrounds from corners and crops
-
-
Convert images to RGBA format if needed
-
Extract the alpha channel
-
Find the bounding box of non-transparent pixels
-
Crop the image to this bounding box
-
Deliver the result directly to your browser
-
Privacy & Security
- User privacy is prioritized. Uploaded images are processed on the server
- entirely in memory. No original or cropped images are saved to the
- server's disk. The resulting image is streamed directly back to the user's browser.
- When the application is self-hosted, images are processed locally and do not
- leave the user's machine.
+ All processing happens in memory. No images are saved to disk. Results are streamed directly to your browser.
Open Source
- CropTransparent is open source software, licensed under MIT. You can view, modify, and
- distribute the code according to the license terms. Contributions are welcome!
+ MIT licensed open source software. View, modify, and distribute freely.