diff --git a/src/ImageSharp/Formats/Jpeg/Common/Decoder/IRawJpegData.cs b/src/ImageSharp/Formats/Jpeg/Common/Decoder/IRawJpegData.cs index afc1472d0..0e4f953f3 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/Decoder/IRawJpegData.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/Decoder/IRawJpegData.cs @@ -1,15 +1,32 @@ -namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder -{ - using System.Collections.Generic; +using System.Collections.Generic; - using SixLabors.Primitives; +using SixLabors.Primitives; +namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder +{ + /// + /// Represents decompressed, unprocessed jpeg data with spectral space -s. + /// internal interface IRawJpegData { + /// + /// Gets the image size in pixels. + /// Size ImageSizeInPixels { get; } + /// + /// Gets the number of coponents. + /// int ComponentCount { get; } + /// + /// Gets the color space + /// + JpegColorSpace ColorSpace { get; } + + /// + /// Gets the components. + /// IEnumerable Components { get; } /// diff --git a/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorSpace.cs b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorSpace.cs new file mode 100644 index 000000000..da353d279 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorSpace.cs @@ -0,0 +1,20 @@ +namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder +{ + /// + /// Identifies the colorspace of a Jpeg image + /// + internal enum JpegColorSpace + { + Undefined = 0, + + GrayScale, + + Ycck, + + Cmyk, + + RGB, + + YCbCr + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs index 6ff71af63..445578ff5 100644 --- a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs @@ -4,14 +4,12 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Numerics; using System.Runtime.CompilerServices; using System.Threading.Tasks; + using SixLabors.ImageSharp.Formats.Jpeg.Common; using SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder; using SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Decoder; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.MetaData; using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.MetaData.Profiles.Icc; @@ -119,6 +117,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort /// public SubsampleRatio SubsampleRatio { get; private set; } + public JpegColorSpace ColorSpace { get; private set; } + /// /// Gets the component array /// @@ -592,22 +592,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort 0, image.Height, y => - { - // TODO: Simplify + optimize + share duplicate code across converter methods - int yo = this.ycbcrImage.GetRowYOffset(y); - int co = this.ycbcrImage.GetRowCOffset(y); - - for (int x = 0; x < image.Width; x++) { - byte cyan = this.ycbcrImage.YChannel[yo + x]; - byte magenta = this.ycbcrImage.CbChannel[co + (x / scale)]; - byte yellow = this.ycbcrImage.CrChannel[co + (x / scale)]; - - TPixel packed = default(TPixel); - this.PackCmyk(ref packed, cyan, magenta, yellow, x, y); - pixels[x, y] = packed; - } - }); + // TODO: Simplify + optimize + share duplicate code across converter methods + int yo = this.ycbcrImage.GetRowYOffset(y); + int co = this.ycbcrImage.GetRowCOffset(y); + + for (int x = 0; x < image.Width; x++) + { + byte cyan = this.ycbcrImage.YChannel[yo + x]; + byte magenta = this.ycbcrImage.CbChannel[co + (x / scale)]; + byte yellow = this.ycbcrImage.CrChannel[co + (x / scale)]; + + TPixel packed = default(TPixel); + this.PackCmyk(ref packed, cyan, magenta, yellow, x, y); + pixels[x, y] = packed; + } + }); } this.AssignResolution(image); @@ -691,34 +691,34 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort using (PixelAccessor pixels = image.Lock()) { Parallel.For( - 0, - image.Height, - image.Configuration.ParallelOptions, - y => - { - // TODO. This Parallel loop doesn't give us the boost it should. - ref byte ycRef = ref this.ycbcrImage.YChannel[0]; - ref byte cbRef = ref this.ycbcrImage.CbChannel[0]; - ref byte crRef = ref this.ycbcrImage.CrChannel[0]; - fixed (YCbCrToRgbTables* tables = &yCbCrToRgbTables) - { - // TODO: Simplify + optimize + share duplicate code across converter methods - int yo = this.ycbcrImage.GetRowYOffset(y); - int co = this.ycbcrImage.GetRowCOffset(y); - - for (int x = 0; x < image.Width; x++) - { - int cOff = co + (x / scale); - byte yy = Unsafe.Add(ref ycRef, yo + x); - byte cb = Unsafe.Add(ref cbRef, cOff); - byte cr = Unsafe.Add(ref crRef, cOff); - - TPixel packed = default(TPixel); - YCbCrToRgbTables.Pack(ref packed, tables, yy, cb, cr); - pixels[x, y] = packed; - } - } - }); + 0, + image.Height, + image.Configuration.ParallelOptions, + y => + { + // TODO. This Parallel loop doesn't give us the boost it should. + ref byte ycRef = ref this.ycbcrImage.YChannel[0]; + ref byte cbRef = ref this.ycbcrImage.CbChannel[0]; + ref byte crRef = ref this.ycbcrImage.CrChannel[0]; + fixed (YCbCrToRgbTables* tables = &yCbCrToRgbTables) + { + // TODO: Simplify + optimize + share duplicate code across converter methods + int yo = this.ycbcrImage.GetRowYOffset(y); + int co = this.ycbcrImage.GetRowCOffset(y); + + for (int x = 0; x < image.Width; x++) + { + int cOff = co + (x / scale); + byte yy = Unsafe.Add(ref ycRef, yo + x); + byte cb = Unsafe.Add(ref cbRef, cOff); + byte cr = Unsafe.Add(ref crRef, cOff); + + TPixel packed = default(TPixel); + YCbCrToRgbTables.Pack(ref packed, tables, yy, cb, cr); + pixels[x, y] = packed; + } + } + }); } this.AssignResolution(image); @@ -921,12 +921,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort byte[] profile = new byte[remaining]; this.InputProcessor.ReadFull(profile, 0, remaining); - if (profile[0] == 'E' && - profile[1] == 'x' && - profile[2] == 'i' && - profile[3] == 'f' && - profile[4] == '\0' && - profile[5] == '\0') + if (profile[0] == 'E' && profile[1] == 'x' && profile[2] == 'i' && profile[3] == 'f' && profile[4] == '\0' + && profile[5] == '\0') { this.isExif = true; this.MetaData.ExifProfile = new ExifProfile(profile); @@ -951,18 +947,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort this.InputProcessor.ReadFull(identifier, 0, Icclength); remaining -= Icclength; // we have read it by this point - if (identifier[0] == 'I' && - identifier[1] == 'C' && - identifier[2] == 'C' && - identifier[3] == '_' && - identifier[4] == 'P' && - identifier[5] == 'R' && - identifier[6] == 'O' && - identifier[7] == 'F' && - identifier[8] == 'I' && - identifier[9] == 'L' && - identifier[10] == 'E' && - identifier[11] == '\0') + if (identifier[0] == 'I' && identifier[1] == 'C' && identifier[2] == 'C' && identifier[3] == '_' + && identifier[4] == 'P' && identifier[5] == 'R' && identifier[6] == 'O' && identifier[7] == 'F' + && identifier[8] == 'I' && identifier[9] == 'L' && identifier[10] == 'E' && identifier[11] == '\0') { byte[] profile = new byte[remaining]; this.InputProcessor.ReadFull(profile, 0, remaining); @@ -999,11 +986,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort remaining -= 13; // TODO: We should be using constants for this. - this.isJfif = this.Temp[0] == 'J' && - this.Temp[1] == 'F' && - this.Temp[2] == 'I' && - this.Temp[3] == 'F' && - this.Temp[4] == '\x00'; + this.isJfif = this.Temp[0] == 'J' && this.Temp[1] == 'F' && this.Temp[2] == 'I' && this.Temp[3] == 'F' + && this.Temp[4] == '\x00'; if (this.isJfif) { @@ -1178,7 +1162,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort int width = (this.Temp[3] << 8) + this.Temp[4]; this.ImageSizeInPixels = new Size(width, height); - + if (this.Temp[5] != this.ComponentCount) { throw new ImageFormatException("SOF has wrong length"); @@ -1204,7 +1188,43 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort component.InitializeDerivedData(this); } + this.ColorSpace = this.DeduceJpegColorSpace(); + this.SubsampleRatio = ComponentUtils.GetSubsampleRatio(this.Components); } + + private JpegColorSpace DeduceJpegColorSpace() + { + switch (this.ComponentCount) + { + case 1: return JpegColorSpace.GrayScale; + case 3: return this.IsRGB() ? JpegColorSpace.RGB : JpegColorSpace.YCbCr; + case 4: + + if (!this.adobeTransformValid) + { + throw new ImageFormatException( + "Unknown color model: 4-component JPEG doesn't have Adobe APP14 metadata"); + } + + // See http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html#Adobe + // See https://docs.oracle.com/javase/8/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html + // TODO: YCbCrA? + if (this.adobeTransform == OrigJpegConstants.Adobe.ColorTransformYcck) + { + return JpegColorSpace.Ycck; + } + else if (this.adobeTransform == OrigJpegConstants.Adobe.ColorTransformUnknown) + { + // Assume CMYK + return JpegColorSpace.Cmyk; + } + + goto default; + + default: + throw new ImageFormatException("JpegDecoder only supports RGB, CMYK and Grayscale color spaces."); + } + } } -} +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs index 058681870..e56e91207 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs @@ -1,4 +1,7 @@ +using System; + using SixLabors.ImageSharp.Formats.Jpeg.Common; +using SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder; using SixLabors.ImageSharp.Formats.Jpeg.GolangPort; using SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Decoder; using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils; @@ -20,6 +23,21 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg this.Output = output; } + [Theory] + [InlineData(TestImages.Jpeg.Baseline.Testorig420, JpegColorSpace.YCbCr)] + [InlineData(TestImages.Jpeg.Baseline.Jpeg400, JpegColorSpace.GrayScale)] + [InlineData(TestImages.Jpeg.Baseline.Ycck, JpegColorSpace.Ycck)] + [InlineData(TestImages.Jpeg.Baseline.Cmyk, JpegColorSpace.Cmyk)] + public void ColorSpace_IsDeducedCorrectly(string imageFile, object expectedColorSpaceValue) + { + var expecteColorSpace = (JpegColorSpace)expectedColorSpaceValue; + + using (OrigJpegDecoderCore decoder = JpegFixture.ParseStream(imageFile, true)) + { + Assert.Equal(expecteColorSpace, decoder.ColorSpace); + } + } + [Fact] public void ComponentScalingIsCorrect_1ChannelJpeg() { diff --git a/tests/Images/Input/Jpg/baseline/jpeg420small.jpg.dctdump b/tests/Images/Input/Jpg/baseline/jpeg420small.jpg.dctdump deleted file mode 100644 index 15ba4698a..000000000 Binary files a/tests/Images/Input/Jpg/baseline/jpeg420small.jpg.dctdump and /dev/null differ