using System; using System.Collections.Generic; using System.Drawing.Imaging; using System.Globalization; using System.IO; using System.Text.RegularExpressions; namespace PDFPatcher.Processor.Imaging { static class JpgHelper { static readonly ImageCodecInfo _jpgCodec = BitmapHelper.GetCodec("image/jpeg"); static EncoderParameters GetEncoderParameters(int quality) { return new EncoderParameters(2) { Param = new EncoderParameter[] { new EncoderParameter (Encoder.Compression, (long)EncoderValue.RenderProgressive), new EncoderParameter (Encoder.Quality, quality) } }; } // JPEG 编码器不支持 8 位图像输出 static EncoderParameters GetEncoderParameters(int quality, int colorDepth) { return new EncoderParameters(3) { Param = new EncoderParameter[] { new EncoderParameter (Encoder.Compression, (long)EncoderValue.RenderProgressive), new EncoderParameter (Encoder.Quality, quality), new EncoderParameter (Encoder.ColorDepth, colorDepth) } }; } internal static void Save(this System.Drawing.Image bmp, string fileName, int quality) { //if (bmp.IsIndexed ()) { // bmp.Save (fileName, _jpgCodec, GetEncoderParameters (quality, 8)); //} //else { using (var p = GetEncoderParameters(quality)) { bmp.Save(fileName, _jpgCodec, p); foreach (var item in p.Param) { item.Dispose(); } } //} } internal static void Save(this System.Drawing.Image bmp, Stream stream, int quality) { using (var p = GetEncoderParameters(quality)) { bmp.Save(stream, _jpgCodec, p); foreach (var item in p.Param) { item.Dispose(); } } } internal static bool TryGetExifOrientation(string fileName, out ushort b) { try { using (var r = new ExifReader(fileName)) { return r.GetTagValue(ExifTags.Orientation, out b); } } catch (Exception) { b = 0; return false; } } /// /// A class for reading Exif data from a JPEG file. The file will be open for reading for as long as the class exists. /// /// sealed class ExifReader : IDisposable { private static readonly Regex _nullDateTimeMatcher = new Regex(@"^[\s0]{4}[:\s][\s0]{2}[:\s][\s0]{5}[:\s][\s0]{2}[:\s][\s0]{2}$"); private readonly bool _leaveOpen; private Stream _stream; private BinaryReader _reader; /// /// The main tag id/absolute file offset catalogue /// private Dictionary _ifd0Catalogue; /// /// The thumbnail tag id/absolute file offset catalogue /// /// JPEG images contain 2 main sections - one for the main image (which contains most of the useful EXIF data), and one for the thumbnail /// image (which contains little more than the thumbnail itself). This catalogue is only used by . private Dictionary _ifd1Catalogue; /// /// Indicates whether to read data using big or little endian byte aligns /// private bool _isLittleEndian; /// /// The position in the filestream at which the TIFF header starts /// private long _tiffHeaderStart; /// /// The location of the thumbnail IFD /// private uint _ifd1Offset; private bool _isInitialized; public ExifReader(string fileName) : this(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { } public ExifReader(Stream stream) : this(stream, false) { } public ExifReader(Stream stream, bool leaveOpen) { if (stream == null) throw new ArgumentNullException("stream"); if (!stream.CanSeek) throw new ExifLibException("ExifLib requires a seekable stream"); // Leave the stream open if the user wants it so _leaveOpen = leaveOpen; _stream = stream; } private void Initialize() { if (_isInitialized) return; _isInitialized = true; // JPEG encoding uses big endian (i.e. Motorola) byte aligns. The TIFF encoding // found later in the document will specify the byte aligns used for the // rest of the document. _isLittleEndian = false; // Open the file in a stream _reader = new BinaryReader(_stream, System.Text.Encoding.UTF8); // Make sure the file's a JPEG. if (ReadUShort() != 0xFFD8) throw new ExifLibException("File is not a valid JPEG"); // Scan to the start of the Exif content ReadToExifStart(); // Create an index of all Exif tags found within the document CreateTagIndex(); } #region TIFF methods /// /// Returns the length (in bytes) per component of the specified TIFF data type /// /// private static byte GetTIFFFieldLength(ushort tiffDataType) { return tiffDataType switch { 1 or 2 or 7 or 6 => 1, 3 or 8 => 2, 4 or 9 or 11 => 4, 5 or 10 or 12 => 8, _ => throw new ExifLibException(string.Format("Unknown TIFF datatype: {0}", tiffDataType)), }; } #endregion #region Methods for reading data directly from the filestream /// /// Gets a 2 byte unsigned integer from the file /// /// private ushort ReadUShort() { return ToUShort(ReadBytes(2)); } /// /// Gets a 4 byte unsigned integer from the file /// /// private uint ReadUint() { return ToUint(ReadBytes(4)); } private string ReadString(int chars) { var bytes = ReadBytes(chars); return System.Text.Encoding.UTF8.GetString(bytes, 0, bytes.Length); } private byte[] ReadBytes(int byteCount) { return _reader.ReadBytes(byteCount); } /// /// Reads some bytes from the specified TIFF offset /// /// /// /// private byte[] ReadBytes(ushort tiffOffset, int byteCount) { // Keep the current file offset var originalOffset = _stream.Position; // Move to the TIFF offset and retrieve the data _stream.Seek(tiffOffset + _tiffHeaderStart, SeekOrigin.Begin); var data = _reader.ReadBytes(byteCount); // Restore the file offset _stream.Position = originalOffset; return data; } #endregion #region Data conversion methods for interpreting datatypes from a byte array /// /// Converts 2 bytes to a ushort using the current byte aligns /// /// private ushort ToUShort(byte[] data) { if (_isLittleEndian != BitConverter.IsLittleEndian) Array.Reverse(data); return BitConverter.ToUInt16(data, 0); } /// /// Converts 8 bytes to the numerator and denominator /// components of an unsigned rational using the current byte aligns /// private uint[] ToURationalFraction(byte[] data) { var numeratorData = new byte[4]; var denominatorData = new byte[4]; Array.Copy(data, numeratorData, 4); Array.Copy(data, 4, denominatorData, 0, 4); var numerator = ToUint(numeratorData); var denominator = ToUint(denominatorData); return new[] { numerator, denominator }; } /// /// Converts 8 bytes to an unsigned rational using the current byte aligns /// /// private double ToURational(byte[] data) { var fraction = ToURationalFraction(data); return fraction[0] / (double)fraction[1]; } /// /// Converts 8 bytes to the numerator and denominator /// components of an unsigned rational using the current byte aligns /// /// /// A TIFF rational contains 2 4-byte integers, the first of which is /// the numerator, and the second of which is the denominator. /// private int[] ToRationalFraction(byte[] data) { var numeratorData = new byte[4]; var denominatorData = new byte[4]; Array.Copy(data, numeratorData, 4); Array.Copy(data, 4, denominatorData, 0, 4); int numerator = ToInt(numeratorData); int denominator = ToInt(denominatorData); return new[] { numerator, denominator }; } /// /// Converts 8 bytes to a signed rational using the current byte aligns. /// /// private double ToRational(byte[] data) { var fraction = ToRationalFraction(data); return fraction[0] / (double)fraction[1]; } /// /// Converts 4 bytes to a uint using the current byte aligns /// private uint ToUint(byte[] data) { if (_isLittleEndian != BitConverter.IsLittleEndian) Array.Reverse(data); return BitConverter.ToUInt32(data, 0); } /// /// Converts 4 bytes to an int using the current byte aligns /// private int ToInt(byte[] data) { if (_isLittleEndian != BitConverter.IsLittleEndian) Array.Reverse(data); return BitConverter.ToInt32(data, 0); } private double ToDouble(byte[] data) { if (_isLittleEndian != BitConverter.IsLittleEndian) Array.Reverse(data); return BitConverter.ToDouble(data, 0); } private float ToSingle(byte[] data) { if (_isLittleEndian != BitConverter.IsLittleEndian) Array.Reverse(data); return BitConverter.ToSingle(data, 0); } private short ToShort(byte[] data) { if (_isLittleEndian != BitConverter.IsLittleEndian) Array.Reverse(data); return BitConverter.ToInt16(data, 0); } private sbyte ToSByte(byte[] data) { // An sbyte should just be a byte with an offset range. return (sbyte)(data[0] - byte.MaxValue); } /// /// Retrieves an array from a byte array using the supplied converter /// to read each individual element from the supplied byte array /// /// /// /// /// private static Array GetArray(byte[] data, int elementLengthBytes, ConverterMethod converter) { Array convertedData = new T[data.Length / elementLengthBytes]; var buffer = new byte[elementLengthBytes]; // Read each element from the array for (int elementCount = 0; elementCount < data.Length / elementLengthBytes; elementCount++) { // Place the data for the current element into the buffer Array.Copy(data, elementCount * elementLengthBytes, buffer, 0, elementLengthBytes); // Process the data and place it into the output array convertedData.SetValue(converter(buffer), elementCount); } return convertedData; } /// /// A delegate used to invoke any of the data conversion methods /// /// /// /// Although this could be defined as covariant, it wouldn't work on Windows Phone private delegate T ConverterMethod(byte[] data); #endregion #region Stream seek methods - used to get to locations within the JPEG /// /// Scans to the Exif block /// private void ReadToExifStart() { // The file has a number of blocks (Exif/JFIF), each of which // has a tag number followed by a length. We scan the document until the required tag (0xFFE1) // is found. All tags start with FF, so a non FF tag indicates an error. // Get the next tag. byte markerStart; byte markerNumber = 0; while (((markerStart = _reader.ReadByte()) == 0xFF) && (markerNumber = _reader.ReadByte()) != 0xE1) { // Get the length of the data. ushort dataLength = ReadUShort(); // Jump to the end of the data (note that the size field includes its own size)! _stream.Seek(dataLength - 2, SeekOrigin.Current); } // It's only success if we found the 0xFFE1 marker if (markerStart != 0xFF || markerNumber != 0xE1) throw new ExifLibException("Could not find Exif data block"); } /// /// Reads through the Exif data and builds an index of all Exif tags in the document /// /// private void CreateTagIndex() { // The next 4 bytes are the size of the Exif data. ReadUShort(); // Next is the Exif data itself. It starts with the ASCII "Exif" followed by 2 zero bytes. if (ReadString(4) != "Exif") throw new ExifLibException("Exif data not found"); // 2 zero bytes if (ReadUShort() != 0) throw new ExifLibException("Malformed Exif data"); // We're now into the TIFF format _tiffHeaderStart = _stream.Position; // What byte align will be used for the TIFF part of the document? II for Intel, MM for Motorola _isLittleEndian = ReadString(2) == "II"; // Next 2 bytes are always the same. if (ReadUShort() != 0x002A) throw new ExifLibException("Error in TIFF data"); // Get the offset to the IFD (image file directory) var ifdOffset = ReadUint(); // Note that this offset is from the first byte of the TIFF header. Jump to the IFD. _stream.Position = ifdOffset + _tiffHeaderStart; // Catalogue this first IFD (there will be another IFD) _ifd0Catalogue = new Dictionary(); CatalogueIFD(ref _ifd0Catalogue); // The address to the IFD1 (the thumbnail IFD) is located immediately after the main IFD _ifd1Offset = ReadUint(); // There's more data stored in the subifd, the offset to which is found in tag 0x8769. // As with all TIFF offsets, it will be relative to the first byte of the TIFF header. uint offset; if (!GetTagValue(_ifd0Catalogue, 0x8769, out offset)) // throw new ExifLibException("Unable to locate Exif data"); // DO NOT throw exception, not all has data return; // Jump to the exif SubIFD _stream.Position = offset + _tiffHeaderStart; // Add the subIFD to the catalogue too CatalogueIFD(ref _ifd0Catalogue); // Go to the GPS IFD and catalogue that too. It's an optional // section. if (GetTagValue(_ifd0Catalogue, 0x8825, out offset)) { // Jump to the GPS SubIFD _stream.Position = offset + _tiffHeaderStart; // Add the subIFD to the catalogue too CatalogueIFD(ref _ifd0Catalogue); } // Finally, catalogue the thumbnail IFD if it's present if (_ifd1Offset != 0) { _stream.Position = _ifd1Offset + _tiffHeaderStart; _ifd1Catalogue = new Dictionary(); CatalogueIFD(ref _ifd1Catalogue); } } #endregion #region Exif data catalog and retrieval methods public bool GetTagValue(ExifTags tag, out T result) { return GetTagValue((ushort)tag, out result); } public bool GetTagValue(ushort tagId, out T result) { // All useful EXIF tags are stored in the ifd0 catalogue. The ifd1 catalogue is only for thumbnail retrieval. Initialize(); return GetTagValue(_ifd0Catalogue, tagId, out result); } /// /// Retrieves an Exif value with the requested tag ID /// private bool GetTagValue(IDictionary tagDictionary, ushort tagId, out T result) { ushort tiffDataType; uint numberOfComponents; var tagData = GetTagBytes(tagDictionary, tagId, out tiffDataType, out numberOfComponents); if (tagData == null) { result = default(T); return false; } var fieldLength = GetTIFFFieldLength(tiffDataType); // Convert the data to the appropriate datatype. Note the weird boxing via object. // The compiler doesn't like it otherwise. switch (tiffDataType) { case 1: // unsigned byte if (numberOfComponents == 1) result = (T)(object)tagData[0]; else result = (T)(object)tagData; return true; case 2: // ascii string var str = System.Text.Encoding.UTF8.GetString(tagData, 0, tagData.Length); // There may be a null character within the string var nullCharIndex = str.IndexOf('\0'); if (nullCharIndex != -1) str = str.Substring(0, nullCharIndex); // Special processing for dates. if (typeof(T) == typeof(DateTime)) { DateTime dateResult; var success = ToDateTime(str, out dateResult); result = (T)(object)dateResult; return success; } result = (T)(object)str; return true; case 3: // unsigned short if (numberOfComponents == 1) result = (T)(object)ToUShort(tagData); else result = (T)(object)GetArray(tagData, fieldLength, ToUShort); return true; case 4: // unsigned long if (numberOfComponents == 1) result = (T)(object)ToUint(tagData); else result = (T)(object)GetArray(tagData, fieldLength, ToUint); return true; case 5: // unsigned rational if (numberOfComponents == 1) { // Special case - sometimes it's useful to retrieve the numerator and // denominator in their raw format if (typeof(T).IsArray) result = (T)(object)ToURationalFraction(tagData); else result = (T)(object)ToURational(tagData); } else result = (T)(object)GetArray(tagData, fieldLength, ToURational); return true; case 6: // signed byte if (numberOfComponents == 1) result = (T)(object)ToSByte(tagData); else result = (T)(object)GetArray(tagData, fieldLength, ToSByte); return true; case 7: // undefined. Treat it as a byte. if (numberOfComponents == 1) result = (T)(object)tagData[0]; else result = (T)(object)tagData; return true; case 8: // Signed short if (numberOfComponents == 1) result = (T)(object)ToShort(tagData); else result = (T)(object)GetArray(tagData, fieldLength, ToShort); return true; case 9: // Signed long if (numberOfComponents == 1) result = (T)(object)ToInt(tagData); else result = (T)(object)GetArray(tagData, fieldLength, ToInt); return true; case 10: // signed rational if (numberOfComponents == 1) { // Special case - sometimes it's useful to retrieve the numerator and // denominator in their raw format if (typeof(T).IsArray) result = (T)(object)ToRationalFraction(tagData); else result = (T)(object)ToRational(tagData); } else result = (T)(object)GetArray(tagData, fieldLength, ToRational); return true; case 11: // single float if (numberOfComponents == 1) result = (T)(object)ToSingle(tagData); else result = (T)(object)GetArray(tagData, fieldLength, ToSingle); return true; case 12: // double float if (numberOfComponents == 1) result = (T)(object)ToDouble(tagData); else result = (T)(object)GetArray(tagData, fieldLength, ToDouble); return true; default: throw new ExifLibException(string.Format("Unknown TIFF datatype: {0}", tiffDataType)); } } private static bool ToDateTime(string str, out DateTime result) { // From page 28 of the Exif 2.2 spec (http://www.exif.org/Exif2-2.PDF): // "When the field is left blank, it is treated as unknown ... When the date and time are unknown, // all the character spaces except colons (":") may be filled with blank characters" if (string.IsNullOrEmpty(str) || _nullDateTimeMatcher.IsMatch(str)) { result = DateTime.MinValue; return false; } // There are 2 types of date - full date/time stamps, and plain dates. Dates are 10 characters long. if (str.Length == 10) { result = DateTime.ParseExact(str, "yyyy:MM:dd", CultureInfo.InvariantCulture); return true; } // "The format is "YYYY:MM:DD HH:MM:SS" with time shown in 24-hour format, and the date and time separated by one blank character [20.H]. result = DateTime.ParseExact(str, "yyyy:MM:dd HH:mm:ss", CultureInfo.InvariantCulture); return true; } /// /// Gets the data in the specified tag ID, starting from before the IFD block. /// /// /// The number of items which make up the data item - i.e. for a string, this will be the /// number of characters in the string /// /// private byte[] GetTagBytes(IDictionary tagDictionary, ushort tagId, out ushort tiffDataType, out uint numberOfComponents) { // Get the tag's offset from the catalogue and do some basic error checks if (_stream == null || _reader == null || tagDictionary == null || !tagDictionary.ContainsKey(tagId)) { tiffDataType = 0; numberOfComponents = 0; return null; } var tagOffset = tagDictionary[tagId]; // Jump to the TIFF offset _stream.Position = tagOffset; // Read the tag number from the file var currentTagId = ReadUShort(); if (currentTagId != tagId) throw new ExifLibException("Tag number not at expected offset"); // Read the offset to the Exif IFD tiffDataType = ReadUShort(); numberOfComponents = ReadUint(); var tagData = ReadBytes(4); // If the total space taken up by the field is longer than the // 2 bytes afforded by the tagData, tagData will contain an offset // to the actual data. var dataSize = (int)(numberOfComponents * GetTIFFFieldLength(tiffDataType)); if (dataSize > 4) { var offsetAddress = ToUShort(tagData); return ReadBytes(offsetAddress, dataSize); } // The value is stored in the tagData starting from the left Array.Resize(ref tagData, dataSize); return tagData; } /// /// Records all Exif tags and their offsets within /// the file from the current IFD /// private void CatalogueIFD(ref Dictionary tagOffsets) { // Assume we're just before the IFD. // First 2 bytes is the number of entries in this IFD var entryCount = ReadUShort(); for (ushort currentEntry = 0; currentEntry < entryCount; currentEntry++) { var currentTagNumber = ReadUShort(); // Record this in the catalogue tagOffsets[currentTagNumber] = _stream.Position - 2; // Go to the end of this item (10 bytes, as each entry is 12 bytes long) _stream.Seek(10, SeekOrigin.Current); } } #endregion #region Thumbnail retrieval /// /// Retrieves a JPEG thumbnail from the image if one is present. Note that this method cannot retrieve thumbnails encoded in other formats, /// but since the DCF specification specifies that thumbnails must be JPEG, this method will be sufficient for most purposes /// See http://gvsoft.homedns.org/exif/exif-explanation.html#TIFFThumbs or http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf for /// details on the encoding of TIFF thumbnails /// /// public byte[] GetJpegThumbnailBytes() { Initialize(); if (_ifd1Catalogue == null) return null; // Get the thumbnail encoding ushort compression; if (!GetTagValue(_ifd1Catalogue, (ushort)ExifTags.Compression, out compression)) return null; // This method only handles JPEG thumbnails (compression type 6) if (compression != 6) return null; // Get the location of the thumbnail uint offset; if (!GetTagValue(_ifd1Catalogue, (ushort)ExifTags.JPEGInterchangeFormat, out offset)) return null; // Get the length of the thumbnail data uint length; if (!GetTagValue(_ifd1Catalogue, (ushort)ExifTags.JPEGInterchangeFormatLength, out length)) return null; _stream.Position = offset; // The thumbnail may be padded, so we scan forward until we reach the JPEG header (0xFFD8) or the end of the file int currentByte; var previousByte = -1; while ((currentByte = _stream.ReadByte()) != -1) { if (previousByte == 0xFF && currentByte == 0xD8) break; previousByte = currentByte; } if (currentByte != 0xD8) return null; // Step back to the start of the JPEG header _stream.Position -= 2; var imageBytes = new byte[length]; _stream.Read(imageBytes, 0, (int)length); // A valid JPEG stream ends with 0xFFD9. The stream may be padded at the end with multiple 0xFF bytes. var jpegStreamEnd = (int)length - 1; while (jpegStreamEnd > 0 && imageBytes[jpegStreamEnd] == 0xFF) jpegStreamEnd--; if (jpegStreamEnd <= 0 || imageBytes[jpegStreamEnd] != 0xD9 || imageBytes[jpegStreamEnd - 1] != 0xFF) return null; return imageBytes; } #endregion #region IDisposable Members ~ExifReader() { Dispose(false); } private void Dispose(bool disposing) { if (disposing) { if (_reader != null) _reader.Dispose(); if (!_leaveOpen) { // Make sure the file handle is released _stream?.Dispose(); } } _reader = null; _stream = null; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #endregion } sealed class ExifLibException : Exception { public ExifLibException() { } public ExifLibException(string message) : base(message) { } public ExifLibException(string message, Exception innerException) : base(message, innerException) { } } /// /// All exif tags as per the Exif standard 2.2, JEITA CP-2451 /// public enum ExifTags : ushort { // IFD0 items ImageWidth = 0x100, ImageLength = 0x101, BitsPerSample = 0x102, Compression = 0x103, PhotometricInterpretation = 0x106, ImageDescription = 0x10E, Make = 0x10F, Model = 0x110, StripOffsets = 0x111, Orientation = 0x112, SamplesPerPixel = 0x115, RowsPerStrip = 0x116, StripByteCounts = 0x117, XResolution = 0x11A, YResolution = 0x11B, PlanarConfiguration = 0x11C, ResolutionUnit = 0x128, TransferFunction = 0x12D, Software = 0x131, DateTime = 0x132, Artist = 0x13B, WhitePoint = 0x13E, PrimaryChromaticities = 0x13F, JPEGInterchangeFormat = 0x201, JPEGInterchangeFormatLength = 0x202, YCbCrCoefficients = 0x211, YCbCrSubSampling = 0x212, YCbCrPositioning = 0x213, ReferenceBlackWhite = 0x214, Copyright = 0x8298, // SubIFD items ExposureTime = 0x829A, FNumber = 0x829D, ExposureProgram = 0x8822, SpectralSensitivity = 0x8824, ISOSpeedRatings = 0x8827, OECF = 0x8828, ExifVersion = 0x9000, DateTimeOriginal = 0x9003, DateTimeDigitized = 0x9004, ComponentsConfiguration = 0x9101, CompressedBitsPerPixel = 0x9102, ShutterSpeedValue = 0x9201, ApertureValue = 0x9202, BrightnessValue = 0x9203, ExposureBiasValue = 0x9204, MaxApertureValue = 0x9205, SubjectDistance = 0x9206, MeteringMode = 0x9207, LightSource = 0x9208, Flash = 0x9209, FocalLength = 0x920A, SubjectArea = 0x9214, MakerNote = 0x927C, UserComment = 0x9286, SubsecTime = 0x9290, SubsecTimeOriginal = 0x9291, SubsecTimeDigitized = 0x9292, FlashpixVersion = 0xA000, ColorSpace = 0xA001, PixelXDimension = 0xA002, PixelYDimension = 0xA003, RelatedSoundFile = 0xA004, FlashEnergy = 0xA20B, SpatialFrequencyResponse = 0xA20C, FocalPlaneXResolution = 0xA20E, FocalPlaneYResolution = 0xA20F, FocalPlaneResolutionUnit = 0xA210, SubjectLocation = 0xA214, ExposureIndex = 0xA215, SensingMethod = 0xA217, FileSource = 0xA300, SceneType = 0xA301, CFAPattern = 0xA302, CustomRendered = 0xA401, ExposureMode = 0xA402, WhiteBalance = 0xA403, DigitalZoomRatio = 0xA404, FocalLengthIn35mmFilm = 0xA405, SceneCaptureType = 0xA406, GainControl = 0xA407, Contrast = 0xA408, Saturation = 0xA409, Sharpness = 0xA40A, DeviceSettingDescription = 0xA40B, SubjectDistanceRange = 0xA40C, ImageUniqueID = 0xA420, // GPS subifd items GPSVersionID = 0x0, GPSLatitudeRef = 0x1, GPSLatitude = 0x2, GPSLongitudeRef = 0x3, GPSLongitude = 0x4, GPSAltitudeRef = 0x5, GPSAltitude = 0x6, GPSTimestamp = 0x7, GPSSatellites = 0x8, GPSStatus = 0x9, GPSMeasureMode = 0xA, GPSDOP = 0xB, GPSSpeedRef = 0xC, GPSSpeed = 0xD, GPSTrackRef = 0xE, GPSTrack = 0xF, GPSImgDirectionRef = 0x10, GPSImgDirection = 0x11, GPSMapDatum = 0x12, GPSDestLatitudeRef = 0x13, GPSDestLatitude = 0x14, GPSDestLongitudeRef = 0x15, GPSDestLongitude = 0x16, GPSDestBearingRef = 0x17, GPSDestBearing = 0x18, GPSDestDistanceRef = 0x19, GPSDestDistance = 0x1A, GPSProcessingMethod = 0x1B, GPSAreaInformation = 0x1C, GPSDateStamp = 0x1D, GPSDifferential = 0x1E } } }