diff --git a/app/app.py b/app/app.py index ce6f90e..e2d8467 100644 --- a/app/app.py +++ b/app/app.py @@ -692,10 +692,19 @@ def check_resize(): stem = re.sub(r"_\d+x\d+$", "", target.stem) parent = target.parent + all_dims = [(s, s) for s in sizes] + for cs in request.form.getlist("custom_sizes"): + try: + w, h = (int(x.strip()) for x in re.split(r"[x×]", cs.lower())) + if w >= 1 and h >= 1: + all_dims.append((w, h)) + except (ValueError, TypeError): + pass + conflicts = [] for fmt in formats: - for size in sizes: - out_name = f"{stem}_{size}x{size}.{fmt}" + for w, h in all_dims: + out_name = f"{stem}_{w}x{h}.{fmt}" if (parent / out_name).exists(): conflicts.append(out_name) @@ -710,11 +719,12 @@ def resize_image(): if redir: return redir - subpath = request.form.get("path", "").strip() + subpath = request.form.get("path", "").strip() raw_sizes = request.form.getlist("sizes") raw_formats = request.form.getlist("formats") + raw_customs = request.form.getlist("custom_sizes") - if not subpath or not raw_sizes or not raw_formats: + if not subpath or not raw_formats: return jsonify({"error": "Paramètres manquants"}), 400 target = _safe_path(subpath) @@ -730,13 +740,32 @@ def resize_image(): abort(403) try: - sizes = sorted({int(s) for s in raw_sizes if int(s) in RESIZE_SIZES}) + square_dims = [(s, s) for s in sorted({int(s) for s in raw_sizes if int(s) in RESIZE_SIZES})] except (ValueError, TypeError): - return jsonify({"error": "Tailles invalides"}), 400 + square_dims = [] formats = [f for f in raw_formats if f in RESIZE_FORMATS] - if not sizes or not formats: - return jsonify({"error": "Tailles ou formats invalides"}), 400 + + # Dimensions libres — validées contre la résolution source + src_img = Image.open(target) + max_w, max_h = src_img.width, src_img.height + src_img.close() + + custom_dims = [] + for cs in raw_customs: + try: + w, h = (int(x.strip()) for x in re.split(r"[x×]", cs.lower())) + if w < 1 or h < 1: + continue + if w > max_w or h > max_h: + continue + custom_dims.append((w, h)) + except (ValueError, TypeError): + pass + + all_dims = square_dims + custom_dims + if not all_dims or not formats: + return jsonify({"error": "Aucune dimension ou format valide"}), 400 conflict = request.form.get("conflict", "skip") if conflict not in ("backup", "overwrite", "rename", "skip"): @@ -747,9 +776,20 @@ def resize_image(): parent = target.parent created, errors = [], [] + # Signaler les dimensions hors-bornes + for cs in raw_customs: + try: + w, h = (int(x.strip()) for x in re.split(r"[x×]", cs.lower())) + if w > max_w or h > max_h: + for f in formats: + errors.append({"name": f"{stem}_{w}x{h}.{f}", + "reason": f"{w}×{h} dépasse la résolution source ({max_w}×{max_h})"}) + except (ValueError, TypeError): + pass + for fmt in formats: - for size in sizes: - out_name = f"{stem}_{size}x{size}.{fmt}" + for w, h in all_dims: + out_name = f"{stem}_{w}x{h}.{fmt}" out_path = parent / out_name out_rel = str(out_path.relative_to(ASSETS_ROOT)) @@ -768,7 +808,6 @@ def resize_image(): out_path = _auto_rename(out_path) out_name = out_path.name out_rel = str(out_path.relative_to(ASSETS_ROOT)) - # conflict == "overwrite" : on continue sans rien faire try: if fmt == "svg" and is_svg: @@ -785,17 +824,17 @@ def resize_image(): if fmt in ("png", "ico"): buf = io.BytesIO() _cairosvg.svg2png(url=str(target), write_to=buf, - output_width=size, output_height=size) + output_width=w, output_height=h) buf.seek(0) img = Image.open(buf).convert("RGBA") if fmt == "ico": - img.save(out_path, format="ICO", sizes=[(size, size)]) + img.save(out_path, format="ICO", sizes=[(w, h)]) else: img.save(out_path, format="PNG") - else: # jpg + else: buf = io.BytesIO() _cairosvg.svg2png(url=str(target), write_to=buf, - output_width=size, output_height=size) + output_width=w, output_height=h) buf.seek(0) img = Image.open(buf).convert("RGB") img.save(out_path, format="JPEG", quality=90) @@ -803,7 +842,7 @@ def resize_image(): else: img = Image.open(target) - img = img.resize((size, size), Image.LANCZOS) + img = img.resize((w, h), Image.LANCZOS) if fmt == "png": if img.mode not in ("RGBA", "RGB", "L"): img = img.convert("RGBA") @@ -813,7 +852,7 @@ def resize_image(): img.save(out_path, format="JPEG", quality=90) elif fmt == "ico": img = img.convert("RGBA") - img.save(out_path, format="ICO", sizes=[(size, size)]) + img.save(out_path, format="ICO", sizes=[(w, h)]) created.append({"name": out_name, "path": out_rel}) except Exception as exc: diff --git a/app/static/app.css b/app/static/app.css index fa7f37e..c09e010 100644 --- a/app/static/app.css +++ b/app/static/app.css @@ -212,6 +212,16 @@ main { max-width: 1100px; margin: 2rem auto; padding: 0 1.5rem 3rem; display: fl .resize-actions { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; } .resize-hint { font-size: .82rem; color: var(--muted); font-style: italic; } +.resize-custom-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; } +.custom-dim { width: 5.5rem; padding: .35rem .5rem; border: 1.5px solid var(--border); border-radius: 6px; + font-size: .9rem; text-align: center; background: var(--surface); color: var(--text); } +.custom-dim:focus { outline: none; border-color: var(--accent); } +.custom-dim:invalid { border-color: #ef4444; } +.custom-dim-sep { font-size: 1rem; color: var(--muted); } +.custom-dim-unit { font-size: .82rem; color: var(--muted); } +.custom-square-lock { display: flex; align-items: center; gap: .3rem; font-size: .82rem; + color: var(--muted); cursor: pointer; user-select: none; } + .resize-result { border-top: 1px solid var(--border); padding-top: .9rem; font-size: .88rem; } .resize-ok-title { color: #15803d; font-weight: 600; margin-bottom: .35rem; } .resize-err-title { color: #b91c1c; font-weight: 600; margin-bottom: .35rem; } diff --git a/app/templates/preview_image.html b/app/templates/preview_image.html index fb76e26..919ba87 100644 --- a/app/templates/preview_image.html +++ b/app/templates/preview_image.html @@ -122,6 +122,26 @@ + {% if meta and meta.width and meta.height %} +