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 0000000000..15ba4698a0 Binary files /dev/null and b/tests/Images/Input/Jpg/baseline/jpeg420small.jpg.dctdump differ