From 31ddff2a7566a9cc2aab53abbc38594eba8ba5da Mon Sep 17 00:00:00 2001 From: Alpinux Date: Wed, 6 May 2026 09:41:31 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20affichage=20des=20m=C3=A9tadonn=C3=A9es?= =?UTF-8?q?=20image=20dans=20la=20page=20de=20pr=C3=A9visualisation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute une carte de propriétés au-dessus de l'image : - Dimensions (largeur × hauteur), format, mode couleur, résolution DPI - Données EXIF complètes si présentes (appareil, exposition, ISO, focale, balance des blancs, auteur, copyright…) - Coordonnées GPS avec lien OpenStreetMap si le champ est renseigné Nouvelle fonction _image_meta() et _parse_gps() dans app.py (Pillow). Grille CSS responsive, n'apparaît pas si aucune métadonnée disponible. Co-Authored-By: Claude Sonnet 4.6 --- app/app.py | 61 ++++++++++++++++++++++++++++++++ app/static/app.css | 8 +++++ app/templates/preview_image.html | 58 ++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+) diff --git a/app/app.py b/app/app.py index 1a6c410..e20597d 100644 --- a/app/app.py +++ b/app/app.py @@ -97,6 +97,66 @@ def _humansize(n: int) -> str: return f"{n:.1f} To" +_EXIF_WANTED = frozenset({ + "Make", "Model", "Software", "DateTime", "DateTimeOriginal", "DateTimeDigitized", + "ExposureTime", "FNumber", "ISOSpeedRatings", "FocalLength", "Flash", + "WhiteBalance", "ExposureProgram", "MeteringMode", "Orientation", + "XResolution", "YResolution", "ResolutionUnit", "ColorSpace", + "PixelXDimension", "PixelYDimension", "Artist", "Copyright", "GPSInfo", +}) + + +def _image_meta(path: Path) -> dict: + try: + from PIL import ExifTags + img = Image.open(path) + meta = { + "width": img.width, + "height": img.height, + "format": img.format or path.suffix.lstrip(".").upper(), + "mode": img.mode, + "dpi": img.info.get("dpi"), + } + exif = img.getexif() + if exif: + rows = {} + for tag_id, val in exif.items(): + tag = ExifTags.TAGS.get(tag_id, str(tag_id)) + if tag in _EXIF_WANTED and tag != "GPSInfo": + rows[tag] = str(val) + sub = exif.get_ifd(0x8769) + for tag_id, val in sub.items(): + tag = ExifTags.TAGS.get(tag_id, str(tag_id)) + if tag in _EXIF_WANTED and tag != "GPSInfo": + rows[tag] = str(val) + gps_ifd = exif.get_ifd(0x8825) + if gps_ifd: + rows["GPS"] = _parse_gps(gps_ifd) + if rows: + meta["exif"] = rows + return meta + except Exception: + return {} + + +def _parse_gps(gps_ifd: dict): + try: + from PIL import ExifTags + def _dms(coords): + d, m, s = coords + return float(d) + float(m) / 60 + float(s) / 3600 + + lat = _dms(gps_ifd.get(2, (0, 0, 0))) + lon = _dms(gps_ifd.get(4, (0, 0, 0))) + latR = gps_ifd.get(1, "N") + lonR = gps_ifd.get(3, "E") + if latR == "S": lat = -lat + if lonR == "W": lon = -lon + return f"{lat:.6f}, {lon:.6f}" + except Exception: + return None + + def _backup_path(p: Path) -> Path: ts = datetime.now().strftime("%Y%m%d%H%M%S") return p.parent / f"{p.stem}_bak_{ts}{p.suffix}" @@ -304,6 +364,7 @@ def _file_preview(path: Path, subpath: str): ext=ext, ) if ext in IMAGE_EXT: + ctx["meta"] = _image_meta(path) return render_template("preview_image.html", **ctx) if ext in TEXT_EXT: content = path.read_text(errors="replace") diff --git a/app/static/app.css b/app/static/app.css index 8a0be89..fa7f37e 100644 --- a/app/static/app.css +++ b/app/static/app.css @@ -176,6 +176,14 @@ main { max-width: 1100px; margin: 2rem auto; padding: 0 1.5rem 3rem; display: fl .drop-text { font-size: .9rem; color: var(--muted); line-height: 1.5; pointer-events: none; } .drop-names { font-size: .82rem; color: var(--blue-dark); font-style: italic; word-break: break-all; pointer-events: none; } +/* ── Métadonnées image ────────────────────────────────────────────── */ +.meta-card { padding: 1rem 1.8rem; } +.meta-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: .4rem .8rem; } +.meta-item { display: flex; flex-direction: column; gap: .1rem; padding: .4rem .5rem; border-radius: 6px; background: var(--bg); } +.meta-label { font-size: .7rem; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; } +.meta-value { font-size: .88rem; color: var(--text); font-weight: 500; word-break: break-word; } +.meta-value a { color: var(--blue); font-weight: 400; } + /* ── Redimensionnement ────────────────────────────────────────────── */ .resize-card h2 { margin-bottom: 1rem; } .resize-body { display: flex; flex-direction: column; gap: 1rem; } diff --git a/app/templates/preview_image.html b/app/templates/preview_image.html index 758c7d5..00c9641 100644 --- a/app/templates/preview_image.html +++ b/app/templates/preview_image.html @@ -26,6 +26,64 @@ +{% if meta %} +
+
+ {% if meta.width and meta.height %} +
+ Dimensions + {{ meta.width }} × {{ meta.height }} px +
+ {% endif %} + {% if meta.format %} +
+ Format + {{ meta.format }} +
+ {% endif %} + {% if meta.mode %} +
+ Mode couleur + {{ meta.mode }} +
+ {% endif %} + {% if meta.dpi %} +
+ Résolution + {{ meta.dpi[0]|int }} × {{ meta.dpi[1]|int }} DPI +
+ {% endif %} + {% if meta.exif %} + {% set labels = { + 'Make':'Appareil', 'Model':'Modèle', + 'Software':'Logiciel', 'DateTime':'Modifié le', + 'DateTimeOriginal':'Pris le','DateTimeDigitized':'Numérisé le', + 'ExposureTime':'Exposition','FNumber':'Ouverture', + 'ISOSpeedRatings':'ISO', 'FocalLength':'Focale', + 'Flash':'Flash', 'WhiteBalance':'Balance blancs', + 'ExposureProgram':'Programme','MeteringMode':'Mesure', + 'Orientation':'Orientation','Artist':'Auteur', + 'Copyright':'Copyright', 'ColorSpace':'Espace colorimétrique', + 'GPS':'Coordonnées GPS', + } %} + {% for key, val in meta.exif.items() %} +
+ {{ labels.get(key, key) }} + + {% if key == 'GPS' and val %} + {{ val }} + {% else %} + {{ val }} + {% endif %} + +
+ {% endfor %} + {% endif %} +
+
+{% endif %} +
{{ filename }}