Script Python qui lit un fichier JSON généré par Exif Notes et écrit les métadonnées EXIF dans les fichiers images (TIFF, JPEG) ou crée des fichiers XMP sidecar pour les formats non supportés. Fonctionnalités : - Validation des fichiers JSON Exif Notes - Mapping complet JSON → tags EXIF standard - Écriture EXIF via exiftool avec fallback XMP - Support des formats de vitesse (1/125, 2", B/Bulb) - Support des dates ISO avec ou sans secondes - CLI avec options --dry-run, --verbose, --force-xmp Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
219 lines
6.8 KiB
Python
219 lines
6.8 KiB
Python
"""Tests pour le module xmp_writer."""
|
|
|
|
from json_to_metadata.xmp_writer import (
|
|
_convert_exif_to_xmp,
|
|
_format_rational,
|
|
_format_xmp_date,
|
|
generate_xmp_content,
|
|
write_xmp_sidecar,
|
|
)
|
|
|
|
|
|
class TestGenerateXmpContent:
|
|
"""Tests pour la fonction generate_xmp_content."""
|
|
|
|
def test_basic_structure(self):
|
|
"""Génère une structure XMP valide."""
|
|
content = generate_xmp_content({})
|
|
|
|
assert '<?xml version="1.0"' in content
|
|
assert 'x:xmpmeta' in content
|
|
assert 'rdf:RDF' in content
|
|
assert 'rdf:Description' in content
|
|
|
|
def test_includes_namespaces(self):
|
|
"""Inclut les namespaces requis."""
|
|
content = generate_xmp_content({})
|
|
|
|
assert 'xmlns:dc=' in content
|
|
assert 'xmlns:exif=' in content
|
|
assert 'xmlns:tiff=' in content
|
|
assert 'xmlns:xmp=' in content
|
|
|
|
def test_maps_camera_info(self):
|
|
"""Mappe les informations de l'appareil."""
|
|
tags = {'Make': 'Nikon', 'Model': 'FM2'}
|
|
|
|
content = generate_xmp_content(tags)
|
|
|
|
assert '<tiff:Make>Nikon</tiff:Make>' in content
|
|
assert '<tiff:Model>FM2</tiff:Model>' in content
|
|
|
|
def test_maps_exposure(self):
|
|
"""Mappe les paramètres d'exposition."""
|
|
tags = {'ISO': 400, 'FNumber': 2.8}
|
|
|
|
content = generate_xmp_content(tags)
|
|
|
|
assert 'ISOSpeedRatings' in content
|
|
assert '400' in content
|
|
assert 'FNumber' in content
|
|
|
|
def test_escapes_special_characters(self):
|
|
"""Échappe les caractères spéciaux XML."""
|
|
tags = {'ImageDescription': 'Test <avec> des & caractères "spéciaux"'}
|
|
|
|
content = generate_xmp_content(tags)
|
|
|
|
assert '<avec>' in content
|
|
assert '&' in content
|
|
|
|
def test_includes_modify_date(self):
|
|
"""Inclut la date de modification."""
|
|
content = generate_xmp_content({})
|
|
|
|
assert 'xmp:ModifyDate' in content
|
|
|
|
|
|
class TestWriteXmpSidecar:
|
|
"""Tests pour la fonction write_xmp_sidecar."""
|
|
|
|
def test_creates_xmp_file(self, tmp_path):
|
|
"""Crée un fichier XMP à côté de l'image."""
|
|
image = tmp_path / 'photo.tif'
|
|
image.write_bytes(b'fake image')
|
|
tags = {'Make': 'Nikon'}
|
|
|
|
xmp_path = write_xmp_sidecar(image, tags)
|
|
|
|
assert xmp_path.exists()
|
|
assert xmp_path.name == 'photo.xmp'
|
|
assert xmp_path.parent == tmp_path
|
|
|
|
def test_xmp_content_valid(self, tmp_path):
|
|
"""Le contenu du fichier XMP est valide."""
|
|
image = tmp_path / 'photo.jpg'
|
|
image.write_bytes(b'fake image')
|
|
tags = {'Make': 'Nikon', 'Model': 'FM2'}
|
|
|
|
xmp_path = write_xmp_sidecar(image, tags)
|
|
content = xmp_path.read_text(encoding='utf-8')
|
|
|
|
assert '<?xml version="1.0"' in content
|
|
assert '<tiff:Make>Nikon</tiff:Make>' in content
|
|
|
|
def test_dry_run_mode(self, tmp_path):
|
|
"""Le mode dry-run ne crée pas de fichier."""
|
|
image = tmp_path / 'photo.tif'
|
|
image.write_bytes(b'fake image')
|
|
|
|
xmp_path = write_xmp_sidecar(image, {'Make': 'Nikon'}, dry_run=True)
|
|
|
|
assert not xmp_path.exists()
|
|
assert xmp_path.name == 'photo.xmp'
|
|
|
|
def test_overwrites_existing_xmp(self, tmp_path):
|
|
"""Écrase un fichier XMP existant."""
|
|
image = tmp_path / 'photo.tif'
|
|
image.write_bytes(b'fake image')
|
|
xmp_path = tmp_path / 'photo.xmp'
|
|
xmp_path.write_text('old content')
|
|
|
|
write_xmp_sidecar(image, {'Make': 'Canon'})
|
|
content = xmp_path.read_text()
|
|
|
|
assert 'Canon' in content
|
|
assert 'old content' not in content
|
|
|
|
def test_handles_different_extensions(self, tmp_path):
|
|
"""Fonctionne avec différentes extensions d'images."""
|
|
for ext in ['.tif', '.avif', '.heic', '.webp', '.png']:
|
|
image = tmp_path / f'photo{ext}'
|
|
image.write_bytes(b'fake image')
|
|
|
|
xmp_path = write_xmp_sidecar(image, {'Make': 'Test'})
|
|
|
|
assert xmp_path.suffix == '.xmp'
|
|
assert xmp_path.stem == 'photo'
|
|
xmp_path.unlink() # Nettoyer
|
|
|
|
|
|
class TestConvertExifToXmp:
|
|
"""Tests pour la fonction _convert_exif_to_xmp."""
|
|
|
|
def test_make_mapping(self):
|
|
"""Mappe Make vers tiff:Make."""
|
|
result = _convert_exif_to_xmp('Make', 'Nikon')
|
|
|
|
assert '<tiff:Make>Nikon</tiff:Make>' in result
|
|
|
|
def test_iso_mapping(self):
|
|
"""Mappe ISO avec structure rdf:Seq."""
|
|
result = _convert_exif_to_xmp('ISO', 400)
|
|
|
|
assert 'ISOSpeedRatings' in result
|
|
assert 'rdf:Seq' in result
|
|
assert '400' in result
|
|
|
|
def test_date_mapping(self):
|
|
"""Mappe DateTimeOriginal vers xmp:CreateDate."""
|
|
result = _convert_exif_to_xmp('DateTimeOriginal', '2024:03:15 14:30:00')
|
|
|
|
assert 'xmp:CreateDate' in result
|
|
assert '2024-03-15T14:30:00' in result
|
|
|
|
def test_gps_mapping(self):
|
|
"""Mappe les coordonnées GPS."""
|
|
lat = _convert_exif_to_xmp('GPSLatitude', 48.8584)
|
|
lat_ref = _convert_exif_to_xmp('GPSLatitudeRef', 'N')
|
|
|
|
assert 'exif:GPSLatitude' in lat
|
|
assert '48.8584' in lat
|
|
assert 'exif:GPSLatitudeRef' in lat_ref
|
|
assert 'N' in lat_ref
|
|
|
|
def test_description_uses_alt(self):
|
|
"""ImageDescription utilise rdf:Alt pour le multilingue."""
|
|
result = _convert_exif_to_xmp('ImageDescription', 'Ma photo')
|
|
|
|
assert 'dc:description' in result
|
|
assert 'rdf:Alt' in result
|
|
assert 'x-default' in result
|
|
|
|
def test_unknown_tag_returns_none(self):
|
|
"""Les tags inconnus retournent None."""
|
|
result = _convert_exif_to_xmp('UnknownTag', 'value')
|
|
|
|
assert result is None
|
|
|
|
|
|
class TestFormatRational:
|
|
"""Tests pour la fonction _format_rational."""
|
|
|
|
def test_integer_value(self):
|
|
"""Formate un entier comme chaîne."""
|
|
assert _format_rational(50) == '50'
|
|
assert _format_rational(100.0) == '100'
|
|
|
|
def test_fraction_value(self):
|
|
"""Formate une fraction correctement."""
|
|
result = _format_rational(0.008) # 1/125
|
|
assert '1/125' in result
|
|
|
|
def test_decimal_passthrough(self):
|
|
"""Retourne les décimaux non-fractions tels quels."""
|
|
result = _format_rational(2.8)
|
|
assert '2.8' in result
|
|
|
|
|
|
class TestFormatXmpDate:
|
|
"""Tests pour la fonction _format_xmp_date."""
|
|
|
|
def test_exif_to_xmp_format(self):
|
|
"""Convertit le format EXIF en ISO."""
|
|
result = _format_xmp_date('2024:03:15 14:30:00')
|
|
|
|
assert result == '2024-03-15T14:30:00'
|
|
|
|
def test_iso_passthrough(self):
|
|
"""Laisse passer les dates déjà au format ISO."""
|
|
result = _format_xmp_date('2024-03-15T14:30:00')
|
|
|
|
assert result == '2024-03-15T14:30:00'
|
|
|
|
def test_date_only(self):
|
|
"""Gère les dates sans heure."""
|
|
result = _format_xmp_date('2024-03-15')
|
|
|
|
assert '2024-03-15' in result
|