From 96a3c1903f38b872e0b9a8d71290e9e859f02e1d Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Fri, 6 May 2022 22:30:50 +0300 Subject: [PATCH] Setup --- .../Components/Decoder/SpectralConverter.cs | 2 +- .../Components/Encoder/HuffmanScanEncoder.cs | 116 ++++++++++++++++ .../Jpeg/Components/Encoder/JpegComponent.cs | 117 ++++++++++++++++ .../Encoder/JpegComponentPostProcessor.cs | 25 ++++ .../Jpeg/Components/Encoder/JpegFrame.cs | 97 +++++++++++++ .../Components/Encoder/SpectralConverter.cs | 12 ++ .../Encoder/SpectralConverter{TPixel}.cs | 128 ++++++++++++++++++ .../Formats/Jpeg/JpegEncoderCore.cs | 39 +++--- 8 files changed, 517 insertions(+), 19 deletions(-) create mode 100644 src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegComponent.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegComponentPostProcessor.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter.cs create mode 100644 src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs index acd98bcfcd..9901639a79 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs @@ -6,7 +6,7 @@ using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters; namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { /// - /// Converter used to convert jpeg spectral data to color pixels. + /// Converter used to convert jpeg spectral data to pixels. /// internal abstract class SpectralConverter { diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs index 6acc6b6db0..7c683af2b3 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs @@ -128,6 +128,72 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder get => this.emitWriteIndex < (uint)this.emitBuffer.Length / 2; } + public void Encode(Image image, Block8x8F[] quantTables, Configuration configuration, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + // DEBUG INITIALIZATION SETUP + var frame = new JpegFrame(configuration.MemoryAllocator, image, componentCount: 3); + frame.Init(1, 1); + frame.AllocateComponents(fullScan: false); + + var spectralConverter = new SpectralConverter(configuration); + spectralConverter.InjectFrameData(frame, image, quantTables); + + // DEBUG ENCODING SETUP + int mcu = 0; + int mcusPerColumn = frame.McusPerColumn; + int mcusPerLine = frame.McusPerLine; + + for (int j = 0; j < mcusPerColumn; j++) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Convert from pixels to spectral via given converter + spectralConverter.ConvertStrideBaseline(); + + // decode from binary to spectral + for (int i = 0; i < mcusPerLine; i++) + { + // Scan an interleaved mcu... process components in order + int mcuCol = mcu % mcusPerLine; + for (int k = 0; k < frame.ComponentCount; k++) + { + JpegComponent component = frame.Components[k]; + + ref HuffmanLut dcHuffmanTable = ref this.huffmanTables[component.DcTableId]; + ref HuffmanLut acHuffmanTable = ref this.huffmanTables[component.AcTableId]; + + int h = component.HorizontalSamplingFactor; + int v = component.VerticalSamplingFactor; + + // Scan out an mcu's worth of this component; that's just determined + // by the basic H and V specified for the component + for (int y = 0; y < v; y++) + { + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (int x = 0; x < h; x++) + { + int blockCol = (mcuCol * h) + x; + + this.WriteBlock( + component, + ref Unsafe.Add(ref blockRef, blockCol), + ref dcHuffmanTable, + ref acHuffmanTable); + } + } + } + + // After all interleaved components, that's an interleaved MCU + mcu++; + } + } + + this.FlushRemainingBytes(); + } + /// /// Encodes the image with no subsampling. /// @@ -441,6 +507,56 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder return dc; } + private void WriteBlock( + JpegComponent component, + ref Block8x8 block, + ref HuffmanLut dcTable, + ref HuffmanLut acTable) + { + // Emit the DC delta. + int dc = block[0]; + this.EmitHuffRLE(dcTable.Values, 0, dc - component.DcPredictor); + component.DcPredictor = dc; + + // Emit the AC components. + int[] acHuffTable = acTable.Values; + + nint lastValuableIndex = block.GetLastNonZeroIndex(); + + int runLength = 0; + ref short blockRef = ref Unsafe.As(ref block); + for (nint zig = 1; zig <= lastValuableIndex; zig++) + { + const int zeroRun1 = 1 << 4; + const int zeroRun16 = 16 << 4; + + int ac = Unsafe.Add(ref blockRef, zig); + if (ac == 0) + { + runLength += zeroRun1; + } + else + { + while (runLength >= zeroRun16) + { + this.EmitHuff(acHuffTable, 0xf0); + runLength -= zeroRun16; + } + + this.EmitHuffRLE(acHuffTable, runLength, ac); + runLength = 0; + } + } + + // if mcu block contains trailing zeros - we must write end of block (EOB) value indicating that current block is over + // this can be done for any number of trailing zeros, even when all 63 ac values are zero + // (Block8x8F.Size - 1) == 63 - last index of the mcu elements + if (lastValuableIndex != Block8x8F.Size - 1) + { + this.EmitHuff(acHuffTable, 0x00); + } + } + /// /// Emits the most significant count of bits to the buffer. /// diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegComponent.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegComponent.cs new file mode 100644 index 0000000000..3293f55d1e --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegComponent.cs @@ -0,0 +1,117 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder +{ + /// + /// Represents a single frame component. + /// + internal class JpegComponent : IDisposable + { + private readonly MemoryAllocator memoryAllocator; + + public JpegComponent(MemoryAllocator memoryAllocator, int horizontalFactor, int verticalFactor, byte quantizationTableIndex) + { + this.memoryAllocator = memoryAllocator; + + this.HorizontalSamplingFactor = horizontalFactor; + this.VerticalSamplingFactor = verticalFactor; + this.SamplingFactors = new Size(this.HorizontalSamplingFactor, this.VerticalSamplingFactor); + + this.QuantizationTableIndex = quantizationTableIndex; + } + + /// + /// Gets or sets DC coefficient predictor. + /// + public int DcPredictor { get; set; } + + /// + /// Gets the horizontal sampling factor. + /// + public int HorizontalSamplingFactor { get; } + + /// + /// Gets the vertical sampling factor. + /// + public int VerticalSamplingFactor { get; } + + /// + public Buffer2D SpectralBlocks { get; private set; } + + /// + public Size SubSamplingDivisors { get; private set; } + + /// + public int QuantizationTableIndex { get; } + + /// + public Size SizeInBlocks { get; private set; } + + /// + public Size SamplingFactors { get; set; } + + /// + /// Gets the number of blocks per line. + /// + public int WidthInBlocks { get; private set; } + + /// + /// Gets the number of blocks per column. + /// + public int HeightInBlocks { get; private set; } + + /// + /// Gets or sets the index for the DC Huffman table. + /// + public int DcTableId { get; set; } + + /// + /// Gets or sets the index for the AC Huffman table. + /// + public int AcTableId { get; set; } + + /// + public void Dispose() + { + this.SpectralBlocks?.Dispose(); + this.SpectralBlocks = null; + } + + /// + /// Initializes component for future buffers initialization. + /// + /// Maximal horizontal subsampling factor among all the components. + /// Maximal vertical subsampling factor among all the components. + public void Init(JpegFrame frame, int maxSubFactorH, int maxSubFactorV) + { + this.WidthInBlocks = (int)MathF.Ceiling( + MathF.Ceiling(frame.PixelWidth / 8F) * this.HorizontalSamplingFactor / maxSubFactorH); + + this.HeightInBlocks = (int)MathF.Ceiling( + MathF.Ceiling(frame.PixelHeight / 8F) * this.VerticalSamplingFactor / maxSubFactorV); + + int blocksPerLineForMcu = frame.McusPerLine * this.HorizontalSamplingFactor; + int blocksPerColumnForMcu = frame.McusPerColumn * this.VerticalSamplingFactor; + this.SizeInBlocks = new Size(blocksPerLineForMcu, blocksPerColumnForMcu); + + this.SubSamplingDivisors = new Size(maxSubFactorH, maxSubFactorV).DivideBy(this.SamplingFactors); + + if (this.SubSamplingDivisors.Width == 0 || this.SubSamplingDivisors.Height == 0) + { + JpegThrowHelper.ThrowBadSampling(); + } + } + + public void AllocateSpectral(bool fullScan) + { + int spectralAllocWidth = this.SizeInBlocks.Width; + int spectralAllocHeight = fullScan ? this.SizeInBlocks.Height : this.VerticalSamplingFactor; + + this.SpectralBlocks = this.memoryAllocator.Allocate2D(spectralAllocWidth, spectralAllocHeight, AllocationOptions.Clean); + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegComponentPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegComponentPostProcessor.cs new file mode 100644 index 0000000000..dd9d485a84 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegComponentPostProcessor.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder +{ + internal class JpegComponentPostProcessor : IDisposable + { + private readonly JpegComponent component; + + private readonly Block8x8F dequantTable; + + public JpegComponentPostProcessor(JpegComponent component, Block8x8F dequantTable) + { + this.component = component; + this.dequantTable = dequantTable; + } + + public void Dispose() + { + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs new file mode 100644 index 0000000000..585e3bafaa --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs @@ -0,0 +1,97 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder +{ + /// + /// Represent a single jpeg frame. + /// + internal sealed class JpegFrame : IDisposable + { + public JpegFrame(MemoryAllocator allocator, Image image, byte componentCount) + { + this.PixelWidth = image.Width; + this.PixelHeight = image.Height; + + if (componentCount != 3) + { + throw new ArgumentException("This is YCbCr debug path only."); + } + + this.Components = new JpegComponent[] + { + new JpegComponent(allocator, 1, 1, 0), + new JpegComponent(allocator, 1, 1, 1), + new JpegComponent(allocator, 1, 1, 1), + }; + } + + /// + /// Gets the number of pixel per row. + /// + public int PixelHeight { get; private set; } + + /// + /// Gets the number of pixels per line. + /// + public int PixelWidth { get; private set; } + + /// + /// Gets the number of components within a frame. + /// + public int ComponentCount => this.Components.Length; + + /// + /// Gets the frame component collection. + /// + public JpegComponent[] Components { get; } + + /// + /// Gets or sets the number of MCU's per line. + /// + public int McusPerLine { get; set; } + + /// + /// Gets or sets the number of MCU's per column. + /// + public int McusPerColumn { get; set; } + + /// + public void Dispose() + { + for (int i = 0; i < this.Components.Length; i++) + { + this.Components[i]?.Dispose(); + } + } + + /// + /// Allocates the frame component blocks. + /// + /// Maximal horizontal subsampling factor among all the components. + /// Maximal vertical subsampling factor among all the components. + public void Init(int maxSubFactorH, int maxSubFactorV) + { + this.McusPerLine = (int)Numerics.DivideCeil((uint)this.PixelWidth, (uint)maxSubFactorH * 8); + this.McusPerColumn = (int)Numerics.DivideCeil((uint)this.PixelHeight, (uint)maxSubFactorV * 8); + + for (int i = 0; i < this.ComponentCount; i++) + { + JpegComponent component = this.Components[i]; + component.Init(this, maxSubFactorH, maxSubFactorV); + } + } + + public void AllocateComponents(bool fullScan) + { + for (int i = 0; i < this.ComponentCount; i++) + { + JpegComponent component = this.Components[i]; + component.AllocateSpectral(fullScan); + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter.cs new file mode 100644 index 0000000000..b85cf1d371 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder +{ + /// + /// Converter used to convert pixel data to jpeg spectral data. + /// + internal abstract class SpectralConverter + { + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs new file mode 100644 index 0000000000..1065e69c56 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs @@ -0,0 +1,128 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Linq; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder +{ + + /// + internal class SpectralConverter : SpectralConverter + where TPixel : unmanaged, IPixel + { + private readonly Configuration configuration; + + private JpegComponentPostProcessor[] componentProcessors; + + private int pixelRowsPerStep; + + private int pixelRowCounter; + + private IMemoryOwner rgbBuffer; + + private IMemoryOwner paddedProxyPixelRow; + + private Buffer2D pixelBuffer; + + public SpectralConverter(Configuration configuration) => + this.configuration = configuration; + + public void InjectFrameData(JpegFrame frame, Image image, Block8x8F[] dequantTables) + { + 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 of the image + // currently codec only supports encoding single frame jpegs + this.pixelBuffer = image.GetRootFramePixelBuffer(); + + // ??? + //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++) + { + JpegComponent component = frame.Components[i]; + this.componentProcessors[i] = new JpegComponentPostProcessor(component, dequantTables[component.QuantizationTableIndex]); + } + + // 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 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); + } + + private void ConvertStride(int spectralStep) + { + // 1. Unpack from TPixel to r/g/b planes + // 2. Byte r/g/b planes to normalized float r/g/b planes + // 3. Convert from r/g/b planes to target pixel type with JpegColorConverter + // 4. Convert color buffer to spectral blocks with component post processors + + int maxY = Math.Min(this.pixelBuffer.Height, this.pixelRowCounter + this.pixelRowsPerStep); + + //for (int i = 0; i < this.componentProcessors.Length; i++) + //{ + // this.componentProcessors[i].CopyBlocksToColorBuffer(spectralStep); + //} + + //int width = this.pixelBuffer.Width; + + //for (int yy = this.pixelRowCounter; yy < maxY; yy++) + //{ + // int y = yy - this.pixelRowCounter; + + // var values = new JpegColorConverterBase.ComponentValues(this.componentProcessors, y); + + // this.colorConverter.ConvertToRgbInplace(values); + // values = values.Slice(0, width); // slice away Jpeg padding + + // Span r = this.rgbBuffer.Slice(0, width); + // Span g = this.rgbBuffer.Slice(width, width); + // Span b = this.rgbBuffer.Slice(width * 2, width); + + // SimdUtils.NormalizedFloatToByteSaturate(values.Component0, r); + // SimdUtils.NormalizedFloatToByteSaturate(values.Component1, g); + // SimdUtils.NormalizedFloatToByteSaturate(values.Component2, b); + + // // PackFromRgbPlanes expects the destination to be padded, so try to get padded span containing extra elements from the next row. + // // If we can't get such a padded row because we are on a MemoryGroup boundary or at the last row, + // // pack pixels to a temporary, padded proxy buffer, then copy the relevant values to the destination row. + // if (this.pixelBuffer.DangerousTryGetPaddedRowSpan(yy, 3, out Span destRow)) + // { + // PixelOperations.Instance.PackFromRgbPlanes(this.configuration, r, g, b, destRow); + // } + // else + // { + // Span proxyRow = this.paddedProxyPixelRow.GetSpan(); + // PixelOperations.Instance.PackFromRgbPlanes(this.configuration, r, g, b, proxyRow); + // proxyRow.Slice(0, width).CopyTo(this.pixelBuffer.DangerousGetRowSpan(yy)); + // } + //} + + this.pixelRowCounter += this.pixelRowsPerStep; + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index a3cff8f31d..c20babd3d5 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -131,25 +131,28 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // Write the scan header. this.WriteStartOfScan(componentCount, componentIds); + var quantTables = new Block8x8F[] { luminanceQuantTable, chrominanceQuantTable }; + new HuffmanScanEncoder(3, stream).Encode(image, quantTables, Configuration.Default, cancellationToken); + // Write the scan compressed data. - switch (this.colorType) - { - case JpegColorType.YCbCrRatio444: - new HuffmanScanEncoder(3, stream).Encode444(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken); - break; - case JpegColorType.YCbCrRatio420: - new HuffmanScanEncoder(6, stream).Encode420(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken); - break; - case JpegColorType.Luminance: - new HuffmanScanEncoder(1, stream).EncodeGrayscale(image, ref luminanceQuantTable, cancellationToken); - break; - case JpegColorType.Rgb: - new HuffmanScanEncoder(3, stream).EncodeRgb(image, ref luminanceQuantTable, cancellationToken); - break; - default: - // all other non-supported color types are checked at the start of this method - break; - } + //switch (this.colorType) + //{ + // case JpegColorType.YCbCrRatio444: + // new HuffmanScanEncoder(3, stream).Encode444(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken); + // break; + // case JpegColorType.YCbCrRatio420: + // new HuffmanScanEncoder(6, stream).Encode420(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken); + // break; + // case JpegColorType.Luminance: + // new HuffmanScanEncoder(1, stream).EncodeGrayscale(image, ref luminanceQuantTable, cancellationToken); + // break; + // case JpegColorType.Rgb: + // new HuffmanScanEncoder(3, stream).EncodeRgb(image, ref luminanceQuantTable, cancellationToken); + // break; + // default: + // // all other non-supported color types are checked at the start of this method + // break; + //} // Write the End Of Image marker. this.WriteEndOfImageMarker();