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
3 changes: 1 addition & 2 deletions .github/workflows/docker-publish-multiarch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ jobs:
context: .
file: ./Dockerfile
# Specify the platforms to build for
platforms: linux/amd64,linux/arm64 # Common platforms
# You can add more like: linux/arm/v7
platforms: linux/amd64,linux/arm64
push: true # Push the multi-arch manifest list
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.11-slim
FROM python:3.13.5-slim

WORKDIR /app

Expand All @@ -11,4 +11,4 @@ COPY . .

EXPOSE 5000

CMD ["python", "wsgi.py"]
CMD ["python", "asgi.py"]
6 changes: 6 additions & 0 deletions asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import uvicorn
from src.app import app

if __name__ == "__main__":
print("Starting Transparent Image Cropper FastAPI app on http://0.0.0.0:5000")
uvicorn.run(app, host="0.0.0.0", port=5000)
13 changes: 7 additions & 6 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Flask==2.3.3
Pillow==10.0.0
numpy==1.26.0
waitress==2.1.2
Werkzeug==2.3.7
python-dotenv==1.0.0
fastapi==0.116.1
uvicorn==0.35.0
Pillow==11.3.0
numpy==2.3.2
python-dotenv==1.1.1
jinja2==3.1.6
python-multipart==0.0.20
24 changes: 20 additions & 4 deletions src/app.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
from flask import Flask
from werkzeug.middleware.proxy_fix import ProxyFix
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from src.controllers.image_controller import register_routes
from dotenv import load_dotenv
import os

# Load environment variables from .env file
load_dotenv()

# Create and configure the app
app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app)
app = FastAPI(
title="Smart Image Cropper API",
description="API for automatically cropping transparent areas and backgrounds from images",
version="1.0.0"
)

# Mount static files with absolute path
static_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
app.mount("/static", StaticFiles(directory=static_dir), name="static")

# Configure templates with absolute path
templates_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates")
templates = Jinja2Templates(directory=templates_dir)

# Make templates available to controllers
app.state.templates = templates

# Register routes
register_routes(app)
217 changes: 105 additions & 112 deletions src/controllers/image_controller.py
Original file line number Diff line number Diff line change
@@ -1,121 +1,114 @@
from flask import Flask, request, send_file, render_template, jsonify
from fastapi import APIRouter, Request, UploadFile, File, HTTPException
from fastapi.responses import JSONResponse, StreamingResponse, HTMLResponse
from src.services.image_service import auto_crop_image
import base64
import io
import os

def register_routes(app):
@app.route('/')
def index():
return render_template('index.html')

@app.route('/about')
def about():
return render_template('about.html')

@app.route('/api/app-info')
def app_info():
environment = os.environ.get('FLASK_ENV', 'unknown environment')
version = os.environ.get('APP_VERSION', 'unknown version')

return jsonify({
"environment": environment,
"version": version
})
router = APIRouter()

@router.get("/", response_class=HTMLResponse, include_in_schema=False)
def index(request: Request):
templates = request.app.state.templates
environment = os.environ.get('FLASK_ENV', 'unknown')
version = os.environ.get('APP_VERSION', 'unknown')
version_url = f"https://github.com/Pianonic/CropTransparent/releases/tag/{version}"
return templates.TemplateResponse("index.html", {
"request": request,
"environment": environment,
"version": version,
"version_url": version_url
})

@router.get("/about", response_class=HTMLResponse, include_in_schema=False)
def about(request: Request):
templates = request.app.state.templates
environment = os.environ.get('FLASK_ENV', 'unknown')
version = os.environ.get('APP_VERSION', 'unknown')
version_url = f"https://github.com/Pianonic/CropTransparent/releases/tag/{version}"
return templates.TemplateResponse("about.html", {
"request": request,
"environment": environment,
"version": version,
"version_url": version_url
})

@router.get("/api/app-info", tags=["App Info"])
def app_info():
environment = os.environ.get('FLASK_ENV', 'unknown environment')
version = os.environ.get('APP_VERSION', 'unknown version')
return {"environment": environment, "version": version}

@app.route('/process', methods=['POST'])
def process_image():
if 'file' not in request.files:
return jsonify({"error": "No file part"}), 400
file = request.files['file']
if file.filename == '':
return jsonify({"error": "No selected file"}), 400

try:
image_data = file.read()
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 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()
@router.post("/api/process", tags=["Image Processing"])
async def process_image(file: UploadFile = File(...)):
if not file:
raise HTTPException(status_code=400, detail="No file part")
if file.filename == '':
raise HTTPException(status_code=400, detail="No selected file")
try:
image_data = await file.read()
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 or 'image.png'
original_extension = filename.rsplit('.', 1)[1].lower() if '.' in filename else 'png'
if output_format:
extension = output_format.lower()
if extension == 'jpeg':
extension = 'jpg'
else:
if original_extension not in ['png', 'gif', 'webp', 'jpg', 'jpeg']:
extension = 'png'
else:
extension = original_extension
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/{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,
"output_format": extension
}

if background_info:
response_data["background_color"] = background_info

return jsonify(response_data)
except Exception as e:
return jsonify({"error": str(e)}), 500
mime_type = 'png'
if extension == 'jpg':
mime_type = 'jpeg'
elif extension in ['gif', 'webp']:
mime_type = extension
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/{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,
"output_format": extension
}
if background_info:
response_data["background_color"] = background_info
return JSONResponse(content=response_data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

@app.route('/download', methods=['POST'])
def download_image():
try:
data = request.get_json()
if not data or 'image' not in data or 'filename' not in data:
return jsonify({"error": "Missing data"}), 400

image_data = data['image'].split(',')[1]
image_binary = base64.b64decode(image_data)

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=filename,
as_attachment=True,
mimetype=mime_type
)
except Exception as e:
return jsonify({"error": str(e)}), 500
@router.post("/api/download", tags=["Image Processing"])
async def download_image(data: dict):
try:
if not data or 'image' not in data or 'filename' not in data:
raise HTTPException(status_code=400, detail="Missing data")
image_data = data['image'].split(',')[1]
image_binary = base64.b64decode(image_data)
output = io.BytesIO(image_binary)
output.seek(0)
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 StreamingResponse(output, media_type=mime_type, headers={
"Content-Disposition": f"attachment; filename={filename}"
})
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

def register_routes(app):
app.include_router(router)
47 changes: 4 additions & 43 deletions src/static/js/app-info.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,7 @@
document.addEventListener('DOMContentLoaded', () => {
async function loadAppInfo() {
try {
const response = await fetch('/api/app-info');
if (response.ok) {
const info = await response.json();
const versionElement = document.getElementById('version');
const environmentElement = document.getElementById('environment');

if (versionElement) {
versionElement.textContent = info.version || 'N/A';
if (info.version) {
versionElement.href = `https://github.com/Pianonic/CropTransparent/releases/tag/${info.version}`;
} else {
versionElement.removeAttribute('href');
versionElement.style.pointerEvents = 'none';
}
} else {
console.warn('Footer element with ID "version" not found.');
}

if (environmentElement) {
environmentElement.textContent = info.environment || 'N/A';
} else {
console.warn('Footer element with ID "environment" not found.');
}
} else {
console.error(
`Failed to load app info: ${response.status} ${response.statusText}`
);
const versionElement = document.getElementById('version');
const environmentElement = document.getElementById('environment');
if (versionElement) versionElement.textContent = 'Error';
if (environmentElement) environmentElement.textContent = 'Error';
}
} catch (error) {
console.error('Failed to load app info:', error);
const versionElement = document.getElementById('version');
const environmentElement = document.getElementById('environment');
if (versionElement) versionElement.textContent = 'Error';
if (environmentElement) environmentElement.textContent = 'Error';
}
// Set current year in footer
const currentYearElement = document.getElementById('currentYear');
if (currentYearElement) {
currentYearElement.textContent = new Date().getFullYear();
}

loadAppInfo();
});
4 changes: 2 additions & 2 deletions src/static/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class ImageProcessor {
formData.append('file', file);

try {
const response = await fetch('/process', {
const response = await fetch('/api/process', {
method: 'POST',
body: formData,
});
Expand Down Expand Up @@ -131,7 +131,7 @@ class ImageProcessor {
if (!this.processedImage || !this.processedFilename) return;

try {
const response = await fetch('/download', {
const response = await fetch('/api/download', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
Loading
Loading