202 lines
6.0 KiB
Python
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)
|