diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs index 6e3866d15..ae97c7e54 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs @@ -247,8 +247,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder this.scanBuffer = new JpegBitReader(this.stream); - bool fullScan = this.frame.Progressive || this.frame.MultiScan; - this.frame.AllocateComponents(fullScan); + this.frame.AllocateComponents(); if (this.frame.Progressive) { @@ -326,11 +325,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder if (this.scanComponentCount != 1) { + this.spectralConverter.PrepareForDecoding(); this.ParseBaselineDataInterleaved(); this.spectralConverter.CommitConversion(); } else if (this.frame.ComponentCount == 1) { + this.spectralConverter.PrepareForDecoding(); this.ParseBaselineDataSingleComponent(); this.spectralConverter.CommitConversion(); } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterAvx.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterAvx.cs index 4879a5020..b333d3268 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterAvx.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterAvx.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. #if SUPPORTS_RUNTIME_INTRINSICS +using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters @@ -25,7 +26,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters { } - public override bool IsAvailable => Avx.IsSupported; + public sealed override bool IsAvailable => Avx.IsSupported; + + public sealed override int ElementsPerBatch => Vector256.Count; } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterBase.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterBase.cs index d5b0c679b..8339206b4 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterBase.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterBase.cs @@ -35,6 +35,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters /// public abstract bool IsAvailable { get; } + /// + /// Gets a value indicating how many pixels are processed in a single batch. + /// + /// + /// This generally should be equal to register size, + /// e.g. 1 for scalar implementation, 8 for AVX implementation and so on. + /// + public abstract int ElementsPerBatch { get; } + /// /// Gets the of this converter. /// @@ -219,7 +228,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters /// /// List of component color processors. /// Row to convert - public ComponentValues(IReadOnlyList processors, int row) + public ComponentValues(IReadOnlyList processors, int row) { DebugGuard.MustBeGreaterThan(processors.Count, 0, nameof(processors)); diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterScalar.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterScalar.cs index f3e5bdd5a..8cf8ad1d9 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterScalar.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterScalar.cs @@ -16,7 +16,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters { } - public override bool IsAvailable => true; + public sealed override bool IsAvailable => true; + + public sealed override int ElementsPerBatch => 1; } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterVector.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterVector.cs index 51b5a0db5..6e0c0cff3 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterVector.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterVector.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters /// Even though real life data is guaranteed to be of size /// divisible by 8 newer SIMD instructions like AVX512 won't work with /// such data out of the box. These converters have fallback code - /// for 'remainder' data. + /// for remainder data. /// internal abstract class JpegColorConverterVector : JpegColorConverterBase { @@ -28,7 +28,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters public sealed override bool IsAvailable => Vector.IsHardwareAccelerated && Vector.Count % 4 == 0; - public override void ConvertToRgbInplace(in ComponentValues values) + public sealed override int ElementsPerBatch => Vector.Count; + + public sealed override void ConvertToRgbInplace(in ComponentValues values) { DebugGuard.IsTrue(this.IsAvailable, $"{this.GetType().Name} converter is not supported on current hardware."); diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/ComponentProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/ComponentProcessor.cs new file mode 100644 index 000000000..87e85686c --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/ComponentProcessor.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder +{ + /// + /// Base class for processing component spectral data and converting it to raw color data. + /// + internal abstract class ComponentProcessor : IDisposable + { + public ComponentProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, Size postProcessorBufferSize, IJpegComponent component, int blockSize) + { + this.Frame = frame; + this.Component = component; + + this.BlockAreaSize = component.SubSamplingDivisors * blockSize; + this.ColorBuffer = memoryAllocator.Allocate2DOveraligned( + postProcessorBufferSize.Width, + postProcessorBufferSize.Height, + this.BlockAreaSize.Height); + } + + protected JpegFrame Frame { get; } + + protected IJpegComponent Component { get; } + + protected Buffer2D ColorBuffer { get; } + + protected Size BlockAreaSize { get; } + + /// + /// Converts spectral data to color data accessible via . + /// + /// Spectral row index to convert. + public abstract void CopyBlocksToColorBuffer(int row); + + /// + /// Clears spectral buffers. + /// + /// + /// Should only be called during baseline interleaved decoding. + /// + public void ClearSpectralBuffers() + { + Buffer2D spectralBlocks = this.Component.SpectralBlocks; + for (int i = 0; i < spectralBlocks.Height; i++) + { + spectralBlocks.DangerousGetRowSpan(i).Clear(); + } + } + + /// + /// Gets converted color buffer row. + /// + /// Row index. + /// Color buffer row. + public Span GetColorBufferRowSpan(int row) => + this.ColorBuffer.DangerousGetRowSpan(row); + + public void Dispose() => this.ColorBuffer.Dispose(); + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DirectComponentProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DirectComponentProcessor.cs new file mode 100644 index 000000000..80cc689e3 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DirectComponentProcessor.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder +{ + /// + /// Processes component spectral data and converts it to color data in 1-to-1 scale. + /// + internal sealed class DirectComponentProcessor : ComponentProcessor + { + private Block8x8F dequantizationTable; + + public DirectComponentProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) + : base(memoryAllocator, frame, postProcessorBufferSize, component, blockSize: 8) + { + this.dequantizationTable = rawJpeg.QuantizationTables[component.QuantizationTableIndex]; + FloatingPointDCT.AdjustToIDCT(ref this.dequantizationTable); + } + + public override void CopyBlocksToColorBuffer(int spectralStep) + { + Buffer2D spectralBuffer = this.Component.SpectralBlocks; + + float maximumValue = this.Frame.MaxColorChannelValue; + + int destAreaStride = this.ColorBuffer.Width; + + int blocksRowsPerStep = this.Component.SamplingFactors.Height; + + int yBlockStart = spectralStep * blocksRowsPerStep; + + Size subSamplingDivisors = this.Component.SubSamplingDivisors; + + Block8x8F workspaceBlock = default; + + for (int y = 0; y < blocksRowsPerStep; y++) + { + int yBuffer = y * this.BlockAreaSize.Height; + + Span colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); + Span blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); + + for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) + { + // Integer to float + workspaceBlock.LoadFrom(ref blockRow[xBlock]); + + // Dequantize + workspaceBlock.MultiplyInPlace(ref this.dequantizationTable); + + // Convert from spectral to color + FloatingPointDCT.TransformIDCT(ref workspaceBlock); + + // To conform better to libjpeg we actually NEED TO loose precision here. + // This is because they store blocks as Int16 between all the operations. + // To be "more accurate", we need to emulate this by rounding! + workspaceBlock.NormalizeColorsAndRoundInPlace(maximumValue); + + // Write to color buffer acording to sampling factors + int xColorBufferStart = xBlock * this.BlockAreaSize.Width; + workspaceBlock.ScaledCopyTo( + ref colorBufferRow[xColorBufferStart], + destAreaStride, + subSamplingDivisors.Width, + subSamplingDivisors.Height); + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor2.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor2.cs new file mode 100644 index 000000000..801b2a3fb --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor2.cs @@ -0,0 +1,102 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder +{ + /// + /// Processes component spectral data and converts it to color data in 2-to-1 scale. + /// + internal sealed class DownScalingComponentProcessor2 : ComponentProcessor + { + private Block8x8F dequantizationTable; + + public DownScalingComponentProcessor2(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) + : base(memoryAllocator, frame, postProcessorBufferSize, component, 4) + { + this.dequantizationTable = rawJpeg.QuantizationTables[component.QuantizationTableIndex]; + ScaledFloatingPointDCT.AdjustToIDCT(ref this.dequantizationTable); + } + + public override void CopyBlocksToColorBuffer(int spectralStep) + { + Buffer2D spectralBuffer = this.Component.SpectralBlocks; + + float maximumValue = this.Frame.MaxColorChannelValue; + float normalizationValue = MathF.Ceiling(maximumValue / 2); + + int destAreaStride = this.ColorBuffer.Width; + + int blocksRowsPerStep = this.Component.SamplingFactors.Height; + Size subSamplingDivisors = this.Component.SubSamplingDivisors; + + Block8x8F workspaceBlock = default; + + int yBlockStart = spectralStep * blocksRowsPerStep; + + for (int y = 0; y < blocksRowsPerStep; y++) + { + int yBuffer = y * this.BlockAreaSize.Height; + + Span colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); + Span blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); + + for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) + { + // Integer to float + workspaceBlock.LoadFrom(ref blockRow[xBlock]); + + // IDCT/Normalization/Range + ScaledFloatingPointDCT.TransformIDCT_4x4(ref workspaceBlock, ref this.dequantizationTable, normalizationValue, maximumValue); + + // Save to the intermediate buffer + int xColorBufferStart = xBlock * this.BlockAreaSize.Width; + ScaledCopyTo( + ref workspaceBlock, + ref colorBufferRow[xColorBufferStart], + destAreaStride, + subSamplingDivisors.Width, + subSamplingDivisors.Height); + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void ScaledCopyTo(ref Block8x8F block, ref float destRef, int destStrideWidth, int horizontalScale, int verticalScale) + { + // TODO: Optimize: implement all cases with scale-specific, loopless code! + CopyArbitraryScale(ref block, ref destRef, destStrideWidth, horizontalScale, verticalScale); + + [MethodImpl(InliningOptions.ColdPath)] + static void CopyArbitraryScale(ref Block8x8F block, ref float areaOrigin, int areaStride, int horizontalScale, int verticalScale) + { + for (int y = 0; y < 4; y++) + { + int yy = y * verticalScale; + int y8 = y * 8; + + for (int x = 0; x < 4; x++) + { + int xx = x * horizontalScale; + + float value = block[y8 + x]; + + for (int i = 0; i < verticalScale; i++) + { + int baseIdx = ((yy + i) * areaStride) + xx; + + for (int j = 0; j < horizontalScale; j++) + { + // area[xx + j, yy + i] = value; + Unsafe.Add(ref areaOrigin, (nint)(uint)(baseIdx + j)) = value; + } + } + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor4.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor4.cs new file mode 100644 index 000000000..1c63abc93 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor4.cs @@ -0,0 +1,102 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder +{ + /// + /// Processes component spectral data and converts it to color data in 4-to-1 scale. + /// + internal sealed class DownScalingComponentProcessor4 : ComponentProcessor + { + private Block8x8F dequantizationTable; + + public DownScalingComponentProcessor4(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) + : base(memoryAllocator, frame, postProcessorBufferSize, component, 2) + { + this.dequantizationTable = rawJpeg.QuantizationTables[component.QuantizationTableIndex]; + ScaledFloatingPointDCT.AdjustToIDCT(ref this.dequantizationTable); + } + + public override void CopyBlocksToColorBuffer(int spectralStep) + { + Buffer2D spectralBuffer = this.Component.SpectralBlocks; + + float maximumValue = this.Frame.MaxColorChannelValue; + float normalizationValue = MathF.Ceiling(maximumValue / 2); + + int destAreaStride = this.ColorBuffer.Width; + + int blocksRowsPerStep = this.Component.SamplingFactors.Height; + Size subSamplingDivisors = this.Component.SubSamplingDivisors; + + Block8x8F workspaceBlock = default; + + int yBlockStart = spectralStep * blocksRowsPerStep; + + for (int y = 0; y < blocksRowsPerStep; y++) + { + int yBuffer = y * this.BlockAreaSize.Height; + + Span colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); + Span blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); + + for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) + { + // Integer to float + workspaceBlock.LoadFrom(ref blockRow[xBlock]); + + // IDCT/Normalization/Range + ScaledFloatingPointDCT.TransformIDCT_2x2(ref workspaceBlock, ref this.dequantizationTable, normalizationValue, maximumValue); + + // Save to the intermediate buffer + int xColorBufferStart = xBlock * this.BlockAreaSize.Width; + ScaledCopyTo( + ref workspaceBlock, + ref colorBufferRow[xColorBufferStart], + destAreaStride, + subSamplingDivisors.Width, + subSamplingDivisors.Height); + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void ScaledCopyTo(ref Block8x8F block, ref float destRef, int destStrideWidth, int horizontalScale, int verticalScale) + { + // TODO: Optimize: implement all cases with scale-specific, loopless code! + CopyArbitraryScale(ref block, ref destRef, destStrideWidth, horizontalScale, verticalScale); + + [MethodImpl(InliningOptions.ColdPath)] + static void CopyArbitraryScale(ref Block8x8F block, ref float areaOrigin, int areaStride, int horizontalScale, int verticalScale) + { + for (int y = 0; y < 2; y++) + { + int yy = y * verticalScale; + int y8 = y * 8; + + for (int x = 0; x < 2; x++) + { + int xx = x * horizontalScale; + + float value = block[y8 + x]; + + for (int i = 0; i < verticalScale; i++) + { + int baseIdx = ((yy + i) * areaStride) + xx; + + for (int j = 0; j < horizontalScale; j++) + { + // area[xx + j, yy + i] = value; + Unsafe.Add(ref areaOrigin, (nint)(uint)(baseIdx + j)) = value; + } + } + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor8.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor8.cs new file mode 100644 index 000000000..03f0de741 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor8.cs @@ -0,0 +1,88 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder +{ + /// + /// Processes component spectral data and converts it to color data in 8-to-1 scale. + /// + internal sealed class DownScalingComponentProcessor8 : ComponentProcessor + { + private readonly float dcDequantizatizer; + + public DownScalingComponentProcessor8(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) + : base(memoryAllocator, frame, postProcessorBufferSize, component, 1) + => this.dcDequantizatizer = 0.125f * rawJpeg.QuantizationTables[component.QuantizationTableIndex][0]; + + public override void CopyBlocksToColorBuffer(int spectralStep) + { + Buffer2D spectralBuffer = this.Component.SpectralBlocks; + + float maximumValue = this.Frame.MaxColorChannelValue; + float normalizationValue = MathF.Ceiling(maximumValue / 2); + + int destAreaStride = this.ColorBuffer.Width; + + int blocksRowsPerStep = this.Component.SamplingFactors.Height; + Size subSamplingDivisors = this.Component.SubSamplingDivisors; + + int yBlockStart = spectralStep * blocksRowsPerStep; + + for (int y = 0; y < blocksRowsPerStep; y++) + { + int yBuffer = y * this.BlockAreaSize.Height; + + Span colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); + Span blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); + + for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) + { + float dc = ScaledFloatingPointDCT.TransformIDCT_1x1(blockRow[xBlock][0], this.dcDequantizatizer, normalizationValue, maximumValue); + + // Save to the intermediate buffer + int xColorBufferStart = xBlock * this.BlockAreaSize.Width; + ScaledCopyTo( + dc, + ref colorBufferRow[xColorBufferStart], + destAreaStride, + subSamplingDivisors.Width, + subSamplingDivisors.Height); + } + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void ScaledCopyTo(float value, ref float destRef, int destStrideWidth, int horizontalScale, int verticalScale) + { + if (horizontalScale == 1 && verticalScale == 1) + { + destRef = value; + return; + } + + if (horizontalScale == 2 && verticalScale == 2) + { + destRef = value; + Unsafe.Add(ref destRef, 1) = value; + Unsafe.Add(ref destRef, 0 + (nint)(uint)destStrideWidth) = value; + Unsafe.Add(ref destRef, 1 + (nint)(uint)destStrideWidth) = value; + return; + } + + // TODO: Optimize: implement all cases with scale-specific, loopless code! + for (int y = 0; y < verticalScale; y++) + { + for (int x = 0; x < horizontalScale; x++) + { + Unsafe.Add(ref destRef, (nint)(uint)x) = value; + } + + destRef = ref Unsafe.Add(ref destRef, (nint)(uint)destStrideWidth); + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs index aa0a2b1ba..6f57dff99 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs @@ -109,10 +109,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder // The successive approximation low bit end. public int SuccessiveLow { get; set; } - /// - /// Decodes the entropy coded data. - /// - /// Component count in the current scan. + /// public void ParseEntropyCodedData(int scanComponentCount) { this.cancellationToken.ThrowIfCancellationRequested(); @@ -121,8 +118,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder this.scanBuffer = new JpegBitReader(this.stream); - bool fullScan = this.frame.Progressive || this.frame.MultiScan; - this.frame.AllocateComponents(fullScan); + this.frame.AllocateComponents(); if (!this.frame.Progressive) { @@ -152,11 +148,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { if (this.scanComponentCount != 1) { + this.spectralConverter.PrepareForDecoding(); this.ParseBaselineDataInterleaved(); this.spectralConverter.CommitConversion(); } else if (this.frame.ComponentCount == 1) { + this.spectralConverter.PrepareForDecoding(); this.ParseBaselineDataSingleComponent(); this.spectralConverter.CommitConversion(); } @@ -269,7 +267,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder private void ParseBaselineDataSingleComponent() { - var component = this.frame.Components[0] as JpegComponent; + JpegComponent component = this.frame.Components[0]; int mcuLines = this.frame.McusPerColumn; int w = component.WidthInBlocks; int h = component.SamplingFactors.Height; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs deleted file mode 100644 index 65b4c4262..000000000 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder -{ - /// - /// Encapsulates spectral data to rgba32 processing for one component. - /// - internal class JpegComponentPostProcessor : IDisposable - { - /// - /// The size of the area in corresponding to one 8x8 Jpeg block - /// - private readonly Size blockAreaSize; - - /// - /// Jpeg frame instance containing required decoding metadata. - /// - private readonly JpegFrame frame; - - /// - /// Gets the maximal number of block rows being processed in one step. - /// - private readonly int blockRowsPerStep; - - /// - /// Gets the component containing decoding meta information. - /// - private readonly IJpegComponent component; - - /// - /// Gets the instance containing decoding meta information. - /// - private readonly IRawJpegData rawJpeg; - - /// - /// Initializes a new instance of the class. - /// - public JpegComponentPostProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component) - { - this.frame = frame; - - this.component = component; - this.rawJpeg = rawJpeg; - this.blockAreaSize = this.component.SubSamplingDivisors * 8; - this.ColorBuffer = memoryAllocator.Allocate2DOveraligned( - postProcessorBufferSize.Width, - postProcessorBufferSize.Height, - this.blockAreaSize.Height); - - this.blockRowsPerStep = postProcessorBufferSize.Height / 8 / this.component.SubSamplingDivisors.Height; - } - - /// - /// Gets the temporary working buffer of color values. - /// - public Buffer2D ColorBuffer { get; } - - /// - public void Dispose() => this.ColorBuffer.Dispose(); - - /// - /// Convert raw spectral DCT data to color data and copy it to the color buffer . - /// - public void CopyBlocksToColorBuffer(int spectralStep) - { - Buffer2D spectralBuffer = this.component.SpectralBlocks; - - float maximumValue = this.frame.MaxColorChannelValue; - - int destAreaStride = this.ColorBuffer.Width; - - int yBlockStart = spectralStep * this.blockRowsPerStep; - - Size subSamplingDivisors = this.component.SubSamplingDivisors; - - Block8x8F dequantTable = this.rawJpeg.QuantizationTables[this.component.QuantizationTableIndex]; - Block8x8F workspaceBlock = default; - - for (int y = 0; y < this.blockRowsPerStep; y++) - { - int yBuffer = y * this.blockAreaSize.Height; - - Span colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); - Span blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); - - for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) - { - // Integer to float - workspaceBlock.LoadFrom(ref blockRow[xBlock]); - - // Dequantize - workspaceBlock.MultiplyInPlace(ref dequantTable); - - // Convert from spectral to color - FastFloatingPointDCT.TransformIDCT(ref workspaceBlock); - - // To conform better to libjpeg we actually NEED TO loose precision here. - // This is because they store blocks as Int16 between all the operations. - // To be "more accurate", we need to emulate this by rounding! - workspaceBlock.NormalizeColorsAndRoundInPlace(maximumValue); - - // Write to color buffer according to sampling factors - int xColorBufferStart = xBlock * this.blockAreaSize.Width; - workspaceBlock.ScaledCopyTo( - ref colorBufferRow[xColorBufferStart], - destAreaStride, - subSamplingDivisors.Width, - subSamplingDivisors.Height); - } - } - } - - public void ClearSpectralBuffers() - { - Buffer2D spectralBlocks = this.component.SpectralBlocks; - for (int i = 0; i < spectralBlocks.Height; i++) - { - spectralBlocks.DangerousGetRowSpan(i).Clear(); - } - } - - public Span GetColorBufferRowSpan(int row) => - this.ColorBuffer.DangerousGetRowSpan(row); - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs index ad800a8ca..5f55a1063 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs @@ -139,8 +139,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder } } - public void AllocateComponents(bool fullScan) + public void AllocateComponents() { + bool fullScan = this.Progressive || this.MultiScan; for (int i = 0; i < this.ComponentCount; i++) { IJpegComponent component = this.Components[i]; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs index 96d0416eb..bef23d89b 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs @@ -10,6 +10,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// internal abstract class SpectralConverter { + /// + /// Supported scaled spectral block sizes for scaled IDCT decoding. + /// + private static readonly int[] ScaledBlockSizes = new int[] + { + // 8 => 1, 1/8 of the original size + 1, + + // 8 => 2, 1/4 of the original size + 2, + + // 8 => 4, 1/2 of the original size + 4, + }; + /// /// Gets a value indicating whether this converter has converted spectral /// data of the current image or not. @@ -20,12 +35,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// Injects jpeg image decoding metadata. /// /// - /// This is guaranteed to be called only once at SOF marker by . + /// This should be called exactly once during SOF (Start Of Frame) marker. /// - /// instance containing decoder-specific parameters. - /// instance containing decoder-specific parameters. + /// Instance containing decoder-specific parameters. + /// Instance containing decoder-specific parameters. public abstract void InjectFrameData(JpegFrame frame, IRawJpegData jpegData); + /// + /// Initializes this spectral decoder instance for decoding. + /// This should be called exactly once after all markers which can alter + /// spectral decoding parameters. + /// + public abstract void PrepareForDecoding(); + /// /// Converts single spectral jpeg stride to color stride in baseline /// decoding mode. @@ -58,5 +80,48 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// The raw JPEG data. /// The color converter. protected virtual JpegColorConverterBase GetColorConverter(JpegFrame frame, IRawJpegData jpegData) => JpegColorConverterBase.GetConverter(jpegData.ColorSpace, frame.Precision); + + /// + /// Calculates image size with optional scaling. + /// + /// + /// Does not apply scalling if is null. + /// + /// Size of the image. + /// Target size of the image. + /// Spectral block size, equals to 8 if scaling is not applied. + /// Resulting image size, equals to if scaling is not applied. + public static Size CalculateResultingImageSize(Size size, Size? targetSize, out int blockPixelSize) + { + const int blockNativePixelSize = 8; + + blockPixelSize = blockNativePixelSize; + if (targetSize != null) + { + Size tSize = targetSize.Value; + + int fullBlocksWidth = (int)((uint)size.Width / blockNativePixelSize); + int fullBlocksHeight = (int)((uint)size.Height / blockNativePixelSize); + + // & (blockNativePixelSize - 1) is Numerics.Modulo8(), basically + int blockWidthRemainder = size.Width & (blockNativePixelSize - 1); + int blockHeightRemainder = size.Height & (blockNativePixelSize - 1); + + for (int i = 0; i < ScaledBlockSizes.Length; i++) + { + int blockSize = ScaledBlockSizes[i]; + int scaledWidth = (fullBlocksWidth * blockSize) + (int)Numerics.DivideCeil((uint)(blockWidthRemainder * blockSize), blockNativePixelSize); + int scaledHeight = (fullBlocksHeight * blockSize) + (int)Numerics.DivideCeil((uint)(blockHeightRemainder * blockSize), blockNativePixelSize); + + if (scaledWidth >= tSize.Width && scaledHeight >= tSize.Height) + { + blockPixelSize = blockSize; + return new Size(scaledWidth, scaledHeight); + } + } + } + + return size; + } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs index 9eea22055..d460d9497 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs @@ -31,10 +31,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// private readonly Configuration configuration; + private JpegFrame frame; + + private IRawJpegData jpegData; + /// /// Jpeg component converters from decompressed spectral to color data. /// - private JpegComponentPostProcessor[] componentProcessors; + private ComponentProcessor[] componentProcessors; /// /// Color converter from jpeg color space to target pixel color space. @@ -66,13 +70,26 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// private int pixelRowCounter; + /// + /// Represent target size after decoding for scaling decoding mode. + /// + /// + /// Null if no scaling is required. + /// + private Size? targetSize; + /// /// Initializes a new instance of the class. /// /// The configuration. - public SpectralConverter(Configuration configuration) => + /// Optional target size for decoded image. + public SpectralConverter(Configuration configuration, Size? targetSize = null) + { this.configuration = configuration; + this.targetSize = targetSize; + } + /// /// Gets converted pixel buffer. /// @@ -86,6 +103,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { if (!this.Converted) { + this.PrepareForDecoding(); + int steps = (int)Math.Ceiling(this.pixelBuffer.Height / (float)this.pixelRowsPerStep); for (int step = 0; step < steps; step++) @@ -95,60 +114,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder } } - var buffer = this.pixelBuffer; + Buffer2D buffer = this.pixelBuffer; this.pixelBuffer = null; return buffer; } - /// - public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) - { - MemoryAllocator allocator = this.configuration.MemoryAllocator; - - // iteration data - int majorBlockWidth = frame.Components.Max((component) => component.SizeInBlocks.Width); - int majorVerticalSamplingFactor = frame.Components.Max((component) => component.SamplingFactors.Height); - - const int blockPixelHeight = 8; - this.pixelRowsPerStep = majorVerticalSamplingFactor * blockPixelHeight; - - // pixel buffer for resulting image - this.pixelBuffer = allocator.Allocate2D( - frame.PixelWidth, - frame.PixelHeight, - this.configuration.PreferContiguousImageBuffers); - this.paddedProxyPixelRow = allocator.Allocate(frame.PixelWidth + 3); - - // component processors from spectral to Rgba32 - const int blockPixelWidth = 8; - var postProcessorBufferSize = new Size(majorBlockWidth * blockPixelWidth, this.pixelRowsPerStep); - this.componentProcessors = new JpegComponentPostProcessor[frame.Components.Length]; - for (int i = 0; i < this.componentProcessors.Length; i++) - { - this.componentProcessors[i] = new JpegComponentPostProcessor(allocator, frame, jpegData, postProcessorBufferSize, frame.Components[i]); - } - - // single 'stride' rgba32 buffer for conversion between spectral and TPixel - this.rgbBuffer = allocator.Allocate(frame.PixelWidth * 3); - - // color converter from Rgba32 to TPixel - this.colorConverter = this.GetColorConverter(frame, jpegData); - } - - /// - public override void ConvertStrideBaseline() - { - // Convert next pixel stride using single spectral `stride' - // Note that zero passing eliminates the need of virtual call - // from JpegComponentPostProcessor - this.ConvertStride(spectralStep: 0); - - foreach (JpegComponentPostProcessor cpp in this.componentProcessors) - { - cpp.ClearSpectralBuffers(); - } - } - /// /// Converts single spectral jpeg stride to color stride. /// @@ -199,12 +169,88 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder this.pixelRowCounter += this.pixelRowsPerStep; } + /// + public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) + { + this.frame = frame; + this.jpegData = jpegData; + } + + /// + public override void PrepareForDecoding() + { + DebugGuard.IsTrue(this.colorConverter == null, "SpectralConverter.PrepareForDecoding() must be called once."); + + MemoryAllocator allocator = this.configuration.MemoryAllocator; + + // color converter from RGB to TPixel + JpegColorConverterBase converter = this.GetColorConverter(this.frame, this.jpegData); + this.colorConverter = converter; + + // resulting image size + Size pixelSize = CalculateResultingImageSize(this.frame.PixelSize, this.targetSize, out int blockPixelSize); + + // iteration data + int majorBlockWidth = this.frame.Components.Max((component) => component.SizeInBlocks.Width); + int majorVerticalSamplingFactor = this.frame.Components.Max((component) => component.SamplingFactors.Height); + + this.pixelRowsPerStep = majorVerticalSamplingFactor * blockPixelSize; + + // pixel buffer for resulting image + this.pixelBuffer = allocator.Allocate2D( + pixelSize.Width, + pixelSize.Height, + this.configuration.PreferContiguousImageBuffers); + this.paddedProxyPixelRow = allocator.Allocate(pixelSize.Width + 3); + + // component processors from spectral to RGB + int bufferWidth = majorBlockWidth * blockPixelSize; + int batchSize = converter.ElementsPerBatch; + int batchRemainder = bufferWidth & (batchSize - 1); + var postProcessorBufferSize = new Size(bufferWidth + (batchSize - batchRemainder), this.pixelRowsPerStep); + this.componentProcessors = this.CreateComponentProcessors(this.frame, this.jpegData, blockPixelSize, postProcessorBufferSize); + + // single 'stride' rgba32 buffer for conversion between spectral and TPixel + this.rgbBuffer = allocator.Allocate(pixelSize.Width * 3); + } + + /// + public override void ConvertStrideBaseline() + { + // Convert next pixel stride using single spectral `stride' + // Note that zero passing eliminates extra virtual call + this.ConvertStride(spectralStep: 0); + + foreach (ComponentProcessor cpp in this.componentProcessors) + { + cpp.ClearSpectralBuffers(); + } + } + + protected ComponentProcessor[] CreateComponentProcessors(JpegFrame frame, IRawJpegData jpegData, int blockPixelSize, Size processorBufferSize) + { + MemoryAllocator allocator = this.configuration.MemoryAllocator; + var componentProcessors = new ComponentProcessor[frame.Components.Length]; + for (int i = 0; i < componentProcessors.Length; i++) + { + componentProcessors[i] = blockPixelSize switch + { + 4 => new DownScalingComponentProcessor2(allocator, frame, jpegData, processorBufferSize, frame.Components[i]), + 2 => new DownScalingComponentProcessor4(allocator, frame, jpegData, processorBufferSize, frame.Components[i]), + 1 => new DownScalingComponentProcessor8(allocator, frame, jpegData, processorBufferSize, frame.Components[i]), + _ => new DirectComponentProcessor(allocator, frame, jpegData, processorBufferSize, frame.Components[i]), + }; + } + + return componentProcessors; + } + /// public void Dispose() { if (this.componentProcessors != null) { - foreach (JpegComponentPostProcessor cpp in this.componentProcessors) + foreach (ComponentProcessor cpp in this.componentProcessors) { cpp.Dispose(); } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs index 85b72ab6e..24ab9f8d1 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs @@ -139,8 +139,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder public void Encode444(Image pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - FastFloatingPointDCT.AdjustToFDCT(ref luminanceQuantTable); - FastFloatingPointDCT.AdjustToFDCT(ref chrominanceQuantTable); + FloatingPointDCT.AdjustToFDCT(ref luminanceQuantTable); + FloatingPointDCT.AdjustToFDCT(ref chrominanceQuantTable); this.huffmanTables = HuffmanLut.TheHuffmanLut; @@ -202,8 +202,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder public void Encode420(Image pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - FastFloatingPointDCT.AdjustToFDCT(ref luminanceQuantTable); - FastFloatingPointDCT.AdjustToFDCT(ref chrominanceQuantTable); + FloatingPointDCT.AdjustToFDCT(ref luminanceQuantTable); + FloatingPointDCT.AdjustToFDCT(ref chrominanceQuantTable); this.huffmanTables = HuffmanLut.TheHuffmanLut; @@ -271,7 +271,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder public void EncodeGrayscale(Image pixels, ref Block8x8F luminanceQuantTable, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - FastFloatingPointDCT.AdjustToFDCT(ref luminanceQuantTable); + FloatingPointDCT.AdjustToFDCT(ref luminanceQuantTable); this.huffmanTables = HuffmanLut.TheHuffmanLut; @@ -319,7 +319,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder public void EncodeRgb(Image pixels, ref Block8x8F quantTable, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - FastFloatingPointDCT.AdjustToFDCT(ref quantTable); + FloatingPointDCT.AdjustToFDCT(ref quantTable); this.huffmanTables = HuffmanLut.TheHuffmanLut; @@ -391,7 +391,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder block.AddInPlace(-128f); // Discrete cosine transform - FastFloatingPointDCT.TransformFDCT(ref block); + FloatingPointDCT.TransformFDCT(ref block); // Quantization Block8x8F.Quantize(ref block, ref spectralBlock, ref quant); diff --git a/src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.Intrinsic.cs b/src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.Intrinsic.cs similarity index 99% rename from src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.Intrinsic.cs rename to src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.Intrinsic.cs index 22dee95b4..19349e454 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.Intrinsic.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.Intrinsic.cs @@ -7,7 +7,7 @@ using System.Runtime.Intrinsics.X86; namespace SixLabors.ImageSharp.Formats.Jpeg.Components { - internal static partial class FastFloatingPointDCT + internal static partial class FloatingPointDCT { /// /// Apply floating point FDCT inplace using simd operations. diff --git a/src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.cs b/src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.cs similarity index 93% rename from src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.cs rename to src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.cs index 34277f1ae..37ec6e190 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.cs @@ -12,9 +12,12 @@ using System.Runtime.Intrinsics.X86; namespace SixLabors.ImageSharp.Formats.Jpeg.Components { /// - /// Contains inaccurate, but fast forward and inverse DCT implementations. + /// Contains floating point forward and inverse DCT implementations /// - internal static partial class FastFloatingPointDCT + /// + /// Based on "Arai, Agui and Nakajima" algorithm. + /// + internal static partial class FloatingPointDCT { #pragma warning disable SA1310, SA1311, IDE1006 // naming rules violation warnings private static readonly Vector4 mm128_F_0_7071 = new(0.707106781f); @@ -70,8 +73,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components ref float multipliersRef = ref MemoryMarshal.GetReference(AdjustmentCoefficients); for (nint i = 0; i < Block8x8F.Size; i++) { - tableRef = 0.125f * tableRef * Unsafe.Add(ref multipliersRef, i); - tableRef = ref Unsafe.Add(ref tableRef, 1); + ref float elemRef = ref Unsafe.Add(ref tableRef, i); + elemRef = 0.125f * elemRef * Unsafe.Add(ref multipliersRef, i); } // Spectral macroblocks are transposed before quantization @@ -89,8 +92,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components ref float multipliersRef = ref MemoryMarshal.GetReference(AdjustmentCoefficients); for (nint i = 0; i < Block8x8F.Size; i++) { - tableRef = 0.125f / (tableRef * Unsafe.Add(ref multipliersRef, i)); - tableRef = ref Unsafe.Add(ref tableRef, 1); + ref float elemRef = ref Unsafe.Add(ref tableRef, i); + elemRef = 0.125f / (elemRef * Unsafe.Add(ref multipliersRef, i)); } // Spectral macroblocks are not transposed before quantization @@ -103,7 +106,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Apply 2D floating point IDCT inplace. /// /// - /// Input block must be dequantized before this method with table + /// Input block must be dequantized with quantization table /// adjusted by . /// /// Input block. @@ -125,8 +128,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Apply 2D floating point IDCT inplace. /// /// - /// Input block must be quantized after this method with table adjusted - /// by . + /// Input block must be quantized after this method with quantization + /// table adjusted by . /// /// Input block. public static void TransformFDCT(ref Block8x8F block) @@ -221,7 +224,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Apply floating point FDCT inplace using API. /// /// Input block. - public static void FDCT_Vector4(ref Block8x8F block) + private static void FDCT_Vector4(ref Block8x8F block) { // First pass - process columns FDCT8x4_Vector4(ref block.V0L); diff --git a/src/ImageSharp/Formats/Jpeg/Components/ScaledFloatingPointDCT.cs b/src/ImageSharp/Formats/Jpeg/Components/ScaledFloatingPointDCT.cs new file mode 100644 index 000000000..037995ea1 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ScaledFloatingPointDCT.cs @@ -0,0 +1,220 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.CompilerServices; + +#pragma warning disable IDE0078 +namespace SixLabors.ImageSharp.Formats.Jpeg.Components +{ + /// + /// Contains floating point forward DCT implementations with built-in scaling. + /// + /// + /// Based on "Loeffler, Ligtenberg, and Moschytz" algorithm. + /// + internal static class ScaledFloatingPointDCT + { +#pragma warning disable SA1310 + private const float FP32_0_541196100 = 0.541196100f; + private const float FP32_0_765366865 = 0.765366865f; + private const float FP32_1_847759065 = 1.847759065f; + private const float FP32_0_211164243 = 0.211164243f; + private const float FP32_1_451774981 = 1.451774981f; + private const float FP32_2_172734803 = 2.172734803f; + private const float FP32_1_061594337 = 1.061594337f; + private const float FP32_0_509795579 = 0.509795579f; + private const float FP32_0_601344887 = 0.601344887f; + private const float FP32_0_899976223 = 0.899976223f; + private const float FP32_2_562915447 = 2.562915447f; + private const float FP32_0_720959822 = 0.720959822f; + private const float FP32_0_850430095 = 0.850430095f; + private const float FP32_1_272758580 = 1.272758580f; + private const float FP32_3_624509785 = 3.624509785f; +#pragma warning restore SA1310 + + /// + /// Adjusts given quantization table for usage with IDCT algorithms + /// from . + /// + /// Quantization table to adjust. + public static void AdjustToIDCT(ref Block8x8F quantTable) + { + ref float tableRef = ref Unsafe.As(ref quantTable); + for (nint i = 0; i < Block8x8F.Size; i++) + { + ref float elemRef = ref Unsafe.Add(ref tableRef, i); + elemRef = 0.125f * elemRef; + } + + // Spectral macroblocks are transposed before quantization + // so we must transpose quantization table + quantTable.TransposeInplace(); + } + + /// + /// Apply 2D floating point 'donwscaling' IDCT inplace producing + /// 8x8 -> 4x4 result. + /// + /// + /// Resulting matrix is stored in the top left 4x4 part of the + /// . + /// + /// Input block. + /// Dequantization table adjusted by . + /// Output range normalization value, 1/2 of the . + /// Maximum value of the output range. + public static void TransformIDCT_4x4(ref Block8x8F block, ref Block8x8F dequantTable, float normalizationValue, float maxValue) + { + for (int ctr = 0; ctr < 8; ctr++) + { + // Don't process row 4, second pass doesn't use it + if (ctr == 4) + { + continue; + } + + // Even part + float tmp0 = block[(ctr * 8) + 0] * dequantTable[(ctr * 8) + 0] * 2; + + float z2 = block[(ctr * 8) + 2] * dequantTable[(ctr * 8) + 2]; + float z3 = block[(ctr * 8) + 6] * dequantTable[(ctr * 8) + 6]; + + float tmp2 = (z2 * FP32_1_847759065) + (z3 * -FP32_0_765366865); + + float tmp10 = tmp0 + tmp2; + float tmp12 = tmp0 - tmp2; + + // Odd part + float z1 = block[(ctr * 8) + 7] * dequantTable[(ctr * 8) + 7]; + z2 = block[(ctr * 8) + 5] * dequantTable[(ctr * 8) + 5]; + z3 = block[(ctr * 8) + 3] * dequantTable[(ctr * 8) + 3]; + float z4 = block[(ctr * 8) + 1] * dequantTable[(ctr * 8) + 1]; + + tmp0 = (z1 * -FP32_0_211164243) + + (z2 * FP32_1_451774981) + + (z3 * -FP32_2_172734803) + + (z4 * FP32_1_061594337); + + tmp2 = (z1 * -FP32_0_509795579) + + (z2 * -FP32_0_601344887) + + (z3 * FP32_0_899976223) + + (z4 * FP32_2_562915447); + + // temporal result is saved to +4 shifted indices + // because result is saved into the top left 2x2 region of the + // input block + block[(ctr * 8) + 0 + 4] = (tmp10 + tmp2) / 2; + block[(ctr * 8) + 3 + 4] = (tmp10 - tmp2) / 2; + block[(ctr * 8) + 1 + 4] = (tmp12 + tmp0) / 2; + block[(ctr * 8) + 2 + 4] = (tmp12 - tmp0) / 2; + } + + for (int ctr = 0; ctr < 4; ctr++) + { + // Even part + float tmp0 = block[ctr + (8 * 0) + 4] * 2; + + float tmp2 = (block[ctr + (8 * 2) + 4] * FP32_1_847759065) + (block[ctr + (8 * 6) + 4] * -FP32_0_765366865); + + float tmp10 = tmp0 + tmp2; + float tmp12 = tmp0 - tmp2; + + // Odd part + float z1 = block[ctr + (8 * 7) + 4]; + float z2 = block[ctr + (8 * 5) + 4]; + float z3 = block[ctr + (8 * 3) + 4]; + float z4 = block[ctr + (8 * 1) + 4]; + + tmp0 = (z1 * -FP32_0_211164243) + + (z2 * FP32_1_451774981) + + (z3 * -FP32_2_172734803) + + (z4 * FP32_1_061594337); + + tmp2 = (z1 * -FP32_0_509795579) + + (z2 * -FP32_0_601344887) + + (z3 * FP32_0_899976223) + + (z4 * FP32_2_562915447); + + // Save results to the top left 4x4 subregion + block[(ctr * 8) + 0] = MathF.Round(Numerics.Clamp(((tmp10 + tmp2) / 2) + normalizationValue, 0, maxValue)); + block[(ctr * 8) + 3] = MathF.Round(Numerics.Clamp(((tmp10 - tmp2) / 2) + normalizationValue, 0, maxValue)); + block[(ctr * 8) + 1] = MathF.Round(Numerics.Clamp(((tmp12 + tmp0) / 2) + normalizationValue, 0, maxValue)); + block[(ctr * 8) + 2] = MathF.Round(Numerics.Clamp(((tmp12 - tmp0) / 2) + normalizationValue, 0, maxValue)); + } + } + + /// + /// Apply 2D floating point 'donwscaling' IDCT inplace producing + /// 8x8 -> 2x2 result. + /// + /// + /// Resulting matrix is stored in the top left 2x2 part of the + /// . + /// + /// Input block. + /// Dequantization table adjusted by . + /// Output range normalization value, 1/2 of the . + /// Maximum value of the output range. + public static void TransformIDCT_2x2(ref Block8x8F block, ref Block8x8F dequantTable, float normalizationValue, float maxValue) + { + for (int ctr = 0; ctr < 8; ctr++) + { + // Don't process rows 2/4/6, second pass doesn't use it + if (ctr == 2 || ctr == 4 || ctr == 6) + { + continue; + } + + // Even part + float tmp0; + float z1 = block[(ctr * 8) + 0] * dequantTable[(ctr * 8) + 0]; + float tmp10 = z1 * 4; + + // Odd part + z1 = block[(ctr * 8) + 7] * dequantTable[(ctr * 8) + 7]; + tmp0 = z1 * -FP32_0_720959822; + z1 = block[(ctr * 8) + 5] * dequantTable[(ctr * 8) + 5]; + tmp0 += z1 * FP32_0_850430095; + z1 = block[(ctr * 8) + 3] * dequantTable[(ctr * 8) + 3]; + tmp0 += z1 * -FP32_1_272758580; + z1 = block[(ctr * 8) + 1] * dequantTable[(ctr * 8) + 1]; + tmp0 += z1 * FP32_3_624509785; + + // temporal result is saved to +2 shifted indices + // because result is saved into the top left 2x2 region of the + // input block + block[(ctr * 8) + 2] = (tmp10 + tmp0) / 4; + block[(ctr * 8) + 3] = (tmp10 - tmp0) / 4; + } + + for (int ctr = 0; ctr < 2; ctr++) + { + // Even part + float tmp10 = block[ctr + (8 * 0) + 2] * 4; + + // Odd part + float tmp0 = (block[ctr + (8 * 7) + 2] * -FP32_0_720959822) + + (block[ctr + (8 * 5) + 2] * FP32_0_850430095) + + (block[ctr + (8 * 3) + 2] * -FP32_1_272758580) + + (block[ctr + (8 * 1) + 2] * FP32_3_624509785); + + // Save results to the top left 2x2 subregion + block[(ctr * 8) + 0] = MathF.Round(Numerics.Clamp(((tmp10 + tmp0) / 4) + normalizationValue, 0, maxValue)); + block[(ctr * 8) + 1] = MathF.Round(Numerics.Clamp(((tmp10 - tmp0) / 4) + normalizationValue, 0, maxValue)); + } + } + + /// + /// Apply 2D floating point 'donwscaling' IDCT inplace producing + /// 8x8 -> 1x1 result. + /// + /// Direct current term value from input block. + /// Dequantization value. + /// Output range normalization value, 1/2 of the . + /// Maximum value of the output range. + public static float TransformIDCT_1x1(float dc, float dequantizer, float normalizationValue, float maxValue) + => MathF.Round(Numerics.Clamp((dc * dequantizer) + normalizationValue, 0, maxValue)); + } +} +#pragma warning restore IDE0078 diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index 9df1a7b4f..7f23d1ac8 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -3,6 +3,8 @@ using System.IO; using System.Threading; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jpeg @@ -29,6 +31,42 @@ namespace SixLabors.ImageSharp.Formats.Jpeg public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) => this.Decode(configuration, stream, cancellationToken); + /// + /// Placeholder summary. + /// + /// Placeholder2 + /// Placeholder3 + /// Placeholder4 + /// Placeholder5 + /// Placeholder6 + internal Image DecodeInto(Configuration configuration, Stream stream, Size targetSize, CancellationToken cancellationToken) + => this.DecodeInto(configuration, stream, targetSize, cancellationToken); + + /// + /// Decodes and downscales the image from the specified stream if possible. + /// + /// The pixel format. + /// Configuration. + /// Stream. + /// Target size. + /// Cancellation token. + internal Image DecodeInto(Configuration configuration, Stream stream, Size targetSize, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(stream, nameof(stream)); + + using var decoder = new JpegDecoderCore(configuration, this); + using var bufferedReadStream = new BufferedReadStream(configuration, stream); + try + { + return decoder.DecodeInto(bufferedReadStream, targetSize, cancellationToken); + } + catch (InvalidMemoryOperationException ex) + { + throw new InvalidImageContentException(((IImageDecoderInternals)decoder).Dimensions, ex); + } + } + /// public IImageInfo Identify(Configuration configuration, Stream stream, CancellationToken cancellationToken) { diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 6b8d3b0d2..11187fb91 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -86,14 +86,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg private byte[] xmpData; /// - /// Contains information about the JFIF marker. + /// Whether the image has a APP14 adobe marker. This is needed to determine image encoded colorspace. /// - private JFifMarker jFif; + private bool hasAdobeMarker; /// - /// Whether the image has a JFIF marker. This is needed to determine, if the colorspace is YCbCr. + /// Contains information about the JFIF marker. /// - private bool hasJFif; + private JFifMarker jFif; /// /// Contains information about the Adobe marker. @@ -201,34 +201,48 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel - { - using var spectralConverter = new SpectralConverter(this.Configuration); + => this.Decode(stream, targetSize: null, cancellationToken); - this.ParseStream(stream, spectralConverter, cancellationToken); + /// + public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + { + this.ParseStream(stream, spectralConverter: null, cancellationToken); this.InitExifProfile(); this.InitIccProfile(); this.InitIptcProfile(); this.InitXmpProfile(); this.InitDerivedMetadataProperties(); - return new Image( - this.Configuration, - spectralConverter.GetPixelBuffer(cancellationToken), - this.Metadata); + Size pixelSize = this.Frame.PixelSize; + return new ImageInfo(new PixelTypeInfo(this.Frame.BitsPerPixel), pixelSize.Width, pixelSize.Height, this.Metadata); } - /// - public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + /// + /// Decodes and downscales the image from the specified stream if possible. + /// + /// The pixel format. + /// Stream. + /// Target size. + /// Cancellation token. + internal Image DecodeInto(BufferedReadStream stream, Size targetSize, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + => this.Decode(stream, targetSize, cancellationToken); + + private Image Decode(BufferedReadStream stream, Size? targetSize, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel { - this.ParseStream(stream, spectralConverter: null, cancellationToken); + using var spectralConverter = new SpectralConverter(this.Configuration, targetSize); + this.ParseStream(stream, spectralConverter, cancellationToken); this.InitExifProfile(); this.InitIccProfile(); this.InitIptcProfile(); this.InitXmpProfile(); this.InitDerivedMetadataProperties(); - Size pixelSize = this.Frame.PixelSize; - return new ImageInfo(new PixelTypeInfo(this.Frame.BitsPerPixel), pixelSize.Width, pixelSize.Height, this.Metadata); + return new Image( + this.Configuration, + spectralConverter.GetPixelBuffer(cancellationToken), + this.Metadata); } /// @@ -503,11 +517,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - /// Returns the correct colorspace based on the image component count and the jpeg frame component id's. + /// Returns encoded colorspace based on the adobe APP14 marker. /// - /// The number of components. + /// Number of components. + /// Parsed adobe APP14 marker. /// The - private JpegColorSpace DeduceJpegColorSpace(byte componentCount) + internal static JpegColorSpace DeduceJpegColorSpace(byte componentCount, ref AdobeMarker adobeMarker) { if (componentCount == 1) { @@ -516,80 +531,47 @@ namespace SixLabors.ImageSharp.Formats.Jpeg if (componentCount == 3) { - // We prioritize adobe marker over jfif marker, if somebody really encoded this image with redundant adobe marker, - // then it's most likely an adobe jfif image. - if (!this.adobe.Equals(default)) + if (adobeMarker.ColorTransform == JpegConstants.Adobe.ColorTransformUnknown) { - if (this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformYCbCr) - { - return JpegColorSpace.YCbCr; - } - - if (this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformUnknown) - { - return JpegColorSpace.RGB; - } - - // Fallback to the id color deduction: If these values are 1-3 for a 3-channel image, then the image is assumed to be YCbCr. - if (this.Components[2].Id == 3 && this.Components[1].Id == 2 && this.Components[0].Id == 1) - { - return JpegColorSpace.YCbCr; - } - - JpegThrowHelper.ThrowNotSupportedColorSpace(); + return JpegColorSpace.RGB; } - if (this.hasJFif) - { - // JFIF implies YCbCr. - return JpegColorSpace.YCbCr; - } + return JpegColorSpace.YCbCr; + } - // Fallback to the id color deduction. - // If the component Id's are R, G, B in ASCII the colorspace is RGB and not YCbCr. - // See: https://docs.oracle.com/javase/7/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html#color - if (this.Components[2].Id == 66 && this.Components[1].Id == 71 && this.Components[0].Id == 82) + if (componentCount == 4) + { + if (adobeMarker.ColorTransform == JpegConstants.Adobe.ColorTransformYcck) { - return JpegColorSpace.RGB; + return JpegColorSpace.Ycck; } - // If these values are 1-3 for a 3-channel image, then the image is assumed to be YCbCr. - if (this.Components[2].Id == 3 && this.Components[1].Id == 2 && this.Components[0].Id == 1) - { - return JpegColorSpace.YCbCr; - } + return JpegColorSpace.Cmyk; + } - // 3-channel non-subsampled images are assumed to be RGB. - if (this.Components[2].VerticalSamplingFactor == 1 && this.Components[1].VerticalSamplingFactor == 1 && this.Components[0].VerticalSamplingFactor == 1 && - this.Components[2].HorizontalSamplingFactor == 1 && this.Components[1].HorizontalSamplingFactor == 1 && this.Components[0].HorizontalSamplingFactor == 1) - { - return JpegColorSpace.RGB; - } + JpegThrowHelper.ThrowNotSupportedComponentCount(componentCount); + return default; + } - // Some images are poorly encoded and contain incorrect colorspace transform metadata. - // We ignore that and always fall back to the default colorspace. + /// + /// Returns encoded colorspace based on the component count. + /// + /// Number of components. + /// The + internal static JpegColorSpace DeduceJpegColorSpace(byte componentCount) + { + if (componentCount == 1) + { + return JpegColorSpace.Grayscale; + } + + if (componentCount == 3) + { return JpegColorSpace.YCbCr; } if (componentCount == 4) { - // jfif images doesn't not support 4 component images, so we only check adobe. - if (!this.adobe.Equals(default)) - { - if (this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformYcck) - { - return JpegColorSpace.Ycck; - } - - if (this.adobe.ColorTransform == JpegConstants.Adobe.ColorTransformUnknown) - { - return JpegColorSpace.Cmyk; - } - - JpegThrowHelper.ThrowNotSupportedColorSpace(); - } - - // Fallback to cmyk as neither of cmyk nor ycck have 'special' component ids. return JpegColorSpace.Cmyk; } @@ -757,8 +739,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The remaining bytes in the segment block. private void ProcessApplicationHeaderMarker(BufferedReadStream stream, int remaining) { - this.hasJFif = true; - // We can only decode JFif identifiers. // Some images contain multiple JFIF markers (Issue 1932) so we check to see // if it's already been read. @@ -1061,7 +1041,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg stream.Read(this.temp, 0, MarkerLength); remaining -= MarkerLength; - AdobeMarker.TryParse(this.temp, out this.adobe); + if (AdobeMarker.TryParse(this.temp, out this.adobe)) + { + this.hasAdobeMarker = true; + } if (remaining > 0) { @@ -1169,9 +1152,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg break; } } - - // Adjusting table for IDCT step during decompression - FastFloatingPointDCT.AdjustToIDCT(ref table); } } @@ -1257,7 +1237,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg int maxH = 0; int maxV = 0; int index = 0; - for (int i = 0; i < componentCount; i++) + for (int i = 0; i < this.Frame.Components.Length; i++) { // 1 byte: component identifier byte componentId = this.temp[index]; @@ -1308,7 +1288,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg index += componentBytes; } - this.ColorSpace = this.DeduceJpegColorSpace(componentCount); + this.ColorSpace = this.hasAdobeMarker + ? DeduceJpegColorSpace(componentCount, ref this.adobe) + : DeduceJpegColorSpace(componentCount); this.Metadata.GetJpegMetadata().ColorType = this.DeduceJpegColorType(); if (!metadataOnly) diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs index d0543917a..0d9a6ba13 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs @@ -59,7 +59,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors case TiffPhotometricInterpretation.BlackIsZero: case TiffPhotometricInterpretation.WhiteIsZero: { - using SpectralConverter spectralConverterGray = new GrayJpegSpectralConverter(this.configuration); + using SpectralConverter spectralConverterGray = + new GrayJpegSpectralConverter(this.configuration); var scanDecoderGray = new HuffmanScanDecoder(stream, spectralConverterGray, CancellationToken.None); jpegDecoder.LoadTables(this.jpegTables, scanDecoderGray); jpegDecoder.ParseStream(stream, spectralConverterGray, CancellationToken.None); @@ -73,8 +74,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors case TiffPhotometricInterpretation.YCbCr: case TiffPhotometricInterpretation.Rgb: { - using SpectralConverter spectralConverter = this.photometricInterpretation == TiffPhotometricInterpretation.YCbCr ? - new RgbJpegSpectralConverter(this.configuration) : new SpectralConverter(this.configuration); + using SpectralConverter spectralConverter = + new TiffJpegSpectralConverter(this.configuration, this.photometricInterpretation); var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, CancellationToken.None); jpegDecoder.LoadTables(this.jpegTables, scanDecoder); jpegDecoder.ParseStream(stream, spectralConverter, CancellationToken.None); diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs deleted file mode 100644 index f8cf3e2a9..000000000 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; -using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors -{ - /// - /// Spectral converter for YCbCr TIFF's which use the JPEG compression. - /// The jpeg data should be always treated as RGB color space. - /// - /// The type of the pixel. - internal sealed class RgbJpegSpectralConverter : SpectralConverter - where TPixel : unmanaged, IPixel - { - /// - /// Initializes a new instance of the class. - /// This Spectral converter will always convert the pixel data to RGB color. - /// - /// The configuration. - public RgbJpegSpectralConverter(Configuration configuration) - : base(configuration) - { - } - - /// - protected override JpegColorConverterBase GetColorConverter(JpegFrame frame, IRawJpegData jpegData) => JpegColorConverterBase.GetConverter(JpegColorSpace.RGB, frame.Precision); - } -} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs new file mode 100644 index 000000000..ced6b9027 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/TiffJpegSpectralConverter{TPixel}.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + /// + /// Spectral converter for YCbCr TIFF's which use the JPEG compression. + /// The jpeg data should be always treated as RGB color space. + /// + /// The type of the pixel. + internal sealed class TiffJpegSpectralConverter : SpectralConverter + where TPixel : unmanaged, IPixel + { + private readonly TiffPhotometricInterpretation photometricInterpretation; + + /// + /// Initializes a new instance of the class. + /// This Spectral converter will always convert the pixel data to RGB color. + /// + /// The configuration. + /// Tiff photometric interpretation. + public TiffJpegSpectralConverter(Configuration configuration, TiffPhotometricInterpretation photometricInterpretation) + : base(configuration) + => this.photometricInterpretation = photometricInterpretation; + + /// + protected override JpegColorConverterBase GetColorConverter(JpegFrame frame, IRawJpegData jpegData) + { + JpegColorSpace colorSpace = GetJpegColorSpaceFromPhotometricInterpretation(this.photometricInterpretation); + return JpegColorConverterBase.GetConverter(colorSpace, frame.Precision); + } + + /// + /// This converter must be used only for RGB and YCbCr color spaces for performance reasons. + /// For grayscale images must be used. + /// + private static JpegColorSpace GetJpegColorSpaceFromPhotometricInterpretation(TiffPhotometricInterpretation interpretation) + => interpretation switch + { + TiffPhotometricInterpretation.Rgb => JpegColorSpace.RGB, + TiffPhotometricInterpretation.YCbCr => JpegColorSpace.RGB, + _ => throw new InvalidImageContentException($"Invalid tiff photometric interpretation for jpeg encoding: {interpretation}"), + }; + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs new file mode 100644 index 000000000..0d63382ff --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + /// + /// Class to handle cases where TIFF image data is compressed as a webp stream. + /// + internal class WebpTiffCompression : TiffBaseDecompressor + { + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The width of the image. + /// The bits per pixel. + /// The predictor. + public WebpTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, TiffPredictor predictor = TiffPredictor.None) + : base(memoryAllocator, width, bitsPerPixel, predictor) + { + } + + /// + protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer) + { + using var image = Image.Load(stream, new WebpDecoder()); + CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer); + } + + private static void CopyImageBytesToBuffer(Span buffer, Buffer2D pixelBuffer) + { + int offset = 0; + for (int y = 0; y < pixelBuffer.Height; y++) + { + Span pixelRowSpan = pixelBuffer.DangerousGetRowSpan(y); + Span rgbBytes = MemoryMarshal.AsBytes(pixelRowSpan); + rgbBytes.CopyTo(buffer.Slice(offset)); + offset += rgbBytes.Length; + } + } + + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs index f6ee06db6..ab8f51844 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs @@ -41,6 +41,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression UndoGray32Bit(pixelBytes, width, isBigEndian); break; case TiffColorType.Rgb888: + case TiffColorType.CieLab: UndoRgb24Bit(pixelBytes, width); break; case TiffColorType.Rgba8888: diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs index 44caba5b0..3188b9e9a 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecoderCompressionType.cs @@ -47,5 +47,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression /// The image data is compressed as a JPEG stream. /// Jpeg = 7, + + /// + /// The image data is compressed as a WEBP stream. + /// + Webp = 8, } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs index 5d6793660..5a8aa3b98 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs @@ -60,6 +60,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); return new JpegTiffCompression(configuration, allocator, width, bitsPerPixel, jpegTables, photometricInterpretation); + case TiffDecoderCompressionType.Webp: + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new WebpTiffCompression(allocator, width, bitsPerPixel); + default: throw TiffThrowHelper.NotSupportedDecompressor(nameof(method)); } diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs index e8eecf2e4..fdb1d6aa1 100644 --- a/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs @@ -103,5 +103,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Constants /// if this is chosen. /// OldDeflate = 32946, + + /// + /// Pixel data is compressed with webp encoder. + /// + /// Note: The TIFF encoder does not support this compression and will default to use no compression instead, + /// if this is chosen. + /// + Webp = 50001, } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabPlanarTiffColor{TPixel}.cs new file mode 100644 index 000000000..3baf60e78 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabPlanarTiffColor{TPixel}.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Numerics; +using SixLabors.ImageSharp.ColorSpaces; +using SixLabors.ImageSharp.ColorSpaces.Conversion; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements decoding pixel data with photometric interpretation of type 'CieLab' with the planar configuration. + /// + internal class CieLabPlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private static readonly ColorSpaceConverter ColorSpaceConverter = new(); + + private const float Inv255 = 1.0f / 255.0f; + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + Span l = data[0].GetSpan(); + Span a = data[1].GetSpan(); + Span b = data[2].GetSpan(); + + var color = default(TPixel); + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) + { + var lab = new CieLab((l[offset] & 0xFF) * 100f * Inv255, (sbyte)a[offset], (sbyte)b[offset]); + var rgb = ColorSpaceConverter.ToRgb(lab); + + color.FromVector4(new Vector4(rgb.R, rgb.G, rgb.B, 1.0f)); + pixelRow[x] = color; + + offset++; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabTiffColor{TPixel}.cs new file mode 100644 index 000000000..5b272b2be --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/CieLabTiffColor{TPixel}.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.ColorSpaces; +using SixLabors.ImageSharp.ColorSpaces.Conversion; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements decoding pixel data with photometric interpretation of type 'CieLab'. + /// + internal class CieLabTiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private static readonly ColorSpaceConverter ColorSpaceConverter = new(); + + private const float Inv255 = 1.0f / 255.0f; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + var color = default(TPixel); + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + + for (int x = 0; x < pixelRow.Length; x++) + { + float l = (data[offset] & 0xFF) * 100f * Inv255; + var lab = new CieLab(l, (sbyte)data[offset + 1], (sbyte)data[offset + 2]); + var rgb = ColorSpaceConverter.ToRgb(lab); + + color.FromVector4(new Vector4(rgb.R, rgb.G, rgb.B, 1.0f)); + pixelRow[x] = color; + + offset += 3; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index 5c1fba5ef..2ef46dd84 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -385,8 +385,23 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation return new PaletteTiffColor(bitsPerSample, colorMap); case TiffColorType.YCbCr: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 8 + && bitsPerSample.Channel1 == 8 + && bitsPerSample.Channel0 == 8, + "bitsPerSample"); return new YCbCrTiffColor(memoryAllocator, referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling); + case TiffColorType.CieLab: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 8 + && bitsPerSample.Channel1 == 8 + && bitsPerSample.Channel0 == 8, + "bitsPerSample"); + return new CieLabTiffColor(); + default: throw TiffThrowHelper.InvalidColorType(colorType.ToString()); } @@ -415,6 +430,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation case TiffColorType.YCbCrPlanar: return new YCbCrPlanarTiffColor(referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling); + case TiffColorType.CieLabPlanar: + return new CieLabPlanarTiffColor(); + case TiffColorType.Rgb161616Planar: DebugGuard.IsTrue(colorMap == null, "colorMap"); return new Rgb16PlanarTiffColor(byteOrder == ByteOrder.BigEndian); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs index 619bfab4d..cce5c95ad 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -276,6 +276,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// /// The pixels are stored in YCbCr format as planar. /// - YCbCrPlanar + YCbCrPlanar, + + /// + /// The pixels are stored in CieLab format. + /// + CieLab, + + /// + /// The pixels are stored in CieLab format as planar. + /// + CieLabPlanar, } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs index c53b75543..6abec16fa 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs @@ -9,6 +9,9 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements decoding pixel data with photometric interpretation of type 'YCbCr' with the planar configuration. + /// internal class YCbCrPlanarTiffColor : TiffBasePlanarColorDecoder where TPixel : unmanaged, IPixel { diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs index a850c6787..f4ff75c15 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs @@ -9,6 +9,9 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { + /// + /// Implements decoding pixel data with photometric interpretation of type 'YCbCr'. + /// internal class YCbCrTiffColor : TiffBaseColorDecoder where TPixel : unmanaged, IPixel { diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md index 8cb327a7b..00d46c415 100644 --- a/src/ImageSharp/Formats/Tiff/README.md +++ b/src/ImageSharp/Formats/Tiff/README.md @@ -37,11 +37,12 @@ |PackBits | Y | Y | | |CcittGroup3Fax | Y | Y | | |CcittGroup4Fax | Y | Y | | -|Lzw | Y | Y | Based on ImageSharp GIF LZW implementation - this code could be modified to be (i) shared, or (ii) optimised for each case | -|Old Jpeg | | | We should not even try to support this | +|Lzw | Y | Y | Based on ImageSharp GIF LZW implementation - this code could be modified to be (i) shared, or (ii) optimised for each case. | +|Old Jpeg | | | We should not even try to support this. | |Jpeg (Technote 2) | Y | Y | | |Deflate (Technote 2) | Y | Y | Based on PNG Deflate. | |Old Deflate (Technote 2) | | Y | | +|Webp | | Y | | ### Photometric Interpretation Formats @@ -55,7 +56,7 @@ |TransparencyMask | | | | |Separated (TIFF Extension) | | | | |YCbCr (TIFF Extension) | | Y | | -|CieLab (TIFF Extension) | | | | +|CieLab (TIFF Extension) | | Y | | |IccLab (TechNote 1) | | | | ### Baseline TIFF Tags diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index a5d58c2dd..d3686852d 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -381,7 +381,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff options.ColorMap = exifProfile.GetValue(ExifTag.ColorMap)?.Value; if (options.BitsPerSample.Channels != 3) { - TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported."); + TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported for YCbCr images."); } ushort bitsPerChannel = options.BitsPerSample.Channel0; @@ -395,6 +395,24 @@ namespace SixLabors.ImageSharp.Formats.Tiff break; } + case TiffPhotometricInterpretation.CieLab: + { + if (options.BitsPerSample.Channels != 3) + { + TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported for CieLab images."); + } + + ushort bitsPerChannel = options.BitsPerSample.Channel0; + if (bitsPerChannel != 8) + { + TiffThrowHelper.ThrowNotSupported("Only 8 bits per channel is supported for CieLab images."); + } + + options.ColorType = options.PlanarConfiguration == TiffPlanarConfiguration.Chunky ? TiffColorType.CieLab : TiffColorType.CieLabPlanar; + + break; + } + default: { TiffThrowHelper.ThrowNotSupported($"The specified TIFF photometric interpretation is not supported: {options.PhotometricInterpretation}"); @@ -470,6 +488,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff break; } + case TiffCompression.Webp: + { + options.CompressionType = TiffDecoderCompressionType.Webp; + break; + } + default: { TiffThrowHelper.ThrowNotSupported($"The specified TIFF compression format '{compression}' is not supported"); diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 2fac988c2..96be06eb7 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -24,11 +24,13 @@ net7.0;net6.0 + true net6.0 + true diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifTagDescriptionAttribute.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifTagDescriptionAttribute.cs index aeef257ba..8b4efad9a 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifTagDescriptionAttribute.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifTagDescriptionAttribute.cs @@ -32,7 +32,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif public static string GetDescription(ExifTag tag, object value) { var tagValue = (ExifTagValue)(ushort)tag; - FieldInfo field = tagValue.GetType().GetTypeInfo().GetDeclaredField(tagValue.ToString()); + FieldInfo field = typeof(ExifTagValue).GetField(tagValue.ToString(), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); if (field is null) { diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg.cs index 6895c391b..79daa7df4 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg.cs @@ -79,4 +79,21 @@ Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores | 'Baseline 4:2:0 Interleaved' | 8.458 ms | 0.0289 ms | 0.0256 ms | | 'Baseline 4:0:0 (grayscale)' | 1.550 ms | 0.0050 ms | 0.0044 ms | | 'Progressive 4:2:0 Non-Interleaved' | 13.220 ms | 0.0449 ms | 0.0398 ms | + + +FRESH BENCHMARKS FOR NEW SPECTRAL CONVERSION SETUP + +BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19044 +Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores +.NET SDK=6.0.100-preview.3.21202.5 + [Host] : .NET Core 3.1.21 (CoreCLR 4.700.21.51404, CoreFX 4.700.21.51508), X64 RyuJIT + DefaultJob : .NET Core 3.1.21 (CoreCLR 4.700.21.51404, CoreFX 4.700.21.51508), X64 RyuJIT + + +| Method | Mean | Error | StdDev | +|------------------------------------ |----------:|----------:|----------:| +| 'Baseline 4:4:4 Interleaved' | 10.734 ms | 0.0287 ms | 0.0254 ms | +| 'Baseline 4:2:0 Interleaved' | 8.517 ms | 0.0401 ms | 0.0356 ms | +| 'Baseline 4:0:0 (grayscale)' | 1.442 ms | 0.0051 ms | 0.0045 ms | +| 'Progressive 4:2:0 Non-Interleaved' | 12.740 ms | 0.0832 ms | 0.0730 ms | */ diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs index 90cbbb53c..34aa11144 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs @@ -57,6 +57,10 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) { } + + public override void PrepareForDecoding() + { + } } } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/LoadResizeSave_Aggregate.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/LoadResizeSave_Aggregate.cs deleted file mode 100644 index 57dffffe3..000000000 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/LoadResizeSave_Aggregate.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; -using System.IO; -using BenchmarkDotNet.Attributes; -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Tests; - -// ReSharper disable InconsistentNaming -namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg -{ - [Config(typeof(Config.ShortMultiFramework))] - public class LoadResizeSave_Aggregate : MultiImageBenchmarkBase - { - protected override IEnumerable InputImageSubfoldersOrFiles => - new[] - { - TestImages.Jpeg.BenchmarkSuite.Jpeg400_SmallMonochrome, - TestImages.Jpeg.BenchmarkSuite.Jpeg420Exif_MidSizeYCbCr, - TestImages.Jpeg.BenchmarkSuite.Lake_Small444YCbCr, - TestImages.Jpeg.BenchmarkSuite.MissingFF00ProgressiveBedroom159_MidSize420YCbCr, - TestImages.Jpeg.BenchmarkSuite.ExifGetString750Transform_Huge420YCbCr, - }; - - [Params(InputImageCategory.AllImages)] - public override InputImageCategory InputCategory { get; set; } - - private readonly Configuration configuration = new Configuration(new JpegConfigurationModule()); - - private byte[] destBytes; - - public override void Setup() - { - base.Setup(); - - this.configuration.MaxDegreeOfParallelism = 1; - const int MaxOutputSizeInBytes = 2 * 1024 * 1024; // ~2 MB - this.destBytes = new byte[MaxOutputSizeInBytes]; - } - - [Benchmark(Baseline = true)] - public void SystemDrawing() - => this.ForEachStream( - sourceStream => - { - using (var destStream = new MemoryStream(this.destBytes)) - using (var source = System.Drawing.Image.FromStream(sourceStream)) - using (var destination = new Bitmap(source.Width / 4, source.Height / 4)) - { - using (var g = Graphics.FromImage(destination)) - { - g.InterpolationMode = InterpolationMode.HighQualityBicubic; - g.PixelOffsetMode = PixelOffsetMode.HighQuality; - g.CompositingQuality = CompositingQuality.HighQuality; - g.DrawImage(source, 0, 0, 400, 400); - } - - destination.Save(destStream, ImageFormat.Jpeg); - } - - return null; - }); - - [Benchmark] - public void ImageSharp() - => this.ForEachStream( - sourceStream => - { - using (var source = Image.Load( - this.configuration, - sourceStream, - new JpegDecoder { IgnoreMetadata = true })) - { - using var destStream = new MemoryStream(this.destBytes); - source.Mutate(c => c.Resize(source.Width / 4, source.Height / 4)); - source.SaveAsJpeg(destStream); - } - - return null; - }); - } -} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/LoadResizeSave_ImageSpecific.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/LoadResizeSave_ImageSpecific.cs deleted file mode 100644 index b0621ca18..000000000 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/LoadResizeSave_ImageSpecific.cs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; -using System.IO; -using BenchmarkDotNet.Attributes; -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Tests; -using SDImage = System.Drawing.Image; - -// ReSharper disable InconsistentNaming -namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg -{ - [Config(typeof(Config.ShortMultiFramework))] - public class LoadResizeSave_ImageSpecific - { - private readonly Configuration configuration = new Configuration(new JpegConfigurationModule()); - - private byte[] sourceBytes; - - private byte[] destBytes; - - private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); - - [Params( - TestImages.Jpeg.BenchmarkSuite.Lake_Small444YCbCr, - TestImages.Jpeg.BenchmarkSuite.BadRstProgressive518_Large444YCbCr, - TestImages.Jpeg.BenchmarkSuite.Jpeg420Exif_MidSizeYCbCr)] - - public string TestImage { get; set; } - - [Params(false, true)] - public bool ParallelExec { get; set; } - - [GlobalSetup] - public void Setup() - { - this.configuration.MaxDegreeOfParallelism = - this.ParallelExec ? Environment.ProcessorCount : 1; - - this.sourceBytes = File.ReadAllBytes(this.TestImageFullPath); - - this.destBytes = new byte[this.sourceBytes.Length * 2]; - } - - [Benchmark(Baseline = true)] - public void SystemDrawing() - { - using var sourceStream = new MemoryStream(this.sourceBytes); - using var destStream = new MemoryStream(this.destBytes); - using var source = SDImage.FromStream(sourceStream); - using var destination = new Bitmap(source.Width / 4, source.Height / 4); - using (var g = Graphics.FromImage(destination)) - { - g.InterpolationMode = InterpolationMode.HighQualityBicubic; - g.PixelOffsetMode = PixelOffsetMode.HighQuality; - g.CompositingQuality = CompositingQuality.HighQuality; - g.DrawImage(source, 0, 0, 400, 400); - } - - destination.Save(destStream, ImageFormat.Jpeg); - } - - [Benchmark] - public void ImageSharp() - { - using (var source = Image.Load(this.configuration, this.sourceBytes, new JpegDecoder { IgnoreMetadata = true })) - using (var destStream = new MemoryStream(this.destBytes)) - { - source.Mutate(c => c.Resize(source.Width / 4, source.Height / 4)); - source.SaveAsJpeg(destStream); - } - } - - // RESULTS (2018 October 31): - // - // BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134 - // Intel Core i7-7700HQ CPU 2.80GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores - // Frequency=2742191 Hz, Resolution=364.6719 ns, Timer=TSC - // .NET Core SDK=2.1.403 - // [Host] : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT - // Job-ZPEZGV : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3190.0 - // Job-SGOCJT : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT - // - // Method | Runtime | TestImage | ParallelExec | Mean | Error | StdDev | Scaled | ScaledSD | Gen 0 | Allocated | - // -------------- |-------- |----------------------------- |------------- |----------:|----------:|----------:|-------:|---------:|---------:|----------:| - // SystemDrawing | Clr | Jpg/baseline/jpeg420exif.jpg | False | 64.88 ms | 3.735 ms | 0.2110 ms | 1.00 | 0.00 | 250.0000 | 791.07 KB | - // ImageSharp | Clr | Jpg/baseline/jpeg420exif.jpg | False | 129.53 ms | 23.423 ms | 1.3234 ms | 2.00 | 0.02 | - | 50.09 KB | - // | | | | | | | | | | | - // SystemDrawing | Core | Jpg/baseline/jpeg420exif.jpg | False | 65.87 ms | 10.488 ms | 0.5926 ms | 1.00 | 0.00 | 250.0000 | 789.79 KB | - // ImageSharp | Core | Jpg/baseline/jpeg420exif.jpg | False | 92.00 ms | 7.241 ms | 0.4091 ms | 1.40 | 0.01 | - | 46.36 KB | - // | | | | | | | | | | | - // SystemDrawing | Clr | Jpg/baseline/jpeg420exif.jpg | True | 64.23 ms | 5.998 ms | 0.3389 ms | 1.00 | 0.00 | 250.0000 | 791.07 KB | - // ImageSharp | Clr | Jpg/baseline/jpeg420exif.jpg | True | 82.63 ms | 29.320 ms | 1.6566 ms | 1.29 | 0.02 | - | 57.59 KB | - // | | | | | | | | | | | - // SystemDrawing | Core | Jpg/baseline/jpeg420exif.jpg | True | 64.20 ms | 6.560 ms | 0.3707 ms | 1.00 | 0.00 | 250.0000 | 789.79 KB | - // ImageSharp | Core | Jpg/baseline/jpeg420exif.jpg | True | 68.08 ms | 18.376 ms | 1.0383 ms | 1.06 | 0.01 | - | 50.49 KB | - } -} diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressBenchmarks.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressBenchmarks.cs index 47f9b36c1..dd9d05cd4 100644 --- a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressBenchmarks.cs +++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressBenchmarks.cs @@ -3,7 +3,6 @@ using System; using BenchmarkDotNet.Attributes; -using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave { @@ -37,10 +36,10 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave public int[] ParallelismValues { get; } = { - Environment.ProcessorCount, + // Environment.ProcessorCount, // Environment.ProcessorCount / 2, // Environment.ProcessorCount / 4, - // 1 + 1 }; [Benchmark] @@ -49,10 +48,7 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave [Benchmark(Baseline = true)] [ArgumentsSource(nameof(ParallelismValues))] - public void ImageSharp(int maxDegreeOfParallelism) - { - this.ForEachImage(this.runner.ImageSharpResize, maxDegreeOfParallelism); - } + public void ImageSharp(int maxDegreeOfParallelism) => this.ForEachImage(this.runner.ImageSharpResize, maxDegreeOfParallelism); [Benchmark] [ArgumentsSource(nameof(ParallelismValues))] @@ -75,3 +71,24 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave public void NetVips(int maxDegreeOfParallelism) => this.ForEachImage(this.runner.NetVipsResize, maxDegreeOfParallelism); } } + +/* +BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19044 +Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores +.NET SDK=6.0.300 + [Host] : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT + ShortRun : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT + +Job=ShortRun IterationCount=3 LaunchCount=1 +WarmupCount=3 + +| Method | maxDegreeOfParallelism | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | +|----------------------------- |----------------------- |-----------:|------------:|----------:|------:|--------:|------:|------:|------:|----------:| +| SystemDrawing | 1 | 3,624.2 ms | 721.39 ms | 39.54 ms | 3.30 | 0.04 | - | - | - | 12 KB | +| ImageSharp | 1 | 1,098.4 ms | 45.64 ms | 2.50 ms | 1.00 | 0.00 | - | - | - | 717 KB | +| Magick | 1 | 4,089.8 ms | 905.06 ms | 49.61 ms | 3.72 | 0.04 | - | - | - | 43 KB | +| MagicScaler | 1 | 888.0 ms | 168.33 ms | 9.23 ms | 0.81 | 0.01 | - | - | - | 105 KB | +| SkiaBitmap | 1 | 2,934.4 ms | 2,023.43 ms | 110.91 ms | 2.67 | 0.10 | - | - | - | 43 KB | +| SkiaBitmapDecodeToTargetSize | 1 | 892.3 ms | 115.54 ms | 6.33 ms | 0.81 | 0.01 | - | - | - | 48 KB | +| NetVips | 1 | 806.8 ms | 86.23 ms | 4.73 ms | 0.73 | 0.01 | - | - | - | 42 KB | +*/ diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs index 37d5f4ede..c26de9159 100644 --- a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs +++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs @@ -204,23 +204,30 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave public void ImageSharpResize(string input) { - using FileStream output = File.Open(this.OutputPath(input), FileMode.Create); + using FileStream inputStream = File.Open(input, FileMode.Open); + using FileStream outputStream = File.Open(this.OutputPath(input), FileMode.Create); // Resize it to fit a 150x150 square - using var image = ImageSharpImage.Load(input); + var targetSize = new ImageSharpSize(this.ThumbnailSize, this.ThumbnailSize); + var decoder = new JpegDecoder(); + using ImageSharpImage image = decoder.DecodeInto(Configuration.Default, inputStream, targetSize, default); this.LogImageProcessed(image.Width, image.Height); image.Mutate(i => i.Resize(new ResizeOptions { - Size = new ImageSharpSize(this.ThumbnailSize, this.ThumbnailSize), - Mode = ResizeMode.Max + Size = targetSize, + Mode = ResizeMode.Max, + Sampler = KnownResamplers.Box })); // Reduce the size of the file image.Metadata.ExifProfile = null; + image.Metadata.XmpProfile = null; + image.Metadata.IccProfile = null; + image.Metadata.IptcProfile = null; // Save the results - image.Save(output, this.imageSharpJpegEncoder); + image.Save(outputStream, this.imageSharpJpegEncoder); } public async Task ImageSharpResizeAsync(string input) @@ -231,6 +238,7 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave using ImageSharpImage image = await ImageSharpImage.LoadAsync(input); this.LogImageProcessed(image.Width, image.Height); + // Resize checks whether image size and target sizes are equal image.Mutate(i => i.Resize(new ResizeOptions { Size = new ImageSharpSize(this.ThumbnailSize, this.ThumbnailSize), diff --git a/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs index 0bf118973..fc5e8f6bd 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs @@ -3,6 +3,7 @@ using System; using SixLabors.ImageSharp.Formats.Jpeg.Components; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils; using SixLabors.ImageSharp.Tests.TestUtilities; using Xunit; @@ -14,8 +15,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [Trait("Format", "Jpg")] public static class DCTTests { - private const int MaxAllowedValue = short.MaxValue; - private const int MinAllowedValue = short.MinValue; + // size of input values is 10 bit max + private const float MaxInputValue = 1023; + private const float MinInputValue = -1024; + + // output value range is 12 bit max + private const float MaxOutputValue = 4096; + private const float NormalizationValue = MaxOutputValue / 2; internal static Block8x8F CreateBlockFromScalar(float value) { @@ -41,7 +47,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [InlineData(3)] public void LLM_TransformIDCT_CompareToNonOptimized(int seed) { - float[] sourceArray = Create8x8RoundedRandomFloatData(MinAllowedValue, MaxAllowedValue, seed); + float[] sourceArray = Create8x8RandomFloatData(MinInputValue, MaxInputValue, seed); var srcBlock = Block8x8F.Load(sourceArray); @@ -56,14 +62,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Block8x8F dequantMatrix = CreateBlockFromScalar(1); // This step is needed to apply adjusting multipliers to the input block - FastFloatingPointDCT.AdjustToIDCT(ref dequantMatrix); + FloatingPointDCT.AdjustToIDCT(ref dequantMatrix); // IDCT implementation tranforms blocks after transposition srcBlock.TransposeInplace(); srcBlock.MultiplyInPlace(ref dequantMatrix); // IDCT calculation - FastFloatingPointDCT.TransformIDCT(ref srcBlock); + FloatingPointDCT.TransformIDCT(ref srcBlock); this.CompareBlocks(expected, srcBlock, 1f); } @@ -74,7 +80,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [InlineData(3)] public void LLM_TransformIDCT_CompareToAccurate(int seed) { - float[] sourceArray = Create8x8RoundedRandomFloatData(MinAllowedValue, MaxAllowedValue, seed); + float[] sourceArray = Create8x8RandomFloatData(MinInputValue, MaxInputValue, seed); var srcBlock = Block8x8F.Load(sourceArray); @@ -89,21 +95,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Block8x8F dequantMatrix = CreateBlockFromScalar(1); // This step is needed to apply adjusting multipliers to the input block - FastFloatingPointDCT.AdjustToIDCT(ref dequantMatrix); + FloatingPointDCT.AdjustToIDCT(ref dequantMatrix); // IDCT implementation tranforms blocks after transposition srcBlock.TransposeInplace(); srcBlock.MultiplyInPlace(ref dequantMatrix); // IDCT calculation - FastFloatingPointDCT.TransformIDCT(ref srcBlock); + FloatingPointDCT.TransformIDCT(ref srcBlock); this.CompareBlocks(expected, srcBlock, 1f); } - // Inverse transform - // This test covers entire IDCT conversion chain - // This test checks all hardware implementations [Theory] [InlineData(1)] [InlineData(2)] @@ -113,7 +116,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg { int seed = FeatureTestRunner.Deserialize(serialized); - Span src = Create8x8RoundedRandomFloatData(MinAllowedValue, MaxAllowedValue, seed); + Span src = Create8x8RandomFloatData(MinInputValue, MaxInputValue, seed); var srcBlock = default(Block8x8F); srcBlock.LoadFrom(src); @@ -132,13 +135,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // Dequantization using unit matrix - no values are upscaled // as quant matrix is all 1's // This step is needed to apply adjusting multipliers to the input block - FastFloatingPointDCT.AdjustToIDCT(ref dequantMatrix); + FloatingPointDCT.AdjustToIDCT(ref dequantMatrix); srcBlock.MultiplyInPlace(ref dequantMatrix); // testee // IDCT implementation tranforms blocks after transposition srcBlock.TransposeInplace(); - FastFloatingPointDCT.TransformIDCT(ref srcBlock); + FloatingPointDCT.TransformIDCT(ref srcBlock); float[] actualDest = srcBlock.ToArray(); @@ -156,9 +159,170 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } - // Forward transform - // This test covers entire FDCT conversion chain - // This test checks all hardware implementations + [Theory] + [InlineData(1)] + [InlineData(2)] + public void TranformIDCT_4x4(int seed) + { + Span src = Create8x8RandomFloatData(MinInputValue, MaxInputValue, seed, 4, 4); + var srcBlock = default(Block8x8F); + srcBlock.LoadFrom(src); + + float[] expectedDest = new float[64]; + float[] temp = new float[64]; + + // reference + ReferenceImplementations.LLM_FloatingPoint_DCT.IDCT2D_llm(src, expectedDest, temp); + + // testee + // Part of the IDCT calculations is fused into the quantization step + // We must multiply input block with adjusted no-quantization matrix + // before applying IDCT + Block8x8F dequantMatrix = CreateBlockFromScalar(1); + + // Dequantization using unit matrix - no values are upscaled + // as quant matrix is all 1's + // This step is needed to apply adjusting multipliers to the input block + ScaledFloatingPointDCT.AdjustToIDCT(ref dequantMatrix); + + // testee + // IDCT implementation tranforms blocks after transposition + srcBlock.TransposeInplace(); + ScaledFloatingPointDCT.TransformIDCT_4x4(ref srcBlock, ref dequantMatrix, NormalizationValue, MaxOutputValue); + + Span expectedSpan = expectedDest.AsSpan(); + Span actualSpan = srcBlock.ToArray().AsSpan(); + + // resulting matrix is 4x4 + for (int y = 0; y < 4; y++) + { + for (int x = 0; x < 4; x++) + { + AssertScaledElementEquality(expectedSpan.Slice((y * 16) + (x * 2)), actualSpan.Slice((y * 8) + x)); + } + } + + static void AssertScaledElementEquality(Span expected, Span actual) + { + float average2x2 = 0f; + for (int y = 0; y < 2; y++) + { + int y8 = y * 8; + for (int x = 0; x < 2; x++) + { + float clamped = Numerics.Clamp(expected[y8 + x] + NormalizationValue, 0, MaxOutputValue); + average2x2 += clamped; + } + } + + average2x2 = MathF.Round(average2x2 / 4f); + + Assert.Equal((int)average2x2, (int)actual[0]); + } + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + public void TranformIDCT_2x2(int seed) + { + Span src = Create8x8RandomFloatData(MinInputValue, MaxInputValue, seed, 2, 2); + var srcBlock = default(Block8x8F); + srcBlock.LoadFrom(src); + + float[] expectedDest = new float[64]; + float[] temp = new float[64]; + + // reference + ReferenceImplementations.LLM_FloatingPoint_DCT.IDCT2D_llm(src, expectedDest, temp); + + // testee + // Part of the IDCT calculations is fused into the quantization step + // We must multiply input block with adjusted no-quantization matrix + // before applying IDCT + Block8x8F dequantMatrix = CreateBlockFromScalar(1); + + // Dequantization using unit matrix - no values are upscaled + // as quant matrix is all 1's + // This step is needed to apply adjusting multipliers to the input block + ScaledFloatingPointDCT.AdjustToIDCT(ref dequantMatrix); + + // testee + // IDCT implementation tranforms blocks after transposition + srcBlock.TransposeInplace(); + ScaledFloatingPointDCT.TransformIDCT_2x2(ref srcBlock, ref dequantMatrix, NormalizationValue, MaxOutputValue); + + Span expectedSpan = expectedDest.AsSpan(); + Span actualSpan = srcBlock.ToArray().AsSpan(); + + // resulting matrix is 2x2 + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 2; x++) + { + AssertScaledElementEquality(expectedSpan.Slice((y * 32) + (x * 4)), actualSpan.Slice((y * 8) + x)); + } + } + + static void AssertScaledElementEquality(Span expected, Span actual) + { + float average4x4 = 0f; + for (int y = 0; y < 4; y++) + { + int y8 = y * 8; + for (int x = 0; x < 4; x++) + { + float clamped = Numerics.Clamp(expected[y8 + x] + NormalizationValue, 0, MaxOutputValue); + average4x4 += clamped; + } + } + + average4x4 = MathF.Round(average4x4 / 16f); + + Assert.Equal((int)average4x4, (int)actual[0]); + } + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + public void TranformIDCT_1x1(int seed) + { + Span src = Create8x8RandomFloatData(MinInputValue, MaxInputValue, seed, 1, 1); + var srcBlock = default(Block8x8F); + srcBlock.LoadFrom(src); + + float[] expectedDest = new float[64]; + float[] temp = new float[64]; + + // reference + ReferenceImplementations.LLM_FloatingPoint_DCT.IDCT2D_llm(src, expectedDest, temp); + + // testee + // Part of the IDCT calculations is fused into the quantization step + // We must multiply input block with adjusted no-quantization matrix + // before applying IDCT + Block8x8F dequantMatrix = CreateBlockFromScalar(1); + + // Dequantization using unit matrix - no values are upscaled + // as quant matrix is all 1's + // This step is needed to apply adjusting multipliers to the input block + ScaledFloatingPointDCT.AdjustToIDCT(ref dequantMatrix); + + // testee + // IDCT implementation tranforms blocks after transposition + // But DC lays on main diagonal which is not changed by transposition + float actual = ScaledFloatingPointDCT.TransformIDCT_1x1( + srcBlock[0], + dequantMatrix[0], + NormalizationValue, + MaxOutputValue); + + float expected = MathF.Round(Numerics.Clamp(expectedDest[0] + NormalizationValue, 0, MaxOutputValue)); + + Assert.Equal((int)actual, (int)expected); + } + [Theory] [InlineData(1)] [InlineData(2)] @@ -168,7 +332,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg { int seed = FeatureTestRunner.Deserialize(serialized); - Span src = Create8x8RoundedRandomFloatData(MinAllowedValue, MaxAllowedValue, seed); + Span src = Create8x8RandomFloatData(MinInputValue, MaxInputValue, seed); var block = default(Block8x8F); block.LoadFrom(src); @@ -181,14 +345,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // testee // Second transpose call is done by Quantize step // Do this manually here just to be complient to the reference implementation - FastFloatingPointDCT.TransformFDCT(ref block); + FloatingPointDCT.TransformFDCT(ref block); block.TransposeInplace(); // Part of the IDCT calculations is fused into the quantization step // We must multiply input block with adjusted no-quantization matrix // after applying FDCT Block8x8F quantMatrix = CreateBlockFromScalar(1); - FastFloatingPointDCT.AdjustToFDCT(ref quantMatrix); + FloatingPointDCT.AdjustToFDCT(ref quantMatrix); block.MultiplyInPlace(ref quantMatrix); float[] actualDest = block.ToArray(); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Internal.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Internal.cs new file mode 100644 index 000000000..6bf7ae88f --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Internal.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils; +using SixLabors.ImageSharp.Tests.TestUtilities; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; +using Xunit; +using Xunit.Abstractions; + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Tests.Formats.Jpg +{ + [Trait("Format", "Jpg")] + public partial class JpegDecoderTests + { + [Theory] + [InlineData(1, 0, JpegColorSpace.Grayscale)] + [InlineData(3, JpegConstants.Adobe.ColorTransformUnknown, JpegColorSpace.RGB)] + [InlineData(3, JpegConstants.Adobe.ColorTransformYCbCr, JpegColorSpace.YCbCr)] + [InlineData(4, JpegConstants.Adobe.ColorTransformUnknown, JpegColorSpace.Cmyk)] + [InlineData(4, JpegConstants.Adobe.ColorTransformYcck, JpegColorSpace.Ycck)] + internal void DeduceJpegColorSpaceAdobeMarker_ShouldReturnValidColorSpace(byte componentCount, byte adobeFlag, JpegColorSpace expectedColorSpace) + { + byte[] adobeMarkerPayload = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, adobeFlag }; + ProfileResolver.AdobeMarker.CopyTo(adobeMarkerPayload); + _ = AdobeMarker.TryParse(adobeMarkerPayload, out AdobeMarker adobeMarker); + + JpegColorSpace actualColorSpace = JpegDecoderCore.DeduceJpegColorSpace(componentCount, ref adobeMarker); + + Assert.Equal(expectedColorSpace, actualColorSpace); + } + + [Theory] + [InlineData(2)] + [InlineData(5)] + public void DeduceJpegColorSpaceAdobeMarker_ShouldThrowOnUnsupportedComponentCount(byte componentCount) + { + AdobeMarker adobeMarker = default; + + Assert.Throws(() => JpegDecoderCore.DeduceJpegColorSpace(componentCount, ref adobeMarker)); + } + + [Theory] + [InlineData(1, JpegColorSpace.Grayscale)] + [InlineData(3, JpegColorSpace.YCbCr)] + [InlineData(4, JpegColorSpace.Cmyk)] + internal void DeduceJpegColorSpace_ShouldReturnValidColorSpace(byte componentCount, JpegColorSpace expectedColorSpace) + { + JpegColorSpace actualColorSpace = JpegDecoderCore.DeduceJpegColorSpace(componentCount); + + Assert.Equal(expectedColorSpace, actualColorSpace); + } + + [Theory] + [InlineData(2)] + [InlineData(5)] + public void DeduceJpegColorSpace_ShouldThrowOnUnsupportedComponentCount(byte componentCount) + => Assert.Throws(() => JpegDecoderCore.DeduceJpegColorSpace(componentCount)); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementationsTests.FastFloatingPointDCT.cs b/tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementationsTests.FastFloatingPointDCT.cs index 7bcf8ab19..50c2a08bb 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementationsTests.FastFloatingPointDCT.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/ReferenceImplementationsTests.FastFloatingPointDCT.cs @@ -53,7 +53,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [InlineData(2, 200)] public void LLM_IDCT_IsEquivalentTo_AccurateImplementation(int seed, int range) { - float[] sourceArray = Create8x8RoundedRandomFloatData(-range, range, seed); + float[] sourceArray = Create8x8RandomFloatData(-range, range, seed); var source = Block8x8F.Load(sourceArray); @@ -64,23 +64,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg this.CompareBlocks(expected, actual, 0.1f); } - [Theory] - [InlineData(42, 1000)] - [InlineData(1, 1000)] - [InlineData(2, 1000)] - public void LLM_IDCT_CompareToIntegerRoundedAccurateImplementation(int seed, int range) - { - Block8x8F fSource = CreateRoundedRandomFloatBlock(-range, range, seed); - Block8x8 iSource = fSource.RoundAsInt16Block(); - - Block8x8 iExpected = ReferenceImplementations.AccurateDCT.TransformIDCT(ref iSource); - Block8x8F fExpected = iExpected.AsFloatBlock(); - - Block8x8F fActual = ReferenceImplementations.LLM_FloatingPoint_DCT.TransformIDCT(ref fSource); - - this.CompareBlocks(fExpected, fActual, 2); - } - [Theory] [InlineData(42)] [InlineData(1)] diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralConverterTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralConverterTests.cs new file mode 100644 index 000000000..008ca20c3 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralConverterTests.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Jpg +{ + [Trait("Format", "Jpg")] + public class SpectralConverterTests + { + // Test for null target size, i.e. when no scaling is needed + [Theory] + [InlineData(1, 1)] + [InlineData(800, 400)] + [InlineData(2354, 4847)] + public void CalculateResultingImageSize_Null_TargetSize(int width, int height) + { + Size inputSize = new(width, height); + + Size outputSize = SpectralConverter.CalculateResultingImageSize(inputSize, null, out int blockPixelSize); + + Assert.Equal(expected: 8, blockPixelSize); + Assert.Equal(inputSize, outputSize); + } + + // Test for 'perfect' dimensions, i.e. dimensions divisible by 8, with exact scaled size match + [Theory] + [InlineData(800, 400, 800, 400, 8)] + [InlineData(800, 400, 400, 200, 4)] + [InlineData(800, 400, 200, 100, 2)] + [InlineData(800, 400, 100, 50, 1)] + public void CalculateResultingImageSize_Perfect_Dimensions_Exact_Match(int inW, int inH, int tW, int tH, int expectedBlockSize) + { + Size inputSize = new(inW, inH); + Size targetSize = new(tW, tH); + + Size outputSize = SpectralConverter.CalculateResultingImageSize(inputSize, targetSize, out int blockPixelSize); + + Assert.Equal(expectedBlockSize, blockPixelSize); + Assert.Equal(outputSize, targetSize); + } + + // Test for 'imperfect' dimensions, i.e. dimensions NOT divisible by 8, with exact scaled size match + [Theory] + [InlineData(7, 14, 7, 14, 8)] + [InlineData(7, 14, 4, 7, 4)] + [InlineData(7, 14, 2, 4, 2)] + [InlineData(7, 14, 1, 2, 1)] + public void CalculateResultingImageSize_Imperfect_Dimensions_Exact_Match(int inW, int inH, int tW, int tH, int expectedBlockSize) + { + Size inputSize = new(inW, inH); + Size targetSize = new(tW, tH); + + Size outputSize = SpectralConverter.CalculateResultingImageSize(inputSize, targetSize, out int blockPixelSize); + + Assert.Equal(expectedBlockSize, blockPixelSize); + Assert.Equal(outputSize, targetSize); + } + + // Test for inexact target and output sizes match + [Theory] + [InlineData(7, 14, 4, 6, 4, 7, 4)] + [InlineData(7, 14, 1, 1, 1, 2, 1)] + [InlineData(800, 400, 999, 600, 800, 400, 8)] + [InlineData(800, 400, 390, 150, 400, 200, 4)] + [InlineData(804, 1198, 500, 800, 804, 1198, 8)] + public void CalculateResultingImageSize_Inexact_Target_Size(int inW, int inH, int tW, int tH, int exW, int exH, int expectedBlockSize) + { + Size inputSize = new(inW, inH); + Size targetSize = new(tW, tH); + Size expectedSize = new(exW, exH); + + Size outputSize = SpectralConverter.CalculateResultingImageSize(inputSize, targetSize, out int blockPixelSize); + + Assert.Equal(expectedBlockSize, blockPixelSize); + Assert.Equal(expectedSize, outputSize); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs index 17efbe0b6..9824315ff 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs @@ -141,6 +141,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg { private JpegFrame frame; + private IRawJpegData jpegData; + private LibJpegTools.SpectralData spectralData; private int baselineScanRowCounter; @@ -153,6 +155,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // Progressive and multi-scan images must be loaded manually if (this.frame.Progressive || this.frame.MultiScan) { + this.PrepareForDecoding(); LibJpegTools.ComponentData[] components = this.spectralData.Components; for (int i = 0; i < components.Length; i++) { @@ -190,11 +193,15 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) { this.frame = frame; + this.jpegData = jpegData; + } - var spectralComponents = new LibJpegTools.ComponentData[frame.ComponentCount]; + public override void PrepareForDecoding() + { + var spectralComponents = new LibJpegTools.ComponentData[this.frame.ComponentCount]; for (int i = 0; i < spectralComponents.Length; i++) { - var component = frame.Components[i] as JpegComponent; + JpegComponent component = this.frame.Components[i]; spectralComponents[i] = new LibJpegTools.ComponentData(component.WidthInBlocks, component.HeightInBlocks, component.Index); } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs b/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs index 121e1ed2f..ef74549d0 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Utils/JpegFixture.cs @@ -89,33 +89,28 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils return result; } - internal static float[] Create8x8RoundedRandomFloatData(int minValue, int maxValue, int seed = 42) - => Create8x8RandomIntData(minValue, maxValue, seed).ConvertAllToFloat(); - - public static float[] Create8x8RandomFloatData(float minValue, float maxValue, int seed = 42) + public static float[] Create8x8RandomFloatData(float minValue, float maxValue, int seed = 42, int xBorder = 8, int yBorder = 8) { var rnd = new Random(seed); - var result = new float[64]; - for (int i = 0; i < 8; i++) + float[] result = new float[64]; + for (int y = 0; y < yBorder; y++) { - for (int j = 0; j < 8; j++) + int y8 = y * 8; + for (int x = 0; x < xBorder; x++) { double val = rnd.NextDouble(); val *= maxValue - minValue; val += minValue; - result[(i * 8) + j] = (float)val; + result[y8 + x] = (float)val; } } return result; } - internal static Block8x8F CreateRandomFloatBlock(float minValue, float maxValue, int seed = 42) => - Block8x8F.Load(Create8x8RandomFloatData(minValue, maxValue, seed)); - - internal static Block8x8F CreateRoundedRandomFloatBlock(int minValue, int maxValue, int seed = 42) => - Block8x8F.Load(Create8x8RoundedRandomFloatData(minValue, maxValue, seed)); + internal static Block8x8F CreateRandomFloatBlock(float minValue, float maxValue, int seed = 42, int xBorder = 8, int yBorder = 8) => + Block8x8F.Load(Create8x8RandomFloatData(minValue, maxValue, seed, xBorder, yBorder)); internal void Print8x8Data(T[] data) => this.Print8x8Data(new Span(data)); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 12e0b819d..9ad0ed06f 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -318,6 +318,20 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff image.CompareToReferenceOutput(ImageComparer.Exact, provider); } + [Theory] + [WithFile(CieLab, PixelTypes.Rgba32)] + [WithFile(CieLabPlanar, PixelTypes.Rgba32)] + [WithFile(CieLabLzwPredictor, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_CieLab(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // Note: The image from MagickReferenceDecoder does not look right, maybe we are doing something wrong + // converting the pixel data from Magick.NET to our format with CieLab? + using Image image = provider.GetImage(); + image.DebugSave(provider); + image.CompareToReferenceOutput(ImageComparer.Exact, provider); + } + [Theory] [WithFile(FlowerRgb101010Contiguous, PixelTypes.Rgba32)] [WithFile(FlowerRgb101010Planar, PixelTypes.Rgba32)] @@ -653,6 +667,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffDecoder_CanDecode_JpegCompressed(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider, useExactComparer: false); + [Theory] + [WithFile(WebpCompressed, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_WebpCompressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + if (TestEnvironment.IsWindows) + { + TestTiffDecoder(provider, useExactComparer: false); + } + } + // https://github.com/SixLabors/ImageSharp/issues/1891 [Theory] [WithFile(Issues1891, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 616eb3ecd..3efb528a8 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -763,8 +763,8 @@ namespace SixLabors.ImageSharp.Tests public const string Fax4Compressed = "Tiff/basi3p02_fax4.tiff"; public const string Fax4Compressed2 = "Tiff/CCITTGroup4.tiff"; public const string Fax4CompressedLowerOrderBitsFirst = "Tiff/basi3p02_fax4_lowerOrderBitsFirst.tiff"; + public const string WebpCompressed = "Tiff/webp_compressed.tiff"; public const string Fax4CompressedMinIsBlack = "Tiff/CCITTGroup4_minisblack.tiff"; - public const string CcittFax3AllTermCodes = "Tiff/ccitt_fax3_all_terminating_codes.tiff"; public const string CcittFax3AllMakeupCodes = "Tiff/ccitt_fax3_all_makeup_codes.tiff"; public const string HuffmanRleAllTermCodes = "Tiff/huffman_rle_all_terminating_codes.tiff"; @@ -914,6 +914,11 @@ namespace SixLabors.ImageSharp.Tests public const string Rgba32BitPlanarUnassociatedAlphaBigEndian = "Tiff/RgbaUnassociatedAlphaPlanar32bit_msb.tiff"; public const string Rgba32BitPlanarUnassociatedAlphaLittleEndian = "Tiff/RgbaUnassociatedAlphaPlanar32bit_lsb.tiff"; + // Cie Lab color space. + public const string CieLab = "Tiff/CieLab.tiff"; + public const string CieLabPlanar = "Tiff/CieLabPlanar.tiff"; + public const string CieLabLzwPredictor = "Tiff/CieLab_lzwcompressed_predictor.tiff"; + public const string Issues1716Rgb161616BitLittleEndian = "Tiff/Issues/Issue1716.tiff"; public const string Issues1891 = "Tiff/Issues/Issue1891.tiff"; public const string Issues2123 = "Tiff/Issues/Issue2123.tiff"; diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLab.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLab.png new file mode 100644 index 000000000..bdd240663 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLab.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13bc9da102f85124855217fad757ca907f5d68442e54e3b7039ac048d7b2ad3f +size 25791 diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLabPlanar.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLabPlanar.png new file mode 100644 index 000000000..bdd240663 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLabPlanar.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13bc9da102f85124855217fad757ca907f5d68442e54e3b7039ac048d7b2ad3f +size 25791 diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLab_lzwcompressed_predictor.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLab_lzwcompressed_predictor.png new file mode 100644 index 000000000..1938ebe11 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_CieLab_Rgba32_CieLab_lzwcompressed_predictor.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6f9481c91c58ca7bbab9de4b9ae95fe4a2197ae4b6ef6b15b72d4858aba3a1a4 +size 25782 diff --git a/tests/Images/Input/Tiff/CieLab.tiff b/tests/Images/Input/Tiff/CieLab.tiff new file mode 100644 index 000000000..59a667dd5 --- /dev/null +++ b/tests/Images/Input/Tiff/CieLab.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7542b5b3abe049614f2ddaf78ffe995edac13e768f0b2fc9f324c6ef43b379eb +size 1312046 diff --git a/tests/Images/Input/Tiff/CieLabPlanar.tiff b/tests/Images/Input/Tiff/CieLabPlanar.tiff new file mode 100644 index 000000000..d964a9694 --- /dev/null +++ b/tests/Images/Input/Tiff/CieLabPlanar.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28592d9da8d51f60700b7136369d2d6bd40550d5f8c7758e570b5e624c71a3e4 +size 1307488 diff --git a/tests/Images/Input/Tiff/CieLab_lzwcompressed_predictor.tiff b/tests/Images/Input/Tiff/CieLab_lzwcompressed_predictor.tiff new file mode 100644 index 000000000..2284e1e17 --- /dev/null +++ b/tests/Images/Input/Tiff/CieLab_lzwcompressed_predictor.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6affced5550e51441c4cde7f1770d4e57cfa594bd271a12f9571359733c2185d +size 55346 diff --git a/tests/Images/Input/Tiff/webp_compressed.tiff b/tests/Images/Input/Tiff/webp_compressed.tiff new file mode 100644 index 000000000..71248e521 --- /dev/null +++ b/tests/Images/Input/Tiff/webp_compressed.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72fd7fa941aa6201faa5368349764b4c17b582bee9be65861bad6308a8c5e4fe +size 4898