From fb5ec5ebfc89b02732ecdedb263dbb95eb228b26 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 28 Jun 2017 14:35:46 +1000 Subject: [PATCH] Now decodes all images --- .../Formats/Jpeg/Port/Components/JFif.cs | 40 ++- .../Jpeg/Port/Components/ScanDecoder.cs | 7 +- .../Formats/Jpeg/Port/JpegConstants.cs | 94 ++++++- .../Formats/Jpeg/Port/JpegDecoderCore.cs | 251 ++++++++++-------- tests/ImageSharp.Tests/TestFile.cs | 20 +- 5 files changed, 291 insertions(+), 121 deletions(-) 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);