Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 18 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
<img src="https://raw.githubusercontent.com/Pianonic/CropTransparent/main/assets/CropTransparentBorder.png" width="200" alt="CropTransparent Logo">
</p>
<p align="center">
<strong>A modern, lightweight web application for automatically cropping transparent areas from PNG, GIF, and WEBP images.</strong>
Built with Flask and containerized with Docker for easy deployment.
<strong>A lightweight web app for automatically cropping images - removes transparent areas and uniform backgrounds.</strong>
Built with Flask and Docker.
</p>
<p align="center">
<a href="https://github.com/Pianonic/CropTransparent"><img src="https://badgetrack.pianonic.ch/badge?tag=crop-transparent&label=visits&color=f87171&style=flat" alt="visits" /></a>
Expand All @@ -14,11 +14,12 @@
</p>

## 🚀 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)
<p align="center">
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
58 changes: 48 additions & 10 deletions src/controllers/image_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
68 changes: 53 additions & 15 deletions src/services/image_service.py
Original file line number Diff line number Diff line change
@@ -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))

Expand All @@ -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')
Expand Down Expand Up @@ -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

Expand All @@ -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
return output, original_size, cropped_size, "color_background", background_color, final_format
30 changes: 9 additions & 21 deletions src/templates/about.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>About - Transparent Image Cropper</title>
<title>About - Smart Image Cropper</title>
<script src="{{ url_for('static', filename='js/theme-init.js') }}"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
Expand All @@ -27,43 +27,31 @@
<div class="max-w-3xl mx-auto pt-6">
<div class="text-center mb-4 md:mb-6">
<h1 class="text-2xl md:text-3xl font-bold text-text-heading mb-1 md:mb-2">About CropTransparent</h1>
<p class="text-sm md:text-base text-text-secondary">The fast and simple transparent image cropping tool
</p>
<p class="text-sm md:text-base text-text-secondary">Fast and simple image cropping</p>
</div>

<div class="bg-secondary rounded-lg shadow-lg p-5 md:p-6 transition-colors duration-300 ease-in-out">
<div class="prose mx-auto text-text-primary dark:prose-invert">
<h2 class="text-xl font-semibold mb-3 text-text-heading">What is CropTransparent?</h2>
<p class="mb-4 text-text-secondary">
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.
</p>

<h2 class="text-xl font-semibold mb-3 text-text-heading">How It Works</h2>
<p class="mb-2 text-text-secondary">
The application uses the Python Imaging Library (PIL/Pillow) and NumPy to:
<p class="mb-4 text-text-secondary">
Uses PIL/Pillow and NumPy for smart detection:
<br><strong>Transparent images:</strong> Crops based on alpha channel
<br><strong>JPEG/opaque images:</strong> Detects uniform backgrounds from corners and crops
</p>
<ol class="list-decimal list-inside mb-4 space-y-1 text-text-secondary">
<li>Convert images to RGBA format if needed</li>
<li>Extract the alpha channel</li>
<li>Find the bounding box of non-transparent pixels</li>
<li>Crop the image to this bounding box</li>
<li>Deliver the result directly to your browser</li>
</ol>

<h2 class="text-xl font-semibold mb-3 text-text-heading">Privacy & Security</h2>
<p class="mb-4 text-text-secondary">
User privacy is prioritized. Uploaded images are processed on the server
<strong>entirely in memory</strong>. 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.
</p>

<h2 class="text-xl font-semibold mb-3 text-text-heading">Open Source</h2>
<p class="mb-4 text-text-secondary">
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.
</p>

<div class="flex justify-center mt-6">
Expand Down
15 changes: 6 additions & 9 deletions src/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transparent Image Cropper</title>
<title>Smart Image Cropper</title>
<script src="{{ url_for('static', filename='js/theme-init.js') }}"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
Expand All @@ -28,9 +28,8 @@
<div class="flex-grow overflow-auto py-4 px-4 md:py-6 flex flex-col items-center justify-center main-content-padding">
<div class="max-w-3xl w-full mx-auto px-2 sm:px-4 pt-6">
<div class="text-center mb-4 md:mb-6">
<h1 class="text-2xl md:text-3xl font-bold text-text-heading mb-1 md:mb-2">Transparent Image Cropper</h1>
<p class="text-sm md:text-base text-text-secondary">Automatically crop transparent areas from your
images</p>
<h1 class="text-2xl md:text-3xl font-bold text-text-heading mb-1 md:mb-2">Smart Image Cropper</h1>
<p class="text-sm md:text-base text-text-secondary">Auto-crop transparent areas and backgrounds</p>
</div>

<div class="bg-secondary rounded-lg shadow-lg p-4 md:p-6 transition-colors duration-300 ease-in-out">
Expand All @@ -47,13 +46,11 @@ <h1 class="text-2xl md:text-3xl font-bold text-text-heading mb-1 md:mb-2">Transp
<div class="mt-2 md:mt-3 text-center">
<h3 class="text-2xl md:text-3xl text-text-heading mb-1 md:mb-2">Upload Image</h3>
<div class="mt-1 md:mt-2">
<p class="text-xs md:text-sm text-text-secondary">Drag and drop your image here or click to
browse
</p>
<p class="text-xs text-text-tertiary mt-1">Supports PNG, GIF, WEBP with transparency</p>
<p class="text-xs md:text-sm text-text-secondary">Drag and drop or click to browse</p>
<p class="text-xs text-text-tertiary mt-1">PNG, JPEG, GIF, WEBP supported</p>
</div>
</div>
<input type="file" id="fileInput" class="hidden" accept=".png,.gif,.webp">
<input type="file" id="fileInput" class="hidden" accept=".png,.jpg,.jpeg,.gif,.webp">
</div>

<div id="loadingSection" class="hidden mt-4 text-center">
Expand Down
Loading