Les photos en portrait prises par un appareil reflex n’ont pas des dimensions de hauteur et de largeur qui reflètent une photo en portrait.

Dans l’un des projets que je développe, l’objectif est d’afficher une boutique en ligne de photographie professionnelle pour des événements.

Une fois que le photographe uploade les photos dans la plateforme de back office, la plateforme les allège et ajoute un filigrane.

Jusqu’ici, rien de très difficile en utilisant la bibliothèque SkiaSharp

Mais il y a quelques jours, le photographe a découvert la photographie en portrait et le client a ouvert un ticket pour m’informer que les photos ne s’affichaient pas dans le bon sens et qu’il serait important de conserver la rotation.

J’ai commencé naïvement par un :

// Object from library : [Image.FromFile method](https://docs.microsoft.com/en-us/dotnet/api/system.drawing.image.fromfile?view=dotnet-plat-ext-3.1&WT.mc_id=WD-MVP-5001937)
var image = Image.FromFile("best-picture-ever.jpg");

Puis j’ai fait une supposition pour déterminer si l’image est en portrait ou non avec :

public bool IsPortrait(Image image){
    return image.Height > image.Width;
}

Content de mes longues secondes de programmation, j’ai décidé de tester. J’avais le pressentiment que le résultat était douteux.

Et j’avais raison ! Une fois la machine lancée, rien ne se passe comme prévu. J’obtiens la hauteur, qui correspond à la largeur, et vice versa.

Perplexe, je suis allé consulter les propriétés de l’image de la photo, et je pouvais voir que la hauteur était plus grande que la largeur. Je place un breakpoint dans Visual Studio et là, c’est l’inverse.

Mon cerveau a fait boom :D

En cherchant un peu, je me suis demandé : comment récupérer la taille stockée dans les métadonnées du fichier ? Et pourquoi n’ai-je pas la même information que dans le fichier ?

En fouillant un peu dans les propriétés de l’image

var image = Image.FromFile("photo.jpg");
foreach (var propertyItem in image.PropertyItems)
{
    // read properties
}

J’ai découvert l’existence de l’EXIF (merci les communautés) :

Exchangeable image file format (officiellement Exif, selon les spécifications JEIDA/JEITA/CIPA) est un standard qui spécifie les formats des images, des sons et des tags annexes utilisés par les appareils photo numériques (y compris les smartphones), les scanners et les autres systèmes manipulant des fichiers image et son enregistrés par des appareils photo numériques. Source : Wikipedia


Le site exiftool.org répertorie les standards et, plus précisément, le contenu qui m’intéresse, à savoir l’orientation (274).

Tag ID Tag Name Writable Group Values / Notes
       
0x0112 Orientation int16u IFD0 1 = Horizontal (normal)
2 = Mirror horizontal
3 = Rotate 180
4 = Mirror vertical
5 = Mirror horizontal and rotate 270 CW
6 = Rotate 90 CW
7 = Mirror horizontal and rotate 90 CW
8 = Rotate 270 CW
       

Finalement, j’ai découvert la bibliothèque MetadataExtractor. Elle est très utile pour naviguer à travers les propriétés d’un fichier.

J’ai développé deux méthodes, une pour déterminer quelle opération de rotation je dois effectuer sur l’image, et une autre pour réaliser la rotation.

/// <summary>
/// Retrieve the rotate flip type to apply to the modified picture
/// </summary>
/// <param name="path">Original picture</param>
/// <returns>Nullabe RotateFlipType</returns>
private RotateFlipType? ComputeRotateFlipType(string path)
{
    IEnumerable<MetadataExtractor.Directory> directories = ImageMetadataReader.ReadMetadata(path);
    var orientation = directories
        .OfType<ExifIfd0Directory>()
        .FirstOrDefault()
        ?.GetObject(ExifDirectoryBase.TagOrientation) as ushort?;

    if (orientation != null)
    {
        // Values provided by Standard EXIF : https://exiftool.org/TagNames/EXIF.html
        switch (orientation)
        {
            // 1 = Horizontal(normal)
            case 1: return RotateFlipType.RotateNoneFlipNone;
            // 2 = Mirror horizontal
            case 2: return RotateFlipType.RotateNoneFlipX;
            // 3 = Rotate 180
            case 3: return RotateFlipType.Rotate180FlipNone;
            // 4 = Mirror vertical
            case 4: return RotateFlipType.RotateNoneFlipY;
            // 5 = Mirror horizontal and rotate 270 CW
            case 5: return RotateFlipType.Rotate270FlipX;
            // 6 = Rotate 90 CW
            case 6: return RotateFlipType.Rotate90FlipNone;
            // 7 = Mirror horizontal and rotate 90 CW
            case 7: return RotateFlipType.Rotate90FlipX;
            // 8 = Rotate 270 CW
            case 8: return RotateFlipType.Rotate270FlipNone;
        }
    }
    return null;
}

/// <summary>
/// Rotate the modified picture based on the orientation property of the original picture
/// </summary>
/// <param name="inputPath">Original picture path</param>
/// <param name="outputPath">Modified picture path</param>
private void RotationWhenTransformationIsRequired(string inputPath, string outputPath)
{
    var rotationDegree = ComputeRotateFlipType(inputPath);
    if (rotationDegree.HasValue && rotationDegree.Value != RotateFlipType.RotateNoneFlipNone)
    {
        var bitmap = (Bitmap)Bitmap.FromFile(outputPath);
        bitmap.RotateFlip(rotationDegree.Value);
        using var stream = new FileStream(outputPath, FileMode.Create, FileAccess.Write);
        bitmap.Save(stream, ImageFormat.Jpeg);
    }
}