import os import zipfile import io from flask import Flask, render_template, request, send_file, after_this_request from PIL import Image, ImageDraw, ImageFont, ImageOps import tempfile import shutil import time app = Flask(__name__) app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50MB limit def process_image(img, options): # 1. Strip EXIF (by creating a new image) if options.get('strip_exif'): data = list(img.getdata()) image_without_exif = Image.new(img.mode, img.size) image_without_exif.putdata(data) img = image_without_exif # 2. Resize resize_mode = options.get('resize_mode') if resize_mode == 'percentage': scale = int(options.get('resize_value', 100)) / 100.0 if scale != 1.0: new_size = (int(img.width * scale), int(img.height * scale)) img = img.resize(new_size, Image.Resampling.LANCZOS) elif resize_mode == 'width': target_width = int(options.get('resize_value', img.width)) if target_width != img.width: ratio = target_width / img.width new_size = (target_width, int(img.height * ratio)) img = img.resize(new_size, Image.Resampling.LANCZOS) # 3. Watermark watermark_text = options.get('watermark_text') if watermark_text: # Create a transparent layer txt_layer = Image.new('RGBA', img.size, (255, 255, 255, 0)) draw = ImageDraw.Draw(txt_layer) # Calculate font size (relative to image width) fontsize = int(img.width / 20) if fontsize < 10: fontsize = 10 try: # Try to load a default font, otherwise load default # On Linux/Docker, paths might differ. # We'll use default bitmap font if TTF not found or just try-except font = ImageFont.truetype("DejaVuSans.ttf", fontsize) except IOError: font = ImageFont.load_default() # Position: Bottom Right with padding # Get text size bbox = draw.textbbox((0, 0), watermark_text, font=font) textwidth = bbox[2] - bbox[0] textheight = bbox[3] - bbox[1] padding = 10 x = img.width - textwidth - padding y = img.height - textheight - padding # Draw semi-transparent text draw.text((x, y), watermark_text, font=font, fill=(255, 255, 255, 128)) if img.mode != 'RGBA': img = img.convert('RGBA') img = Image.alpha_composite(img, txt_layer) # 4. Format Conversion handled during save return img @app.route('/', methods=['GET']) def index(): return render_template('index.html') @app.route('/process', methods=['POST']) def process(): if 'files' not in request.files: return "No files uploaded", 400 files = request.files.getlist('files') if not files or files[0].filename == '': return "No files selected", 400 # Get options target_format = request.form.get('target_format', 'original') resize_mode = request.form.get('resize_mode', 'none') # none, percentage, width resize_value = request.form.get('resize_value', 0) watermark_text = request.form.get('watermark_text', '').strip() strip_exif = 'strip_exif' in request.form options = { 'resize_mode': resize_mode, 'resize_value': resize_value, 'watermark_text': watermark_text, 'strip_exif': strip_exif } # Create a temporary directory for processing temp_dir = tempfile.mkdtemp() try: zip_buffer = io.BytesIO() with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file: for file in files: if not file.filename: continue try: img = Image.open(file.stream) # Process img = process_image(img, options) # Determine output filename and format original_name = os.path.splitext(file.filename)[0] ext = os.path.splitext(file.filename)[1].lower() if target_format != 'original': ext = '.' + target_format.lower() if target_format.lower() == 'jpeg': img = img.convert('RGB') elif target_format.lower() == 'png': # Keep RGBA if possible, or convert if needed pass # Save to temp buffer img_byte_arr = io.BytesIO() save_format = ext.strip('.').upper() if save_format == 'JPG': save_format = 'JPEG' # Handle WEBP/JPEG quality if needed (using default for now) img.save(img_byte_arr, format=save_format) # Add to ZIP zip_file.writestr(f"processed/{original_name}{ext}", img_byte_arr.getvalue()) except Exception as e: print(f"Error processing {file.filename}: {e}") # Optionally add an error log to the zip zip_file.writestr(f"processed/errors/{file.filename}.txt", str(e)) zip_buffer.seek(0) # Cleanup temp dir (we didn't actually write files to disk, just memory, # but if we did, we'd clean here. The `img_byte_arr` approach avoids disk IO) return send_file( zip_buffer, mimetype='application/zip', as_attachment=True, download_name=f'processed_images_{int(time.time())}.zip' ) finally: shutil.rmtree(temp_dir) if __name__ == '__main__': port = int(os.environ.get('PORT', 7860)) app.run(host='0.0.0.0', port=port)