From 0efb9b72e75c8a14a6dc6989d746e4cfbe6f84a4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 10 Feb 2015 16:33:59 +1100 Subject: [PATCH] Adds small exif library to read meta data from jpg's so we can attempt to read width/height from exif instead of loading the file into mem with GDI, makes for much faster and far less memory processing.U4-6246 Uploading specific images causes GDI+ Errors --- src/Umbraco.Core/IO/UmbracoMediaFile.cs | 6 +- src/Umbraco.Core/Media/Exif/ExifIO.cs | 104 ++++++++ src/Umbraco.Core/Media/Exif/ExifIds.cs | 133 +++++++++++ src/Umbraco.Core/Media/Exif/ExifReader.cs | 255 ++++++++++++++++++++ src/Umbraco.Core/Media/Exif/ExifTag.cs | 258 ++++++++++++++++++++ src/Umbraco.Core/Media/Exif/JpegInfo.cs | 275 ++++++++++++++++++++++ src/Umbraco.Core/Media/ImageHelper.cs | 35 ++- src/Umbraco.Core/StringExtensions.cs | 16 +- src/Umbraco.Core/Umbraco.Core.csproj | 5 + 9 files changed, 1076 insertions(+), 11 deletions(-) create mode 100644 src/Umbraco.Core/Media/Exif/ExifIO.cs create mode 100644 src/Umbraco.Core/Media/Exif/ExifIds.cs create mode 100644 src/Umbraco.Core/Media/Exif/ExifReader.cs create mode 100644 src/Umbraco.Core/Media/Exif/ExifTag.cs create mode 100644 src/Umbraco.Core/Media/Exif/JpegInfo.cs diff --git a/src/Umbraco.Core/IO/UmbracoMediaFile.cs b/src/Umbraco.Core/IO/UmbracoMediaFile.cs index a8a55e5d8a..b8fe310f54 100644 --- a/src/Umbraco.Core/IO/UmbracoMediaFile.cs +++ b/src/Umbraco.Core/IO/UmbracoMediaFile.cs @@ -154,12 +154,8 @@ namespace Umbraco.Core.IO EnsureFileSupportsResizing(); using (var fs = _fs.OpenFile(Path)) - using (var image = Image.FromStream(fs)) { - - var fileWidth = image.Width; - var fileHeight = image.Height; - _size = new Size(fileWidth, fileHeight); + _size = ImageHelper.GetDimensions(fs); } } else diff --git a/src/Umbraco.Core/Media/Exif/ExifIO.cs b/src/Umbraco.Core/Media/Exif/ExifIO.cs new file mode 100644 index 0000000000..3818385955 --- /dev/null +++ b/src/Umbraco.Core/Media/Exif/ExifIO.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Media.Exif +{ + /// + /// Utility to handle multi-byte primitives in both big and little endian. + /// http://msdn.microsoft.com/en-us/library/system.bitconverter.islittleendian.aspx + /// http://en.wikipedia.org/wiki/Endianness + /// + internal static class ExifIO + { + public static short ReadShort(byte[] Data, int offset, bool littleEndian) + { + if ((littleEndian && BitConverter.IsLittleEndian) || + (!littleEndian && !BitConverter.IsLittleEndian)) + { + return BitConverter.ToInt16(Data, offset); + } + else + { + byte[] beBytes = new byte[2] { Data[offset + 1], Data[offset] }; + return BitConverter.ToInt16(beBytes, 0); + } + } + + public static ushort ReadUShort(byte[] Data, int offset, bool littleEndian) + { + if ((littleEndian && BitConverter.IsLittleEndian) || + (!littleEndian && !BitConverter.IsLittleEndian)) + { + return BitConverter.ToUInt16(Data, offset); + } + else + { + byte[] beBytes = new byte[2] { Data[offset + 1], Data[offset] }; + return BitConverter.ToUInt16(beBytes, 0); + } + } + + public static int ReadInt(byte[] Data, int offset, bool littleEndian) + { + if ((littleEndian && BitConverter.IsLittleEndian) || + (!littleEndian && !BitConverter.IsLittleEndian)) + { + return BitConverter.ToInt32(Data, offset); + } + else + { + byte[] beBytes = new byte[4] { Data[offset + 3], Data[offset + 2], Data[offset + 1], Data[offset] }; + return BitConverter.ToInt32(beBytes, 0); + } + } + + public static uint ReadUInt(byte[] Data, int offset, bool littleEndian) + { + if ((littleEndian && BitConverter.IsLittleEndian) || + (!littleEndian && !BitConverter.IsLittleEndian)) + { + return BitConverter.ToUInt32(Data, offset); + } + else + { + byte[] beBytes = new byte[4] { Data[offset + 3], Data[offset + 2], Data[offset + 1], Data[offset] }; + return BitConverter.ToUInt32(beBytes, 0); + } + } + + public static float ReadSingle(byte[] Data, int offset, bool littleEndian) + { + if ((littleEndian && BitConverter.IsLittleEndian) || + (!littleEndian && !BitConverter.IsLittleEndian)) + { + return BitConverter.ToSingle(Data, offset); + } + else + { + // need to swap the data first + byte[] beBytes = new byte[4] { Data[offset + 3], Data[offset + 2], Data[offset + 1], Data[offset] }; + return BitConverter.ToSingle(beBytes, 0); + } + } + + public static double ReadDouble(byte[] Data, int offset, bool littleEndian) + { + if ((littleEndian && BitConverter.IsLittleEndian) || + (!littleEndian && !BitConverter.IsLittleEndian)) + { + return BitConverter.ToDouble(Data, offset); + } + else + { + // need to swap the data first + byte[] beBytes = new byte[8] { + Data[offset + 7], Data[offset + 6], Data[offset + 5], Data[offset + 4], + Data[offset + 3], Data[offset + 2], Data[offset + 1], Data[offset]}; + return BitConverter.ToDouble(beBytes, 0); + } + } + } +} diff --git a/src/Umbraco.Core/Media/Exif/ExifIds.cs b/src/Umbraco.Core/Media/Exif/ExifIds.cs new file mode 100644 index 0000000000..3c28892934 --- /dev/null +++ b/src/Umbraco.Core/Media/Exif/ExifIds.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Media.Exif +{ + internal static class JpegId + { + public const int START = 0xFF; + public const int SOI = 0xD8; + public const int SOS = 0xDA; + public const int EOI = 0xD9; + public const int COM = 0xFE; + public const int JFIF = 0xE0; + public const int EXIF = 0xE1; + public const int IPTC = 0xED; + } + + internal enum ExifIFD + { + Exif = 0x8769, + Gps = 0x8825 + } + + internal enum ExifId + { + Unknown = -1, + + ImageWidth = 0x100, + ImageHeight = 0x101, + Orientation = 0x112, + XResolution = 0x11A, + YResolution = 0x11B, + ResolutionUnit = 0x128, + DateTime = 0x132, + Description = 0x10E, + Make = 0x10F, + Model = 0x110, + Software = 0x131, + Artist = 0x13B, + ThumbnailOffset = 0x201, + ThumbnailLength = 0x202, + ExposureTime = 0x829A, + FNumber = 0x829D, + Copyright = 0x8298, + DateTimeOriginal = 0x9003, + FlashUsed = 0x9209, + UserComment = 0x9286 + } + + internal enum ExifGps + { + Version = 0x0, + LatitudeRef = 0x1, + Latitude = 0x2, + LongitudeRef = 0x3, + Longitude = 0x4, + AltitudeRef = 0x5, + Altitude = 0x6, + TimeStamp = 0x7, + Satellites = 0x8, + Status = 0x9, + MeasureMode = 0xA, + DOP = 0xB, + SpeedRef = 0xC, + Speed = 0xD, + TrackRef = 0xE, + Track = 0xF, + ImgDirectionRef = 0x10, + ImgDirection = 0x11, + MapDatum = 0x12, + DestLatitudeRef = 0x13, + DestLatitude = 0x14, + DestLongitudeRef = 0x15, + DestLongitude = 0x16, + DestBearingRef = 0x17, + DestBearing = 0x18, + DestDistanceRef = 0x19, + DestDistance = 0x1A, + ProcessingMethod = 0x1B, + AreaInformation = 0x1C, + DateStamp = 0x1D, + Differential = 0x1E + } + + internal enum ExifOrientation + { + TopLeft = 1, + BottomRight = 3, + TopRight = 6, + BottomLeft = 8, + Undefined = 9 + } + + internal enum ExifUnit + { + Undefined = 1, + Inch = 2, + Centimeter = 3 + } + + /// + /// As per http://www.exif.org/Exif2-2.PDF + /// + [Flags] + internal enum ExifFlash + { + No = 0x0, + Fired = 0x1, + StrobeReturnLightDetected = 0x6, + On = 0x8, + Off = 0x10, + Auto = 0x18, + FlashFunctionPresent = 0x20, + RedEyeReduction = 0x40 + } + + internal enum ExifGpsLatitudeRef + { + Unknown = 0, + North, + South + } + + internal enum ExifGpsLongitudeRef + { + Unknown = 0, + East, + West + } +} diff --git a/src/Umbraco.Core/Media/Exif/ExifReader.cs b/src/Umbraco.Core/Media/Exif/ExifReader.cs new file mode 100644 index 0000000000..d98a0fb563 --- /dev/null +++ b/src/Umbraco.Core/Media/Exif/ExifReader.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Media.Exif +{ + /// + /// Based on http://www.media.mit.edu/pia/Research/deepview/exif.html + /// http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html + /// + internal class ExifReader + { + public JpegInfo info { get; private set; } + private bool littleEndian = false; + + public static JpegInfo ReadJpeg(Stream stream) + { + DateTime then = DateTime.Now; + ExifReader reader = new ExifReader(stream); + reader.info.LoadTime = (DateTime.Now - then); + return reader.info; + } + + protected ExifReader(Stream stream) + { + info = new JpegInfo(); + int a = stream.ReadByte(); + + // ensure SOI marker + if (a != JpegId.START || stream.ReadByte() != JpegId.SOI) + return; + + info.IsValid = true; + + for (; ; ) + { + int marker = 0, prev = 0; + + // find next marker + for (a = 0; ; ++a) + { + marker = stream.ReadByte(); + if (marker != JpegId.START && prev == JpegId.START) break; + prev = marker; + } + + // read section length + int lenHigh = stream.ReadByte(); + int lenLow = stream.ReadByte(); + int itemlen = (lenHigh << 8) | lenLow; + + // read the section + byte[] section = new byte[itemlen]; + section[0] = (byte)lenHigh; + section[1] = (byte)lenLow; + int bytesRead = stream.Read(section, 2, itemlen - 2); + if (bytesRead != itemlen - 2) + return; + + switch (marker) + { + case JpegId.SOS: + // start of stream: and we're done + return; + case JpegId.EOI: + // no data? no good. + return; + case JpegId.EXIF: + { + if (section[2] == 'E' && + section[3] == 'x' && + section[4] == 'i' && + section[5] == 'f') + { + ProcessExif(section); + } + } break; + case JpegId.IPTC: + { + // don't care. + } break; + case 0xC0: + case 0xC1: + case 0xC2: + case 0xC3: + // case 0xC4: // not SOF + case 0xC5: + case 0xC6: + case 0xC7: + // case 0xC8: // not SOF + case 0xC9: + case 0xCA: + case 0xCB: + // case 0xCC: // not SOF + case 0xCD: + case 0xCE: + case 0xCF: + { + ProcessSOF(section, marker); + } break; + default: + { + // don't care. + } break; + } + + section = null; + GC.Collect(); + } + } + + private void ProcessExif(byte[] section) + { + int idx = 6; + if (section[idx++] != 0 || + section[idx++] != 0) + { + // "Exif" is not followed by 2 null bytes. + return; + } + + if (section[idx] == 'I' && section[idx + 1] == 'I') + { + // intel order + littleEndian = true; + } + else + { + if (section[idx] == 'M' && section[idx + 1] == 'M') + littleEndian = false; + else + { + // unknown order... + return; + } + } + idx += 2; + + int a = ExifIO.ReadUShort(section, idx, littleEndian); + idx += 2; + + if (a != 0x002A) + { + // bad start... + return; + } + + a = ExifIO.ReadInt(section, idx, littleEndian); + idx += 4; + + if (a < 8 || a > 16) + { + if (a < 16 || a > section.Length - 16) + { + // invalid offset + return; + } + } + + ProcessExifDir(section, a + 8, 8, section.Length - 8, 0, ExifIFD.Exif); + } + + private int DirOffset(int start, int num) + { + return start + 2 + 12 * num; + } + + private void ProcessExifDir(byte[] section, int offsetDir, int offsetBase, int length, int depth, ExifIFD ifd) + { + if (depth > 4) + { + // corrupted Exif header... + return; + } + + ushort numEntries = ExifIO.ReadUShort(section, offsetDir, littleEndian); + if (offsetDir + 2 + 12 * numEntries >= offsetDir + length) + { + // too long + return; + } + + int offset = 0; + + for (int de = 0; de < numEntries; ++de) + { + offset = DirOffset(offsetDir, de); + ExifTag exifTag = new ExifTag(section, offset, offsetBase, length, littleEndian); + + if (!exifTag.IsValid) + continue; + + switch (exifTag.Tag) + { + case (int)ExifIFD.Exif: + { + int dirStart = offsetBase + exifTag.GetInt(0); + if (dirStart <= offsetBase + length) + { + ProcessExifDir(section, dirStart, offsetBase, length, depth + 1, ExifIFD.Exif); + } + } break; + case (int)ExifIFD.Gps: + { + int dirStart = offsetBase + exifTag.GetInt(0); + if (dirStart <= offsetBase + length) + { + ProcessExifDir(section, dirStart, offsetBase, length, depth + 1, ExifIFD.Gps); + } + } break; + default: + { + exifTag.Populate(info, ifd); + } break; + } + } + + // final link defined? + offset = DirOffset(offsetDir, numEntries) + 4; + if (offset <= offsetBase + length) + { + offset = ExifIO.ReadInt(section, offsetDir + 2 + 12 * numEntries, littleEndian); + if (offset > 0) + { + int subDirStart = offsetBase + offset; + if (subDirStart <= offsetBase + length && subDirStart >= offsetBase) + { + ProcessExifDir(section, subDirStart, offsetBase, length, depth + 1, ifd); + } + } + } + + if (info.ThumbnailData == null && info.ThumbnailOffset > 0 && info.ThumbnailSize > 0) + { + // store it. + info.ThumbnailData = new byte[info.ThumbnailSize]; + Array.Copy(section, offsetBase + info.ThumbnailOffset, info.ThumbnailData, 0, info.ThumbnailSize); + } + } + + private void ProcessSOF(byte[] section, int marker) + { + // bytes 1,2 is section len + // byte 3 is precision (bytes per sample) + info.Height = ((int)section[3] << 8) | (int)section[4]; + info.Width = ((int)section[5] << 8) | (int)section[6]; + int components = (int)section[7]; + + info.IsColor = (components == 3); + } + } +} diff --git a/src/Umbraco.Core/Media/Exif/ExifTag.cs b/src/Umbraco.Core/Media/Exif/ExifTag.cs new file mode 100644 index 0000000000..7d9a56de46 --- /dev/null +++ b/src/Umbraco.Core/Media/Exif/ExifTag.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Media.Exif +{ + /// + /// As per: http://www.media.mit.edu/pia/Research/deepview/exif.html + /// + internal enum ExifTagFormat + { + BYTE = 1, + STRING = 2, + USHORT = 3, + ULONG = 4, + URATIONAL = 5, + SBYTE = 6, + UNDEFINED = 7, + SSHORT = 8, + SLONG = 9, + SRATIONAL = 10, + SINGLE = 11, + DOUBLE = 12, + + NUM_FORMATS = 12 + } + + internal class ExifTag + { + public int Tag { get; private set; } + public ExifTagFormat Format { get; private set; } + public int Components { get; private set; } + public byte[] Data { get; private set; } + public bool LittleEndian { get; private set; } + + private static int[] BytesPerFormat = new int[] { 0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8 }; + + public ExifTag(byte[] section, int sectionOffset, int offsetBase, int length, bool littleEndian) + { + this.IsValid = false; + this.Tag = ExifIO.ReadUShort(section, sectionOffset, littleEndian); + int format = ExifIO.ReadUShort(section, sectionOffset + 2, littleEndian); + if (format < 1 || format > 12) + return; + this.Format = (ExifTagFormat)format; + this.Components = ExifIO.ReadInt(section, sectionOffset + 4, littleEndian); + if (this.Components > 0x10000) + return; + this.LittleEndian = littleEndian; + + int byteCount = this.Components * BytesPerFormat[format]; + int valueOffset = 0; + + if (byteCount > 4) + { + int offsetVal = ExifIO.ReadInt(section, sectionOffset + 8, littleEndian); + if (offsetVal + byteCount > length) + { + // bad offset... + return; + } + valueOffset = offsetBase + offsetVal; + } + else + { + valueOffset = sectionOffset + 8; + } + this.Data = new byte[byteCount]; + Array.Copy(section, valueOffset, this.Data, 0, byteCount); + this.IsValid = true; + } + + public bool IsValid { get; private set; } + + private short ReadShort(int offset) + { + return ExifIO.ReadShort(Data, offset, LittleEndian); + } + + private ushort ReadUShort(int offset) + { + return ExifIO.ReadUShort(Data, offset, LittleEndian); + } + + private int ReadInt(int offset) + { + return ExifIO.ReadInt(Data, offset, LittleEndian); + } + + private uint ReadUInt(int offset) + { + return ExifIO.ReadUInt(Data, offset, LittleEndian); + } + + private float ReadSingle(int offset) + { + return ExifIO.ReadSingle(Data, offset, LittleEndian); + } + + private double ReadDouble(int offset) + { + return ExifIO.ReadDouble(Data, offset, LittleEndian); + } + + public bool IsNumeric + { + get + { + switch (Format) + { + case ExifTagFormat.STRING: + case ExifTagFormat.UNDEFINED: + return false; + default: + return true; + } + } + } + + public int GetInt(int componentIndex) + { + return (int)GetNumericValue(componentIndex); + } + + public double GetNumericValue(int componentIndex) + { + switch (Format) + { + case ExifTagFormat.BYTE: return (double)this.Data[componentIndex]; + case ExifTagFormat.USHORT: return (double)ReadUShort(componentIndex * 2); + case ExifTagFormat.ULONG: return (double)ReadUInt(componentIndex * 4); + case ExifTagFormat.URATIONAL: return (double)ReadUInt(componentIndex * 8) / (double)ReadUInt((componentIndex * 8) + 4); + case ExifTagFormat.SBYTE: + { + unchecked + { + return (double)(sbyte)this.Data[componentIndex]; + } + } + case ExifTagFormat.SSHORT: return (double)ReadShort(componentIndex * 2); + case ExifTagFormat.SLONG: return (double)ReadInt(componentIndex * 4); + case ExifTagFormat.SRATIONAL: return (double)ReadInt(componentIndex * 8) / (double)ReadInt((componentIndex * 8) + 4); + case ExifTagFormat.SINGLE: return (double)ReadSingle(componentIndex * 4); + case ExifTagFormat.DOUBLE: return ReadDouble(componentIndex * 8); + default: return 0.0; + } + } + + public string GetStringValue() + { + return GetStringValue(0); + } + + public string GetStringValue(int componentIndex) + { + switch (Format) + { + case ExifTagFormat.STRING: + case ExifTagFormat.UNDEFINED: + return Encoding.UTF8.GetString(this.Data, 0, this.Data.Length).Trim(' ', '\t', '\r', '\n', '\0'); + case ExifTagFormat.URATIONAL: + return ReadUInt(componentIndex * 8).ToString() + "/" + ReadUInt((componentIndex * 8) + 4).ToString(); + case ExifTagFormat.SRATIONAL: + return ReadInt(componentIndex * 8).ToString() + "/" + ReadInt((componentIndex * 8) + 4).ToString(); + default: + return GetNumericValue(componentIndex).ToString(); + } + } + + public virtual void Populate(JpegInfo info, ExifIFD ifd) + { + if (ifd == ExifIFD.Exif) + { + switch ((ExifId)this.Tag) + { + case ExifId.ImageWidth: info.Width = GetInt(0); break; + case ExifId.ImageHeight: info.Height = GetInt(0); break; + case ExifId.Orientation: info.Orientation = (ExifOrientation)GetInt(0); break; + case ExifId.XResolution: info.XResolution = GetNumericValue(0); break; + case ExifId.YResolution: info.YResolution = GetNumericValue(0); break; + case ExifId.ResolutionUnit: info.ResolutionUnit = (ExifUnit)GetInt(0); break; + case ExifId.DateTime: info.DateTime = GetStringValue(); break; + case ExifId.DateTimeOriginal: info.DateTimeOriginal = GetStringValue(); break; + case ExifId.Description: info.Description = GetStringValue(); break; + case ExifId.Make: info.Make = GetStringValue(); break; + case ExifId.Model: info.Model = GetStringValue(); break; + case ExifId.Software: info.Software = GetStringValue(); break; + case ExifId.Artist: info.Artist = GetStringValue(); break; + case ExifId.ThumbnailOffset: info.ThumbnailOffset = GetInt(0); break; + case ExifId.ThumbnailLength: info.ThumbnailSize = GetInt(0); break; + case ExifId.Copyright: info.Copyright = GetStringValue(); break; + case ExifId.UserComment: info.UserComment = GetStringValue(); break; + case ExifId.ExposureTime: info.ExposureTime = GetNumericValue(0); break; + case ExifId.FNumber: info.FNumber = GetNumericValue(0); break; + case ExifId.FlashUsed: info.Flash = (ExifFlash)GetInt(0); break; + default: break; + } + } + else if (ifd == ExifIFD.Gps) + { + switch ((ExifGps)this.Tag) + { + case ExifGps.LatitudeRef: + { + if (GetStringValue() == "N") info.GpsLatitudeRef = ExifGpsLatitudeRef.North; + else if (GetStringValue() == "S") info.GpsLatitudeRef = ExifGpsLatitudeRef.South; + } break; + case ExifGps.LongitudeRef: + { + if (GetStringValue() == "E") info.GpsLongitudeRef = ExifGpsLongitudeRef.East; + else if (GetStringValue() == "W") info.GpsLongitudeRef = ExifGpsLongitudeRef.West; + } break; + case ExifGps.Latitude: + { + if (Components == 3) + { + info.GpsLatitude[0] = GetNumericValue(0); + info.GpsLatitude[1] = GetNumericValue(1); + info.GpsLatitude[2] = GetNumericValue(2); + } + } break; + case ExifGps.Longitude: + { + if (Components == 3) + { + info.GpsLongitude[0] = GetNumericValue(0); + info.GpsLongitude[1] = GetNumericValue(1); + info.GpsLongitude[2] = GetNumericValue(2); + } + } break; + } + } + } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(64); + sb.Append("0x"); + sb.Append(this.Tag.ToString("X4")); + sb.Append("-"); + sb.Append(((ExifId)this.Tag).ToString()); + if (this.Components > 0) + { + sb.Append(": ("); + sb.Append(GetStringValue(0)); + if (Format != ExifTagFormat.UNDEFINED && Format != ExifTagFormat.STRING) + { + for (int i = 1; i < Components; ++i) + sb.Append(", " + GetStringValue(i)); + } + sb.Append(")"); + } + return sb.ToString(); + } + } +} diff --git a/src/Umbraco.Core/Media/Exif/JpegInfo.cs b/src/Umbraco.Core/Media/Exif/JpegInfo.cs new file mode 100644 index 0000000000..1df90841b9 --- /dev/null +++ b/src/Umbraco.Core/Media/Exif/JpegInfo.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Media.Exif +{ + internal class JpegInfo + { + /// + /// The Jpeg file name (excluding path). + /// + public string FileName + { + get; + set; + } + + /// + /// The Jpeg file size, in bytes. + /// + public int FileSize + { + get; + set; + } + + /// + /// True if the provided Stream was detected to be a Jpeg image, False otherwise. + /// + public bool IsValid + { + get; + set; + } + + /// + /// Image height in pixels. + /// + public int Height + { + get; + set; + } + + /// + /// Image width in pixels. + /// + public int Width + { + get; + set; + } + + /// + /// True if the image data is expressed in 3 components (RGB), False otherwise. + /// + public bool IsColor + { + get; + set; + } + + /// + /// Orientation of the image. + /// + public ExifOrientation Orientation + { + get; + set; + } + + /// + /// The X resolution of the image, expressed in ResolutionUnit. + /// + public double XResolution + { + get; + set; + } + + /// + /// The Y resolution of the image, expressed in ResolutionUnit. + /// + public double YResolution + { + get; + set; + } + + /// + /// Resolution unit of the image. + /// + public ExifUnit ResolutionUnit + { + get; + set; + } + + /// + /// Date at which the image was taken. + /// + public string DateTime + { + get; + set; + } + + /// + /// Date at which the image was taken. Created by Lumia devices. + /// + public string DateTimeOriginal + { + get; + set; + } + + /// + /// Description of the image. + /// + public string Description + { + get; + set; + } + + /// + /// Camera manufacturer. + /// + public string Make + { + get; + set; + } + + /// + /// Camera model. + /// + public string Model + { + get; + set; + } + + /// + /// Software used to create the image. + /// + public string Software + { + get; + set; + } + + /// + /// Image artist. + /// + public string Artist + { + get; + set; + } + + /// + /// Image copyright. + /// + public string Copyright + { + get; + set; + } + + /// + /// Image user comments. + /// + public string UserComment + { + get; + set; + } + + /// + /// Exposure time, in seconds. + /// + public double ExposureTime + { + get; + set; + } + + /// + /// F-number (F-stop) of the camera lens when the image was taken. + /// + public double FNumber + { + get; + set; + } + + /// + /// Flash settings of the camera when the image was taken. + /// + public ExifFlash Flash + { + get; + set; + } + + /// + /// GPS latitude reference (North, South). + /// + public ExifGpsLatitudeRef GpsLatitudeRef + { + get; + set; + } + + /// + /// GPS latitude (degrees, minutes, seconds). + /// + public double[] GpsLatitude = new double[3]; + + /// + /// GPS longitude reference (East, West). + /// + public ExifGpsLongitudeRef GpsLongitudeRef + { + get; + set; + } + + /// + /// GPS longitude (degrees, minutes, seconds). + /// + public double[] GpsLongitude = new double[3]; + + /// + /// Byte offset of the thumbnail data within the Exif section of the image file. + /// Used internally. + /// + public int ThumbnailOffset + { + get; + set; + } + + /// + /// Byte size of the thumbnail data within the Exif section of the image file. + /// Used internally. + /// + public int ThumbnailSize + { + get; + set; + } + + /// + /// Thumbnail data found in the Exif section. + /// + public byte[] ThumbnailData + { + get; + set; + } + + /// + /// Time taken to load the image information. + /// + public TimeSpan LoadTime + { + get; + set; + } + } +} diff --git a/src/Umbraco.Core/Media/ImageHelper.cs b/src/Umbraco.Core/Media/ImageHelper.cs index a6cd19ddc0..3a2827f82d 100644 --- a/src/Umbraco.Core/Media/ImageHelper.cs +++ b/src/Umbraco.Core/Media/ImageHelper.cs @@ -6,10 +6,8 @@ using System.Drawing.Imaging; using System.Globalization; using System.IO; using System.Linq; -using System.Threading.Tasks; -using Umbraco.Core.Configuration; using Umbraco.Core.IO; - +using Umbraco.Core.Media.Exif; namespace Umbraco.Core.Media { @@ -18,6 +16,37 @@ namespace Umbraco.Core.Media /// internal static class ImageHelper { + /// + /// Gets the dimensions of an image based on a stream + /// + /// + /// + /// + /// First try with EXIF, this is because it is insanely faster and doesn't use any memory to read exif data than to load in the entire + /// image via GDI. Otherwise loading an image into GDI consumes a crazy amount of memory on large images. + /// + /// Of course EXIF data might not exist in every file and can only exist in JPGs + /// + public static Size GetDimensions(Stream imageStream) + { + //Try to load with exif + var jpgInfo = ExifReader.ReadJpeg(imageStream); + if (jpgInfo.IsValid && jpgInfo.Width > 0 && jpgInfo.Height > 0) + { + return new Size(jpgInfo.Width, jpgInfo.Height); + } + + //we have no choice but to try to read in via GDI + using (var image = Image.FromStream(imageStream)) + { + + var fileWidth = image.Width; + var fileHeight = image.Height; + return new Size(fileWidth, fileHeight); + } + + } + public static string GetMimeType(this Image image) { var format = image.RawFormat; diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index fe18f018bb..778e40d613 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -482,17 +482,27 @@ namespace Umbraco.Core /// public static string ConvertToHex(this string input) { - if (String.IsNullOrEmpty(input)) return String.Empty; + if (string.IsNullOrEmpty(input)) return string.Empty; var sb = new StringBuilder(input.Length); - foreach (char c in input) + foreach (var c in input) { - int tmp = c; sb.AppendFormat("{0:x2}", Convert.ToUInt32(c)); } return sb.ToString(); } + public static string DecodeFromHex(this string hexValue) + { + var strValue = ""; + while (hexValue.Length > 0) + { + strValue += Convert.ToChar(Convert.ToUInt32(hexValue.Substring(0, 2), 16)).ToString(); + hexValue = hexValue.Substring(2, hexValue.Length - 2); + } + return strValue; + } + /// /// Encodes a string to a safe URL base64 string /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 0a61de7198..ca06404daf 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -314,6 +314,11 @@ + + + + +