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:
1
Pipfile
1
Pipfile
@@ -6,6 +6,7 @@ name = "pypi"
|
|||||||
[packages]
|
[packages]
|
||||||
bottle = "*"
|
bottle = "*"
|
||||||
pillow = "*"
|
pillow = "*"
|
||||||
|
tifffile = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
|
|
||||||
|
|||||||
62
Pipfile.lock
generated
62
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "32de86725c93602d28c892b89df1895b4d6e587b1c4dc93365217e5146a0812a"
|
"sha256": "3b903e30841d47458bcc4b52b597eb6a669b5a2e24c886664c4181923959f1e6"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -24,6 +24,57 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.13.4"
|
"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": {
|
"pillow": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2",
|
"sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2",
|
||||||
@@ -136,6 +187,15 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.9'",
|
"markers": "python_version >= '3.9'",
|
||||||
"version": "==11.3.0"
|
"version": "==11.3.0"
|
||||||
|
},
|
||||||
|
"tifffile": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2c9508fe768962e30f87def61819183fb07692c258cb175b3c114828368485a4",
|
||||||
|
"sha256:8bc59a8f02a2665cd50a910ec64961c5373bee0b8850ec89d3b7b485bf7be7ad"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==2024.8.30"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"develop": {}
|
"develop": {}
|
||||||
|
|||||||
160
app.py
160
app.py
@@ -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
|
from PIL import Image
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
|
import json
|
||||||
from concurrent.futures import ProcessPoolExecutor
|
from concurrent.futures import ProcessPoolExecutor
|
||||||
from functools import partial
|
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()
|
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')
|
OUTPUT_DIR = os.path.join(UPLOAD_DIR, 'resized')
|
||||||
ZIP_PATH = os.path.join(UPLOAD_DIR, 'resized_images.zip')
|
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)
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||||
|
|
||||||
def resize_image(filepath, output_dir, ratio):
|
|
||||||
from PIL import Image # Re-importé dans chaque processus
|
def indexed_resize(args):
|
||||||
import os
|
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:
|
try:
|
||||||
with Image.open(filepath) as img:
|
output_path = None
|
||||||
exif_data = img.info.get('exif')
|
|
||||||
|
if ext in ['.tif', '.tiff']:
|
||||||
|
# ✅ Lecture via tifffile
|
||||||
|
original_array = tifffile.imread(filepath)
|
||||||
|
img = Image.fromarray(original_array)
|
||||||
|
|
||||||
|
# Redimensionner
|
||||||
width, height = img.size
|
width, height = img.size
|
||||||
new_size = (int(width / ratio), int(height / ratio))
|
new_size = (int(width / ratio), int(height / ratio))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
img = img.resize(new_size, Image.LANCZOS)
|
img = img.resize(new_size, Image.LANCZOS)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
img = Image.open(filepath).point(lambda x: x / 256).convert("L")
|
img = Image.open(filepath).point(lambda x: x / 256).convert("L")
|
||||||
img = img.resize(new_size, Image.LANCZOS)
|
img = img.resize(new_size, Image.LANCZOS)
|
||||||
|
|
||||||
name, ext = os.path.splitext(os.path.basename(filepath))
|
# Convertir en array NumPy et écrire
|
||||||
output_path = os.path.join(output_dir, f"{name}_resized{ext}")
|
resized_array = np.array(img)
|
||||||
img.save(output_path, quality=85, optimize=True, exif=exif_data or [])
|
output_path = os.path.join(output_dir, f"{name}_resized.tif")
|
||||||
print(f"✅ Image enregistrée : {output_path} === ({width, height}) => ({new_size})") # DEBUG
|
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
|
return output_path
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Erreur pour {filepath}: {e}")
|
print(f"❌ Erreur pour {filepath}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
return template('index.tpl')
|
return template('index.tpl')
|
||||||
|
|
||||||
|
|
||||||
@app.post('/upload')
|
@app.post('/upload')
|
||||||
def upload():
|
def upload():
|
||||||
files = request.files.getall('files')
|
files = request.files.getall('files')
|
||||||
ratio = float(request.forms.get('ratio', 2))
|
ratio = float(request.forms.get('ratio', 2))
|
||||||
|
|
||||||
if not files:
|
|
||||||
return "Aucun fichier reçu."
|
|
||||||
|
|
||||||
saved_paths = []
|
saved_paths = []
|
||||||
for file in files:
|
for file in files:
|
||||||
filename = os.path.basename(file.filename)
|
filename = os.path.basename(file.filename)
|
||||||
@@ -60,19 +151,24 @@ def upload():
|
|||||||
file.save(save_path, overwrite=True)
|
file.save(save_path, overwrite=True)
|
||||||
saved_paths.append(save_path)
|
saved_paths.append(save_path)
|
||||||
|
|
||||||
# 🧠 Traitement en parallèle
|
with progress_lock:
|
||||||
resize_func = partial(resize_image, output_dir=OUTPUT_DIR, ratio=ratio)
|
progress_data["total"] = len(saved_paths)
|
||||||
with ProcessPoolExecutor() as executor:
|
progress_data["current"] = 0
|
||||||
resized_files = list(executor.map(resize_func, saved_paths))
|
|
||||||
|
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:
|
with zipfile.ZipFile(ZIP_PATH, 'w') as zipf:
|
||||||
for path in resized_files:
|
for path in resized_files:
|
||||||
if path: # Skip si erreur
|
if path:
|
||||||
zipf.write(path, arcname=os.path.basename(path))
|
zipf.write(path, arcname=os.path.basename(path))
|
||||||
|
|
||||||
return template(
|
return template('result.tpl',
|
||||||
'result.tpl',
|
|
||||||
count=len([p for p in resized_files if p]),
|
count=len([p for p in resized_files if p]),
|
||||||
ratio=ratio,
|
ratio=ratio,
|
||||||
images=[os.path.basename(p) for p in resized_files if p]
|
images=[os.path.basename(p) for p in resized_files if p]
|
||||||
@@ -83,7 +179,15 @@ def upload():
|
|||||||
def download():
|
def download():
|
||||||
if not os.path.exists(ZIP_PATH):
|
if not os.path.exists(ZIP_PATH):
|
||||||
return "ZIP non généré ❌"
|
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>')
|
@app.route('/resized/<filename>')
|
||||||
@@ -93,7 +197,5 @@ def serve_resized(filename):
|
|||||||
response.headers['Expires'] = '0'
|
response.headers['Expires'] = '0'
|
||||||
return static_file(filename, root=OUTPUT_DIR)
|
return static_file(filename, root=OUTPUT_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
run(app, host='localhost', port=8080, debug=True)
|
run(app, host='0.0.0.0', port=4000, debug=True, reloader=True)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ FROM python:3.11-slim
|
|||||||
|
|
||||||
# 📦 Installe les dépendances système pour Pillow
|
# 📦 Installe les dépendances système pour Pillow
|
||||||
RUN apt-get update && apt-get install -y \
|
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/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# 📁 Dossier de travail dans le conteneur
|
# 📁 Dossier de travail dans le conteneur
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
<!-- views/index.tpl -->
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Réduction d'images</title>
|
<title>Réduction d’images</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', sans-serif;
|
font-family: 'Segoe UI', sans-serif;
|
||||||
background: #f0f4f8;
|
background: #f8f9fa;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,29 +51,25 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: #1d4ed8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader {
|
.loader {
|
||||||
display: none;
|
display: none;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader div {
|
.progress-bar-container {
|
||||||
border: 8px solid #f3f3f3;
|
width: 100%;
|
||||||
border-top: 8px solid #2563eb;
|
background-color: #e0e0e0;
|
||||||
border-radius: 50%;
|
border-radius: 6px;
|
||||||
width: 60px;
|
height: 20px;
|
||||||
height: 60px;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
margin: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
.progress-bar {
|
||||||
0% { transform: rotate(0deg); }
|
height: 100%;
|
||||||
100% { transform: rotate(360deg); }
|
background-color: #2563eb;
|
||||||
|
width: 0%;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -88,28 +83,47 @@
|
|||||||
<label>Ratio de réduction :</label>
|
<label>Ratio de réduction :</label>
|
||||||
<select name="ratio" required>
|
<select name="ratio" required>
|
||||||
<option value="1">1 (original)</option>
|
<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="2" selected>2 (moitié)</option>
|
||||||
<option value="3">3 (forte)</option>
|
<option value="3">3 (fort)</option>
|
||||||
<option value="4">4 (très forte)</option>
|
<option value="4">4 (très fort)</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<button type="submit">📥 Réduire et télécharger</button>
|
<button type="submit">📥 Réduire et télécharger</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="loader" id="loader">
|
<div class="loader" id="loader">
|
||||||
<p>Traitement des images en cours... Patientez ⏳</p>
|
<p id="progressText">Traitement des images en cours...</p>
|
||||||
<div></div>
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar" id="progressBar"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const form = document.getElementById("uploadForm");
|
const form = document.getElementById("uploadForm");
|
||||||
const loader = document.getElementById("loader");
|
const loader = document.getElementById("loader");
|
||||||
|
const progressBar = document.getElementById("progressBar");
|
||||||
|
const progressText = document.getElementById("progressText");
|
||||||
|
|
||||||
form.addEventListener("submit", function() {
|
form.addEventListener("submit", function() {
|
||||||
form.style.display = "none";
|
form.style.display = "none";
|
||||||
loader.style.display = "block";
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
<!-- views/result.tpl -->
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Téléchargement prêt</title>
|
<title>Vos images sont prêtes</title>
|
||||||
|
|
||||||
<!-- Lightbox CSS -->
|
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.4/css/lightbox.min.css" rel="stylesheet">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.4/css/lightbox.min.css" rel="stylesheet">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', sans-serif;
|
font-family: 'Segoe UI', sans-serif;
|
||||||
@@ -24,16 +20,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 { color: #1e3a8a; }
|
||||||
color: #1e3a8a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
background-color: #f1f5f9;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery {
|
.gallery {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -47,11 +34,6 @@
|
|||||||
width: 80px;
|
width: 80px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
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 {
|
a.download-btn {
|
||||||
@@ -65,32 +47,31 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.download-btn:hover {
|
.info {
|
||||||
background-color: #1d4ed8;
|
margin-bottom: 25px;
|
||||||
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>📦 Vos images sont prêtes !</h1>
|
<h1>📦 Vos images sont prêtes !</h1>
|
||||||
|
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<p><strong>{{ count }}</strong> images ont été redimensionnées</p>
|
<p>{{ count }} images redimensionnées</p>
|
||||||
<p>Ratio appliqué : <strong>{{ ratio }}</strong></p>
|
<p>Ratio utilisé : {{ ratio }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gallery">
|
<div class="gallery">
|
||||||
% for img in images:
|
% for img in images:
|
||||||
<a href="/resized/{{ img }}" data-lightbox="gallery" data-title="{{ img }}">
|
<a href="/resized/{{ img }}" data-lightbox="gallery" data-title="{{ img }}">
|
||||||
<img src="/resized/{{ img }}" alt="{{ img }}">
|
<img src="/resized/{{ img }}">
|
||||||
</a>
|
</a>
|
||||||
% end
|
% end
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Lightbox JS -->
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.4/js/lightbox.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.4/js/lightbox.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user