From 8759f971ebbd10b28c5ce061a3426b57704e0642 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Tue, 29 Aug 2017 04:21:27 +0200 Subject: [PATCH] reached consistent state with component scaling data --- .../Formats/Jpeg/Common/ComponentUtils.cs | 24 +---- .../Formats/Jpeg/Common/IRawJpegData.cs | 3 - .../PostProcessing/JpegImagePostProcessor.cs | 5 +- .../Formats/Jpeg/Common/SizeExtensions.cs | 31 +++++++ .../Components/Decoder/OrigComponent.cs | 30 +++++-- .../Jpeg/GolangPort/OrigJpegDecoderCore.cs | 6 +- .../Formats/Jpg/ParseStreamTests.cs | 84 ++++++++++++------ .../Formats/Jpg/Utils/JpegFixture.cs | 4 +- .../Jpg/baseline/jpeg420small.jpg.dctdump | Bin 0 -> 64910 bytes 9 files changed, 119 insertions(+), 68 deletions(-) create mode 100644 src/ImageSharp/Formats/Jpeg/Common/SizeExtensions.cs create mode 100644 tests/Images/Input/Jpg/baseline/jpeg420small.jpg.dctdump diff --git a/src/ImageSharp/Formats/Jpeg/Common/ComponentUtils.cs b/src/ImageSharp/Formats/Jpeg/Common/ComponentUtils.cs index 12ca674287..0b89dd1645 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/ComponentUtils.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/ComponentUtils.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Numerics; + using SixLabors.Primitives; namespace SixLabors.ImageSharp.Formats.Jpeg.Common @@ -11,14 +11,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common /// internal static class ComponentUtils { - //public static Size SizeInBlocks(this IJpegComponent component) => new Size(component.WidthInBlocks, component.HeightInBlocks); - - // In Jpeg these are really useful operations: - - public static Size MultiplyBy(this Size a, Size b) => new Size(a.Width * b.Width, a.Height * b.Height); - - public static Size DivideBy(this Size a, Size b) => new Size(a.Width / b.Width, a.Height / b.Height); - public static ref Block8x8 GetBlockReference(this IJpegComponent component, int bx, int by) { return ref component.SpectralBlocks[bx, by]; @@ -74,22 +66,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common { (int divX, int divY) = ratio.GetChrominanceSubSampling(); var size = new Size(width, height); - return size.GetSubSampledSize(divX, divY); + return size.DivideRoundUp(divX, divY); } - // TODO: Find a better place for this method - public static Size GetSubSampledSize(this Size originalSize, int divX, int divY) - { - var sizeVect = (Vector2)(SizeF)originalSize; - sizeVect /= new Vector2(divX, divY); - sizeVect.X = MathF.Ceiling(sizeVect.X); - sizeVect.Y = MathF.Ceiling(sizeVect.Y); - - return new Size((int)sizeVect.X, (int)sizeVect.Y); - } - public static Size GetSubSampledSize(this Size originalSize, int subsamplingDivisor) => - GetSubSampledSize(originalSize, subsamplingDivisor, subsamplingDivisor); // TODO: Not needed by new JpegImagePostprocessor public static (int divX, int divY) GetChrominanceSubSampling(this SubsampleRatio ratio) diff --git a/src/ImageSharp/Formats/Jpeg/Common/IRawJpegData.cs b/src/ImageSharp/Formats/Jpeg/Common/IRawJpegData.cs index b3d1870d20..9ffd18d50b 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/IRawJpegData.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/IRawJpegData.cs @@ -7,9 +7,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common { Size ImageSizeInPixels { get; } - // TODO: Kill this - Size ImageSizeInBlocks { get; } - int ComponentCount { get; } IEnumerable Components { get; } diff --git a/src/ImageSharp/Formats/Jpeg/Common/PostProcessing/JpegImagePostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Common/PostProcessing/JpegImagePostProcessor.cs index 3953e5616d..4837e190f6 100644 --- a/src/ImageSharp/Formats/Jpeg/Common/PostProcessing/JpegImagePostProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Common/PostProcessing/JpegImagePostProcessor.cs @@ -21,8 +21,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.PostProcessing public JpegImagePostProcessor(IRawJpegData rawJpeg) { this.RawJpeg = rawJpeg; - this.NumberOfPostProcessorSteps = rawJpeg.ImageSizeInBlocks.Height / BlockRowsPerStep; - this.PostProcessorBufferSize = new Size(rawJpeg.ImageSizeInBlocks.Width * 8, PixelRowsPerStep); + IJpegComponent c0 = rawJpeg.Components.First(); + this.NumberOfPostProcessorSteps = c0.SizeInBlocks.Height / BlockRowsPerStep; + this.PostProcessorBufferSize = new Size(c0.SizeInBlocks.Width * 8, PixelRowsPerStep); this.componentProcessors = rawJpeg.Components.Select(c => new JpegComponentPostProcessor(this, c)).ToArray(); } diff --git a/src/ImageSharp/Formats/Jpeg/Common/SizeExtensions.cs b/src/ImageSharp/Formats/Jpeg/Common/SizeExtensions.cs new file mode 100644 index 0000000000..b51cd203dd --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Common/SizeExtensions.cs @@ -0,0 +1,31 @@ +using System.Numerics; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Common +{ + /// + /// Extension methods for + /// + internal static class SizeExtensions + { + public static Size MultiplyBy(this Size a, Size b) => new Size(a.Width * b.Width, a.Height * b.Height); + + public static Size DivideBy(this Size a, Size b) => new Size(a.Width / b.Width, a.Height / b.Height); + + public static Size DivideRoundUp(this Size originalSize, int divX, int divY) + { + var sizeVect = (Vector2)(SizeF)originalSize; + sizeVect /= new Vector2(divX, divY); + sizeVect.X = MathF.Ceiling(sizeVect.X); + sizeVect.Y = MathF.Ceiling(sizeVect.Y); + + return new Size((int)sizeVect.X, (int)sizeVect.Y); + } + + public static Size DivideRoundUp(this Size originalSize, int divisor) => + DivideRoundUp(originalSize, divisor, divisor); + + public static Size DivideRoundUp(this Size originalSize, Size divisor) => + DivideRoundUp(originalSize, divisor.Width, divisor.Height); + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/OrigComponent.cs b/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/OrigComponent.cs index e0694afb46..49bbc8f477 100644 --- a/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/OrigComponent.cs +++ b/src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/OrigComponent.cs @@ -33,7 +33,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Decoder public Size SamplingFactors { get; private set; } - public Size SubSamplingDivisors { get; private set; } = new Size(1, 1); + public Size SubSamplingDivisors { get; private set; } public int HorizontalSamplingFactor => this.SamplingFactors.Width; @@ -57,15 +57,29 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Decoder /// The instance public void InitializeDerivedData(OrigJpegDecoderCore decoder) { - this.SizeInBlocks = decoder.ImageSizeInBlocks.MultiplyBy(this.SamplingFactors); - - this.SpectralBlocks = Buffer2D.CreateClean(this.SizeInBlocks); - - if (decoder.ComponentCount > 1 && (this.Index == 1 || this.Index == 2)) + // For 4-component images (either CMYK or YCbCrK), we only support two + // hv vectors: [0x11 0x11 0x11 0x11] and [0x22 0x11 0x11 0x22]. + // Theoretically, 4-component JPEG images could mix and match hv values + // but in practice, those two combinations are the only ones in use, + // and it simplifies the applyBlack code below if we can assume that: + // - for CMYK, the C and K channels have full samples, and if the M + // and Y channels subsample, they subsample both horizontally and + // vertically. + // - for YCbCrK, the Y and K channels have full samples. + + this.SizeInBlocks = decoder.ImageSizeInMCU.MultiplyBy(this.SamplingFactors); + + if (this.Index == 0 || this.Index == 3) { - Size s0 = decoder.Components[0].SamplingFactors; - this.SubSamplingDivisors = s0.DivideBy(this.SamplingFactors); + this.SubSamplingDivisors = new Size(1, 1); } + else + { + OrigComponent c0 = decoder.Components[0]; + this.SubSamplingDivisors = c0.SamplingFactors.DivideBy(this.SamplingFactors); + } + + this.SpectralBlocks = Buffer2D.CreateClean(this.SizeInBlocks); } /// diff --git a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs index 5f2306a7ea..3e185db415 100644 --- a/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/GolangPort/OrigJpegDecoderCore.cs @@ -139,8 +139,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort public Size ImageSizeInPixels { get; private set; } - public Size ImageSizeInBlocks { get; private set; } - public Size ImageSizeInMCU { get; private set; } /// @@ -1180,7 +1178,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort this.ImageSizeInPixels = new Size(width, height); - if (this.Temp[5] != this.ComponentCount) { throw new ImageFormatException("SOF has wrong length"); @@ -1199,14 +1196,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort int h0 = this.Components[0].HorizontalSamplingFactor; int v0 = this.Components[0].VerticalSamplingFactor; - this.ImageSizeInMCU = this.ImageSizeInPixels.GetSubSampledSize(8 * h0, 8 * v0); + this.ImageSizeInMCU = this.ImageSizeInPixels.DivideRoundUp(8 * h0, 8 * v0); foreach (OrigComponent component in this.Components) { component.InitializeDerivedData(this); } - this.ImageSizeInBlocks = this.Components[0].SizeInBlocks; this.SubsampleRatio = ComponentUtils.GetSubsampleRatio(this.Components); } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs index dd954c61a2..058681870c 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs @@ -8,7 +8,9 @@ using Xunit.Abstractions; // ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Tests.Formats.Jpg -{ +{ + using System.Text; + public class ParseStreamTests { private ITestOutputHelper Output { get; } @@ -21,39 +23,68 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [Fact] public void ComponentScalingIsCorrect_1ChannelJpeg() { - using (OrigJpegDecoderCore decoder = JpegFixture.ParseStream(TestImages.Jpeg.Baseline.Jpeg400)) + using (OrigJpegDecoderCore decoder = JpegFixture.ParseStream(TestImages.Jpeg.Baseline.Jpeg400, true)) { Assert.Equal(1, decoder.ComponentCount); Assert.Equal(1, decoder.Components.Length); + + Size expectedSizeInBlocks = decoder.ImageSizeInPixels.DivideRoundUp(8); - Size sizeInBlocks = decoder.ImageSizeInBlocks; - - Size expectedSizeInBlocks = decoder.ImageSizeInPixels.GetSubSampledSize(8); - - Assert.Equal(expectedSizeInBlocks, sizeInBlocks); - Assert.Equal(sizeInBlocks, decoder.ImageSizeInMCU); + Assert.Equal(expectedSizeInBlocks, decoder.ImageSizeInMCU); var uniform1 = new Size(1, 1); OrigComponent c0 = decoder.Components[0]; VerifyJpeg.VerifyComponent(c0, expectedSizeInBlocks, uniform1, uniform1); } } - + [Theory] - [InlineData(TestImages.Jpeg.Baseline.Jpeg444, 3, 1, 1)] - [InlineData(TestImages.Jpeg.Baseline.Jpeg420Exif, 3, 2, 2)] - [InlineData(TestImages.Jpeg.Baseline.Jpeg420Small, 3, 2, 2)] - [InlineData(TestImages.Jpeg.Baseline.Ycck, 4, 1, 1)] // TODO: Find Ycck or Cmyk images with different subsampling - [InlineData(TestImages.Jpeg.Baseline.Cmyk, 4, 1, 1)] + [InlineData(TestImages.Jpeg.Baseline.Jpeg444)] + [InlineData(TestImages.Jpeg.Baseline.Jpeg420Exif)] + [InlineData(TestImages.Jpeg.Baseline.Jpeg420Small)] + [InlineData(TestImages.Jpeg.Baseline.Testorig420)] + [InlineData(TestImages.Jpeg.Baseline.Ycck)] + [InlineData(TestImages.Jpeg.Baseline.Cmyk)] + public void PrintComponentData(string imageFile) + { + StringBuilder bld = new StringBuilder(); + + using (OrigJpegDecoderCore decoder = JpegFixture.ParseStream(imageFile, true)) + { + bld.AppendLine(imageFile); + bld.AppendLine($"Size:{decoder.ImageSizeInPixels} MCU:{decoder.ImageSizeInMCU}"); + OrigComponent c0 = decoder.Components[0]; + OrigComponent c1 = decoder.Components[1]; + + bld.AppendLine($"Luma: SAMP: {c0.SamplingFactors} BLOCKS: {c0.SizeInBlocks}"); + bld.AppendLine($"Chroma: {c1.SamplingFactors} BLOCKS: {c1.SizeInBlocks}"); + } + this.Output.WriteLine(bld.ToString()); + } + + public static readonly TheoryData ComponentVerificationData = new TheoryData() + { + { TestImages.Jpeg.Baseline.Jpeg444, 3, new Size(1, 1), new Size(1, 1) }, + { TestImages.Jpeg.Baseline.Jpeg420Exif, 3, new Size(2, 2), new Size(1, 1) }, + { TestImages.Jpeg.Baseline.Jpeg420Small, 3, new Size(2, 2), new Size(1, 1) }, + { TestImages.Jpeg.Baseline.Testorig420, 3, new Size(2, 2), new Size(1, 1) }, + // TODO: Find Ycck or Cmyk images with different subsampling + { TestImages.Jpeg.Baseline.Ycck, 4, new Size(1, 1), new Size(1, 1) }, + { TestImages.Jpeg.Baseline.Cmyk, 4, new Size(1, 1), new Size(1, 1) }, + }; + + [Theory] + [MemberData(nameof(ComponentVerificationData))] public void ComponentScalingIsCorrect_MultiChannelJpeg( string imageFile, int componentCount, - int hDiv, - int vDiv) + object expectedLumaFactors, + object expectedChromaFactors) { - Size divisor = new Size(hDiv, vDiv); + Size fLuma = (Size)expectedLumaFactors; + Size fChroma = (Size)expectedChromaFactors; - using (OrigJpegDecoderCore decoder = JpegFixture.ParseStream(imageFile)) + using (OrigJpegDecoderCore decoder = JpegFixture.ParseStream(imageFile, true)) { Assert.Equal(componentCount, decoder.ComponentCount); Assert.Equal(componentCount, decoder.Components.Length); @@ -63,20 +94,21 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg OrigComponent c2 = decoder.Components[2]; var uniform1 = new Size(1, 1); - Size expectedLumaSizeInBlocks = decoder.ImageSizeInPixels.GetSubSampledSize(8); - Size expectedChromaSizeInBlocks = expectedLumaSizeInBlocks.DivideBy(divisor); - Size expectedLumaSamplingFactors = expectedLumaSizeInBlocks.DivideBy(decoder.ImageSizeInMCU); - Size expectedChromaSamplingFactors = expectedLumaSamplingFactors.DivideBy(divisor); + Size expectedLumaSizeInBlocks = decoder.ImageSizeInMCU.MultiplyBy(fLuma) ; - VerifyJpeg.VerifyComponent(c0, expectedLumaSizeInBlocks, expectedLumaSamplingFactors, uniform1); - VerifyJpeg.VerifyComponent(c1, expectedChromaSizeInBlocks, expectedChromaSamplingFactors, divisor); - VerifyJpeg.VerifyComponent(c2, expectedChromaSizeInBlocks, expectedChromaSamplingFactors, divisor); + Size divisor = fLuma.DivideBy(fChroma); + + Size expectedChromaSizeInBlocks = expectedLumaSizeInBlocks.DivideRoundUp(divisor); + + VerifyJpeg.VerifyComponent(c0, expectedLumaSizeInBlocks, fLuma, uniform1); + VerifyJpeg.VerifyComponent(c1, expectedChromaSizeInBlocks, fChroma, divisor); + VerifyJpeg.VerifyComponent(c2, expectedChromaSizeInBlocks, fChroma, divisor); if (componentCount == 4) { OrigComponent c3 = decoder.Components[2]; - VerifyJpeg.VerifyComponent(c3, expectedLumaSizeInBlocks, expectedLumaSamplingFactors, uniform1); + VerifyJpeg.VerifyComponent(c3, expectedLumaSizeInBlocks, fLuma, uniform1); } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs b/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs index 4404d2cfea..2049b3f946 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs @@ -173,13 +173,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils Assert.False(failed); } - internal static OrigJpegDecoderCore ParseStream(string testFileName) + internal static OrigJpegDecoderCore ParseStream(string testFileName, bool metaDataOnly = false) { byte[] bytes = TestFile.Create(testFileName).Bytes; using (var ms = new MemoryStream(bytes)) { var decoder = new OrigJpegDecoderCore(Configuration.Default, new JpegDecoder()); - decoder.ParseStream(ms); + decoder.ParseStream(ms, metaDataOnly); return decoder; } } diff --git a/tests/Images/Input/Jpg/baseline/jpeg420small.jpg.dctdump b/tests/Images/Input/Jpg/baseline/jpeg420small.jpg.dctdump new file mode 100644 index 0000000000000000000000000000000000000000..15ba4698a044160bfc6436b411188d93f324e202 GIT binary patch literal 64910 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