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"
|
||||
|
||||
|
||||
_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")
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -26,6 +26,64 @@
|
|||
</div>
|
||||
</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">
|
||||
<img src="{{ raw_url }}" alt="{{ filename }}">
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue