From f0017556cf06ee0d881b723f1fd6277b858732e4 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Thu, 20 May 2021 16:46:55 +0300 Subject: [PATCH] [WIP] Partially moved encoding logic to a separate class --- .../Encoder/YCbCrEncoder{TPixel}.cs | 532 ++++++++++++++++++ .../Formats/Jpeg/JpegEncoderCore.cs | 28 +- 2 files changed, 539 insertions(+), 21 deletions(-) create mode 100644 src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrEncoder{TPixel}.cs diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrEncoder{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrEncoder{TPixel}.cs new file mode 100644 index 0000000000..2ef053eb1c --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrEncoder{TPixel}.cs @@ -0,0 +1,532 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder +{ + internal class YCbCrEncoder + { + /// + /// A buffer for reducing the number of stream writes when emitting Huffman tables. 64 seems to be enough. + /// + private byte[] emitBuffer = new byte[64]; + + /// + /// The accumulated bits to write to the stream. + /// + private uint accumulatedBits; + + /// + /// The accumulated bit count. + /// + private uint bitCount; + + /// + /// The scaled chrominance table, in zig-zag order. + /// + private Block8x8F chrominanceQuantTable; + + /// + /// The scaled luminance table, in zig-zag order. + /// + private Block8x8F luminanceQuantTable; + + /// + /// The output stream. All attempted writes after the first error become no-ops. + /// + private Stream outputStream; + + /// + /// Gets the counts the number of bits needed to hold an integer. + /// + // The C# compiler emits this as a compile-time constant embedded in the PE file. + // This is effectively compiled down to: return new ReadOnlySpan(&data, length) + // More details can be found: https://github.com/dotnet/roslyn/pull/24621 + private static ReadOnlySpan BitCountLut => new byte[] + { + 0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, + }; + + /// + /// Gets the unscaled quantization tables in zig-zag order. Each + /// encoder copies and scales the tables according to its quality parameter. + /// The values are derived from section K.1 after converting from natural to + /// zig-zag order. + /// + // The C# compiler emits this as a compile-time constant embedded in the PE file. + // This is effectively compiled down to: return new ReadOnlySpan(&data, length) + // More details can be found: https://github.com/dotnet/roslyn/pull/24621 + private static ReadOnlySpan UnscaledQuant_Luminance => new byte[] + { + // Luminance. + 16, 11, 12, 14, 12, 10, 16, 14, 13, 14, 18, 17, 16, 19, 24, + 40, 26, 24, 22, 22, 24, 49, 35, 37, 29, 40, 58, 51, 61, 60, + 57, 51, 56, 55, 64, 72, 92, 78, 64, 68, 87, 69, 55, 56, 80, + 109, 81, 87, 95, 98, 103, 104, 103, 62, 77, 113, 121, 112, + 100, 120, 92, 101, 103, 99, + }; + + /// + /// Gets the unscaled quantization tables in zig-zag order. Each + /// encoder copies and scales the tables according to its quality parameter. + /// The values are derived from section K.1 after converting from natural to + /// zig-zag order. + /// + // The C# compiler emits this as a compile-time constant embedded in the PE file. + // This is effectively compiled down to: return new ReadOnlySpan(&data, length) + // More details can be found: https://github.com/dotnet/roslyn/pull/24621 + private static ReadOnlySpan UnscaledQuant_Chrominance => new byte[] + { + // Chrominance. + 17, 18, 18, 24, 21, 24, 47, 26, 26, 47, 99, 66, 56, 66, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + }; + + + public ref Block8x8F ChrominanceQuantizationTable => ref this.chrominanceQuantTable; + + public ref Block8x8F LuminanceQuantizationTable => ref this.luminanceQuantTable; + + + public YCbCrEncoder(Stream outputStream, int componentCount, int quality) + { + this.outputStream = outputStream; + + // Convert from a quality rating to a scaling factor. + int scale; + if (quality < 50) + { + scale = 5000 / quality; + } + else + { + scale = 200 - (quality * 2); + } + + // Initialize the quantization tables. + InitQuantizationTable(0, scale, ref this.luminanceQuantTable); + if (componentCount > 1) + { + InitQuantizationTable(1, scale, ref this.chrominanceQuantTable); + } + } + + /// + /// Encodes the image with no subsampling. + /// + /// The pixel format. + /// The pixel accessor providing access to the image pixels. + /// The token to monitor for cancellation. + /// The reference to the emit buffer. + public void Encode444(Image pixels, CancellationToken cancellationToken, ref byte emitBufferBase) + where TPixel : unmanaged, IPixel + { + // TODO: Need a JpegScanEncoder class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.) + // (Partially done with YCbCrForwardConverter) + Block8x8F temp1 = default; + Block8x8F temp2 = default; + + Block8x8F onStackLuminanceQuantTable = this.luminanceQuantTable; + Block8x8F onStackChrominanceQuantTable = this.chrominanceQuantTable; + + var unzig = ZigZag.CreateUnzigTable(); + + // ReSharper disable once InconsistentNaming + int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; + + var pixelConverter = YCbCrForwardConverter.Create(); + ImageFrame frame = pixels.Frames.RootFrame; + Buffer2D pixelBuffer = frame.PixelBuffer; + RowOctet currentRows = default; + + for (int y = 0; y < pixels.Height; y += 8) + { + cancellationToken.ThrowIfCancellationRequested(); + currentRows.Update(pixelBuffer, y); + + for (int x = 0; x < pixels.Width; x += 8) + { + pixelConverter.Convert(frame, x, y, ref currentRows); + + prevDCY = this.WriteBlock( + QuantIndex.Luminance, + prevDCY, + ref pixelConverter.Y, + ref temp1, + ref temp2, + ref onStackLuminanceQuantTable, + ref unzig, + ref emitBufferBase); + + prevDCCb = this.WriteBlock( + QuantIndex.Chrominance, + prevDCCb, + ref pixelConverter.Cb, + ref temp1, + ref temp2, + ref onStackChrominanceQuantTable, + ref unzig, + ref emitBufferBase); + + prevDCCr = this.WriteBlock( + QuantIndex.Chrominance, + prevDCCr, + ref pixelConverter.Cr, + ref temp1, + ref temp2, + ref onStackChrominanceQuantTable, + ref unzig, + ref emitBufferBase); + } + } + } + + /// + /// Encodes the image with subsampling. The Cb and Cr components are each subsampled + /// at a factor of 2 both horizontally and vertically. + /// + /// The pixel format. + /// The pixel accessor providing access to the image pixels. + /// The token to monitor for cancellation. + /// The reference to the emit buffer. + public void Encode420(Image pixels, CancellationToken cancellationToken, ref byte emitBufferBase) + where TPixel : unmanaged, IPixel + { + // TODO: Need a JpegScanEncoder class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.) + Block8x8F b = default; + Span cb = stackalloc Block8x8F[4]; + Span cr = stackalloc Block8x8F[4]; + + Block8x8F temp1 = default; + Block8x8F temp2 = default; + + Block8x8F onStackLuminanceQuantTable = this.luminanceQuantTable; + Block8x8F onStackChrominanceQuantTable = this.chrominanceQuantTable; + + var unzig = ZigZag.CreateUnzigTable(); + + var pixelConverter = YCbCrForwardConverter.Create(); + + // ReSharper disable once InconsistentNaming + int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; + ImageFrame frame = pixels.Frames.RootFrame; + Buffer2D pixelBuffer = frame.PixelBuffer; + RowOctet currentRows = default; + + for (int y = 0; y < pixels.Height; y += 16) + { + cancellationToken.ThrowIfCancellationRequested(); + for (int x = 0; x < pixels.Width; x += 16) + { + for (int i = 0; i < 4; i++) + { + int xOff = (i & 1) * 8; + int yOff = (i & 2) * 4; + + currentRows.Update(pixelBuffer, y + yOff); + pixelConverter.Convert(frame, x + xOff, y + yOff, ref currentRows); + + cb[i] = pixelConverter.Cb; + cr[i] = pixelConverter.Cr; + + prevDCY = this.WriteBlock( + QuantIndex.Luminance, + prevDCY, + ref pixelConverter.Y, + ref temp1, + ref temp2, + ref onStackLuminanceQuantTable, + ref unzig, + ref emitBufferBase); + } + + Block8x8F.Scale16X16To8X8(ref b, cb); + prevDCCb = this.WriteBlock( + QuantIndex.Chrominance, + prevDCCb, + ref b, + ref temp1, + ref temp2, + ref onStackChrominanceQuantTable, + ref unzig, + ref emitBufferBase); + + Block8x8F.Scale16X16To8X8(ref b, cr); + prevDCCr = this.WriteBlock( + QuantIndex.Chrominance, + prevDCCr, + ref b, + ref temp1, + ref temp2, + ref onStackChrominanceQuantTable, + ref unzig, + ref emitBufferBase); + } + } + } + + + /// + /// Encodes the image with no chroma, just luminance. + /// + /// The pixel format. + /// The pixel accessor providing access to the image pixels. + /// The token to monitor for cancellation. + /// The reference to the emit buffer. + public void EncodeGrayscale(Image pixels, CancellationToken cancellationToken, ref byte emitBufferBase) + where TPixel : unmanaged, IPixel + { + // TODO: Need a JpegScanEncoder class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.) + // (Partially done with YCbCrForwardConverter) + Block8x8F temp1 = default; + Block8x8F temp2 = default; + + Block8x8F onStackLuminanceQuantTable = this.luminanceQuantTable; + + var unzig = ZigZag.CreateUnzigTable(); + + // ReSharper disable once InconsistentNaming + int prevDCY = 0; + + var pixelConverter = LuminanceForwardConverter.Create(); + ImageFrame frame = pixels.Frames.RootFrame; + Buffer2D pixelBuffer = frame.PixelBuffer; + RowOctet currentRows = default; + + for (int y = 0; y < pixels.Height; y += 8) + { + cancellationToken.ThrowIfCancellationRequested(); + currentRows.Update(pixelBuffer, y); + + for (int x = 0; x < pixels.Width; x += 8) + { + pixelConverter.Convert(frame, x, y, ref currentRows); + + prevDCY = this.WriteBlock( + QuantIndex.Luminance, + prevDCY, + ref pixelConverter.Y, + ref temp1, + ref temp2, + ref onStackLuminanceQuantTable, + ref unzig, + ref emitBufferBase); + } + } + } + + + /// + /// Writes a block of pixel data using the given quantization table, + /// returning the post-quantized DC value of the DCT-transformed block. + /// The block is in natural (not zig-zag) order. + /// + /// The quantization table index. + /// The previous DC value. + /// Source block + /// Temporal block to be used as FDCT Destination + /// Temporal block 2 + /// Quantization table + /// The 8x8 Unzig block. + /// The reference to the emit buffer. + /// The . + private int WriteBlock( + QuantIndex index, + int prevDC, + ref Block8x8F src, + ref Block8x8F tempDest1, + ref Block8x8F tempDest2, + ref Block8x8F quant, + ref ZigZag unZig, + ref byte emitBufferBase) + { + FastFloatingPointDCT.TransformFDCT(ref src, ref tempDest1, ref tempDest2); + + Block8x8F.Quantize(ref tempDest1, ref tempDest2, ref quant, ref unZig); + + int dc = (int)tempDest2[0]; + + // Emit the DC delta. + this.EmitHuffRLE((HuffIndex)((2 * (int)index) + 0), 0, dc - prevDC, ref emitBufferBase); + + // Emit the AC components. + var h = (HuffIndex)((2 * (int)index) + 1); + int runLength = 0; + + for (int zig = 1; zig < Block8x8F.Size; zig++) + { + int ac = (int)tempDest2[zig]; + + if (ac == 0) + { + runLength++; + } + else + { + while (runLength > 15) + { + this.EmitHuff(h, 0xf0, ref emitBufferBase); + runLength -= 16; + } + + this.EmitHuffRLE(h, runLength, ac, ref emitBufferBase); + runLength = 0; + } + } + + if (runLength > 0) + { + this.EmitHuff(h, 0x00, ref emitBufferBase); + } + + return dc; + } + + /// + /// Emits the least significant count of bits of bits to the bit-stream. + /// The precondition is bits + /// + /// < 1<<nBits && nBits <= 16 + /// + /// . + /// + /// The packed bits. + /// The number of bits + /// The reference to the emitBuffer. + [MethodImpl(InliningOptions.ShortMethod)] + private void Emit(uint bits, uint count, ref byte emitBufferBase) + { + count += this.bitCount; + bits <<= (int)(32 - count); + bits |= this.accumulatedBits; + + // Only write if more than 8 bits. + if (count >= 8) + { + // Track length + int len = 0; + while (count >= 8) + { + byte b = (byte)(bits >> 24); + Unsafe.Add(ref emitBufferBase, len++) = b; + if (b == byte.MaxValue) + { + Unsafe.Add(ref emitBufferBase, len++) = byte.MinValue; + } + + bits <<= 8; + count -= 8; + } + + if (len > 0) + { + this.outputStream.Write(this.emitBuffer, 0, len); + } + } + + this.accumulatedBits = bits; + this.bitCount = count; + } + + /// + /// Emits the given value with the given Huffman encoder. + /// + /// The index of the Huffman encoder + /// The value to encode. + /// The reference to the emit buffer. + [MethodImpl(InliningOptions.ShortMethod)] + private void EmitHuff(HuffIndex index, int value, ref byte emitBufferBase) + { + uint x = HuffmanLut.TheHuffmanLut[(int)index].Values[value]; + this.Emit(x & ((1 << 24) - 1), x >> 24, ref emitBufferBase); + } + + /// + /// Emits a run of runLength copies of value encoded with the given Huffman encoder. + /// + /// The index of the Huffman encoder + /// The number of copies to encode. + /// The value to encode. + /// The reference to the emit buffer. + [MethodImpl(InliningOptions.ShortMethod)] + private void EmitHuffRLE(HuffIndex index, int runLength, int value, ref byte emitBufferBase) + { + int a = value; + int b = value; + if (a < 0) + { + a = -value; + b = value - 1; + } + + uint bt; + if (a < 0x100) + { + bt = BitCountLut[a]; + } + else + { + bt = 8 + (uint)BitCountLut[a >> 8]; + } + + this.EmitHuff(index, (int)((uint)(runLength << 4) | bt), ref emitBufferBase); + if (bt > 0) + { + this.Emit((uint)b & (uint)((1 << ((int)bt)) - 1), bt, ref emitBufferBase); + } + } + + + /// + /// Initializes quantization table. + /// + /// The quantization index. + /// The scaling factor. + /// The quantization table. + private static void InitQuantizationTable(int i, int scale, ref Block8x8F quant) + { + DebugGuard.MustBeBetweenOrEqualTo(i, 0, 1, nameof(i)); + ReadOnlySpan unscaledQuant = (i == 0) ? UnscaledQuant_Luminance : UnscaledQuant_Chrominance; + + for (int j = 0; j < Block8x8F.Size; j++) + { + int x = unscaledQuant[j]; + x = ((x * scale) + 50) / 100; + if (x < 1) + { + x = 1; + } + + if (x > 255) + { + x = 255; + } + + quant[j] = x; + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 79f0d3022a..14cb87af3b 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -183,23 +183,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg int qlty = Numerics.Clamp(this.quality ?? metadata.GetJpegMetadata().Quality, 1, 100); this.subsample ??= qlty >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; - // Convert from a quality rating to a scaling factor. - int scale; - if (qlty < 50) - { - scale = 5000 / qlty; - } - else - { - scale = 200 - (qlty * 2); - } - - // Initialize the quantization tables. - InitQuantizationTable(0, scale, ref this.luminanceQuantTable); - if (componentCount > 1) - { - InitQuantizationTable(1, scale, ref this.chrominanceQuantTable); - } + YCbCrEncoder scanEncoder = new YCbCrEncoder(stream, componentCount, qlty); + this.luminanceQuantTable = scanEncoder.LuminanceQuantizationTable; + this.chrominanceQuantTable = scanEncoder.ChrominanceQuantizationTable; // Write the Start Of Image marker. this.WriteApplicationHeader(metadata); @@ -208,7 +194,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.WriteProfiles(metadata); // Write the quantization tables. - this.WriteDefineQuantizationTables(); + this.WriteDefineQuantizationTables(ref scanEncoder.LuminanceQuantizationTable, ref scanEncoder.ChrominanceQuantizationTable); // Write the image dimensions. this.WriteStartOfFrame(image.Width, image.Height, componentCount); @@ -669,7 +655,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Writes the Define Quantization Marker and tables. /// - private void WriteDefineQuantizationTables() + private void WriteDefineQuantizationTables(ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable) { // Marker + quantization table lengths int markerlen = 2 + (QuantizationTableCount * (1 + Block8x8F.Size)); @@ -681,8 +667,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg byte[] dqt = new byte[dqtCount]; int offset = 0; - WriteDataToDqt(dqt, ref offset, QuantIndex.Luminance, ref this.luminanceQuantTable); - WriteDataToDqt(dqt, ref offset, QuantIndex.Chrominance, ref this.chrominanceQuantTable); + WriteDataToDqt(dqt, ref offset, QuantIndex.Luminance, ref luminanceQuantTable); + WriteDataToDqt(dqt, ref offset, QuantIndex.Chrominance, ref chrominanceQuantTable); this.outputStream.Write(dqt, 0, dqtCount); }