Vérifier l’orientation EXIF et normaliser les pixels d’une image iOS

Vérifier l’orientation EXIF et normaliser les pixels d’une image iOS

Une photo apparaît en portrait dans le sélecteur, puis sa vignette se retrouve couchée après l’envoi. Avant de modifier le ratio de recadrage, vérifiez son orientation : un JPEG peut stocker ses pixels à l’horizontale tout en demandant au lecteur, via une balise EXIF, de les afficher après rotation. Sur iOS, UIImage peut afficher la photo correctement sans que le code d’exportation ou le destinataire traite cette orientation de la même manière. Nous allons créer un échantillon reproductible dans une cible de test iOS sur un Mac distant, afin de vérifier séparément l’affichage et le fichier produit.

Distinguer la balise d’orientation des pixels

Relevez trois valeurs : l’orientation EXIF du fichier, la propriété UIImage.imageOrientation après décodage et les dimensions en pixels du CGImage sous-jacent. La valeur EXIF 6 indique qu’il faut tourner l’image dans le sens horaire pour l’afficher ; elle ne signifie pas que les pixels ont déjà été tournés. Si les pixels sous-jacents mesurent 120 de large et 80 de haut, l’image correctement affichée doit mesurer 80 de large et 120 de haut.

Ne concluez pas que le problème est réglé sur la seule foi d’une capture de l’aperçu. Le composant d’aperçu peut appliquer la balise d’orientation alors que la suite du traitement transmet toujours les pixels d’origine.

Définissez d’abord ce que chaque étape doit conserver. Si le fichier d’origine doit être préservé, enregistrez-le séparément de sa version normalisée. Si seul un fichier à l’orientation uniforme est nécessaire pour l’envoi, redessinez l’image avant l’exportation, puis décodez à nouveau le fichier exporté pour le contrôler. Ne vous contentez pas de remplacer la valeur d’orientation du fichier d’origine par 1 : cela ne déplacera aucun pixel.

Créer un échantillon reproductible dans un test iOS

Ajoutez le test suivant à une cible de tests unitaires iOS utilisant UIKit. Il génère à l’exécution un JPEG de 120×80 pixels et lui attribue la valeur d’orientation 6. Il ne dépend d’aucune photo présente sur l’ordinateur du développeur et évite de déposer de vraies images ou des données utilisateur dans le dépôt. Le code vérifie à la fois la balise lue par ImageIO et l’orientation obtenue après décodage par UIKit.

import XCTest
import UIKit
import ImageIO
import UniformTypeIdentifiers

final class ImageOrientationTests: XCTestCase {
    func testOrientationSurvivesExport() throws {
        let format = UIGraphicsImageRendererFormat()
        format.scale = 1
        format.opaque = true

        let source = UIGraphicsImageRenderer(
            size: CGSize(width: 120, height: 80),
            format: format
        ).image { _ in
            UIColor.red.setFill()
            UIRectFill(CGRect(x: 0, y: 0, width: 60, height: 80))
            UIColor.blue.setFill()
            UIRectFill(CGRect(x: 60, y: 0, width: 60, height: 80))
        }

        let jpeg = NSMutableData()
        let destination = try XCTUnwrap(
            CGImageDestinationCreateWithData(
                jpeg, UTType.jpeg.identifier as CFString, 1, nil
            )
        )
        CGImageDestinationAddImage(
            destination,
            try XCTUnwrap(source.cgImage),
            [kCGImagePropertyOrientation: 6] as CFDictionary
        )
        XCTAssertTrue(CGImageDestinationFinalize(destination))

        let input = jpeg as Data
        let imageSource = try XCTUnwrap(
            CGImageSourceCreateWithData(input as CFData, nil)
        )
        let properties = try XCTUnwrap(
            CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil)
                as? [CFString: Any]
        )
        XCTAssertEqual(
            (properties[kCGImagePropertyOrientation] as? NSNumber)?.intValue,
            6
        )

        let decoded = try XCTUnwrap(UIImage(data: input))
        XCTAssertEqual(decoded.imageOrientation, .right)
        XCTAssertEqual(decoded.cgImage?.width, 120)
        XCTAssertEqual(decoded.cgImage?.height, 80)

        let upright = UIGraphicsImageRenderer(
            size: CGSize(width: 80, height: 120),
            format: format
        ).image { _ in
            decoded.draw(in: CGRect(x: 0, y: 0, width: 80, height: 120))
        }
        XCTAssertEqual(upright.imageOrientation, .up)

        let exported = try XCTUnwrap(upright.pngData())
        let reopened = try XCTUnwrap(UIImage(data: exported))
        XCTAssertEqual(reopened.imageOrientation, .up)
        XCTAssertEqual(reopened.cgImage?.width, 80)
        XCTAssertEqual(reopened.cgImage?.height, 120)
    }
}

Avant l’exécution, vérifiez que cette source Swift est incluse dans la cible de test et choisissez un simulateur iOS pris en charge par le projet. En ligne de commande, renseignez -workspace, -scheme et -destination avec les valeurs réelles du projet. Ne supposez pas que le nom d’appareil donné en exemple correspond à une cible disponible sur toutes les machines.

Tester le véritable chemin d’exportation

Le test ci-dessus montre qu’il est possible de normaliser l’image, mais ne prouve pas que le code métier applique cette méthode. Intégrez maintenant les étapes « décoder, dessiner aux dimensions cibles, encoder » à la fonction d’exportation d’images déjà présente dans le projet, puis faites appeler cette même fonction par le test. L’entrée du test peut toujours être générée à l’exécution ; en revanche, les assertions doivent porter sur le fichier exporté après sa réouverture, et non sur le UIImage avant le retour de la fonction.

Définir clairement les dimensions promises par la fonction métier

Précisez d’abord que la fonction reçoit les données du fichier d’origine et renvoie des données dont l’orientation d’affichage est fixée. Pour cet échantillon d’orientation 6, les dimensions en pixels doivent passer de 120×80 à 80×120. Si le traitement redimensionne également l’image, vérifiez le ratio largeur/hauteur en sortie et les dimensions cibles exactes après redimensionnement, puis testez séparément la règle de redimensionnement. Ne réunissez pas les échecs de rotation, de compression et de recadrage dans une seule assertion.

Les deux couleurs, à gauche et à droite, permettent de repérer visuellement une inversion en miroir du contenu. Pour vérifier automatiquement la position des couleurs, comparez les pixels avec une tolérance : la compression avec perte du JPEG peut modifier les couleurs près de la frontière. N’exigez donc pas une égalité bit à bit de chaque canal.

Suivre le chemin qui réussit dans l’aperçu mais échoue à l’exportation

Repérez d’abord l’étape où l’image change de représentation : données du fichier, UIImage, CGImage, contexte graphique ou fichier réencodé. Relever l’orientation et les dimensions en pixels à chaque conversion aide généralement à trouver une bifurcation du type « extraire le cgImage puis l’encoder directement ». Le cgImage fournit les pixels ; le traitement de l’orientation effectué lors du dessin d’un UIImage n’est pas nécessairement inscrit dans ces pixels.

Vérifiez aussi le résultat de l’exportation. Redessiner l’image produit de nouveaux pixels et de nouvelles données de fichier ; le format d’encodage, les métadonnées et la taille du fichier peuvent également changer. Si le flux doit préserver d’autres métadonnées du fichier d’origine, listez les champs indispensables et écrivez un test pour chacun, au lieu de supposer que le redessin les recopiera à l’identique. Dans le parcours d’envoi, il est préférable de décoder une dernière fois le fichier final avant sa transmission : un objet intermédiaire correct dans le lecteur n’est pas le livrable.

Vérifications avant la fusion

Couvrez au minimum une image ordinaire d’orientation 1, une image tournée d’orientation 6 et les orientations en miroir réellement autorisées par l’application. Pour chaque échantillon, vérifiez séparément la balise en entrée, les dimensions en pixels après normalisation et l’orientation d’affichage après un nouveau décodage. Si le code de production autorise le recadrage, ajoutez aussi un échantillon non carré : sur un carré, une rotation incorrecte ne se voit pas dans les assertions sur la largeur et la hauteur.

Enfin, exécutez le test dans la même cible de test iOS que celle utilisée pour la livraison. En cas d’échec, il suffit de conserver l’orientation en entrée, les dimensions en sortie et l’étape du traitement ; n’inscrivez pas la photo originale d’un utilisateur dans des journaux publics. Ainsi, une régression désigne une étape du traitement du fichier, plutôt que de signaler seulement qu’« une photo semble mal orientée ».

Questions fréquentes

Pourquoi une image correcte dans UIImage pivote-t-elle après export ?

UIImage peut appliquer l’orientation EXIF lors du dessin, tandis que le destinataire du fichier peut l’interpréter autrement. Redécodez le fichier exporté pour vérifier ses pixels.

Suffit-il de remplacer la valeur d’orientation par 1 ?

Non. Sans réorganiser les pixels, changer seulement la métadonnée modifie l’apparence. Redessinez l’image selon son orientation initiale, puis contrôlez la sortie.

Confiez votre prochain build à un Mac cloud dédié.

Comparez les modèles, les régions et les durées de location ViewMini, puis choisissez un nœud physique adapté à votre workflow.

Configurer un Mac cloud

Comparez les équipements et les durées de location.

Les trois modèles disponibles proposent des tarifs à la journée, à la semaine, au mois ou au trimestre. Consultez les offres complètes avant de commander.

Voir les modèles et les tarifs Configurer un Mac cloud