from bottle import Bottle, response, request, run, static_file, template, BaseRequest from PIL import Image import os import tempfile import zipfile from concurrent.futures import ProcessPoolExecutor from functools import partial BaseRequest.MEMFILE_MAX = 100 * 1024 * 1024 # Supporte jusqu'à 100 Mo app = Bottle() UPLOAD_DIR = tempfile.mkdtemp() OUTPUT_DIR = os.path.join(UPLOAD_DIR, 'resized') ZIP_PATH = os.path.join(UPLOAD_DIR, 'resized_images.zip') os.makedirs(OUTPUT_DIR, exist_ok=True) def resize_image(filepath, output_dir, ratio): from PIL import Image # Re-importé dans chaque processus import os try: with Image.open(filepath) as img: exif_data = img.info.get('exif') width, height = img.size new_size = (int(width / ratio), int(height / ratio)) try: img = img.resize(new_size, Image.LANCZOS) except ValueError: img = Image.open(filepath).point(lambda x: x / 256).convert("L") img = img.resize(new_size, Image.LANCZOS) name, ext = os.path.splitext(os.path.basename(filepath)) output_path = os.path.join(output_dir, f"{name}_resized{ext}") img.save(output_path, quality=85, optimize=True, exif=exif_data or []) print(f"✅ Image enregistrée : {output_path} === ({width, height}) => ({new_size})") # DEBUG return output_path except Exception as e: print(f"❌ Erreur pour {filepath}: {e}") return None @app.route('/') def index(): return template('index.tpl') @app.post('/upload') def upload(): files = request.files.getall('files') ratio = float(request.forms.get('ratio', 2)) if not files: return "Aucun fichier reçu." saved_paths = [] for file in files: filename = os.path.basename(file.filename) save_path = os.path.join(UPLOAD_DIR, filename) file.save(save_path, overwrite=True) saved_paths.append(save_path) # 🧠 Traitement en parallèle resize_func = partial(resize_image, output_dir=OUTPUT_DIR, ratio=ratio) with ProcessPoolExecutor() as executor: resized_files = list(executor.map(resize_func, saved_paths)) # 📦 Création du ZIP with zipfile.ZipFile(ZIP_PATH, 'w') as zipf: for path in resized_files: if path: # Skip si erreur zipf.write(path, arcname=os.path.basename(path)) return template( 'result.tpl', count=len([p for p in resized_files if p]), ratio=ratio, images=[os.path.basename(p) for p in resized_files if p] ) @app.route('/download') def download(): if not os.path.exists(ZIP_PATH): return "ZIP non généré ❌" return static_file('resized_images.zip', root=UPLOAD_DIR, download='images_reduites.zip') @app.route('/resized/') def serve_resized(filename): response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' response.headers['Pragma'] = 'no-cache' response.headers['Expires'] = '0' return static_file(filename, root=OUTPUT_DIR) if __name__ == "__main__": run(app, host='localhost', port=8080, debug=True)