From 92f9e04f3ccf89fe2e937f2a475466428529bcbf Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Wed, 30 Aug 2017 01:20:13 +0200 Subject: [PATCH] DeduceJpegColorSpace() --- .../Jpeg/Common/Decoder/IRawJpegData.cs | 25 ++- .../Jpeg/Common/Decoder/JpegColorSpace.cs | 20 +++ .../Jpeg/GolangPort/OrigJpegDecoderCore.cs | 162 ++++++++++-------- .../Formats/Jpg/ParseStreamTests.cs | 18 ++ .../Jpg/baseline/jpeg420small.jpg.dctdump | Bin 64910 -> 0 bytes 5 files changed, 150 insertions(+), 75 deletions(-) create mode 100644 src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorSpace.cs delete mode 100644 tests/Images/Input/Jpg/baseline/jpeg420small.jpg.dctdump 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 15ba4698a044160bfc6436b411188d93f324e202..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64910 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