public static void CompareJpegMetaData(JpegMetaData meta1, JpegMetaData meta2, string filename, bool GpsIfdExists, bool ExifIfdExists = true) { Debug.WriteLine("file: " + filename); TestUtil.AreEqual(meta1.App1Data, meta2.App1Data, filename + " App1 data"); TestUtil.AreEqual(meta1.PrimaryIfd, meta2.PrimaryIfd, filename + "Primary IFD"); if (ExifIfdExists) { TestUtil.AreEqual(meta1.ExifIfd, meta2.ExifIfd, filename + "Exif IFD"); } else { Assert.IsNull(meta1.ExifIfd); Assert.IsNull(meta2.ExifIfd); } if (GpsIfdExists) { TestUtil.AreEqual(meta1.GpsIfd, meta2.GpsIfd, filename + "Gps IFD"); } else { Assert.IsNull(meta1.GpsIfd); Assert.IsNull(meta2.GpsIfd); } }
/// <summary> /// Set exif data to Jpeg file asynchronously. /// Mess of this library; everything are caused by crazy Exif format. /// Note that this function overwrites ALL Exif data in the given image. /// </summary> /// <param name="OriginalImage">Target image</param> /// <param name="MetaData">An Exif data which will be added to the image.</param> /// <returns>New image</returns> public static async Task<byte[]> SetMetaDataAsync(byte[] OriginalImage, JpegMetaData MetaData) { // It seems thrown exceptions will be raised by this "async" ... return await Task<byte[]>.Run(async () => { return SetMetaData(OriginalImage, MetaData); }).ConfigureAwait(false); }
/// <summary> /// Set exif data to Jpeg file; It blocks thread. /// Mess of this library; everything are caused by crazy Exif format. /// Note that this function overwrites ALL Exif data in the given image. /// </summary> /// <param name="OriginalImage">Target image</param> /// <param name="MetaData">An Exif data which will be added to the image.</param> /// <returns>New image</returns> public static byte[] SetMetaData(byte[] OriginalImage, JpegMetaData MetaData) { var OriginalMetaData = JpegMetaDataParser.ParseImage(OriginalImage); // Debug.WriteLine("Original image size: " + OriginalImage.Length.ToString("X")); // It seems there's a bug handling little endian jpeg image on WP OS. var OutputImageMetadataEndian = Definitions.Endian.Big; var App0Offset = 0; if (Util.GetUIntValue(OriginalImage, 2, 2, Definitions.Endian.Big) == Definitions.APP0_MARKER) { App0Offset = 2 + (int)Util.GetUIntValue(OriginalImage, 4, 2, Definitions.Endian.Big); // Debug.WriteLine("APP0 marker detected. " + App0Offset); } // Note: App1 size and ID are fixed to Big endian. UInt32 OriginalApp1DataSize = Util.GetUIntValue(OriginalImage, 4 + App0Offset, 2, Definitions.Endian.Big); // Debug.WriteLine("Original App1 size: " + OriginalApp1DataSize.ToString("X")); // 0-5 byte: Exif identify code. E, x, i, f, \0, \0 // 6-13 byte: TIFF header. endian, 0x002A, Offset to Primary IFD in 4 bytes. generally, 8 is set as the offset. var OriginalApp1Data = new byte[OriginalApp1DataSize]; Array.Copy(OriginalImage, 6 + App0Offset, OriginalApp1Data, 0, (int)OriginalApp1DataSize); var NewApp1Data = CreateApp1Data(OriginalMetaData, MetaData, OriginalApp1Data, OutputImageMetadataEndian); // Only size of App1 data is different. var NewImage = new byte[OriginalImage.Length - OriginalApp1DataSize + NewApp1Data.Length]; // Debug.WriteLine("New image size: " + NewImage.Length.ToString("X")); // Copy SOI, App1 marker var newImageHeader = new byte[4] { 0xFF, 0xD8, 0xFF, 0xE1 }; Array.Copy(newImageHeader, 0, NewImage, 0, 2 + 2); // Important note again: App 1 data size is stored in Big endian. var App1SizeData = Util.ToByte((UInt32)NewApp1Data.Length, 2, Definitions.Endian.Big); Array.Copy(App1SizeData, 0, NewImage, 4, 2); // After that, copy App1 data to new image. Array.Copy(NewApp1Data, 0, NewImage, 6, NewApp1Data.Length); // At last, copy body from original image. Array.Copy(OriginalImage, 2 + 2 + 2 + App0Offset + (int)OriginalApp1DataSize, NewImage, 2 + 2 + 2 + NewApp1Data.Length, OriginalImage.Length - 2 - 2 - 2 - App0Offset - (int)OriginalApp1DataSize); return NewImage; }
/// <summary> /// Set exif data to Jpeg file asynchronously. /// Mess of this library; everything are caused by crazy Exif format. /// Note that this function overwrites ALL Exif data in the given image. /// </summary> /// <param name="OriginalImage">Target image. This will be disposed.</param> /// <param name="MetaData">An Exif data which will be added to the image.</param> /// <returns>New image on new stream.</returns> public static async Task<Stream> SetMetaDataAsync(Stream OriginalImage, JpegMetaData MetaData) { return await Task<Stream>.Run(async () => { return SetMetaData(OriginalImage, MetaData); }).ConfigureAwait(false); }
/// <summary> /// Create app1 data section in byte array. /// Needs many informations.... /// </summary> /// <param name="OriginalMetaData">metadata from original image.</param> /// <param name="NewMetaData">metadata which will be recorded in app1 data section</param> /// <param name="OriginalApp1Data">Original app1 data. Other than Exif meta data will be copied to new one</param> /// <param name="OutputImageMetadataEndian">Endian which will be used in Exif meta data. for WP OS, Big endian is recommended.</param> /// <returns></returns> private static byte[] CreateApp1Data(JpegMetaData OriginalMetaData, JpegMetaData NewMetaData, byte[] OriginalApp1Data, Definitions.Endian OutputImageMetadataEndian) { if (NewMetaData.ExifIfd != null && !NewMetaData.PrimaryIfd.Entries.ContainsKey(Definitions.EXIF_IFD_POINTER_TAG)) { // Add Exif IFD section's pointer entry as dummy. NewMetaData.PrimaryIfd.Entries.Add( Definitions.EXIF_IFD_POINTER_TAG, new Entry() { Tag = Definitions.EXIF_IFD_POINTER_TAG, Type = Entry.EntryType.Long, Count = 1, value = new byte[] { 0, 0, 0, 0 } }); } if (NewMetaData.GpsIfd != null && !NewMetaData.PrimaryIfd.Entries.ContainsKey(Definitions.GPS_IFD_POINTER_TAG)) { // Add GPS IFD section's pointer entry as dummy NewMetaData.PrimaryIfd.Entries.Add( Definitions.GPS_IFD_POINTER_TAG, new Entry() { Tag = Definitions.GPS_IFD_POINTER_TAG, Type = Entry.EntryType.Long, Count = 1, value = new byte[] { 0, 0, 0, 0 } }); } byte[] primaryIfd = IfdComposer.ComposeIfdsection(NewMetaData.PrimaryIfd, OutputImageMetadataEndian); byte[] exifIfd = new byte[] { }; byte[] gpsIfd = new byte[] { }; if (NewMetaData.ExifIfd != null) { exifIfd = IfdComposer.ComposeIfdsection(NewMetaData.ExifIfd, OutputImageMetadataEndian); } if (NewMetaData.GpsIfd != null) { gpsIfd = IfdComposer.ComposeIfdsection(NewMetaData.GpsIfd, OutputImageMetadataEndian); } // Debug.WriteLine("Size fixed. Primary: " + primaryIfd.Length.ToString("X") + " exif: " + exifIfd.Length.ToString("X") + " gps: " + gpsIfd.Length.ToString("X")); // after size fixed, set each IFD sections' offset. NewMetaData.PrimaryIfd.Offset = 8; // fixed value // now it's possible to calcurate pointer to Exif/GPS IFD var exifIfdPointer = 8 + primaryIfd.Length; var gpsIfdPointer = 8 + primaryIfd.Length + exifIfd.Length; if (NewMetaData.PrimaryIfd.Entries.ContainsKey(Definitions.EXIF_IFD_POINTER_TAG)) { NewMetaData.PrimaryIfd.Entries.Remove(Definitions.EXIF_IFD_POINTER_TAG); } var exifIfdPointerEntry = new Entry() { Tag = Definitions.EXIF_IFD_POINTER_TAG, Type = Entry.EntryType.Long, Count = 1, }; exifIfdPointerEntry.UIntValues = new UInt32[] { (UInt32)exifIfdPointer }; NewMetaData.PrimaryIfd.Entries.Add(Definitions.EXIF_IFD_POINTER_TAG, exifIfdPointerEntry); if (NewMetaData.PrimaryIfd.Entries.ContainsKey(Definitions.GPS_IFD_POINTER_TAG)) { NewMetaData.PrimaryIfd.Entries.Remove(Definitions.GPS_IFD_POINTER_TAG); } var gpsIfdPointerEntry = new Entry() { Tag = Definitions.GPS_IFD_POINTER_TAG, Type = Entry.EntryType.Long, Count = 1, }; gpsIfdPointerEntry.UIntValues = new UInt32[] { (UInt32)gpsIfdPointer }; NewMetaData.PrimaryIfd.Entries.Add(Definitions.GPS_IFD_POINTER_TAG, gpsIfdPointerEntry); var nextIfdPointer = 8 + primaryIfd.Length + exifIfd.Length + gpsIfd.Length; NewMetaData.PrimaryIfd.NextIfdPointer = (UInt32)nextIfdPointer; // finally, create byte array again primaryIfd = IfdComposer.ComposeIfdsection(NewMetaData.PrimaryIfd, OutputImageMetadataEndian); if (NewMetaData.ExifIfd != null) { NewMetaData.ExifIfd.Offset = 8 + (UInt32)primaryIfd.Length; exifIfd = IfdComposer.ComposeIfdsection(NewMetaData.ExifIfd, OutputImageMetadataEndian); } if (NewMetaData.GpsIfd != null) { NewMetaData.GpsIfd.Offset = 8 + (UInt32)primaryIfd.Length + (UInt32)exifIfd.Length; gpsIfd = IfdComposer.ComposeIfdsection(NewMetaData.GpsIfd, OutputImageMetadataEndian); } // 1st IFD data (after primary IFD data) should be kept var Original1stIfdData = new byte[OriginalApp1Data.Length - OriginalMetaData.PrimaryIfd.NextIfdPointer]; Array.Copy(OriginalApp1Data, (int)OriginalMetaData.PrimaryIfd.NextIfdPointer, Original1stIfdData, 0, Original1stIfdData.Length); // Build App1 section. From "Exif\0\0" to end of thumbnail data (1st IFD) // Exif00 + TIFF header + 3 IFD sections (Primary, Exif, GPS) + 1st IFD data from original data // new app1 size may overfrow from 0xFFFF bytes. in this case, it should be limited up to 0xFFFF byte. var NewApp1DataSize = Math.Min(0xFFFF, 6 + 8 + primaryIfd.Length + exifIfd.Length + gpsIfd.Length + Original1stIfdData.Length); var NewApp1DataOverflowSize = 0; if (NewApp1DataSize == 0xFFFF) { NewApp1DataOverflowSize = (6 + 8 + primaryIfd.Length + exifIfd.Length + gpsIfd.Length + Original1stIfdData.Length) - 0xFFFF; Debug.WriteLine("App1 overflow size: " + NewApp1DataOverflowSize); } var NewApp1Data = new byte[NewApp1DataSize]; // var NewApp1Data = new byte[6 + 8 + primaryIfd.Length + exifIfd.Length + gpsIfd.Length]; // Debug.WriteLine("New App1 size: " + NewApp1Data.Length.ToString("X")); // Array.Copy(OriginalApp1Data, 0, NewApp1Data, 0, 6 + 8); Array.Copy(OriginalApp1Data, 0, NewApp1Data, 0, 6); // only EXIF00 should be copied. var endianSection = Util.ToByte(0x4d4d, 2, OutputImageMetadataEndian); Array.Copy(endianSection, 0, NewApp1Data, 6, 2); var magicNumber = Util.ToByte(0x002A, 2, OutputImageMetadataEndian); Array.Copy(magicNumber, 0, NewApp1Data, 8, 2); var primaryIfdOffset = Util.ToByte(8, 4, OutputImageMetadataEndian); Array.Copy(primaryIfdOffset, 0, NewApp1Data, 10, 4); Array.Copy(primaryIfd, 0, NewApp1Data, 6 + 8, primaryIfd.Length); Array.Copy(exifIfd, 0, NewApp1Data, 6 + 8 + primaryIfd.Length, exifIfd.Length); Array.Copy(gpsIfd, 0, NewApp1Data, 6 + 8 + primaryIfd.Length + exifIfd.Length, gpsIfd.Length); Array.Copy(Original1stIfdData, 0, NewApp1Data, 6 + 8 + primaryIfd.Length + exifIfd.Length + gpsIfd.Length, Original1stIfdData.Length - NewApp1DataOverflowSize); return NewApp1Data; }
/// <summary> /// Set exif data to Jpeg file; It blocks thread. /// Mess of this library; everything are caused by crazy Exif format. /// Note that this function overwrites ALL Exif data in the given image. /// </summary> /// <param name="OriginalImage">Target image. This will be disposed.</param> /// <param name="MetaData">An Exif data which will be added to the image.</param> /// <returns>New image on new stream.</returns> public static Stream SetMetaData(Stream OriginalImage, JpegMetaData MetaData) { OriginalImage.Seek(0, SeekOrigin.Begin); var OriginalMetaData = JpegMetaDataParser.ParseImage(OriginalImage); var OutputImageMetaDataEndian = Definitions.Endian.Big; var endian = Definitions.Endian.Big; OriginalImage.Position = 2; var FirstMarker = new byte[2]; OriginalImage.Read(FirstMarker, 0, 2); var App0Offset = 0; if (Util.GetUIntValue(FirstMarker, 0, 2, endian) == Definitions.APP0_MARKER) { var App0SizeData = new byte[2]; OriginalImage.Read(App0SizeData, 0, 2); App0Offset = 2 + (int)Util.GetUIntValue(App0SizeData, 0, 2, endian); } OriginalImage.Position = 4 + App0Offset; var app1sizeData = new byte[2]; OriginalImage.Read(app1sizeData, 0, 2); var OriginalApp1Size = Util.GetUIntValue(app1sizeData, 0, 2, endian); OriginalImage.Position = 6 + App0Offset; var OriginalApp1Data = new byte[OriginalApp1Size]; OriginalImage.Read(OriginalApp1Data, 0, (int)OriginalApp1Size); var NewApp1Data = CreateApp1Data(OriginalMetaData, MetaData, OriginalApp1Data, OutputImageMetaDataEndian); // first 6 bytes; var NewImageHeader = new byte[6] { 0xFF, 0xD8, 0xFF, 0xE1, 0x0, 0x0 }; // No APP0 section will be stored on new image. // Important note again: App 1 data size is stored in Big endian. var App1SizeData = Util.ToByte((UInt32)NewApp1Data.Length, 2, Definitions.Endian.Big); Array.Copy(App1SizeData, 0, NewImageHeader, 4, 2); Util.DumpByteArrayAll(NewImageHeader); var NewImage = new MemoryStream(); NewImage.Write(NewImageHeader, 0, 6); NewImage.Write(NewApp1Data, 0, NewApp1Data.Length); // forward pointer to start position of original body data OriginalImage.Position = 6 + App0Offset + OriginalApp1Size; // Copy all other data from original image. int BufSize = 4096; byte[] buffer = new byte[BufSize]; int numBytes; int count = 0; while ((numBytes = OriginalImage.Read(buffer, 0, BufSize)) > 0) { NewImage.Write(buffer, 0, numBytes); count += numBytes; } NewImage.Seek(0, SeekOrigin.Begin); // Debug.WriteLine("image size: " + NewImage.Length); OriginalImage.Dispose(); return NewImage; }
/// <summary> /// Parse given data, App1 section data. /// </summary> /// <param name="App1Data">byte array of app1 data which starts from TIFF header.</param> /// <returns>all meta data.</returns> private static JpegMetaData ParseApp1Data(byte[] App1Data) { var exif = new JpegMetaData(); exif.App1Data = App1Data; // Check TIFF header. var MetaDataEndian = Definitions.Endian.Little; if (Util.GetUIntValue(App1Data, 0, 2, Definitions.Endian.Little) == Definitions.TIFF_LITTLE_ENDIAN) { MetaDataEndian = Definitions.Endian.Little; // Debug.WriteLine("This metadata in Little endian"); } else if (Util.GetUIntValue(App1Data, 0, 2, Definitions.Endian.Little) == Definitions.TIFF_BIG_ENDIAN) { MetaDataEndian = Definitions.Endian.Big; // Debug.WriteLine("This metadata in Big endian"); } else { throw new UnsupportedFileFormatException("Currently, only little endian is supported."); } if (Util.GetUIntValue(App1Data, 2, 2, MetaDataEndian) != Definitions.TIFF_IDENTIFY_CODE) { throw new UnsupportedFileFormatException("TIFF identify code (0x2A00) couldn't find."); } // find out a pointer to 1st IFD var PrimaryIfdPointer = Util.GetUIntValue(App1Data, 4, 4, MetaDataEndian); // Debug.WriteLine("Primary IFD pointer: " + PrimaryIfdPointer); // parse primary (0th) IFD section exif.PrimaryIfd = Parser.IfdParser.ParseIfd(App1Data, PrimaryIfdPointer, MetaDataEndian); // Debug.WriteLine("Primary offset: " + exif.PrimaryIfd.Offset + " length: " + exif.PrimaryIfd.Length + " next ifd: " + exif.PrimaryIfd.NextIfdPointer); // parse Exif IFD section if (exif.PrimaryIfd.Entries.ContainsKey(Definitions.EXIF_IFD_POINTER_TAG)) { exif.ExifIfd = Parser.IfdParser.ParseIfd(App1Data, exif.PrimaryIfd.Entries[Definitions.EXIF_IFD_POINTER_TAG].UIntValues[0], MetaDataEndian); // Debug.WriteLine("Exif offset: " + exif.ExifIfd.Offset + " length: " + exif.ExifIfd.Length); } // parse GPS data. if (exif.PrimaryIfd.Entries.ContainsKey(Definitions.GPS_IFD_POINTER_TAG)) { exif.GpsIfd = Parser.IfdParser.ParseIfd(App1Data, exif.PrimaryIfd.Entries[Definitions.GPS_IFD_POINTER_TAG].UIntValues[0], MetaDataEndian); // Debug.WriteLine("GPS offset: " + exif.GpsIfd.Offset + " length: " + exif.GpsIfd.Length); } return exif; }
public static void IsGpsDataAdded(JpegMetaData original, JpegMetaData added) { if (original.ExifIfd != null && added.ExifIfd != null) { TestUtil.AreEqual(original.ExifIfd, added.ExifIfd, "Exif IFD"); } Assert.IsTrue(added.PrimaryIfd.Entries.ContainsKey(Definitions.GPS_IFD_POINTER_TAG)); Assert.IsNotNull(added.GpsIfd); }
/// <summary> /// Remove GPS information from metadata. /// </summary> /// <param name="meta">Metadata with geotag</param> /// <returns></returns> public static JpegMetaData RemoveGeoinfo(JpegMetaData meta) { if (meta.PrimaryIfd.Entries.ContainsKey(Definitions.GPS_IFD_POINTER_TAG)) { meta.PrimaryIfd.Entries.Remove(Definitions.GPS_IFD_POINTER_TAG); } meta.GpsIfd = null; return meta; }