Browse Source

Now decodes all images

af/merge-core
James Jackson-South 9 years ago
parent
commit
fb5ec5ebfc
  1. 40
      src/ImageSharp/Formats/Jpeg/Port/Components/JFif.cs
  2. 7
      src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs
  3. 94
      src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs
  4. 251
      src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs
  5. 20
      tests/ImageSharp.Tests/TestFile.cs

40
src/ImageSharp/Formats/Jpeg/Port/Components/JFif.cs

@ -5,10 +5,13 @@
namespace ImageSharp.Formats.Jpeg.Port.Components namespace ImageSharp.Formats.Jpeg.Port.Components
{ {
using System;
/// <summary> /// <summary>
/// Provides information about the JFIF marker segment /// Provides information about the JFIF marker segment
/// TODO: Thumbnail?
/// </summary> /// </summary>
internal struct JFif internal struct JFif : IEquatable<JFif>
{ {
/// <summary> /// <summary>
/// The major version /// The major version
@ -38,6 +41,39 @@ namespace ImageSharp.Formats.Jpeg.Port.Components
/// </summary> /// </summary>
public short YDensity; public short YDensity;
// TODO: Thumbnail? /// <inheritdoc/>
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;
}
/// <inheritdoc/>
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
return obj is JFif && this.Equals((JFif)obj);
}
/// <inheritdoc/>
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;
}
}
} }
} }

7
src/ImageSharp/Formats/Jpeg/Port/Components/ScanDecoder.cs

@ -17,6 +17,8 @@ namespace ImageSharp.Formats.Jpeg.Port.Components
/// </summary> /// </summary>
internal struct ScanDecoder internal struct ScanDecoder
{ {
private byte[] markerBuffer;
private int bitsData; private int bitsData;
private int bitsCount; private int bitsCount;
@ -66,6 +68,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components
int successivePrev, int successivePrev,
int successive) int successive)
{ {
this.markerBuffer = new byte[2];
this.compIndex = componentIndex; this.compIndex = componentIndex;
this.specStart = spectralStart; this.specStart = spectralStart;
this.specEnd = spectralEnd; this.specEnd = spectralEnd;
@ -131,7 +134,7 @@ namespace ImageSharp.Formats.Jpeg.Port.Components
// Find marker // Find marker
this.bitsCount = 0; 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 // 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. // 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 // 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. // attempt to find the next valid marker (fixes issue8182.pdf) in original code.

94
src/ImageSharp/Formats/Jpeg/Port/JpegConstants.cs

@ -196,11 +196,6 @@ namespace ImageSharp.Formats.Jpeg.Port
/// </summary> /// </summary>
public const ushort RST7 = 0xFFD7; public const ushort RST7 = 0xFFD7;
/// <summary>
/// Marker prefix. Next byte is a marker.
/// </summary>
public const ushort XFF = 0xFFFF;
/// <summary> /// <summary>
/// Contains JFIF specific markers /// Contains JFIF specific markers
/// </summary> /// </summary>
@ -224,7 +219,7 @@ namespace ImageSharp.Formats.Jpeg.Port
/// <summary> /// <summary>
/// Represents the null "0" marker /// Represents the null "0" marker
/// </summary> /// </summary>
public const byte Null = 0; public const byte Null = 0x0;
} }
/// <summary> /// <summary>
@ -272,6 +267,93 @@ namespace ImageSharp.Formats.Jpeg.Port
/// </summary> /// </summary>
public const byte ColorTransformYcck = 2; public const byte ColorTransformYcck = 2;
} }
/// <summary>
/// Contains EXIF specific markers
/// </summary>
public static class Exif
{
/// <summary>
/// Represents E in ASCII
/// </summary>
public const byte E = 0x45;
/// <summary>
/// Represents x in ASCII
/// </summary>
public const byte X = 0x78;
/// <summary>
/// Represents i in ASCII
/// </summary>
public const byte I = 0x69;
/// <summary>
/// Represents f in ASCII
/// </summary>
public const byte F = 0x66;
/// <summary>
/// Represents the null "0" marker
/// </summary>
public const byte Null = 0x0;
}
/// <summary>
/// Contains ICC specific markers
/// </summary>
public static class ICC
{
/// <summary>
/// Represents I in ASCII
/// </summary>
public const byte I = 0x49;
/// <summary>
/// Represents C in ASCII
/// </summary>
public const byte C = 0x43;
/// <summary>
/// Represents _ in ASCII
/// </summary>
public const byte UnderScore = 0x5F;
/// <summary>
/// Represents P in ASCII
/// </summary>
public const byte P = 0x50;
/// <summary>
/// Represents R in ASCII
/// </summary>
public const byte R = 0x52;
/// <summary>
/// Represents O in ASCII
/// </summary>
public const byte O = 0x4F;
/// <summary>
/// Represents F in ASCII
/// </summary>
public const byte F = 0x46;
/// <summary>
/// Represents L in ASCII
/// </summary>
public const byte L = 0x4C;
/// <summary>
/// Represents E in ASCII
/// </summary>
public const byte E = 0x45;
/// <summary>
/// Represents the null "0" marker
/// </summary>
public const byte Null = 0x0;
}
} }
} }
} }

251
src/ImageSharp/Formats/Jpeg/Port/JpegDecoderCore.cs

@ -7,7 +7,6 @@ namespace ImageSharp.Formats.Jpeg.Port
{ {
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -18,7 +17,7 @@ namespace ImageSharp.Formats.Jpeg.Port
/// <summary> /// <summary>
/// Performs the jpeg decoding operation. /// Performs the jpeg decoding operation.
/// Ported from <see href="https://github.com/mozilla/pdf.js/blob/master/src/core/jpg.js"/> /// Ported from <see href="https://github.com/mozilla/pdf.js/blob/master/src/core/jpg.js"/> with additional fixes to handle common encoding errors
/// </summary> /// </summary>
internal sealed class JpegDecoderCore : IDisposable internal sealed class JpegDecoderCore : IDisposable
{ {
@ -37,7 +36,7 @@ namespace ImageSharp.Formats.Jpeg.Port
/// </summary> /// </summary>
private readonly byte[] temp = new byte[2 * 16 * 4]; 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; private QuantizationTables quantizationTables;
@ -57,7 +56,12 @@ namespace ImageSharp.Formats.Jpeg.Port
private int imageHeight; private int imageHeight;
private int numComponents; private int numberOfComponents;
/// <summary>
/// Whether the image has a EXIF header
/// </summary>
private bool isExif;
/// <summary> /// <summary>
/// Contains information about the JFIF marker /// Contains information about the JFIF marker
@ -94,14 +98,13 @@ namespace ImageSharp.Formats.Jpeg.Port
public Stream InputStream { get; private set; } public Stream InputStream { get; private set; }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="marker">The buffer to read file markers to</param>
/// <param name="stream">The input stream</param> /// <param name="stream">The input stream</param>
/// <returns>The <see cref="FileMarker"/></returns> /// <returns>The <see cref="FileMarker"/></returns>
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); int value = stream.Read(marker, 0, 2);
if (value == 0) if (value == 0)
@ -131,60 +134,7 @@ namespace ImageSharp.Formats.Jpeg.Port
} }
/// <summary> /// <summary>
/// Finds the next file marker within the byte stream /// Decodes the image from the specified <see cref="Stream"/> and sets the data to image.
/// </summary>
/// <param name="stream">The input stream</param>
/// <returns>The <see cref="FileMarker"/></returns>
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);
}
/// <summary>
/// Decodes the image from the specified <see cref="Stream"/> and sets
/// the data to image.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The stream, where the image should be.</param> /// <param name="stream">The stream, where the image should be.</param>
@ -193,10 +143,13 @@ namespace ImageSharp.Formats.Jpeg.Port
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
this.InputStream = stream; this.InputStream = stream;
this.ParseStream();
var image = new Image<TPixel>(this.imageWidth, this.imageHeight); var metadata = new ImageMetaData();
this.GetData(image); this.ParseStream(metadata, false);
var image = new Image<TPixel>(this.configuration, this.imageWidth, this.imageHeight, metadata);
this.FillPixelData(image);
this.AssignResolution(image);
return image; return image;
} }
@ -223,8 +176,11 @@ namespace ImageSharp.Formats.Jpeg.Port
/// <summary> /// <summary>
/// Parses the input stream for file markers /// Parses the input stream for file markers
/// </summary> /// </summary>
private void ParseStream() /// <param name="metaData">Contains the metadata for an image</param>
/// <param name="metadataOnly">Whether to decode metadata only.</param>
private void ParseStream(ImageMetaData metaData, bool metadataOnly)
{ {
// TODO: metadata only logic
// Check for the Start Of Image marker. // Check for the Start Of Image marker.
var fileMarker = new FileMarker(this.ReadUint16(), 0); var fileMarker = new FileMarker(this.ReadUint16(), 0);
if (fileMarker.Marker != JpegConstants.Markers.SOI) if (fileMarker.Marker != JpegConstants.Markers.SOI)
@ -251,7 +207,12 @@ namespace ImageSharp.Formats.Jpeg.Port
break; break;
case JpegConstants.Markers.APP1: case JpegConstants.Markers.APP1:
this.ProcessApp1Marker(remaining, metaData);
break;
case JpegConstants.Markers.APP2: case JpegConstants.Markers.APP2:
this.ProcessApp2Marker(remaining, metaData);
break;
case JpegConstants.Markers.APP3: case JpegConstants.Markers.APP3:
case JpegConstants.Markers.APP4: case JpegConstants.Markers.APP4:
case JpegConstants.Markers.APP5: case JpegConstants.Markers.APP5:
@ -298,36 +259,10 @@ namespace ImageSharp.Formats.Jpeg.Port
case JpegConstants.Markers.SOS: case JpegConstants.Markers.SOS:
this.ProcessStartOfScanMarker(); this.ProcessStartOfScanMarker();
break; 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. // Read on.
fileMarker = FindNextFileMarkerNew(this.InputStream); fileMarker = FindNextFileMarker(this.markerBuffer, this.InputStream);
} }
this.imageWidth = this.frame.SamplesPerLine; this.imageWidth = this.frame.SamplesPerLine;
@ -349,7 +284,7 @@ namespace ImageSharp.Formats.Jpeg.Port
this.components.Components[i] = component; this.components.Components[i] = component;
} }
this.numComponents = this.components.Components.Length; this.numberOfComponents = this.components.Components.Length;
} }
/// <summary> /// <summary>
@ -357,24 +292,24 @@ namespace ImageSharp.Formats.Jpeg.Port
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The image</param> /// <param name="image">The image</param>
private void GetData<TPixel>(Image<TPixel> image) private void FillPixelData<TPixel>(Image<TPixel> image)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
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); this.pixelArea.LinearizeBlockData(this.components, image.Width, image.Height);
if (this.numComponents == 1) if (this.numberOfComponents == 1)
{ {
this.FillGrayScaleImage(image); this.FillGrayScaleImage(image);
return; return;
} }
if (this.numComponents == 3) if (this.numberOfComponents == 3)
{ {
if (this.adobe.Equals(default(Adobe)) || this.adobe.ColorTransform == JpegConstants.Markers.Adobe.ColorTransformYCbCr) 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) if (this.adobe.ColorTransform == JpegConstants.Markers.Adobe.ColorTransformYcck)
{ {
@ -399,6 +334,34 @@ namespace ImageSharp.Formats.Jpeg.Port
} }
} }
/// <summary>
/// Assigns the horizontal and vertical resolution to the image if it has a JFIF header or EXIF metadata.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The image to assign the resolution to.</param>
private void AssignResolution<TPixel>(Image<TPixel> image)
where TPixel : struct, IPixel<TPixel>
{
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;
}
}
/// <summary> /// <summary>
/// Processes the application header containing the JFIF identifier plus extra data. /// Processes the application header containing the JFIF identifier plus extra data.
/// </summary> /// </summary>
@ -440,6 +403,86 @@ namespace ImageSharp.Formats.Jpeg.Port
} }
} }
/// <summary>
/// Processes the App1 marker retrieving any stored metadata
/// </summary>
/// <param name="remaining">The remaining bytes in the segment block.</param>
/// <param name="metadata">The image.</param>
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);
}
}
/// <summary>
/// Processes the App2 marker retrieving any stored ICC profile information
/// </summary>
/// <param name="remaining">The remaining bytes in the segment block.</param>
/// <param name="metadata">The image.</param>
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);
}
}
/// <summary> /// <summary>
/// Processes the application header containing the Adobe identifier /// Processes the application header containing the Adobe identifier
/// which stores image encoding information for DCT filters. /// which stores image encoding information for DCT filters.
@ -951,8 +994,8 @@ namespace ImageSharp.Formats.Jpeg.Port
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private ushort ReadUint16() private ushort ReadUint16()
{ {
this.InputStream.Read(this.uint16Buffer, 0, 2); this.InputStream.Read(this.markerBuffer, 0, 2);
return (ushort)((this.uint16Buffer[0] << 8) | this.uint16Buffer[1]); return (ushort)((this.markerBuffer[0] << 8) | this.markerBuffer[1]);
} }
} }
} }

20
tests/ImageSharp.Tests/TestFile.cs

@ -27,6 +27,8 @@ namespace ImageSharp.Tests
/// </summary> /// </summary>
private static readonly string FormatsDirectory = GetFormatsDirectory(); private static readonly string FormatsDirectory = GetFormatsDirectory();
private static readonly object Locker = new object();
/// <summary> /// <summary>
/// The image. /// The image.
/// </summary> /// </summary>
@ -142,7 +144,10 @@ namespace ImageSharp.Tests
private Image<Rgba32> GetImage() private Image<Rgba32> GetImage()
{ {
return this.image ?? (this.image = Image.Load<Rgba32>(this.Bytes)); lock (Locker)
{
return this.image ?? (this.image = Image.Load<Rgba32>(this.Bytes));
}
} }
/// <summary> /// <summary>
@ -155,16 +160,17 @@ namespace ImageSharp.Tests
{ {
var directories = new List<string> { var directories = new List<string> {
"TestImages/Formats/", // Here for code coverage tests. "TestImages/Formats/", // Here for code coverage tests.
"tests/ImageSharp.Tests/TestImages/Formats/", // from travis/build script "tests/ImageSharp.Tests/TestImages/Formats/", // From travis/build script
"../../../../../ImageSharp.Tests/TestImages/Formats/", // from Sandbox46 "../../../../../ImageSharp.Tests/TestImages/Formats/", // From Sandbox46
"../../../../TestImages/Formats/", "../../../../TestImages/Formats/",
"../../../TestImages/Formats/" "../../../TestImages/Formats/"
}; };
directories = directories.SelectMany(x => new[] directories = directories
{ .SelectMany(x => new[]
Path.GetFullPath(x) {
}).ToList(); Path.GetFullPath(x)
}).ToList();
AddFormatsDirectoryFromTestAssebmlyPath(directories); AddFormatsDirectoryFromTestAssebmlyPath(directories);

Loading…
Cancel
Save