Files
PDFPatcher/App/Processor/Imaging/JpgHelper.cs
T
2022-10-28 19:56:51 +08:00

944 lines
28 KiB
C#

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;
}
}
/// <summary>
/// A class for reading Exif data from a JPEG file. The file will be open for reading for as long as the class exists.
/// <seealso cref="http://gvsoft.homedns.org/exif/Exif-explanation.html"/>
/// </summary>
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;
/// <summary>
/// The main tag id/absolute file offset catalogue
/// </summary>
private Dictionary<ushort, long> _ifd0Catalogue;
/// <summary>
/// The thumbnail tag id/absolute file offset catalogue
/// </summary>
/// <remarks>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 <see cref="GetJpegThumbnailBytes"/>.</remarks>
private Dictionary<ushort, long> _ifd1Catalogue;
/// <summary>
/// Indicates whether to read data using big or little endian byte aligns
/// </summary>
private bool _isLittleEndian;
/// <summary>
/// The position in the filestream at which the TIFF header starts
/// </summary>
private long _tiffHeaderStart;
/// <summary>
/// The location of the thumbnail IFD
/// </summary>
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
/// <summary>
/// Returns the length (in bytes) per component of the specified TIFF data type
/// </summary>
/// <returns></returns>
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
/// <summary>
/// Gets a 2 byte unsigned integer from the file
/// </summary>
/// <returns></returns>
private ushort ReadUShort() {
return ToUShort(ReadBytes(2));
}
/// <summary>
/// Gets a 4 byte unsigned integer from the file
/// </summary>
/// <returns></returns>
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);
}
/// <summary>
/// Reads some bytes from the specified TIFF offset
/// </summary>
/// <param name="tiffOffset"></param>
/// <param name="byteCount"></param>
/// <returns></returns>
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
/// <summary>
/// Converts 2 bytes to a ushort using the current byte aligns
/// </summary>
/// <returns></returns>
private ushort ToUShort(byte[] data) {
if (_isLittleEndian != BitConverter.IsLittleEndian)
Array.Reverse(data);
return BitConverter.ToUInt16(data, 0);
}
/// <summary>
/// Converts 8 bytes to the numerator and denominator
/// components of an unsigned rational using the current byte aligns
/// </summary>
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 };
}
/// <summary>
/// Converts 8 bytes to an unsigned rational using the current byte aligns
/// </summary>
/// <seealso cref="ToRational"/>
private double ToURational(byte[] data) {
var fraction = ToURationalFraction(data);
return fraction[0] / (double)fraction[1];
}
/// <summary>
/// Converts 8 bytes to the numerator and denominator
/// components of an unsigned rational using the current byte aligns
/// </summary>
/// <remarks>
/// A TIFF rational contains 2 4-byte integers, the first of which is
/// the numerator, and the second of which is the denominator.
/// </remarks>
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 };
}
/// <summary>
/// Converts 8 bytes to a signed rational using the current byte aligns.
/// </summary>
/// <seealso cref="ToRationalFraction"/>
private double ToRational(byte[] data) {
var fraction = ToRationalFraction(data);
return fraction[0] / (double)fraction[1];
}
/// <summary>
/// Converts 4 bytes to a uint using the current byte aligns
/// </summary>
private uint ToUint(byte[] data) {
if (_isLittleEndian != BitConverter.IsLittleEndian)
Array.Reverse(data);
return BitConverter.ToUInt32(data, 0);
}
/// <summary>
/// Converts 4 bytes to an int using the current byte aligns
/// </summary>
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);
}
/// <summary>
/// Retrieves an array from a byte array using the supplied converter
/// to read each individual element from the supplied byte array
/// </summary>
/// <param name="data"></param>
/// <param name="elementLengthBytes"></param>
/// <param name="converter"></param>
/// <returns></returns>
private static Array GetArray<T>(byte[] data, int elementLengthBytes, ConverterMethod<T> 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;
}
/// <summary>
/// A delegate used to invoke any of the data conversion methods
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
/// <remarks>Although this could be defined as covariant, it wouldn't work on Windows Phone</remarks>
private delegate T ConverterMethod<out T>(byte[] data);
#endregion
#region Stream seek methods - used to get to locations within the JPEG
/// <summary>
/// Scans to the Exif block
/// </summary>
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");
}
/// <summary>
/// Reads through the Exif data and builds an index of all Exif tags in the document
/// </summary>
/// <returns></returns>
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<ushort, long>();
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");
// 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<ushort, long>();
CatalogueIFD(ref _ifd1Catalogue);
}
}
#endregion
#region Exif data catalog and retrieval methods
public bool GetTagValue<T>(ExifTags tag, out T result) {
return GetTagValue((ushort)tag, out result);
}
public bool GetTagValue<T>(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);
}
/// <summary>
/// Retrieves an Exif value with the requested tag ID
/// </summary>
private bool GetTagValue<T>(IDictionary<ushort, long> 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;
}
/// <summary>
/// Gets the data in the specified tag ID, starting from before the IFD block.
/// </summary>
/// <param name="tiffDataType"></param>
/// <param name="numberOfComponents">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</param>
/// <param name="tagDictionary"></param>
/// <param name="tagId"></param>
private byte[] GetTagBytes(IDictionary<ushort, long> 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;
}
/// <summary>
/// Records all Exif tags and their offsets within
/// the file from the current IFD
/// </summary>
private void CatalogueIFD(ref Dictionary<ushort, long> 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
/// <summary>
/// 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
/// </summary>
/// <returns></returns>
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) {
}
}
/// <summary>
/// All exif tags as per the Exif standard 2.2, JEITA CP-2451
/// </summary>
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
}
}
}