From 1df665158d1054d07debbc3532fd474e04beb92c Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 12 Aug 2021 07:10:39 +0200 Subject: [PATCH] Add option to encode jpeg in rgb colorspace instead of YCbCr --- .../Components/Encoder/HuffmanScanEncoder.cs | 80 ++++++++++-- .../Encoder/RgbForwardConverter{TPixel}.cs | 114 ++++++++++++++++++ .../YCbCrForwardConverter420{TPixel}.cs | 8 +- .../YCbCrForwardConverter444{TPixel}.cs | 8 +- .../Formats/Jpeg/JpegEncoderCore.cs | 67 +++++----- src/ImageSharp/Formats/Jpeg/JpegSubsample.cs | 7 +- 6 files changed, 236 insertions(+), 48 deletions(-) create mode 100644 src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbForwardConverter{TPixel}.cs diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs index 331da275c..5468d93c4 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs @@ -41,7 +41,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder private int emitLen = 0; /// - /// Emmited bits 'micro buffer' before being transfered to the . + /// Emitted bits 'micro buffer' before being transferred to the . /// private int accumulatedBits; @@ -58,18 +58,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder /// private readonly Stream target; - public HuffmanScanEncoder(Stream outputStream) - { - this.target = outputStream; - } + public HuffmanScanEncoder(Stream outputStream) => this.target = outputStream; /// /// Encodes the image with no subsampling. /// /// The pixel format. /// The pixel accessor providing access to the image pixels. - /// Luminance quantization table provided by the callee - /// Chrominance quantization table provided by the callee + /// Luminance quantization table provided by the callee. + /// Chrominance quantization table provided by the callee. /// The token to monitor for cancellation. public void Encode444(Image pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel @@ -128,8 +125,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder /// /// The pixel format. /// The pixel accessor providing access to the image pixels. - /// Luminance quantization table provided by the callee - /// Chrominance quantization table provided by the callee + /// Luminance quantization table provided by the callee. + /// Chrominance quantization table provided by the callee. /// The token to monitor for cancellation. public void Encode420(Image pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel @@ -196,7 +193,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder /// /// The pixel format. /// The pixel accessor providing access to the image pixels. - /// Luminance quantization table provided by the callee + /// Luminance quantization table provided by the callee. /// The token to monitor for cancellation. public void EncodeGrayscale(Image pixels, ref Block8x8F luminanceQuantTable, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel @@ -234,6 +231,65 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder this.FlushInternalBuffer(); } + /// + /// Encodes the image with no subsampling and keeps the pixel data as Rgb24. + /// + /// The pixel format. + /// The pixel accessor providing access to the image pixels. + /// Luminance quantization table provided by the callee. + /// Chrominance quantization table provided by the callee. + /// The token to monitor for cancellation. + public void EncodeRgb(Image pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + this.huffmanTables = HuffmanLut.TheHuffmanLut; + + var unzig = ZigZag.CreateUnzigTable(); + + // ReSharper disable once InconsistentNaming + int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; + + ImageFrame frame = pixels.Frames.RootFrame; + Buffer2D pixelBuffer = frame.PixelBuffer; + RowOctet currentRows = default; + + var pixelConverter = new RgbForwardConverter(frame); + + 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(x, y, ref currentRows); + + prevDCY = this.WriteBlock( + QuantIndex.Luminance, + prevDCY, + ref pixelConverter.R, + ref luminanceQuantTable, + ref unzig); + + prevDCCb = this.WriteBlock( + QuantIndex.Chrominance, + prevDCCb, + ref pixelConverter.G, + ref chrominanceQuantTable, + ref unzig); + + prevDCCr = this.WriteBlock( + QuantIndex.Chrominance, + prevDCCr, + ref pixelConverter.B, + ref chrominanceQuantTable, + ref unzig); + } + } + + this.FlushInternalBuffer(); + } + /// /// Writes a block of pixel data using the given quantization table, /// returning the post-quantized DC value of the DCT-transformed block. @@ -437,7 +493,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder DebugGuard.IsTrue(value <= (1 << 16), "Huffman encoder is supposed to encode a value of 16bit size max"); #if SUPPORTS_BITOPERATIONS // This should have been implemented as (BitOperations.Log2(value) + 1) as in non-intrinsic implementation - // But internal log2 is implementated like this: (31 - (int)Lzcnt.LeadingZeroCount(value)) + // But internal log2 is implemented like this: (31 - (int)Lzcnt.LeadingZeroCount(value)) // BitOperations.Log2 implementation also checks if input value is zero for the convention 0->0 // Lzcnt would return 32 for input value of 0 - no need to check that with branching @@ -449,7 +505,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder // if 0 - return 0 in this case // else - return log2(value) + 1 // - // Hack based on input value constaint: + // Hack based on input value constraint: // We know that input values are guaranteed to be maximum 16 bit large for huffman encoding // We can safely shift input value for one bit -> log2(value << 1) // Because of the 16 bit value constraint it won't overflow diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbForwardConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbForwardConverter{TPixel}.cs new file mode 100644 index 000000000..e23cf348a --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbForwardConverter{TPixel}.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder +{ + /// + /// On-stack worker struct to convert TPixel -> Rgb24 of 8x8 pixel blocks. + /// + /// The pixel type to work on. + internal ref struct RgbForwardConverter + where TPixel : unmanaged, IPixel + { + /// + /// Number of pixels processed per single call + /// + private const int PixelsPerSample = 8 * 8; + + /// + /// Total byte size of processed pixels converted from TPixel to + /// + private const int RgbSpanByteSize = PixelsPerSample * 3; + + /// + /// of sampling area from given frame pixel buffer. + /// + private static readonly Size SampleSize = new Size(8, 8); + + /// + /// The Red component. + /// + public Block8x8F R; + + /// + /// The Green component. + /// + public Block8x8F G; + + /// + /// The Blue component. + /// + public Block8x8F B; + + /// + /// Temporal 64-byte span to hold unconverted TPixel data. + /// + private readonly Span pixelSpan; + + /// + /// Temporal 64-byte span to hold converted Rgb24 data. + /// + private readonly Span rgbSpan; + + /// + /// Sampled pixel buffer size. + /// + private readonly Size samplingAreaSize; + + /// + /// for internal operations. + /// + private readonly Configuration config; + + public RgbForwardConverter(ImageFrame frame) + { + this.R = default; + this.G = default; + this.B = default; + + // temporal pixel buffers + this.pixelSpan = new TPixel[PixelsPerSample].AsSpan(); + this.rgbSpan = MemoryMarshal.Cast(new byte[RgbSpanByteSize + RgbToYCbCrConverterVectorized.AvxCompatibilityPadding].AsSpan()); + + // frame data + this.samplingAreaSize = new Size(frame.Width, frame.Height); + this.config = frame.GetConfiguration(); + } + + /// + /// Converts a 8x8 image area inside 'pixels' at position (x, y) to Rgb24. + /// + public void Convert(int x, int y, ref RowOctet currentRows) + { + YCbCrForwardConverter.LoadAndStretchEdges(currentRows, this.pixelSpan, new Point(x, y), SampleSize, this.samplingAreaSize); + + PixelOperations.Instance.ToRgb24(this.config, this.pixelSpan, this.rgbSpan); + + ref Block8x8F redBlock = ref this.R; + ref Block8x8F greenBlock = ref this.G; + ref Block8x8F blueBlock = ref this.B; + + CopyToBlock(this.rgbSpan, ref redBlock, ref greenBlock, ref blueBlock); + } + + private static void CopyToBlock(Span rgbSpan, ref Block8x8F redBlock, ref Block8x8F greenBlock, ref Block8x8F blueBlock) + { + ref Rgb24 rgbStart = ref rgbSpan[0]; + + for (int i = 0; i < Block8x8F.Size; i++) + { + Rgb24 c = Unsafe.Add(ref rgbStart, i); + + redBlock[i] = c.R; + greenBlock[i] = c.G; + blueBlock[i] = c.B; + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter420{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter420{TPixel}.cs index a4abd532b..bfeafcbb3 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter420{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter420{TPixel}.cs @@ -58,22 +58,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder /// /// Temporal 16x8 block to hold TPixel data /// - private Span pixelSpan; + private readonly Span pixelSpan; /// /// Temporal RGB block /// - private Span rgbSpan; + private readonly Span rgbSpan; /// /// Sampled pixel buffer size /// - private Size samplingAreaSize; + private readonly Size samplingAreaSize; /// /// for internal operations /// - private Configuration config; + private readonly Configuration config; public YCbCrForwardConverter420(ImageFrame frame) { diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter444{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter444{TPixel}.cs index ef589272b..2dbd1a2dc 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter444{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter444{TPixel}.cs @@ -53,22 +53,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder /// /// Temporal 64-byte span to hold unconverted TPixel data /// - private Span pixelSpan; + private readonly Span pixelSpan; /// /// Temporal 64-byte span to hold converted Rgb24 data /// - private Span rgbSpan; + private readonly Span rgbSpan; /// /// Sampled pixel buffer size /// - private Size samplingAreaSize; + private readonly Size samplingAreaSize; /// /// for internal operations /// - private Configuration config; + private readonly Configuration config; public YCbCrForwardConverter444(ImageFrame frame) { diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 88d96f554..d49e40d2f 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -90,6 +90,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // Compute number of components based on color type in options. int componentCount = (this.colorType == JpegColorType.Luminance) ? 1 : 3; + byte[] componentIds = this.GetComponentIds(); // TODO: Right now encoder writes both quantization tables for grayscale images - we shouldn't do that // Initialize the quantization tables. @@ -105,13 +106,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.WriteDefineQuantizationTables(ref luminanceQuantTable, ref chrominanceQuantTable); // Write the image dimensions. - this.WriteStartOfFrame(image.Width, image.Height, componentCount); + this.WriteStartOfFrame(image.Width, image.Height, componentCount, componentIds); // Write the Huffman tables. this.WriteDefineHuffmanTables(componentCount); // Write the scan header. - this.WriteStartOfScan(image, componentCount, cancellationToken); + this.WriteStartOfScan(componentCount, componentIds); // Write the scan compressed data. var scanEncoder = new HuffmanScanEncoder(stream); @@ -131,6 +132,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg case JpegSubsample.Ratio420: scanEncoder.Encode420(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken); break; + case JpegSubsample.Rgb: + scanEncoder.EncodeRgb(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken); + break; } } @@ -141,12 +145,27 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - /// Writes data to "Define Quantization Tables" block for QuantIndex + /// Gets the component ids. + /// For color space RGB this will be RGB as ASCII, otherwise 1, 2, 3. /// - /// The "Define Quantization Tables" block - /// Offset in "Define Quantization Tables" block - /// The quantization index - /// The quantization table to copy data from + /// The component Ids. + private byte[] GetComponentIds() + { + if (this.subsample == JpegSubsample.Rgb) + { + return new byte[] { 82, 71, 66 }; + } + + return new byte[] { 1, 2, 3 }; + } + + /// + /// Writes data to "Define Quantization Tables" block for QuantIndex. + /// + /// The "Define Quantization Tables" block. + /// Offset in "Define Quantization Tables" block. + /// The quantization index. + /// The quantization table to copy data from. private static void WriteDataToDqt(byte[] dqt, ref int offset, QuantIndex i, ref Block8x8F quant) { dqt[offset++] = (byte)i; @@ -343,7 +362,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg throw new ImageFormatException($"Iptc profile size exceeds limit of {Max} bytes"); } - var app13Length = 2 + ProfileResolver.AdobePhotoshopApp13Marker.Length + + int app13Length = 2 + ProfileResolver.AdobePhotoshopApp13Marker.Length + ProfileResolver.AdobeImageResourceBlockMarker.Length + ProfileResolver.AdobeIptcMarker.Length + 2 + 4 + data.Length; @@ -478,12 +497,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - /// Writes the Start Of Frame (Baseline) marker + /// Writes the Start Of Frame (Baseline) marker. /// - /// The width of the image - /// The height of the image - /// The number of components in a pixel - private void WriteStartOfFrame(int width, int height, int componentCount) + /// The width of the image. + /// The height of the image. + /// The number of components in a pixel. + /// The component Id's. + private void WriteStartOfFrame(int width, int height, int componentCount, byte[] componentIds) { // "default" to 4:2:0 Span subsamples = stackalloc byte[] @@ -513,6 +533,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { switch (this.subsample) { + case JpegSubsample.Rgb: case JpegSubsample.Ratio444: subsamples = stackalloc byte[] { @@ -545,8 +566,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg for (int i = 0; i < componentCount; i++) { int i3 = 3 * i; - this.buffer[i3 + 6] = (byte)(i + 1); + // Component ID. + this.buffer[i3 + 6] = componentIds[i]; this.buffer[i3 + 7] = subsamples[i]; this.buffer[i3 + 8] = chroma[i]; } @@ -557,19 +579,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Writes the StartOfScan marker. /// - /// The pixel format. - /// The pixel accessor providing access to the image pixels. /// The number of components in a pixel. - /// The token to monitor for cancellation. - private void WriteStartOfScan(Image image, int componentCount, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + /// The componentId's. + private void WriteStartOfScan(int componentCount, byte[] componentIds) { - Span componentId = stackalloc byte[] - { - 0x01, - 0x02, - 0x03 - }; Span huffmanId = stackalloc byte[] { 0x00, @@ -597,7 +610,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg for (int i = 0; i < componentCount; i++) { int i2 = 2 * i; - this.buffer[i2 + 5] = componentId[i]; // Component Id + this.buffer[i2 + 5] = componentIds[i]; // Component Id this.buffer[i2 + 6] = huffmanId[i]; // DC/AC Huffman table } @@ -633,7 +646,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - /// Initializes quntization tables. + /// Initializes quantization tables. /// /// /// We take quality values in a hierarchical order: diff --git a/src/ImageSharp/Formats/Jpeg/JpegSubsample.cs b/src/ImageSharp/Formats/Jpeg/JpegSubsample.cs index 16488f6d2..760ba3a96 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegSubsample.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegSubsample.cs @@ -18,6 +18,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// Medium Quality - The horizontal sampling is halved and the Cb and Cr channels are only /// sampled on each alternate line. /// - Ratio420 + Ratio420, + + /// + /// The pixel data will be preserved as RGB without any sub sampling. + /// + Rgb, } }