from bottle import Bottle, request, run, static_file, template, BaseRequest, response from PIL import Image import os import tempfile import zipfile import json from concurrent.futures import ProcessPoolExecutor from functools import partial from threading import Lock import shutil import tifffile import tifffile import subprocess import numpy as np def clear_temp(): try: shutil.rmtree(UPLOAD_DIR) print("🧹 Cache temporaire supprimé") except Exception as e: print(f"⚠️ Erreur suppression cache : {e}") # ⚙️ Configuration mémoire upload BaseRequest.MEMFILE_MAX = 5 * 1024 * 1024 * 1024 app = Bottle() def fresh_upload_dir(): path = tempfile.mkdtemp() os.makedirs(os.path.join(path, 'resized'), exist_ok=True) return path UPLOAD_DIR = fresh_upload_dir() OUTPUT_DIR = os.path.join(UPLOAD_DIR, 'resized') ZIP_PATH = os.path.join(UPLOAD_DIR, 'resized_images.zip') 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 indexed_resize(args): idx, file_path, output_dir, ratio, total = args return resize_image(file_path, output_dir, ratio, index=idx + 1, total=total) # 📊 Progression partagée progress_data = {"total": 0, "current": 0} progress_lock = Lock() # 📦 Route pour obtenir la progression en JSON @app.route('/progress') def progress(): response.content_type = 'application/json' with progress_lock: return json.dumps(progress_data) # 🖼️ Vérifie et traite une image def resize_image(filepath, output_dir, ratio, index=None, total=None): try: name, ext = os.path.splitext(os.path.basename(filepath)) ext = ext.lower() # 🧠 Image valide with Image.open(filepath) as test_img: test_img.verify() except Exception: print(f"⛔️ Ignoré (non image ou corrompu) : {filepath}") return None try: output_path = None if ext in ['.tif', '.tiff']: # ✅ Lecture via tifffile original_array = tifffile.imread(filepath) img = Image.fromarray(original_array) # Redimensionner 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) # Convertir en array NumPy et écrire resized_array = np.array(img) output_path = os.path.join(output_dir, f"{name}_resized.tif") tifffile.imwrite(output_path, resized_array) # ✅ Copier les EXIF depuis original subprocess.run([ 'exiftool', '-overwrite_original', f'-tagsFromFile={filepath}', output_path ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) else: # ✅ JPEG, PNG, etc. with Image.open(filepath) as img: exif_data = img.info.get('exif') img = img.convert('RGB') 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) output_path = os.path.join(output_dir, f"{name}_resized.jpg") img.save(output_path, quality=85, optimize=True, exif=exif_data or b"") # ✅ Affichage progression if index and total: print(f"✅ Traitement {index}/{total} : {os.path.basename(output_path)}") with progress_lock: progress_data["current"] += 1 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)) 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) with progress_lock: progress_data["total"] = len(saved_paths) progress_data["current"] = 0 args_list = [ (idx, path, OUTPUT_DIR, ratio, len(saved_paths)) for idx, path in enumerate(saved_paths) ] with ProcessPoolExecutor() as executor: resized_files = list(executor.map(indexed_resize, args_list)) with zipfile.ZipFile(ZIP_PATH, 'w') as zipf: for path in resized_files: if path: 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é ❌" # ⚠️ Sauvegarder ZIP dans une variable avant suppression zip_file = static_file('resized_images.zip', root=UPLOAD_DIR, download='images_reduites.zip') # 🔥 Nettoyage asynchrone (on ne bloque pas la réponse) import threading threading.Timer(2.0, clear_temp).start() # Attend 2s puis supprime return zip_file @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='0.0.0.0', port=4000, debug=True, reloader=True)