Files
ImageReducer/app.py

202 lines
6.0 KiB
Python

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/<filename>')
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)