Browse Source

Add option to encode jpeg in rgb colorspace instead of YCbCr

pull/1734/head
Brian Popow 5 years ago
parent
commit
1df665158d
  1. 80
      src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
  2. 114
      src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbForwardConverter{TPixel}.cs
  3. 8
      src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter420{TPixel}.cs
  4. 8
      src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter444{TPixel}.cs
  5. 67
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  6. 7
      src/ImageSharp/Formats/Jpeg/JpegSubsample.cs

80
src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs

@ -41,7 +41,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
private int emitLen = 0;
/// <summary>
/// Emmited bits 'micro buffer' before being transfered to the <see cref="emitBuffer"/>.
/// Emitted bits 'micro buffer' before being transferred to the <see cref="emitBuffer"/>.
/// </summary>
private int accumulatedBits;
@ -58,18 +58,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
/// </summary>
private readonly Stream target;
public HuffmanScanEncoder(Stream outputStream)
{
this.target = outputStream;
}
public HuffmanScanEncoder(Stream outputStream) => this.target = outputStream;
/// <summary>
/// Encodes the image with no subsampling.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The pixel accessor providing access to the image pixels.</param>
/// <param name="luminanceQuantTable">Luminance quantization table provided by the callee</param>
/// <param name="chrominanceQuantTable">Chrominance quantization table provided by the callee</param>
/// <param name="luminanceQuantTable">Luminance quantization table provided by the callee.</param>
/// <param name="chrominanceQuantTable">Chrominance quantization table provided by the callee.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param>
public void Encode444<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
@ -128,8 +125,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The pixel accessor providing access to the image pixels.</param>
/// <param name="luminanceQuantTable">Luminance quantization table provided by the callee</param>
/// <param name="chrominanceQuantTable">Chrominance quantization table provided by the callee</param>
/// <param name="luminanceQuantTable">Luminance quantization table provided by the callee.</param>
/// <param name="chrominanceQuantTable">Chrominance quantization table provided by the callee.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param>
public void Encode420<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
@ -196,7 +193,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The pixel accessor providing access to the image pixels.</param>
/// <param name="luminanceQuantTable">Luminance quantization table provided by the callee</param>
/// <param name="luminanceQuantTable">Luminance quantization table provided by the callee.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param>
public void EncodeGrayscale<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
@ -234,6 +231,65 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
this.FlushInternalBuffer();
}
/// <summary>
/// Encodes the image with no subsampling and keeps the pixel data as Rgb24.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The pixel accessor providing access to the image pixels.</param>
/// <param name="luminanceQuantTable">Luminance quantization table provided by the callee.</param>
/// <param name="chrominanceQuantTable">Chrominance quantization table provided by the callee.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param>
public void EncodeRgb<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
this.huffmanTables = HuffmanLut.TheHuffmanLut;
var unzig = ZigZag.CreateUnzigTable();
// ReSharper disable once InconsistentNaming
int prevDCY = 0, prevDCCb = 0, prevDCCr = 0;
ImageFrame<TPixel> frame = pixels.Frames.RootFrame;
Buffer2D<TPixel> pixelBuffer = frame.PixelBuffer;
RowOctet<TPixel> currentRows = default;
var pixelConverter = new RgbForwardConverter<TPixel>(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();
}
/// <summary>
/// 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

114
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
{
/// <summary>
/// On-stack worker struct to convert TPixel -> Rgb24 of 8x8 pixel blocks.
/// </summary>
/// <typeparam name="TPixel">The pixel type to work on.</typeparam>
internal ref struct RgbForwardConverter<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Number of pixels processed per single <see cref="Convert(int, int, ref RowOctet{TPixel})"/> call
/// </summary>
private const int PixelsPerSample = 8 * 8;
/// <summary>
/// Total byte size of processed pixels converted from TPixel to <see cref="Rgb24"/>
/// </summary>
private const int RgbSpanByteSize = PixelsPerSample * 3;
/// <summary>
/// <see cref="Size"/> of sampling area from given frame pixel buffer.
/// </summary>
private static readonly Size SampleSize = new Size(8, 8);
/// <summary>
/// The Red component.
/// </summary>
public Block8x8F R;
/// <summary>
/// The Green component.
/// </summary>
public Block8x8F G;
/// <summary>
/// The Blue component.
/// </summary>
public Block8x8F B;
/// <summary>
/// Temporal 64-byte span to hold unconverted TPixel data.
/// </summary>
private readonly Span<TPixel> pixelSpan;
/// <summary>
/// Temporal 64-byte span to hold converted Rgb24 data.
/// </summary>
private readonly Span<Rgb24> rgbSpan;
/// <summary>
/// Sampled pixel buffer size.
/// </summary>
private readonly Size samplingAreaSize;
/// <summary>
/// <see cref="Configuration"/> for internal operations.
/// </summary>
private readonly Configuration config;
public RgbForwardConverter(ImageFrame<TPixel> frame)
{
this.R = default;
this.G = default;
this.B = default;
// temporal pixel buffers
this.pixelSpan = new TPixel[PixelsPerSample].AsSpan();
this.rgbSpan = MemoryMarshal.Cast<byte, Rgb24>(new byte[RgbSpanByteSize + RgbToYCbCrConverterVectorized.AvxCompatibilityPadding].AsSpan());
// frame data
this.samplingAreaSize = new Size(frame.Width, frame.Height);
this.config = frame.GetConfiguration();
}
/// <summary>
/// Converts a 8x8 image area inside 'pixels' at position (x, y) to Rgb24.
/// </summary>
public void Convert(int x, int y, ref RowOctet<TPixel> currentRows)
{
YCbCrForwardConverter<TPixel>.LoadAndStretchEdges(currentRows, this.pixelSpan, new Point(x, y), SampleSize, this.samplingAreaSize);
PixelOperations<TPixel>.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<Rgb24> 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;
}
}
}
}

8
src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter420{TPixel}.cs

@ -58,22 +58,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
/// <summary>
/// Temporal 16x8 block to hold TPixel data
/// </summary>
private Span<TPixel> pixelSpan;
private readonly Span<TPixel> pixelSpan;
/// <summary>
/// Temporal RGB block
/// </summary>
private Span<Rgb24> rgbSpan;
private readonly Span<Rgb24> rgbSpan;
/// <summary>
/// Sampled pixel buffer size
/// </summary>
private Size samplingAreaSize;
private readonly Size samplingAreaSize;
/// <summary>
/// <see cref="Configuration"/> for internal operations
/// </summary>
private Configuration config;
private readonly Configuration config;
public YCbCrForwardConverter420(ImageFrame<TPixel> frame)
{

8
src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter444{TPixel}.cs

@ -53,22 +53,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
/// <summary>
/// Temporal 64-byte span to hold unconverted TPixel data
/// </summary>
private Span<TPixel> pixelSpan;
private readonly Span<TPixel> pixelSpan;
/// <summary>
/// Temporal 64-byte span to hold converted Rgb24 data
/// </summary>
private Span<Rgb24> rgbSpan;
private readonly Span<Rgb24> rgbSpan;
/// <summary>
/// Sampled pixel buffer size
/// </summary>
private Size samplingAreaSize;
private readonly Size samplingAreaSize;
/// <summary>
/// <see cref="Configuration"/> for internal operations
/// </summary>
private Configuration config;
private readonly Configuration config;
public YCbCrForwardConverter444(ImageFrame<TPixel> frame)
{

67
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
}
/// <summary>
/// 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.
/// </summary>
/// <param name="dqt">The "Define Quantization Tables" block</param>
/// <param name="offset">Offset in "Define Quantization Tables" block</param>
/// <param name="i">The quantization index</param>
/// <param name="quant">The quantization table to copy data from</param>
/// <returns>The component Ids.</returns>
private byte[] GetComponentIds()
{
if (this.subsample == JpegSubsample.Rgb)
{
return new byte[] { 82, 71, 66 };
}
return new byte[] { 1, 2, 3 };
}
/// <summary>
/// Writes data to "Define Quantization Tables" block for QuantIndex.
/// </summary>
/// <param name="dqt">The "Define Quantization Tables" block.</param>
/// <param name="offset">Offset in "Define Quantization Tables" block.</param>
/// <param name="i">The quantization index.</param>
/// <param name="quant">The quantization table to copy data from.</param>
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
}
/// <summary>
/// Writes the Start Of Frame (Baseline) marker
/// Writes the Start Of Frame (Baseline) marker.
/// </summary>
/// <param name="width">The width of the image</param>
/// <param name="height">The height of the image</param>
/// <param name="componentCount">The number of components in a pixel</param>
private void WriteStartOfFrame(int width, int height, int componentCount)
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="componentCount">The number of components in a pixel.</param>
/// <param name="componentIds">The component Id's.</param>
private void WriteStartOfFrame(int width, int height, int componentCount, byte[] componentIds)
{
// "default" to 4:2:0
Span<byte> 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
/// <summary>
/// Writes the StartOfScan marker.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The pixel accessor providing access to the image pixels.</param>
/// <param name="componentCount">The number of components in a pixel.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param>
private void WriteStartOfScan<TPixel>(Image<TPixel> image, int componentCount, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
/// <param name="componentIds">The componentId's.</param>
private void WriteStartOfScan(int componentCount, byte[] componentIds)
{
Span<byte> componentId = stackalloc byte[]
{
0x01,
0x02,
0x03
};
Span<byte> 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
}
/// <summary>
/// Initializes quntization tables.
/// Initializes quantization tables.
/// </summary>
/// <remarks>
/// We take quality values in a hierarchical order:

7
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.
/// </summary>
Ratio420
Ratio420,
/// <summary>
/// The pixel data will be preserved as RGB without any sub sampling.
/// </summary>
Rgb,
}
}

Loading…
Cancel
Save