Ajout de la prise en charge des fichiers TIFF, amélioration de la gestion de la progression et mise à jour du Dockerfile pour inclure exiftool

This commit is contained in:
Aristide Manyessé
2025-07-17 14:20:00 +00:00
parent d873dd118a
commit 4d1361057a
6 changed files with 241 additions and 83 deletions

View File

@@ -6,6 +6,7 @@ name = "pypi"
[packages]
bottle = "*"
pillow = "*"
tifffile = "*"
[dev-packages]

62
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "32de86725c93602d28c892b89df1895b4d6e587b1c4dc93365217e5146a0812a"
"sha256": "3b903e30841d47458bcc4b52b597eb6a669b5a2e24c886664c4181923959f1e6"
},
"pipfile-spec": 6,
"requires": {
@@ -24,6 +24,57 @@
"index": "pypi",
"version": "==0.13.4"
},
"numpy": {
"hashes": [
"sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a",
"sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195",
"sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951",
"sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1",
"sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c",
"sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc",
"sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b",
"sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd",
"sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4",
"sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd",
"sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318",
"sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448",
"sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece",
"sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d",
"sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5",
"sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8",
"sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57",
"sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78",
"sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66",
"sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a",
"sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e",
"sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c",
"sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa",
"sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d",
"sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c",
"sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729",
"sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97",
"sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c",
"sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9",
"sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669",
"sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4",
"sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73",
"sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385",
"sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8",
"sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c",
"sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b",
"sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692",
"sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15",
"sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131",
"sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a",
"sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326",
"sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b",
"sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded",
"sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04",
"sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd"
],
"markers": "python_version >= '3.9'",
"version": "==2.0.2"
},
"pillow": {
"hashes": [
"sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2",
@@ -136,6 +187,15 @@
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==11.3.0"
},
"tifffile": {
"hashes": [
"sha256:2c9508fe768962e30f87def61819183fb07692c258cb175b3c114828368485a4",
"sha256:8bc59a8f02a2665cd50a910ec64961c5373bee0b8850ec89d3b7b485bf7be7ad"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==2024.8.30"
}
},
"develop": {}

162
app.py
View File

@@ -1,58 +1,149 @@
from bottle import Bottle, response, request, run, static_file, template, BaseRequest
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
BaseRequest.MEMFILE_MAX = 100 * 1024 * 1024 # Supporte jusqu'à 100 Mo
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()
UPLOAD_DIR = tempfile.mkdtemp()
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 resize_image(filepath, output_dir, ratio):
from PIL import Image # Re-importé dans chaque processus
import os
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:
with Image.open(filepath) as img:
exif_data = img.info.get('exif')
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)
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
# 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))
if not files:
return "Aucun fichier reçu."
saved_paths = []
for file in files:
filename = os.path.basename(file.filename)
@@ -60,19 +151,24 @@ def upload():
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))
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))
# 📦 Création du ZIP
with zipfile.ZipFile(ZIP_PATH, 'w') as zipf:
for path in resized_files:
if path: # Skip si erreur
if path:
zipf.write(path, arcname=os.path.basename(path))
return template(
'result.tpl',
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]
@@ -83,7 +179,15 @@ def upload():
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')
# ⚠️ 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>')
@@ -93,7 +197,5 @@ def serve_resized(filename):
response.headers['Expires'] = '0'
return static_file(filename, root=OUTPUT_DIR)
if __name__ == "__main__":
run(app, host='localhost', port=8080, debug=True)
run(app, host='0.0.0.0', port=4000, debug=True, reloader=True)

View File

@@ -3,7 +3,7 @@ FROM python:3.11-slim
# 📦 Installe les dépendances système pour Pillow
RUN apt-get update && apt-get install -y \
libjpeg-dev zlib1g-dev libpng-dev \
libjpeg-dev zlib1g-dev libpng-dev exiftool libimage-exiftool-perl \
&& rm -rf /var/lib/apt/lists/*
# 📁 Dossier de travail dans le conteneur

View File

@@ -1,13 +1,12 @@
<!-- views/index.tpl -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Réduction d'images</title>
<title>Réduction dimages</title>
<style>
body {
font-family: 'Segoe UI', sans-serif;
background: #f0f4f8;
background: #f8f9fa;
padding: 40px;
}
@@ -52,29 +51,25 @@
cursor: pointer;
}
button:hover {
background-color: #1d4ed8;
}
.loader {
display: none;
text-align: center;
margin-top: 40px;
}
.loader div {
border: 8px solid #f3f3f3;
border-top: 8px solid #2563eb;
border-radius: 50%;
width: 60px;
height: 60px;
animation: spin 1s linear infinite;
margin: auto;
.progress-bar-container {
width: 100%;
background-color: #e0e0e0;
border-radius: 6px;
height: 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
.progress-bar {
height: 100%;
background-color: #2563eb;
width: 0%;
border-radius: 6px;
transition: width 0.3s ease;
}
</style>
</head>
@@ -88,28 +83,47 @@
<label>Ratio de réduction :</label>
<select name="ratio" required>
<option value="1">1 (original)</option>
<option value="1.5">1.5 (réduction légère)</option>
<option value="1.5">1.5 (léger)</option>
<option value="2" selected>2 (moitié)</option>
<option value="3">3 (forte)</option>
<option value="4">4 (très forte)</option>
<option value="3">3 (fort)</option>
<option value="4">4 (très fort)</option>
</select>
<button type="submit">📥 Réduire et télécharger</button>
</form>
<div class="loader" id="loader">
<p>Traitement des images en cours... Patientez ⏳</p>
<div></div>
<p id="progressText">Traitement des images en cours...</p>
<div class="progress-bar-container">
<div class="progress-bar" id="progressBar"></div>
</div>
</div>
</div>
<script>
const form = document.getElementById("uploadForm");
const loader = document.getElementById("loader");
const progressBar = document.getElementById("progressBar");
const progressText = document.getElementById("progressText");
form.addEventListener("submit", function() {
form.style.display = "none";
loader.style.display = "block";
const interval = setInterval(async () => {
const res = await fetch('/progress');
const data = await res.json();
if (data.total === 0) return;
const percent = Math.floor((data.current / data.total) * 100);
progressBar.style.width = percent + '%';
progressText.textContent = `Traitement des images... ${data.current} / ${data.total}`;
if (data.current >= data.total) {
clearInterval(interval);
}
}, 500);
});
</script>
</body>

View File

@@ -1,13 +1,9 @@
<!-- views/result.tpl -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Téléchargement prêt</title>
<!-- Lightbox CSS -->
<title>Vos images sont prêtes</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.4/css/lightbox.min.css" rel="stylesheet">
<style>
body {
font-family: 'Segoe UI', sans-serif;
@@ -24,16 +20,7 @@
text-align: center;
}
h1 {
color: #1e3a8a;
}
.info {
background-color: #f1f5f9;
padding: 15px;
border-radius: 8px;
margin-bottom: 30px;
}
h1 { color: #1e3a8a; }
.gallery {
display: flex;
@@ -47,11 +34,6 @@
width: 80px;
border-radius: 6px;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
transition: transform 0.2s ease;
}
.gallery a img:hover {
transform: scale(1.05);
}
a.download-btn {
@@ -65,32 +47,31 @@
font-weight: bold;
}
a.download-btn:hover {
background-color: #1d4ed8;
.info {
margin-bottom: 25px;
font-size: 18px;
}
</style>
</head>
<body>
<div class="container">
<h1>📦 Vos images sont prêtes !</h1>
<div class="info">
<p><strong>{{ count }}</strong> images ont été redimensionnées</p>
<p>Ratio appliqué : <strong>{{ ratio }}</strong></p>
<p>{{ count }} images redimensionnées</p>
<p>Ratio utilisé : {{ ratio }}</p>
</div>
<div class="gallery">
% for img in images:
<a href="/resized/{{ img }}" data-lightbox="gallery" data-title="{{ img }}">
<img src="/resized/{{ img }}" alt="{{ img }}">
<img src="/resized/{{ img }}">
</a>
% end
</div>
<a class="download-btn" href="/download">⬇️ Télécharger les images réduites</a>
<a class="download-btn" href="/download">⬇️ Télécharger toutes les images</a>
</div>
<!-- Lightbox JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.4/js/lightbox.min.js"></script>
</body>
</html>