diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/JFif.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/JFif.cs
index 7fa6c44d0..6baecdf68 100644
--- a/src/ImageSharp/Formats/Jpeg/Port/Components/JFif.cs
+++ b/src/ImageSharp/Formats/Jpeg/Port/Components/JFif.cs
@@ -5,10 +5,13 @@
namespace ImageSharp.Formats.Jpeg.Port.Components
{
+ using System;
+
///
/// Provides information about the JFIF marker segment
+ /// TODO: Thumbnail?
///
- internal struct JFif
+ internal struct JFif : IEquatable
{
///
/// The major version
@@ -38,6 +41,39 @@ namespace ImageSharp.Formats.Jpeg.Port.Components
///
public short YDensity;
- // TODO: Thumbnail?
+ ///
+ public bool Equals(JFif other)
+ {
+ return this.MajorVersion == other.MajorVersion
+ && this.MinorVersion == other.MinorVersion
+ && this.DensityUnits == other.DensityUnits
+ && this.XDensity == other.XDensity
+ && this.YDensity == other.YDensity;
+ }
+
+ ///
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj))
+ {
+ return false;
+ }
+
+ return obj is JFif && this.Equals((JFif)obj);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int hashCode = this.MajorVersion.GetHashCode();
+ hashCode = (hashCode * 397) ^ this.MinorVersion.GetHashCode();
+ hashCode = (hashCode * 397) ^ this.DensityUnits.GetHashCode();
+ hashCode = (hashCode * 397) ^ this.XDensity.GetHashCode();
+ hashCode = (hashCode * 397) ^ this.YDensity.GetHashCode();
+ return hashCode;
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs
index 73e597ccd..4ec7571b5 100644
--- a/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs
+++ b/src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs
@@ -17,6 +17,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components
///
internal struct ScanDecoder
{
+ private byte[] markerBuffer;
+
private int bitsData;
private int bitsCount;
@@ -66,6 +68,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components
int successivePrev,
int successive)
{
+ this.markerBuffer = new byte[2];
this.compIndex = componentIndex;
this.specStart = spectralStart;
this.specEnd = spectralEnd;
@@ -131,7 +134,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components
// Find marker
this.bitsCount = 0;
- fileMarker = JpegDecoderCore.FindNextFileMarkerNew(stream);
+ fileMarker = JpegDecoderCore.FindNextFileMarker(this.markerBuffer, stream);
// Some bad images seem to pad Scan blocks with e.g. zero bytes, skip past
// those to attempt to find a valid marker (fixes issue4090.pdf) in original code.
@@ -159,7 +162,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components
}
}
- fileMarker = JpegDecoderCore.FindNextFileMarkerNew(stream);
+ fileMarker = JpegDecoderCore.FindNextFileMarker(this.markerBuffer, stream);
// Some images include more Scan blocks than expected, skip past those and
// attempt to find the next valid marker (fixes issue8182.pdf) in original code.
diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs
index 08ae5543d..a02e05591 100644
--- a/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs
+++ b/src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs
@@ -196,11 +196,6 @@ namespace ImageSharp.Formats.Jpeg.Port
///
public const ushort RST7 = 0xFFD7;
- ///
- /// Marker prefix. Next byte is a marker.
- ///
- public const ushort XFF = 0xFFFF;
-
///
/// Contains JFIF specific markers
///
@@ -224,7 +219,7 @@ namespace ImageSharp.Formats.Jpeg.Port
///
/// Represents the null "0" marker
///
- public const byte Null = 0;
+ public const byte Null = 0x0;
}
///
@@ -272,6 +267,93 @@ namespace ImageSharp.Formats.Jpeg.Port
///
public const byte ColorTransformYcck = 2;
}
+
+ ///
+ /// Contains EXIF specific markers
+ ///
+ public static class Exif
+ {
+ ///
+ /// Represents E in ASCII
+ ///
+ public const byte E = 0x45;
+
+ ///
+ /// Represents x in ASCII
+ ///
+ public const byte X = 0x78;
+
+ ///
+ /// Represents i in ASCII
+ ///
+ public const byte I = 0x69;
+
+ ///
+ /// Represents f in ASCII
+ ///
+ public const byte F = 0x66;
+
+ ///
+ /// Represents the null "0" marker
+ ///
+ public const byte Null = 0x0;
+ }
+
+ ///
+ /// Contains ICC specific markers
+ ///
+ public static class ICC
+ {
+ ///
+ /// Represents I in ASCII
+ ///
+ public const byte I = 0x49;
+
+ ///
+ /// Represents C in ASCII
+ ///
+ public const byte C = 0x43;
+
+ ///
+ /// Represents _ in ASCII
+ ///
+ public const byte UnderScore = 0x5F;
+
+ ///
+ /// Represents P in ASCII
+ ///
+ public const byte P = 0x50;
+
+ ///
+ /// Represents R in ASCII
+ ///
+ public const byte R = 0x52;
+
+ ///
+ /// Represents O in ASCII
+ ///
+ public const byte O = 0x4F;
+
+ ///
+ /// Represents F in ASCII
+ ///
+ public const byte F = 0x46;
+
+ ///
+ /// Represents L in ASCII
+ ///
+ public const byte L = 0x4C;
+
+ ///
+ /// Represents E in ASCII
+ ///
+ public const byte E = 0x45;
+
+ ///
+ /// Represents the null "0" marker
+ ///
+ public const byte Null = 0x0;
+ }
}
}
}
\ No newline at end of file
diff --git a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs
index 152a2b43a..6a1d6311c 100644
--- a/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs
+++ b/src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs
@@ -7,7 +7,6 @@ namespace ImageSharp.Formats.Jpeg.Port
{
using System;
using System.Collections.Generic;
- using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
@@ -18,7 +17,7 @@ namespace ImageSharp.Formats.Jpeg.Port
///
/// Performs the jpeg decoding operation.
- /// Ported from
+ /// Ported from with additional fixes to handle common encoding errors
///
internal sealed class JpegDecoderCore : IDisposable
{
@@ -37,7 +36,7 @@ namespace ImageSharp.Formats.Jpeg.Port
///
private readonly byte[] temp = new byte[2 * 16 * 4];
- private readonly byte[] uint16Buffer = new byte[2];
+ private readonly byte[] markerBuffer = new byte[2];
private QuantizationTables quantizationTables;
@@ -57,7 +56,12 @@ namespace ImageSharp.Formats.Jpeg.Port
private int imageHeight;
- private int numComponents;
+ private int numberOfComponents;
+
+ ///
+ /// Whether the image has a EXIF header
+ ///
+ private bool isExif;
///
/// Contains information about the JFIF marker
@@ -94,14 +98,13 @@ namespace ImageSharp.Formats.Jpeg.Port
public Stream InputStream { get; private set; }
///
- /// Finds the next file marker within the byte stream. Not used but I'm keeping it for now for testing
+ /// Finds the next file marker within the byte stream.
///
+ /// The buffer to read file markers to
/// The input stream
/// The
- public static FileMarker FindNextFileMarkerNew(Stream stream)
+ public static FileMarker FindNextFileMarker(byte[] marker, Stream stream)
{
- byte[] marker = new byte[2];
-
int value = stream.Read(marker, 0, 2);
if (value == 0)
@@ -131,60 +134,7 @@ namespace ImageSharp.Formats.Jpeg.Port
}
///
- /// Finds the next file marker within the byte stream
- ///
- /// The input stream
- /// The
- public static FileMarker FindNextFileMarker(Stream stream)
- {
- byte[] buffer = new byte[2];
- long maxPos = stream.Length - 1;
- long currentPos = stream.Position;
- long newPos = currentPos;
-
- if (currentPos >= maxPos)
- {
- return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true);
- }
-
- int value = stream.Read(buffer, 0, 2);
-
- if (value == 0)
- {
- return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true);
- }
-
- ushort currentMarker = (ushort)((buffer[0] << 8) | buffer[1]);
- if (currentMarker >= JpegConstants.Markers.SOF0 && currentMarker <= JpegConstants.Markers.COM)
- {
- return new FileMarker(currentMarker, stream.Position - 2);
- }
-
- value = stream.Read(buffer, 0, 2);
-
- if (value == 0)
- {
- return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true);
- }
-
- ushort newMarker = (ushort)((buffer[0] << 8) | buffer[1]);
- while (!(newMarker >= JpegConstants.Markers.SOF0 && newMarker <= JpegConstants.Markers.COM))
- {
- if (++newPos >= maxPos)
- {
- return new FileMarker(JpegConstants.Markers.EOI, (int)stream.Length, true);
- }
-
- stream.Read(buffer, 0, 2);
- newMarker = (ushort)((buffer[0] << 8) | buffer[1]);
- }
-
- return new FileMarker(newMarker, newPos, true);
- }
-
- ///
- /// Decodes the image from the specified and sets
- /// the data to image.
+ /// Decodes the image from the specified and sets the data to image.
///
/// The pixel format.
/// The stream, where the image should be.
@@ -193,10 +143,13 @@ namespace ImageSharp.Formats.Jpeg.Port
where TPixel : struct, IPixel
{
this.InputStream = stream;
- this.ParseStream();
- var image = new Image(this.imageWidth, this.imageHeight);
- this.GetData(image);
+ var metadata = new ImageMetaData();
+ this.ParseStream(metadata, false);
+
+ var image = new Image(this.configuration, this.imageWidth, this.imageHeight, metadata);
+ this.FillPixelData(image);
+ this.AssignResolution(image);
return image;
}
@@ -223,8 +176,11 @@ namespace ImageSharp.Formats.Jpeg.Port
///
/// Parses the input stream for file markers
///
- private void ParseStream()
+ /// Contains the metadata for an image
+ /// Whether to decode metadata only.
+ private void ParseStream(ImageMetaData metaData, bool metadataOnly)
{
+ // TODO: metadata only logic
// Check for the Start Of Image marker.
var fileMarker = new FileMarker(this.ReadUint16(), 0);
if (fileMarker.Marker != JpegConstants.Markers.SOI)
@@ -251,7 +207,12 @@ namespace ImageSharp.Formats.Jpeg.Port
break;
case JpegConstants.Markers.APP1:
+ this.ProcessApp1Marker(remaining, metaData);
+ break;
+
case JpegConstants.Markers.APP2:
+ this.ProcessApp2Marker(remaining, metaData);
+ break;
case JpegConstants.Markers.APP3:
case JpegConstants.Markers.APP4:
case JpegConstants.Markers.APP5:
@@ -298,36 +259,10 @@ namespace ImageSharp.Formats.Jpeg.Port
case JpegConstants.Markers.SOS:
this.ProcessStartOfScanMarker();
break;
-
- case JpegConstants.Markers.XFF:
- if ((byte)this.InputStream.ReadByte() != 0xFF)
- {
- // Avoid skipping a valid marker
- this.InputStream.Position -= 1;
- }
-
- break;
-
- //default:
-
- // // TODO: Not convinced this is required
- // // Skip back as it could be incorrect encoding -- last 0xFF byte of the previous
- // // block was eaten by the encoder
- // this.InputStream.Position -= 3;
- // this.InputStream.Read(this.temp, 0, 2);
- // if (this.temp[0] == 0xFF && this.temp[1] >= 0xC0 && this.temp[1] <= 0xFE)
- // {
- // // Rewind that last bytes we read
- // this.InputStream.Position -= 2;
- // break;
- // }
-
- // // throw new ImageFormatException($"Unknown Marker {fileMarker.Marker} at {fileMarker.Position}");
- // break;
}
- // Read on. TODO: Test this on damaged images.
- fileMarker = FindNextFileMarkerNew(this.InputStream);
+ // Read on.
+ fileMarker = FindNextFileMarker(this.markerBuffer, this.InputStream);
}
this.imageWidth = this.frame.SamplesPerLine;
@@ -349,7 +284,7 @@ namespace ImageSharp.Formats.Jpeg.Port
this.components.Components[i] = component;
}
- this.numComponents = this.components.Components.Length;
+ this.numberOfComponents = this.components.Components.Length;
}
///
@@ -357,24 +292,24 @@ namespace ImageSharp.Formats.Jpeg.Port
///
/// The pixel format.
/// The image
- private void GetData(Image image)
+ private void FillPixelData(Image image)
where TPixel : struct, IPixel
{
- if (this.numComponents > 4)
+ if (this.numberOfComponents > 4)
{
- throw new ImageFormatException($"Unsupported color mode. Max components 4; found {this.numComponents}");
+ throw new ImageFormatException($"Unsupported color mode. Max components 4; found {this.numberOfComponents}");
}
- this.pixelArea = new JpegPixelArea(image.Width, image.Height, this.numComponents);
+ this.pixelArea = new JpegPixelArea(image.Width, image.Height, this.numberOfComponents);
this.pixelArea.LinearizeBlockData(this.components, image.Width, image.Height);
- if (this.numComponents == 1)
+ if (this.numberOfComponents == 1)
{
this.FillGrayScaleImage(image);
return;
}
- if (this.numComponents == 3)
+ if (this.numberOfComponents == 3)
{
if (this.adobe.Equals(default(Adobe)) || this.adobe.ColorTransform == JpegConstants.Markers.Adobe.ColorTransformYCbCr)
{
@@ -386,7 +321,7 @@ namespace ImageSharp.Formats.Jpeg.Port
}
}
- if (this.numComponents == 4)
+ if (this.numberOfComponents == 4)
{
if (this.adobe.ColorTransform == JpegConstants.Markers.Adobe.ColorTransformYcck)
{
@@ -399,6 +334,34 @@ namespace ImageSharp.Formats.Jpeg.Port
}
}
+ ///
+ /// Assigns the horizontal and vertical resolution to the image if it has a JFIF header or EXIF metadata.
+ ///
+ /// The pixel format.
+ /// The image to assign the resolution to.
+ private void AssignResolution(Image image)
+ where TPixel : struct, IPixel
+ {
+ if (this.isExif)
+ {
+ ExifValue horizontal = image.MetaData.ExifProfile.GetValue(ExifTag.XResolution);
+ ExifValue vertical = image.MetaData.ExifProfile.GetValue(ExifTag.YResolution);
+ double horizontalValue = horizontal != null ? ((Rational)horizontal.Value).ToDouble() : 0;
+ double verticalValue = vertical != null ? ((Rational)vertical.Value).ToDouble() : 0;
+
+ if (horizontalValue > 0 && verticalValue > 0)
+ {
+ image.MetaData.HorizontalResolution = horizontalValue;
+ image.MetaData.VerticalResolution = verticalValue;
+ }
+ }
+ else if (this.jFif.XDensity > 0 && this.jFif.YDensity > 0)
+ {
+ image.MetaData.HorizontalResolution = this.jFif.XDensity;
+ image.MetaData.VerticalResolution = this.jFif.YDensity;
+ }
+ }
+
///
/// Processes the application header containing the JFIF identifier plus extra data.
///
@@ -440,6 +403,86 @@ namespace ImageSharp.Formats.Jpeg.Port
}
}
+ ///
+ /// Processes the App1 marker retrieving any stored metadata
+ ///
+ /// The remaining bytes in the segment block.
+ /// The image.
+ private void ProcessApp1Marker(int remaining, ImageMetaData metadata)
+ {
+ if (remaining < 6 || this.options.IgnoreMetadata)
+ {
+ // Skip the application header length
+ this.InputStream.Skip(remaining);
+ return;
+ }
+
+ byte[] profile = new byte[remaining];
+ this.InputStream.Read(profile, 0, remaining);
+
+ if (profile[0] == JpegConstants.Markers.Exif.E &&
+ profile[1] == JpegConstants.Markers.Exif.X &&
+ profile[2] == JpegConstants.Markers.Exif.I &&
+ profile[3] == JpegConstants.Markers.Exif.F &&
+ profile[4] == JpegConstants.Markers.Exif.Null &&
+ profile[5] == JpegConstants.Markers.Exif.Null)
+ {
+ this.isExif = true;
+ metadata.ExifProfile = new ExifProfile(profile);
+ }
+ }
+
+ ///
+ /// Processes the App2 marker retrieving any stored ICC profile information
+ ///
+ /// The remaining bytes in the segment block.
+ /// The image.
+ private void ProcessApp2Marker(int remaining, ImageMetaData metadata)
+ {
+ // Length is 14 though we only need to check 12.
+ const int Icclength = 14;
+ if (remaining < Icclength || this.options.IgnoreMetadata)
+ {
+ this.InputStream.Skip(remaining);
+ return;
+ }
+
+ byte[] identifier = new byte[Icclength];
+ this.InputStream.Read(identifier, 0, Icclength);
+ remaining -= Icclength; // We have read it by this point
+
+ if (identifier[0] == JpegConstants.Markers.ICC.I &&
+ identifier[1] == JpegConstants.Markers.ICC.C &&
+ identifier[2] == JpegConstants.Markers.ICC.C &&
+ identifier[3] == JpegConstants.Markers.ICC.UnderScore &&
+ identifier[4] == JpegConstants.Markers.ICC.P &&
+ identifier[5] == JpegConstants.Markers.ICC.R &&
+ identifier[6] == JpegConstants.Markers.ICC.O &&
+ identifier[7] == JpegConstants.Markers.ICC.F &&
+ identifier[8] == JpegConstants.Markers.ICC.I &&
+ identifier[9] == JpegConstants.Markers.ICC.L &&
+ identifier[10] == JpegConstants.Markers.ICC.E &&
+ identifier[11] == JpegConstants.Markers.ICC.Null)
+ {
+ byte[] profile = new byte[remaining];
+ this.InputStream.Read(profile, 0, remaining);
+
+ if (metadata.IccProfile == null)
+ {
+ metadata.IccProfile = new IccProfile(profile);
+ }
+ else
+ {
+ metadata.IccProfile.Extend(profile);
+ }
+ }
+ else
+ {
+ // Not an ICC profile we can handle. Skip the remaining bytes so we can carry on and ignore this.
+ this.InputStream.Skip(remaining);
+ }
+ }
+
///
/// Processes the application header containing the Adobe identifier
/// which stores image encoding information for DCT filters.
@@ -951,8 +994,8 @@ namespace ImageSharp.Formats.Jpeg.Port
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ushort ReadUint16()
{
- this.InputStream.Read(this.uint16Buffer, 0, 2);
- return (ushort)((this.uint16Buffer[0] << 8) | this.uint16Buffer[1]);
+ this.InputStream.Read(this.markerBuffer, 0, 2);
+ return (ushort)((this.markerBuffer[0] << 8) | this.markerBuffer[1]);
}
}
}
\ No newline at end of file
diff --git a/tests/ImageSharp.Tests/TestFile.cs b/tests/ImageSharp.Tests/TestFile.cs
index d274d61a2..bcfb6cb13 100644
--- a/tests/ImageSharp.Tests/TestFile.cs
+++ b/tests/ImageSharp.Tests/TestFile.cs
@@ -27,6 +27,8 @@ namespace ImageSharp.Tests
///
private static readonly string FormatsDirectory = GetFormatsDirectory();
+ private static readonly object Locker = new object();
+
///
/// The image.
///
@@ -142,7 +144,10 @@ namespace ImageSharp.Tests
private Image GetImage()
{
- return this.image ?? (this.image = Image.Load(this.Bytes));
+ lock (Locker)
+ {
+ return this.image ?? (this.image = Image.Load(this.Bytes));
+ }
}
///
@@ -155,16 +160,17 @@ namespace ImageSharp.Tests
{
var directories = new List {
"TestImages/Formats/", // Here for code coverage tests.
- "tests/ImageSharp.Tests/TestImages/Formats/", // from travis/build script
- "../../../../../ImageSharp.Tests/TestImages/Formats/", // from Sandbox46
+ "tests/ImageSharp.Tests/TestImages/Formats/", // From travis/build script
+ "../../../../../ImageSharp.Tests/TestImages/Formats/", // From Sandbox46
"../../../../TestImages/Formats/",
"../../../TestImages/Formats/"
};
- directories = directories.SelectMany(x => new[]
- {
- Path.GetFullPath(x)
- }).ToList();
+ directories = directories
+ .SelectMany(x => new[]
+ {
+ Path.GetFullPath(x)
+ }).ToList();
AddFormatsDirectoryFromTestAssebmlyPath(directories);