diff options
| author | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-02-13 00:11:54 -0500 |
|---|---|---|
| committer | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-02-13 00:11:54 -0500 |
| commit | eec9e0482525c400e9dc7cb17bc000434adba105 (patch) | |
| tree | 73f51bc882804ff92b82d1e85a46a6cec10b6d51 /MediaBrowser.Providers/Photos | |
| parent | 9254c37d52af3d16ec9e46b3e211ecc7dc4f1617 (diff) | |
take photos into the core
Diffstat (limited to 'MediaBrowser.Providers/Photos')
| -rw-r--r-- | MediaBrowser.Providers/Photos/ExifReader.cs | 613 | ||||
| -rw-r--r-- | MediaBrowser.Providers/Photos/ExifTags.cs | 132 | ||||
| -rw-r--r-- | MediaBrowser.Providers/Photos/PhotoHelper.cs | 113 | ||||
| -rw-r--r-- | MediaBrowser.Providers/Photos/PhotoMetadataService.cs | 32 | ||||
| -rw-r--r-- | MediaBrowser.Providers/Photos/PhotoProvider.cs | 137 |
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; + } + } +} |
