aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Providers/Photos
diff options
context:
space:
mode:
authorLuke Pulverenti <luke.pulverenti@gmail.com>2014-02-13 00:11:54 -0500
committerLuke Pulverenti <luke.pulverenti@gmail.com>2014-02-13 00:11:54 -0500
commiteec9e0482525c400e9dc7cb17bc000434adba105 (patch)
tree73f51bc882804ff92b82d1e85a46a6cec10b6d51 /MediaBrowser.Providers/Photos
parent9254c37d52af3d16ec9e46b3e211ecc7dc4f1617 (diff)
take photos into the core
Diffstat (limited to 'MediaBrowser.Providers/Photos')
-rw-r--r--MediaBrowser.Providers/Photos/ExifReader.cs613
-rw-r--r--MediaBrowser.Providers/Photos/ExifTags.cs132
-rw-r--r--MediaBrowser.Providers/Photos/PhotoHelper.cs113
-rw-r--r--MediaBrowser.Providers/Photos/PhotoMetadataService.cs32
-rw-r--r--MediaBrowser.Providers/Photos/PhotoProvider.cs137
5 files changed, 1027 insertions, 0 deletions
diff --git a/MediaBrowser.Providers/Photos/ExifReader.cs b/MediaBrowser.Providers/Photos/ExifReader.cs
new file mode 100644
index 000000000..8526a7f2a
--- /dev/null
+++ b/MediaBrowser.Providers/Photos/ExifReader.cs
@@ -0,0 +1,613 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Text;
+
+namespace MediaBrowser.Providers.Photos
+{
+ /// <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>
+ public class ExifReader : IDisposable
+ {
+ private readonly FileStream fileStream = null;
+ private readonly BinaryReader reader = null;
+
+ /// <summary>
+ /// The catalogue of tag ids and their absolute offsets within the
+ /// file
+ /// </summary>
+ private Dictionary<ushort, long> catalogue;
+
+ /// <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;
+
+ public ExifReader(string fileName)
+ {
+ // 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;
+
+ try
+ {
+ // Open the file in a stream
+ fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ reader = new BinaryReader(fileStream);
+
+ // Make sure the file's a JPEG.
+ if (ReadUShort() != 0xFFD8)
+ throw new Exception("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();
+ }
+ catch (Exception)
+ {
+ // If instantiation fails, make sure there's no mess left behind
+ Dispose();
+
+ throw;
+ }
+ }
+
+ #region TIFF methods
+
+ /// <summary>
+ /// Returns the length (in bytes) per component of the specified TIFF data type
+ /// </summary>
+ /// <returns></returns>
+ private byte GetTIFFFieldLength(ushort tiffDataType)
+ {
+ switch (tiffDataType)
+ {
+ case 1:
+ case 2:
+ case 6:
+ return 1;
+ case 3:
+ case 8:
+ return 2;
+ case 4:
+ case 7:
+ case 9:
+ case 11:
+ return 4;
+ case 5:
+ case 10:
+ case 12:
+ return 8;
+ default:
+ throw new Exception(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)
+ {
+ return Encoding.ASCII.GetString(ReadBytes(chars));
+ }
+
+ 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
+ long originalOffset = fileStream.Position;
+
+ // Move to the TIFF offset and retrieve the data
+ fileStream.Seek(tiffOffset + tiffHeaderStart, SeekOrigin.Begin);
+
+ byte[] data = reader.ReadBytes(byteCount);
+
+ // Restore the file offset
+ fileStream.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 an unsigned rational using the current byte aligns.
+ /// </summary>
+ /// <param name="data"></param>
+ /// <returns></returns>
+ /// <seealso cref="ToRational"/>
+ private double ToURational(byte[] data)
+ {
+ var numeratorData = new byte[4];
+ var denominatorData = new byte[4];
+
+ Array.Copy(data, numeratorData, 4);
+ Array.Copy(data, 4, denominatorData, 0, 4);
+
+ uint numerator = ToUint(numeratorData);
+ uint denominator = ToUint(denominatorData);
+
+ return numerator / (double)denominator;
+ }
+
+ /// <summary>
+ /// Converts 8 bytes to a signed 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>
+ /// <param name="data"></param>
+ /// <returns></returns>
+ private double ToRational(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 numerator / (double)denominator;
+ }
+
+ /// <summary>
+ /// Converts 4 bytes to a uint using the current byte aligns
+ /// </summary>
+ /// <returns></returns>
+ 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>
+ /// <returns></returns>
+ 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 Array GetArray<T>(byte[] data, int elementLengthBytes, ConverterMethod<T> converter)
+ {
+ Array convertedData = Array.CreateInstance(typeof(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>
+ 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)!
+ reader.BaseStream.Seek(dataLength - 2, SeekOrigin.Current);
+ }
+
+ // It's only success if we found the 0xFFE1 marker
+ if (markerStart != 0xFF || markerNumber != 0xE1)
+ throw new Exception("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 Exception("Exif data not found");
+
+ // 2 zero bytes
+ if (ReadUShort() != 0)
+ throw new Exception("Malformed Exif data");
+
+ // We're now into the TIFF format
+ tiffHeaderStart = reader.BaseStream.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 Exception("Error in TIFF data");
+
+ // Get the offset to the IFD (image file directory)
+ uint ifdOffset = ReadUint();
+
+ // Note that this offset is from the first byte of the TIFF header. Jump to the IFD.
+ fileStream.Position = ifdOffset + tiffHeaderStart;
+
+ // Catalogue this first IFD (there will be another IFD)
+ CatalogueIFD();
+
+ // 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(0x8769, out offset))
+ throw new Exception("Unable to locate Exif data");
+
+ // Jump to the exif SubIFD
+ fileStream.Position = offset + tiffHeaderStart;
+
+ // Add the subIFD to the catalogue too
+ CatalogueIFD();
+
+ // Go to the GPS IFD and catalogue that too. It's an optional
+ // section.
+ if (GetTagValue(0x8825, out offset))
+ {
+ // Jump to the GPS SubIFD
+ fileStream.Position = offset + tiffHeaderStart;
+
+ // Add the subIFD to the catalogue too
+ CatalogueIFD();
+ }
+ }
+
+ #endregion
+
+ #region Exif data catalog and retrieval methods
+
+ public bool GetTagValue<T>(ExifTags tag, out T result)
+ {
+ return GetTagValue((ushort)tag, out result);
+ }
+
+ /// <summary>
+ /// Retrieves an Exif value with the requested tag ID
+ /// </summary>
+ /// <param name="tagID"></param>
+ /// <param name="result"></param>
+ /// <returns></returns>
+ public bool GetTagValue<T>(ushort tagID, out T result)
+ {
+ ushort tiffDataType;
+ uint numberOfComponents;
+ byte[] tagData = GetTagBytes(tagID, out tiffDataType, out numberOfComponents);
+
+ if (tagData == null)
+ {
+ result = default(T);
+ return false;
+ }
+
+ byte 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
+ string str = Encoding.ASCII.GetString(tagData);
+
+ // There may be a null character within the string
+ int nullCharIndex = str.IndexOf('\0');
+ if (nullCharIndex != -1)
+ str = str.Substring(0, nullCharIndex);
+
+ // Special processing for dates.
+ if (typeof(T) == typeof(DateTime))
+ {
+ result =
+ (T)(object)DateTime.ParseExact(str, "yyyy:MM:dd HH:mm:ss", CultureInfo.InvariantCulture);
+ return true;
+ }
+
+ 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)
+ 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 an unsigned integer.
+ if (numberOfComponents == 1)
+ result = (T)(object)ToUint(tagData);
+ else
+ result = (T)(object)GetArray(tagData, fieldLength, ToUint);
+ 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)
+ 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 Exception(string.Format("Unknown TIFF datatype: {0}", tiffDataType));
+ }
+ }
+
+ /// <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="tagID"></param>
+ private byte[] GetTagBytes(ushort tagID, out ushort tiffDataType, out uint numberOfComponents)
+ {
+ // Get the tag's offset from the catalogue and do some basic error checks
+ if (fileStream == null || reader == null || catalogue == null || !catalogue.ContainsKey(tagID))
+ {
+ tiffDataType = 0;
+ numberOfComponents = 0;
+ return null;
+ }
+
+ long tagOffset = catalogue[tagID];
+
+ // Jump to the TIFF offset
+ fileStream.Position = tagOffset;
+
+ // Read the tag number from the file
+ ushort currentTagID = ReadUShort();
+
+ if (currentTagID != tagID)
+ throw new Exception("Tag number not at expected offset");
+
+ // Read the offset to the Exif IFD
+ tiffDataType = ReadUShort();
+ numberOfComponents = ReadUint();
+ byte[] 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)
+ {
+ ushort 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()
+ {
+ if (catalogue == null)
+ catalogue = new Dictionary<ushort, long>();
+
+ // Assume we're just before the IFD.
+
+ // First 2 bytes is the number of entries in this IFD
+ ushort entryCount = ReadUShort();
+
+ for (ushort currentEntry = 0; currentEntry < entryCount; currentEntry++)
+ {
+ ushort currentTagNumber = ReadUShort();
+
+ // Record this in the catalogue
+ catalogue[currentTagNumber] = fileStream.Position - 2;
+
+ // Go to the end of this item (10 bytes, as each entry is 12 bytes long)
+ reader.BaseStream.Seek(10, SeekOrigin.Current);
+ }
+ }
+
+ #endregion
+
+ #region IDisposable Members
+
+ public void Dispose()
+ {
+ // Make sure the file handle is released
+ if (reader != null)
+ reader.Close();
+ if (fileStream != null)
+ fileStream.Close();
+ }
+
+ #endregion
+ }
+}
diff --git a/MediaBrowser.Providers/Photos/ExifTags.cs b/MediaBrowser.Providers/Photos/ExifTags.cs
new file mode 100644
index 000000000..39e153f2e
--- /dev/null
+++ b/MediaBrowser.Providers/Photos/ExifTags.cs
@@ -0,0 +1,132 @@
+
+namespace MediaBrowser.Providers.Photos
+{
+ /// <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
+ }
+}
diff --git a/MediaBrowser.Providers/Photos/PhotoHelper.cs b/MediaBrowser.Providers/Photos/PhotoHelper.cs
new file mode 100644
index 000000000..a5ce6f81f
--- /dev/null
+++ b/MediaBrowser.Providers/Photos/PhotoHelper.cs
@@ -0,0 +1,113 @@
+using MediaBrowser.Controller.Entities;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MediaBrowser.Providers.Photos
+{
+ public static class PhotoHelper
+ {
+ public static List<BaseItem> ShuffleList(List<BaseItem> list)
+ {
+ var rnd = new Random(DateTime.Now.Second);
+ for (var i = 1; i < list.Count; i++)
+ {
+ var pos = rnd.Next(i + 1);
+ var x = list[i];
+ list[i] = list[pos];
+ list[pos] = x;
+ }
+ return list;
+ }
+
+ public static string Dec2Frac(double dbl)
+ {
+ char neg = ' ';
+ double dblDecimal = dbl;
+ if (dblDecimal == (int)dblDecimal) return dblDecimal.ToString(); //return no if it's not a decimal
+ if (dblDecimal < 0)
+ {
+ dblDecimal = Math.Abs(dblDecimal);
+ neg = '-';
+ }
+ var whole = (int)Math.Truncate(dblDecimal);
+ string decpart = dblDecimal.ToString().Replace(Math.Truncate(dblDecimal) + ".", "");
+ double rN = Convert.ToDouble(decpart);
+ double rD = Math.Pow(10, decpart.Length);
+
+ string rd = Recur(decpart);
+ int rel = Convert.ToInt32(rd);
+ if (rel != 0)
+ {
+ rN = rel;
+ rD = (int)Math.Pow(10, rd.Length) - 1;
+ }
+ //just a few prime factors for testing purposes
+ var primes = new[] { 47, 43, 37, 31, 29, 23, 19, 17, 13, 11, 7, 5, 3, 2 };
+ foreach (int i in primes) ReduceNo(i, ref rD, ref rN);
+
+ rN = rN + (whole * rD);
+ return string.Format("{0}{1}/{2}", neg, rN, rD);
+ }
+
+ /// <summary>
+ /// Finds out the recurring decimal in a specified number
+ /// </summary>
+ /// <param name="db">Number to check</param>
+ /// <returns></returns>
+ private static string Recur(string db)
+ {
+ if (db.Length < 13) return "0";
+ var sb = new StringBuilder();
+ for (int i = 0; i < 7; i++)
+ {
+ sb.Append(db[i]);
+ int dlength = (db.Length / sb.ToString().Length);
+ int occur = Occurence(sb.ToString(), db);
+ if (dlength == occur || dlength == occur - sb.ToString().Length)
+ {
+ return sb.ToString();
+ }
+ }
+ return "0";
+ }
+
+ /// <summary>
+ /// Checks for number of occurence of specified no in a number
+ /// </summary>
+ /// <param name="s">The no to check occurence times</param>
+ /// <param name="check">The number where to check this</param>
+ /// <returns></returns>
+ private static int Occurence(string s, string check)
+ {
+ int i = 0;
+ int d = s.Length;
+ string ds = check;
+ for (int n = (ds.Length / d); n > 0; n--)
+ {
+ if (ds.Contains(s))
+ {
+ i++;
+ ds = ds.Remove(ds.IndexOf(s, System.StringComparison.Ordinal), d);
+ }
+ }
+ return i;
+ }
+
+ /// <summary>
+ /// Reduces a fraction given the numerator and denominator
+ /// </summary>
+ /// <param name="i">Number to use in an attempt to reduce fraction</param>
+ /// <param name="rD">the Denominator</param>
+ /// <param name="rN">the Numerator</param>
+ private static void ReduceNo(int i, ref double rD, ref double rN)
+ {
+ //keep reducing until divisibility ends
+ while ((rD % i) < 1e-10 && (rN % i) < 1e-10)
+ {
+ rN = rN / i;
+ rD = rD / i;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Photos/PhotoMetadataService.cs b/MediaBrowser.Providers/Photos/PhotoMetadataService.cs
new file mode 100644
index 000000000..2a7895e16
--- /dev/null
+++ b/MediaBrowser.Providers/Photos/PhotoMetadataService.cs
@@ -0,0 +1,32 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Providers.Manager;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Providers.Photos
+{
+ class PhotoMetadataService : MetadataService<Photo, ItemLookupInfo>
+ {
+ public PhotoMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, IFileSystem fileSystem)
+ : base(serverConfigurationManager, logger, providerManager, providerRepo, fileSystem)
+ {
+ }
+
+ /// <summary>
+ /// Merges the specified source.
+ /// </summary>
+ /// <param name="source">The source.</param>
+ /// <param name="target">The target.</param>
+ /// <param name="lockedFields">The locked fields.</param>
+ /// <param name="replaceData">if set to <c>true</c> [replace data].</param>
+ /// <param name="mergeMetadataSettings">if set to <c>true</c> [merge metadata settings].</param>
+ protected override void MergeData(Photo source, Photo target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings)
+ {
+ ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Photos/PhotoProvider.cs b/MediaBrowser.Providers/Photos/PhotoProvider.cs
new file mode 100644
index 000000000..23ad5230d
--- /dev/null
+++ b/MediaBrowser.Providers/Photos/PhotoProvider.cs
@@ -0,0 +1,137 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Providers.Photos
+{
+ public class PhotoProvider : ICustomMetadataProvider<Photo>, IHasChangeMonitor
+ {
+ private readonly ILogger _logger;
+
+ public PhotoProvider(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public Task<ItemUpdateType> FetchAsync(Photo item, IDirectoryService directoryService, CancellationToken cancellationToken)
+ {
+ item.SetImagePath(ImageType.Primary, item.Path);
+
+ if (item.Path.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || item.Path.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase))
+ {
+ try
+ {
+ using (var reader = new ExifReader(item.Path))
+ {
+ double aperture = 0;
+ double shutterSpeed = 0;
+
+ DateTime dateTaken;
+
+ string manufacturer;
+ string model;
+
+ int xResolution;
+ int yResolution;
+
+ reader.GetTagValue(ExifTags.FNumber, out aperture);
+ reader.GetTagValue(ExifTags.ExposureTime, out shutterSpeed);
+ reader.GetTagValue(ExifTags.DateTimeOriginal, out dateTaken);
+
+ reader.GetTagValue(ExifTags.Make, out manufacturer);
+ reader.GetTagValue(ExifTags.Model, out model);
+
+ reader.GetTagValue(ExifTags.XResolution, out xResolution);
+ reader.GetTagValue(ExifTags.YResolution, out yResolution);
+
+ if (dateTaken > DateTime.MinValue)
+ {
+ item.DateCreated = dateTaken;
+ item.PremiereDate = dateTaken;
+ item.ProductionYear = dateTaken.Year;
+ }
+
+ var cameraModel = manufacturer ?? string.Empty;
+ cameraModel += " ";
+ cameraModel += model ?? string.Empty;
+
+ item.Overview = "Taken " + dateTaken.ToString("F") + "\n" +
+ (!string.IsNullOrWhiteSpace(cameraModel) ? "With a " + cameraModel : "") +
+ (aperture > 0 && shutterSpeed > 0 ? " at f" + aperture.ToString(CultureInfo.InvariantCulture) + " and " + PhotoHelper.Dec2Frac(shutterSpeed) + "s" : "") + "\n"
+ + (xResolution > 0 ? "\n<br/>Resolution: " + xResolution + "x" + yResolution : "");
+ }
+
+ }
+ catch (Exception e)
+ {
+ _logger.ErrorException("Image Provider - Error reading image tag for {0}", e, item.Path);
+ }
+ }
+
+ //// Get additional tags from xmp
+ //try
+ //{
+ // using (var fs = new FileStream(item.Path, FileMode.Open, FileAccess.Read))
+ // {
+ // var bf = BitmapFrame.Create(fs);
+
+ // if (bf != null)
+ // {
+ // var data = (BitmapMetadata)bf.Metadata;
+ // if (data != null)
+ // {
+
+ // DateTime dateTaken;
+ // var cameraModel = "";
+
+ // DateTime.TryParse(data.DateTaken, out dateTaken);
+ // if (dateTaken > DateTime.MinValue) item.DateCreated = dateTaken;
+ // cameraModel = data.CameraModel;
+
+ // item.PremiereDate = dateTaken;
+ // item.ProductionYear = dateTaken.Year;
+ // item.Overview = "Taken " + dateTaken.ToString("F") + "\n" +
+ // (cameraModel != "" ? "With a " + cameraModel : "") +
+ // (aperture > 0 && shutterSpeed > 0 ? " at f" + aperture.ToString(CultureInfo.InvariantCulture) + " and " + PhotoHelper.Dec2Frac(shutterSpeed) + "s" : "") + "\n"
+ // + (bf.Width > 0 ? "\n<br/>Resolution: " + (int)bf.Width + "x" + (int)bf.Height : "");
+
+ // var photo = item as Photo;
+ // if (data.Keywords != null) item.Genres = photo.Tags = new List<string>(data.Keywords);
+ // item.Name = !string.IsNullOrWhiteSpace(data.Title) ? data.Title : item.Name;
+ // item.CommunityRating = data.Rating;
+ // if (!string.IsNullOrWhiteSpace(data.Subject)) photo.AddTagline(data.Subject);
+ // }
+ // }
+
+ // }
+ //}
+ //catch (NotSupportedException)
+ //{
+ // // No problem - move on
+ //}
+ //catch (Exception e)
+ //{
+ // _logger.ErrorException("Error trying to read extended data from {0}", e, item.Path);
+ //}
+
+ const ItemUpdateType result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport;
+ return Task.FromResult(result);
+ }
+
+ public string Name
+ {
+ get { return "Embedded Information"; }
+ }
+
+ public bool HasChanged(IHasMetadata item, IDirectoryService directoryService, DateTime date)
+ {
+ return item.DateModified > date;
+ }
+ }
+}