feat: affichage des métadonnées image dans la page de prévisualisation
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 <noreply@anthropic.com>
This commit is contained in:
parent
fa5408bb03
commit
31ddff2a75
3 changed files with 127 additions and 0 deletions
61
app/app.py
61
app/app.py
|
|
@ -97,6 +97,66 @@ def _humansize(n: int) -> str:
|
||||||
return f"{n:.1f} To"
|
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:
|
def _backup_path(p: Path) -> Path:
|
||||||
ts = datetime.now().strftime("%Y%m%d%H%M%S")
|
ts = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||||
return p.parent / f"{p.stem}_bak_{ts}{p.suffix}"
|
return p.parent / f"{p.stem}_bak_{ts}{p.suffix}"
|
||||||
|
|
@ -304,6 +364,7 @@ def _file_preview(path: Path, subpath: str):
|
||||||
ext=ext,
|
ext=ext,
|
||||||
)
|
)
|
||||||
if ext in IMAGE_EXT:
|
if ext in IMAGE_EXT:
|
||||||
|
ctx["meta"] = _image_meta(path)
|
||||||
return render_template("preview_image.html", **ctx)
|
return render_template("preview_image.html", **ctx)
|
||||||
if ext in TEXT_EXT:
|
if ext in TEXT_EXT:
|
||||||
content = path.read_text(errors="replace")
|
content = path.read_text(errors="replace")
|
||||||
|
|
|
||||||
|
|
@ -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-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; }
|
.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 ────────────────────────────────────────────── */
|
/* ── Redimensionnement ────────────────────────────────────────────── */
|
||||||
.resize-card h2 { margin-bottom: 1rem; }
|
.resize-card h2 { margin-bottom: 1rem; }
|
||||||
.resize-body { display: flex; flex-direction: column; gap: 1rem; }
|
.resize-body { display: flex; flex-direction: column; gap: 1rem; }
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,64 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{% if meta %}
|
||||||
|
<section class="card meta-card">
|
||||||
|
<div class="meta-grid">
|
||||||
|
{% if meta.width and meta.height %}
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">Dimensions</span>
|
||||||
|
<span class="meta-value">{{ meta.width }} × {{ meta.height }} px</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if meta.format %}
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">Format</span>
|
||||||
|
<span class="meta-value">{{ meta.format }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if meta.mode %}
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">Mode couleur</span>
|
||||||
|
<span class="meta-value">{{ meta.mode }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if meta.dpi %}
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">Résolution</span>
|
||||||
|
<span class="meta-value">{{ meta.dpi[0]|int }} × {{ meta.dpi[1]|int }} DPI</span>
|
||||||
|
</div>
|
||||||
|
{% 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() %}
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="meta-label">{{ labels.get(key, key) }}</span>
|
||||||
|
<span class="meta-value">
|
||||||
|
{% if key == 'GPS' and val %}
|
||||||
|
<a href="https://www.openstreetmap.org/?mlat={{ val.split(',')[0].strip() }}&mlon={{ val.split(',')[1].strip() }}&zoom=15"
|
||||||
|
target="_blank" rel="noopener">{{ val }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ val }}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="preview-image-wrap">
|
<div class="preview-image-wrap">
|
||||||
<img src="{{ raw_url }}" alt="{{ filename }}">
|
<img src="{{ raw_url }}" alt="{{ filename }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue