Skip to content

🔧 fix(ExtractedPreview): exposer l'orientation EXIF#34

Merged
ronan-develop merged 4 commits into
mainfrom
feat/#32-orientation
Jul 17, 2026
Merged

🔧 fix(ExtractedPreview): exposer l'orientation EXIF#34
ronan-develop merged 4 commits into
mainfrom
feat/#32-orientation

Conversation

@ronan-develop

Copy link
Copy Markdown
Owner

Closes #32

Le bug, vu à l'œil

Une photo prise en portrait sortait couchée à 90°. La fixture iPhone 12 Pro a Orientation = 6, et la preview extraite affichait le ciel à gauche et l'herbe à droite.

Le bug était dans nos fixtures depuis le début. 138 tests verts ne l'ont jamais vu — l'extraction fonctionnait, elle rendait juste une image tournée. Il a fallu ouvrir l'image.

Ça touche tout smartphone tenu verticalement, donc une part importante du contenu réel d'une galerie.

On ne pivote pas, on informe

Faire tourner le JPEG demanderait GD — la dépendance que ce package existe pour éviter. On rend l'information, l'appelant décide :

$preview->orientation;                     // Orientation::Rotate90
$preview->orientation->degrees();          // 90
$preview->orientation->isUpright();        // false
$preview->orientation->isMirrored();       // 4 des 8 valeurs EXIF sont des miroirs
$preview->orientation->swapsDimensions();  // true — un quart de tour échange l/h

Un enum plutôt qu'un int : les 8 valeurs mélangent rotations et miroirs, et confondre les deux produit une image inversée.

Lecture par format

Format Où
TIFF (CR2, NEF, ARW, DNG) tag 0x0112 de l'IFD0 — il vaut pour toute la prise de vue
CR3 boîte CMT1, qui est un TIFF complet

Le CR3 imposait une pièce nouvelle : TiffReader::fromRange($path, $offset, $length). CMT1 est un TIFF au milieu du fichier ; sans ça il aurait fallu l'extraire dans un fichier temporaire, ce qui est hors de question dans src/. Le reader travaille désormais sur une fenêtre — ni écriture disque, ni copie mémoire. Le fichier entier n'est plus qu'un cas particulier (base = 0), donc les 138 tests existants passent inchangés.

Cette pièce resservira pour les EXIF du CR3 (#33).

1.1.0, pas 2.0.0

La propriété a une valeur par défaut : new ExtractedPreview($jpeg, 100, 50, Format::CR2) compile toujours. L'API grandit sans rien casser — c'est une mineure.

Le choix compte : Composer bloque les majeures par défaut (^1.0 ne prend pas 2.0). Une 2.0.0 aurait obligé les utilisateurs à éditer leur composer.json pour un correctif dont ils ont besoin.

Vérifié de bout en bout

iphone-12-pro.dng    Rotate90    90°  ⟳ À REDRESSER
les 8 autres         Normal       0°  droite

Rotation appliquée : 4032×3024 → 3024×4032, exactement ce que swapsDimensions() annonçait — et la photo est droite, ciel en haut.

OK (174 tests, 276 assertions)

Les 8 valeurs du tag 0x0112 mélangent rotations et miroirs : les traiter comme
de simples entiers oblige chaque utilisateur à réapprendre la table, et
confondre un miroir avec une rotation produit une image inversée.

L'enum porte ce que l'appelant veut vraiment savoir : degrees() pour
imagerotate() ou une transform CSS, isMirrored() pour les 4 valeurs qui n'en
sont pas, isUpright() pour écarter le cas courant d'un seul test, et
swapsDimensions() parce qu'un quart de tour échange largeur et hauteur.

fromExif() tolère l'inattendu : un tag à 0, 9 ou absent ne doit pas faire
échouer une extraction par ailleurs réussie — on suppose l'image droite.

27 tests couvrant les 8 valeurs sur les 4 méthodes.
Un CR3 range des TIFF complets dans ses boîtes CMT1/CMT2 — c'est là que vit son
orientation. TiffReader ne savait lire qu'un fichier entier ; il aurait fallu
extraire CMT1 dans un fichier temporaire, ce qui est hors de question dans src/.

fromRange($path, $offset, $length) fait travailler le reader sur une fenêtre :
ni écriture disque, ni copie mémoire. Les offsets restent relatifs au début du
TIFF embarqué — une base interne les traduit vers le fichier hôte, et ne fuit
jamais hors de la classe. La fenêtre borne aussi les lectures : un offset qui en
sort est hors bornes, même s'il pointe dans le fichier hôte.

Le fichier entier n'est plus qu'un cas particulier (base = 0), donc les 138
tests existants passent inchangés.

Cette pièce resservira pour les EXIF du CR3 (#33).
…t sortaient couchées

Une photo prise en portrait sortait couchée à 90°. Vérifié à l'œil sur un DNG
d'iPhone 12 Pro : Orientation = 6, et la preview extraite affichait le ciel à
gauche. Le bug était dans nos fixtures depuis le début — 138 tests verts ne
l'ont jamais vu, il a fallu ouvrir l'image.

Ça touche tout smartphone tenu verticalement, donc une part importante du
contenu réel d'une galerie.

On ne peut pas pivoter l'image nous-mêmes : ça exigerait GD, que ce package
existe précisément pour éviter. On rend l'information, l'appelant décide — un
transform: rotate(90deg) en CSS ne coûte rien, et le PHPDoc explique comment
faire avec imagerotate() pour qui a GD.

Lecture par format :
- TIFF : tag 0x0112 de l'IFD0, qui vaut pour toute la prise de vue — l'appareil
  enregistre une fois comment il était tenu, pas par image ;
- CR3 : pas d'IFD TIFF direct, l'orientation vit dans la boîte CMT1 qui EST un
  TIFF complet. TiffReader::fromRange() la lit en place.

Une propriété AVEC valeur par défaut : new ExtractedPreview($jpeg, 100, 50,
Format::CR2) compile toujours. L'API grandit sans rien casser — c'est une
mineure, pas une majeure. Et le choix compte : Composer bloque les majeures par
défaut, une 2.0.0 aurait obligé les utilisateurs à éditer leur composer.json
pour un correctif dont ils ont besoin.

Vérifié de bout en bout : l'iPhone rend Rotate90/90°, la rotation appliquée
redresse la photo (4032x3024 → 3024x4032, comme swapsDimensions() l'annonce),
et les 8 autres fixtures rendent Normal.

Closes #32
Le README montre les deux façons de redresser : un transform CSS, gratuit et
sans dépendance, ou imagerotate() pour qui a GD — en signalant qu'il tourne
dans l'autre sens.

docs/index.md détaille l'enum et surtout d'où l'orientation est lue selon le
format : le tag 0x0112 de l'IFD0 en TIFF, la boîte CMT1 en CR3 — qui est un
TIFF complet, lu en place par TiffReader::fromRange().
@ronan-develop
ronan-develop merged commit 765933a into main Jul 17, 2026
7 checks passed
@ronan-develop
ronan-develop deleted the feat/#32-orientation branch July 17, 2026 07:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🔧 fix(ExtractedPreview): exposer l'orientation EXIF — les previews portrait sortent couchées

1 participant