diff --git a/README.md b/README.md index 2b63e5a..08bd0c0 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@ CropTransparent Logo

- 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.

visits @@ -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

-
    -
  1. Convert images to RGBA format if needed
  2. -
  3. Extract the alpha channel
  4. -
  5. Find the bounding box of non-transparent pixels
  6. -
  7. Crop the image to this bounding box
  8. -
  9. Deliver the result directly to your browser
  10. -

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.

diff --git a/src/templates/index.html b/src/templates/index.html index 353f749..54d9d13 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -4,7 +4,7 @@ - Transparent Image Cropper + Smart Image Cropper @@ -28,9 +28,8 @@
-

Transparent Image Cropper

-

Automatically crop transparent areas from your - images

+

Smart Image Cropper

+

Auto-crop transparent areas and backgrounds

@@ -47,13 +46,11 @@

Transp

Upload Image

-

Drag and drop your image here or click to - browse -

-

Supports PNG, GIF, WEBP with transparency

+

Drag and drop or click to browse

+

PNG, JPEG, GIF, WEBP supported

- +