Windows Phone – Modification des tags Exif d’un JPEG

Nous avons vu dans l’article précédent comment compresser une image JPEG sans perdre ses tags. Dans cet article nous allons voir comment modifier ces mêmes tags pour par exemple ajouter des informations de géolocalisation dans l’image.

 

Tout d’abord il va falloir vous munir d’une bibliothèque permettant d’éditer les tags Exif sur le téléphone. En effet il n’existe pas d’API permettant de le faire nativement sur Windows Phone. A ma connaissance il n’existait aucune bibliothèque gratuite qui permettait de le faire, aussi c’est pourquoi j’ai décidé de porter la bibliothèque Exif Library de Ozgur Ozcitak dont vous pouvez trouver le code source original ici :

http://www.codeproject.com/Articles/43665/ExifLibrary-for-NET

La version Windows Phone a été publiée récemment sur CodePlex en accord avec l’auteur original. Vous pouvez la télécharger ici :

http://wpexiflib.codeplex.com/

Il est important de noter que cette bibliothèque marche pour Windows Phone 7.5 et dans Windows Phone 8 moyennant une migration du projet.

A l’heure où ces lignes sont écrites il n’existe pas de package Nuget pour cette bibliothèque mais ce n’est qu’une question de temps. Vous devrez donc soit l’ajouter à votre code source soit la compiler à part et ajouter la dll.

 

Pour cet article nous allons reprendre le code source de l’article précédent et le modifier un peu pour éditer les tags de géolocalisation.

Puisque l’on va travailler avec des coordonnées GPS voici une méthode permettant de convertir des coordonnées sous forme flottante en coordonnées de type degree/minute/secondes

private static void ConvertToDegreeMinutesSeconds(double source, out uint degree, out uint minutes, out double seconds)
{
    degree = (uint)source;
    minutes = (uint)((source - degree) * 60);
    seconds = (((source - degree) * 60) - minutes) * 60;
}

Et maintenant voici le code écrivant les coordonnées GPS dans l’image :

var exifFile = ExifFile.Read(filePath);

if (!exifFile.Properties.ContainsKey(ExifTag.GPSVersionID))
{
    exifFile.Properties.Add(ExifTag.GPSVersionID, new ExifByteArray(ExifTag.GPSVersionID, new[]
        {
            (byte)0x02,
            (byte)0x02,
            (byte)0x00,
            (byte)0x00
        }));
}

uint degree;
uint minutes;
double seconds;
ConvertToDegreeMinutesSeconds(Math.Abs(currentLocation.Latitude), out degree, out minutes, out seconds);
var gpsLatitude = new GPSLatitudeLongitude(
    ExifTag.GPSLatitude,
    new[]
        {
            new MathEx.UFraction32(degree),
            new MathEx.UFraction32(minutes),
            new MathEx.UFraction32(seconds)
        });
ConvertToDegreeMinutesSeconds(Math.Abs(currentLocation.Longitude), out degree, out minutes, out seconds);
var gpsLongitude = new GPSLatitudeLongitude(
    ExifTag.GPSLongitude,
    new[]
        {
            new MathEx.UFraction32(degree),
            new MathEx.UFraction32(minutes),
            new MathEx.UFraction32(seconds)
        });

if (!exifFile.Properties.ContainsKey(ExifTag.GPSLatitude))
    exifFile.Properties.Add(ExifTag.GPSLatitude, gpsLatitude);
else
    exifFile.Properties[ExifTag.GPSLatitude] = gpsLatitude;

if (!exifFile.Properties.ContainsKey(ExifTag.GPSLongitude))
    exifFile.Properties.Add(ExifTag.GPSLongitude, gpsLongitude);
else
    exifFile.Properties[ExifTag.GPSLongitude] = gpsLongitude;

if (!exifFile.Properties.ContainsKey(ExifTag.GPSLatitudeRef))
    exifFile.Properties.Add(ExifTag.GPSLatitudeRef, new ExifAscii(ExifTag.GPSLatitudeRef, currentLocation.Latitude < 0 ? "S" : "N"));
else
    exifFile.Properties[ExifTag.GPSLatitudeRef] = new ExifAscii(ExifTag.GPSLatitudeRef, currentLocation.Latitude < 0 ? "S" : "N");

if (!exifFile.Properties.ContainsKey(ExifTag.GPSLongitudeRef))
    exifFile.Properties.Add(ExifTag.GPSLongitudeRef, new ExifAscii(ExifTag.GPSLongitudeRef, currentLocation.Longitude < 0 ? "W" : "E"));
else
    exifFile.Properties[ExifTag.GPSLongitudeRef] = new ExifAscii(ExifTag.GPSLongitudeRef, currentLocation.Longitude < 0 ? "W" : "E");

exifFile.Save(filePath, true);

 

Le code complet du scénario qui permet de compresser une image et d’ajouter les coordonnées GPS est donc le suivant :

private static async Task HandleTakenPhoto(Stream photoStream)
{
    string filePath;

    #region Compression
    using (var exifMs = new MemoryStream())
    {
        // Extraction des Exif
        ExtractExifStream(photoStream, exifMs);

        // On revient au début de l'image
        photoStream.Seek(0, SeekOrigin.Begin);

        // Création du stream qui servira à compresser les images
        using (var ms = new MemoryStream())
        {
            var bi = new BitmapImage();
            bi.SetSource(photoStream);

            var wb = new WriteableBitmap(bi);

            // Compression de l'image à 40%
            wb.SaveJpeg(ms, wb.PixelWidth, wb.PixelHeight, 0, 40);
            ms.Seek(0, SeekOrigin.Begin);

            using (var headerMs = new MemoryStream())
            {
                // On passe les header du fichier
                ExtractExifStream(ms, headerMs);

                // On recopie le contenu de l'image sans les header
                await exifMs.WriteAsync(ms.ToArray(), (int)headerMs.Length, (int)(ms.Length - headerMs.Length));
            }
        }

        // On revient au début de l'image
        exifMs.Seek(0, SeekOrigin.Begin);

        const string filename = "YourFileName.jpg";
        // Création de l'image
        var file = await ApplicationData.Current.LocalFolder.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting);

        // Ecriture du Stream sur le disque.
        using (var stream = await file.OpenStreamForWriteAsync())
        {
            await exifMs.CopyToAsync(stream);
        }

        filePath = file.Path;
    }
    #endregion

    #region Recupération de la position courante
    var locator = new Geolocator();
    var position = await locator.GetGeopositionAsync();
    var currentLocation = position.Coordinate;
    #endregion
   
    #region Ecriture des Exif
    var exifFile = ExifFile.Read(filePath);

    if (!exifFile.Properties.ContainsKey(ExifTag.GPSVersionID))
    {
        exifFile.Properties.Add(ExifTag.GPSVersionID, new ExifByteArray(ExifTag.GPSVersionID, new[]
            {
                (byte)0x02,
                (byte)0x02,
                (byte)0x00,
                (byte)0x00
            }));
    }

    uint degree;
    uint minutes;
    double seconds;
    ConvertToDegreeMinutesSeconds(Math.Abs(currentLocation.Latitude), out degree, out minutes, out seconds);
    var gpsLatitude = new GPSLatitudeLongitude(
        ExifTag.GPSLatitude,
        new[]
            {
                new MathEx.UFraction32(degree),
                new MathEx.UFraction32(minutes),
                new MathEx.UFraction32(seconds)
            });
    ConvertToDegreeMinutesSeconds(Math.Abs(currentLocation.Longitude), out degree, out minutes, out seconds);
    var gpsLongitude = new GPSLatitudeLongitude(
        ExifTag.GPSLongitude,
        new[]
            {
                new MathEx.UFraction32(degree),
                new MathEx.UFraction32(minutes),
                new MathEx.UFraction32(seconds)
            });

    if (!exifFile.Properties.ContainsKey(ExifTag.GPSLatitude))
        exifFile.Properties.Add(ExifTag.GPSLatitude, gpsLatitude);
    else
        exifFile.Properties[ExifTag.GPSLatitude] = gpsLatitude;

    if (!exifFile.Properties.ContainsKey(ExifTag.GPSLongitude))
        exifFile.Properties.Add(ExifTag.GPSLongitude, gpsLongitude);
    else
        exifFile.Properties[ExifTag.GPSLongitude] = gpsLongitude;

    if (!exifFile.Properties.ContainsKey(ExifTag.GPSLatitudeRef))
        exifFile.Properties.Add(ExifTag.GPSLatitudeRef, new ExifAscii(ExifTag.GPSLatitudeRef, currentLocation.Latitude < 0 ? "S" : "N"));
    else
        exifFile.Properties[ExifTag.GPSLatitudeRef] = new ExifAscii(ExifTag.GPSLatitudeRef, currentLocation.Latitude < 0 ? "S" : "N");

    if (!exifFile.Properties.ContainsKey(ExifTag.GPSLongitudeRef))
        exifFile.Properties.Add(ExifTag.GPSLongitudeRef, new ExifAscii(ExifTag.GPSLongitudeRef, currentLocation.Longitude < 0 ? "W" : "E"));
    else
        exifFile.Properties[ExifTag.GPSLongitudeRef] = new ExifAscii(ExifTag.GPSLongitudeRef, currentLocation.Longitude < 0 ? "W" : "E");

    exifFile.Save(filePath, true);
    #endregion
}

 

J’espère que ce code vous sera aussi utile qu’il l’a été pour moi.
N’hésitez pas à remercier Ozgur Ozcitak pour son travail original sans lequel il aurait été nettement plus long et compliqué de coder ce scénario.

WP8 – Compression d’un JPEG et conservation des tags Exif

Suite à mes aventures dans le merveilleux monde de la manipulation d’images sous Windows Phone j’ai récemment eu le besoin de compresser un JPEG tout en conservant ses tags Exif. Le problème de cela est que la méthode classique pour compresser un JPEG (décrite dans mon article précédent) fait disparaitre ces tags.

A force de recherches je suis tombé sur cet article montrant comment supprimer les tags Exif et m’en suis servi comme base pour répondre à mon besoin :
http://techmikael.blogspot.co.uk/2009/07/removing-exif-data-continued.html

 

L’idée derrière la méthode que j’ai utilisé consiste à lire les headers du fichier image source jusqu’à la fin du header Exif et de le recopier au début d’un nouveau fichier image. Ensuite il n’y a plus qu’à écrire l’image compressée à la suite.

Tout d’abord voici le code de l’article précédent modifié permettant d’écrire les header du fichier source dans un nouveau Stream :

private static void ExtractExifStream(Stream inStream, Stream outStream)
{
    var jpegHeader = new byte[2];
    jpegHeader[0] = (byte)inStream.ReadByte();
    jpegHeader[1] = (byte)inStream.ReadByte();

    if (jpegHeader[0] == 0xff && jpegHeader[1] == 0xd8) // Vérifie si c'est un JPEG
    {
        outStream.WriteByte(jpegHeader[0]);
        outStream.WriteByte(jpegHeader[1]);

        var header = new byte[2];
        header[0] = (byte)inStream.ReadByte();
        header[1] = (byte)inStream.ReadByte();
        outStream.WriteByte(header[0]);
        outStream.WriteByte(header[1]);

        while (header[0] == 0xff && (header[1] >= 0xe0 && header[1] <= 0xef))
        {
            int exifLength = inStream.ReadByte();
            outStream.WriteByte((byte)exifLength);
            exifLength = exifLength << 8;
            exifLength |= inStream.ReadByte();
            outStream.WriteByte((byte)exifLength);

            for (int i = 0; i < exifLength - 2; i++)
            {
                var b = (byte)inStream.ReadByte();
                outStream.WriteByte(b);
            }

            header[0] = (byte)inStream.ReadByte();
            header[1] = (byte)inStream.ReadByte();
            outStream.WriteByte(header[0]);
            outStream.WriteByte(header[1]);
        }
    }
}

Et voici comment utiliser ce code pour compresser l’image source et garder les tags Exif :

private void ChoosePhoto_OnClick(object sender, RoutedEventArgs e)
{
    var cct = new PhotoChooserTask();
    cct.Completed += async (s, a) =>
    {
        if (a.TaskResult == TaskResult.OK)
            await HandleTakenPhoto(a.ChosenPhoto);
    };
    cct.Show();
}

private static async Task HandleTakenPhoto(Stream photoStream)
{
    using (var exifMs = new MemoryStream())
    {
        // Extraction des Exif
        ExtractExifStream(photoStream, exifMs);

        // On revient au début de l'image
        photoStream.Seek(0, SeekOrigin.Begin);

        // Création du stream qui servira à compresser les images
        using (var ms = new MemoryStream())
        {
            var bi = new BitmapImage();
            bi.SetSource(photoStream);

            var wb = new WriteableBitmap(bi);

            // Compression de l'image à 40%
            wb.SaveJpeg(ms, wb.PixelWidth, wb.PixelHeight, 0, 40);
            ms.Seek(0, SeekOrigin.Begin);

            using (var headerMs = new MemoryStream())
            {
                // On passe les header du fichier
                ExtractExifStream(ms, headerMs);

                // On recopie le contenu de l'image sans les header
                await exifMs.WriteAsync(ms.ToArray(), (int)headerMs.Length, (int)(ms.Length - headerMs.Length));
            }
        }

        // On revient au début de l'image
        exifMs.Seek(0, SeekOrigin.Begin);

        // Enregistrer la photo sur le disque ici
        // Voir article précédent
    }
}

 

Comme d’habitude le code est commenté de façon à expliquer ce que fait chaque étape.

[.NET] – Attention à la position de vos streams

Récemment j’ai voulu baisser la qualité d’une image JPEG dans Windows Phone. J’ai donc recupéré l’image que l’utilisateur souhaite compresser grâce à une PhotoChooserTask et utilisé une manière classique de compresser l’image : passer par un WriteableBitmap et d’utiliser les méthodes d’extensions JPEG (System.Windows.Media.Imaging.Extensions) pour compresser l’image. Une fois ceci fait on sauvegarde le MemoryStream dans un fichier du stockage local et le tour est joué.

private async void photoTask_Completed(object sender, PhotoResult e)
{
    if (e.TaskResult == TaskResult.OK)
    {
        try
        {
            var filename = "test.jpg";
            var bi = new BitmapImage();

            // Récupération de l'image choisie par l'utilisateur
            bi.SetSource(e.ChosenPhoto);

            string filePath = null;

            // Création du Stream qui contiendra l'image compressée
            using (var ms = new MemoryStream())
            {
                // Initialisation du WriteableBitmap
                var wb = new WriteableBitmap(bi);

                // Compression de l'image à 40%
                wb.SaveJpeg(ms, wb.PixelWidth, wb.PixelHeight, 0, 40);

                // filePath représente le chemin absolu de l'image sur le stockage du téléphone.
                var file = await ApplicationData.Current.LocalFolder.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting);

                // Ecriture du Stream sur le disque.
                using (var stream = await file.OpenStreamForWriteAsync())
                {
                     await ms.CopyToAsync(stream);
                }
                filePath = file.Path;
            }
        }
        catch (Exception ex)
        {
             // TODO : Gérer les exception
        }
    }
}

Enfin ça c’est la théorie, si on ne fait pas un minimum attention dans la pratique on oublie un élément important : certaines méthodes peuvent changer la position actuelle du Stream lors de son traitement. C’est le cas de la méthode SaveJpeg qui le positionne à la fin. Du coup, si on essaie d’écrire le MemoryStream sur le disque on a un beau fichier vide à la place de notre image.

Afin de sauvegarder notre image il faut bien penser à remettre le Stream au début avant de l’écrire sur le disque.

private async void photoTask_Completed(object sender, PhotoResult e)
{
    if (e.TaskResult == TaskResult.OK)
    {
        try
        {
            var filename = "test.jpg";
            var bi = new BitmapImage();

            // Récupération de l'image choisie par l'utilisateur
            bi.SetSource(e.ChosenPhoto);

            string filePath = null;

            // Création du Stream qui contiendra l'image compressée
            using (var ms = new MemoryStream())
            {
                // Initialisation du WriteableBitmap
                var wb = new WriteableBitmap(bi);

                // Compression de l'image à 40%
                wb.SaveJpeg(ms, wb.PixelWidth, wb.PixelHeight, 0, 40);

                // On rembobine le Stream
                ms.Seek(0, SeekOrigin.Begin);

                // filePath représente le chemin absolu de l'image sur le stockage du téléphone.
                var file = await ApplicationData.Current.LocalFolder.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting);

                // Ecriture du Stream sur le disque.
                using (var stream = await file.OpenStreamForWriteAsync())
                {
                     await ms.CopyToAsync(stream);
                }
                filePath = file.Path;
            }
        }
        catch (Exception ex)
        {
             // TODO : Gérer les exception
        }
    }
}

Avec le code ci-dessus la sauvegarde se passe correctement.

En résumé, faîtes bien attention à la position actuelle de vos Streams lorsque vous les utilisez.