Browse Source

Merge branch 'main' into bmp_dither

pull/2819/head
James Jackson-South 1 year ago
committed by GitHub
parent
commit
bfbade15b3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
  2. 201
      src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
  3. 60
      src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
  4. 96
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  5. 58
      tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs

2
src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs

@ -119,6 +119,8 @@ internal class HuffmanScanDecoder : IJpegScanDecoder
this.frame.AllocateComponents();
this.todo = this.restartInterval;
if (!this.frame.Progressive)
{
this.ParseBaselineData();

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

@ -87,6 +87,8 @@ internal class HuffmanScanEncoder
/// </remarks>
private readonly byte[] streamWriteBuffer;
private readonly int restartInterval;
/// <summary>
/// Number of jagged bits stored in <see cref="accumulatedBits"/>
/// </summary>
@ -103,13 +105,16 @@ internal class HuffmanScanEncoder
/// Initializes a new instance of the <see cref="HuffmanScanEncoder"/> class.
/// </summary>
/// <param name="blocksPerCodingUnit">Amount of encoded 8x8 blocks per single jpeg macroblock.</param>
/// <param name="restartInterval">Numbers of MCUs between restart markers.</param>
/// <param name="outputStream">Output stream for saving encoded data.</param>
public HuffmanScanEncoder(int blocksPerCodingUnit, Stream outputStream)
public HuffmanScanEncoder(int blocksPerCodingUnit, int restartInterval, Stream outputStream)
{
int emitBufferByteLength = MaxBytesPerBlock * blocksPerCodingUnit;
this.emitBuffer = new uint[emitBufferByteLength / sizeof(uint)];
this.emitWriteIndex = this.emitBuffer.Length;
this.restartInterval = restartInterval;
this.streamWriteBuffer = new byte[emitBufferByteLength * OutputBufferLengthMultiplier];
this.target = outputStream;
@ -211,6 +216,9 @@ internal class HuffmanScanEncoder
ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId];
ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId];
int restarts = 0;
int restartsToGo = this.restartInterval;
for (int i = 0; i < h; i++)
{
cancellationToken.ThrowIfCancellationRequested();
@ -221,6 +229,13 @@ internal class HuffmanScanEncoder
for (nuint k = 0; k < (uint)w; k++)
{
if (this.restartInterval > 0 && restartsToGo == 0)
{
this.FlushRemainingBytes();
this.WriteRestart(restarts % 8);
component.DcPredictor = 0;
}
this.WriteBlock(
component,
ref Unsafe.Add(ref blockRef, k),
@ -231,6 +246,133 @@ internal class HuffmanScanEncoder
{
this.FlushToStream();
}
if (this.restartInterval > 0)
{
if (restartsToGo == 0)
{
restartsToGo = this.restartInterval;
restarts++;
}
restartsToGo--;
}
}
}
this.FlushRemainingBytes();
}
/// <summary>
/// Encodes the DC coefficients for a given component's blocks in a scan.
/// </summary>
/// <param name="component">The component whose DC coefficients need to be encoded.</param>
/// <param name="cancellationToken">The token to request cancellation.</param>
public void EncodeDcScan(Component component, CancellationToken cancellationToken)
{
int h = component.HeightInBlocks;
int w = component.WidthInBlocks;
ref HuffmanLut dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId];
int restarts = 0;
int restartsToGo = this.restartInterval;
for (int i = 0; i < h; i++)
{
cancellationToken.ThrowIfCancellationRequested();
Span<Block8x8> blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y: i);
ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
for (nuint k = 0; k < (uint)w; k++)
{
if (this.restartInterval > 0 && restartsToGo == 0)
{
this.FlushRemainingBytes();
this.WriteRestart(restarts % 8);
component.DcPredictor = 0;
}
this.WriteDc(
component,
ref Unsafe.Add(ref blockRef, k),
ref dcHuffmanTable);
if (this.IsStreamFlushNeeded)
{
this.FlushToStream();
}
if (this.restartInterval > 0)
{
if (restartsToGo == 0)
{
restartsToGo = this.restartInterval;
restarts++;
}
restartsToGo--;
}
}
}
this.FlushRemainingBytes();
}
/// <summary>
/// Encodes the AC coefficients for a specified range of blocks in a component's scan.
/// </summary>
/// <param name="component">The component whose AC coefficients need to be encoded.</param>
/// <param name="start">The starting index of the AC coefficient range to encode.</param>
/// <param name="end">The ending index of the AC coefficient range to encode.</param>
/// <param name="cancellationToken">The token to request cancellation.</param>
public void EncodeAcScan(Component component, nint start, nint end, CancellationToken cancellationToken)
{
int h = component.HeightInBlocks;
int w = component.WidthInBlocks;
int restarts = 0;
int restartsToGo = this.restartInterval;
ref HuffmanLut acHuffmanTable = ref this.acHuffmanTables[component.AcTableId];
for (int i = 0; i < h; i++)
{
cancellationToken.ThrowIfCancellationRequested();
Span<Block8x8> blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y: i);
ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan);
for (nuint k = 0; k < (uint)w; k++)
{
if (this.restartInterval > 0 && restartsToGo == 0)
{
this.FlushRemainingBytes();
this.WriteRestart(restarts % 8);
}
this.WriteAcBlock(
ref Unsafe.Add(ref blockRef, k),
start,
end,
ref acHuffmanTable);
if (this.IsStreamFlushNeeded)
{
this.FlushToStream();
}
if (this.restartInterval > 0)
{
if (restartsToGo == 0)
{
restartsToGo = this.restartInterval;
restarts++;
}
restartsToGo--;
}
}
}
@ -250,6 +392,9 @@ internal class HuffmanScanEncoder
int mcusPerColumn = frame.McusPerColumn;
int mcusPerLine = frame.McusPerLine;
int restarts = 0;
int restartsToGo = this.restartInterval;
for (int j = 0; j < mcusPerColumn; j++)
{
cancellationToken.ThrowIfCancellationRequested();
@ -260,6 +405,16 @@ internal class HuffmanScanEncoder
// Encode spectral to binary
for (int i = 0; i < mcusPerLine; i++)
{
if (this.restartInterval > 0 && restartsToGo == 0)
{
this.FlushRemainingBytes();
this.WriteRestart(restarts % 8);
foreach (var component in frame.Components)
{
component.DcPredictor = 0;
}
}
// Scan an interleaved mcu... process components in order
int mcuCol = mcu % mcusPerLine;
for (int k = 0; k < frame.Components.Length; k++)
@ -300,6 +455,17 @@ internal class HuffmanScanEncoder
{
this.FlushToStream();
}
if (this.restartInterval > 0)
{
if (restartsToGo == 0)
{
restartsToGo = this.restartInterval;
restarts++;
}
restartsToGo--;
}
}
}
@ -371,25 +537,29 @@ internal class HuffmanScanEncoder
this.FlushRemainingBytes();
}
private void WriteBlock(
private void WriteDc(
Component component,
ref Block8x8 block,
ref HuffmanLut dcTable,
ref HuffmanLut acTable)
ref HuffmanLut dcTable)
{
// Emit the DC delta.
int dc = block[0];
this.EmitHuffRLE(dcTable.Values, 0, dc - component.DcPredictor);
component.DcPredictor = dc;
}
private void WriteAcBlock(
ref Block8x8 block,
nint start,
nint end,
ref HuffmanLut acTable)
{
// Emit the AC components.
int[] acHuffTable = acTable.Values;
nint lastValuableIndex = block.GetLastNonZeroIndex();
int runLength = 0;
ref short blockRef = ref Unsafe.As<Block8x8, short>(ref block);
for (nint zig = 1; zig <= lastValuableIndex; zig++)
for (nint zig = start; zig < end; zig++)
{
const int zeroRun1 = 1 << 4;
const int zeroRun16 = 16 << 4;
@ -413,14 +583,25 @@ internal class HuffmanScanEncoder
}
// 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)
if (runLength > 0)
{
this.EmitHuff(acHuffTable, 0x00);
}
}
private void WriteBlock(
Component component,
ref Block8x8 block,
ref HuffmanLut dcTable,
ref HuffmanLut acTable)
{
this.WriteDc(component, ref block, ref dcTable);
this.WriteAcBlock(ref block, 1, 64, ref acTable);
}
private void WriteRestart(int restart) =>
this.target.Write([0xff, (byte)(JpegConstants.Markers.RST0 + restart)], 0, 2);
/// <summary>
/// Emits the most significant count of bits to the buffer.
/// </summary>

60
src/ImageSharp/Formats/Jpeg/JpegEncoder.cs

@ -13,6 +13,16 @@ public sealed class JpegEncoder : ImageEncoder
/// </summary>
private int? quality;
/// <summary>
/// Backing field for <see cref="ProgressiveScans"/>
/// </summary>
private int progressiveScans = 4;
/// <summary>
/// Backing field for <see cref="RestartInterval"/>
/// </summary>
private int restartInterval;
/// <summary>
/// Gets the quality, that will be used to encode the image. Quality
/// index must be between 1 and 100 (compression from max to min).
@ -33,6 +43,56 @@ public sealed class JpegEncoder : ImageEncoder
}
}
/// <summary>
/// Gets a value indicating whether progressive encoding is used.
/// </summary>
public bool Progressive { get; init; }
/// <summary>
/// Gets number of scans per component for progressive encoding.
/// Defaults to <value>4</value>.
/// </summary>
/// <remarks>
/// Number of scans must be between 2 and 64.
/// There is at least one scan for the DC coefficients and one for the remaining 63 AC coefficients.
/// </remarks>
/// <exception cref="ArgumentException">Progressive scans must be in [2..64] range.</exception>
public int ProgressiveScans
{
get => this.progressiveScans;
init
{
if (value is < 2 or > 64)
{
throw new ArgumentException("Progressive scans must be in [2..64] range.");
}
this.progressiveScans = value;
}
}
/// <summary>
/// Gets numbers of MCUs between restart markers.
/// Defaults to <value>0</value>.
/// </summary>
/// <remarks>
/// Currently supported in progressive encoding only.
/// </remarks>
/// <exception cref="ArgumentException">Restart interval must be in [0..65535] range.</exception>
public int RestartInterval
{
get => this.restartInterval;
init
{
if (value is < 0 or > 65535)
{
throw new ArgumentException("Restart interval must be in [0..65535] range.");
}
this.restartInterval = value;
}
}
/// <summary>
/// Gets the component encoding mode.
/// </summary>

96
src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs

@ -100,12 +100,15 @@ internal sealed unsafe partial class JpegEncoderCore
this.WriteStartOfFrame(image.Width, image.Height, frameConfig, buffer);
// Write the Huffman tables.
HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, stream);
HuffmanScanEncoder scanEncoder = new(frame.BlocksPerMcu, this.encoder.RestartInterval, stream);
this.WriteDefineHuffmanTables(frameConfig.HuffmanTables, scanEncoder, buffer);
// Write the quantization tables.
this.WriteDefineQuantizationTables(frameConfig.QuantizationTables, this.encoder.Quality, jpegMetadata, buffer);
// Write define restart interval
this.WriteDri(this.encoder.RestartInterval, buffer);
// Write scans with actual pixel data
using SpectralConverter<TPixel> spectralConverter = new(frame, image, this.QuantizationTables);
this.WriteHuffmanScans(frame, frameConfig, spectralConverter, scanEncoder, buffer, cancellationToken);
@ -426,6 +429,25 @@ internal sealed unsafe partial class JpegEncoderCore
}
}
/// <summary>
/// Writes the DRI marker
/// </summary>
/// <param name="restartInterval">Numbers of MCUs between restart markers.</param>
/// <param name="buffer">Temporary buffer.</param>
private void WriteDri(int restartInterval, Span<byte> buffer)
{
if (restartInterval <= 0)
{
return;
}
this.WriteMarkerHeader(JpegConstants.Markers.DRI, 4, buffer);
buffer[1] = (byte)(restartInterval & 0xff);
buffer[0] = (byte)(restartInterval >> 8);
this.outputStream.Write(buffer, 0, 2);
}
/// <summary>
/// Writes the App1 header.
/// </summary>
@ -563,7 +585,8 @@ internal sealed unsafe partial class JpegEncoderCore
// Length (high byte, low byte), 8 + components * 3.
int markerlen = 8 + (3 * components.Length);
this.WriteMarkerHeader(JpegConstants.Markers.SOF0, markerlen, buffer);
byte marker = this.encoder.Progressive ? JpegConstants.Markers.SOF2 : JpegConstants.Markers.SOF0;
this.WriteMarkerHeader(marker, markerlen, buffer);
buffer[5] = (byte)components.Length;
buffer[0] = 8; // Data Precision. 8 for now, 12 and 16 bit jpegs not supported
buffer[1] = (byte)(height >> 8);
@ -597,7 +620,17 @@ internal sealed unsafe partial class JpegEncoderCore
/// </summary>
/// <param name="components">The collecction of component configuration items.</param>
/// <param name="buffer">Temporary buffer.</param>
private void WriteStartOfScan(Span<JpegComponentConfig> components, Span<byte> buffer)
private void WriteStartOfScan(Span<JpegComponentConfig> components, Span<byte> buffer) =>
this.WriteStartOfScan(components, buffer, 0x00, 0x3f);
/// <summary>
/// Writes the StartOfScan marker.
/// </summary>
/// <param name="components">The collecction of component configuration items.</param>
/// <param name="buffer">Temporary buffer.</param>
/// <param name="spectralStart">Start of spectral selection</param>
/// <param name="spectralEnd">End of spectral selection</param>
private void WriteStartOfScan(Span<JpegComponentConfig> components, Span<byte> buffer, byte spectralStart, byte spectralEnd)
{
// Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes:
// - the marker length "\x00\x0c",
@ -630,8 +663,8 @@ internal sealed unsafe partial class JpegEncoderCore
buffer[i2 + 6] = (byte)tableSelectors;
}
buffer[sosSize - 1] = 0x00; // Ss - Start of spectral selection.
buffer[sosSize] = 0x3f; // Se - End of spectral selection.
buffer[sosSize - 1] = spectralStart; // Ss - Start of spectral selection.
buffer[sosSize] = spectralEnd; // Se - End of spectral selection.
buffer[sosSize + 1] = 0x00; // Ah + Ah (Successive approximation bit position high + low)
this.outputStream.Write(buffer, 0, sosSize + 2);
}
@ -666,7 +699,14 @@ internal sealed unsafe partial class JpegEncoderCore
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
if (frame.Components.Length == 1)
if (this.encoder.Progressive)
{
frame.AllocateComponents(fullScan: true);
spectralConverter.ConvertFull();
this.WriteProgressiveScans<TPixel>(frame, frameConfig, encoder, buffer, cancellationToken);
}
else if (frame.Components.Length == 1)
{
frame.AllocateComponents(fullScan: false);
@ -694,6 +734,50 @@ internal sealed unsafe partial class JpegEncoderCore
}
}
/// <summary>
/// Writes the progressive scans
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="frame">The current frame.</param>
/// <param name="frameConfig">The frame configuration.</param>
/// <param name="encoder">The scan encoder.</param>
/// <param name="buffer">Temporary buffer.</param>
/// <param name="cancellationToken">The cancellation token.</param>
private void WriteProgressiveScans<TPixel>(
JpegFrame frame,
JpegFrameConfig frameConfig,
HuffmanScanEncoder encoder,
Span<byte> buffer,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Span<JpegComponentConfig> components = frameConfig.Components;
// Phase 1: DC scan
for (int i = 0; i < frame.Components.Length; i++)
{
this.WriteStartOfScan(components.Slice(i, 1), buffer, 0x00, 0x00);
encoder.EncodeDcScan(frame.Components[i], cancellationToken);
}
// Phase 2: AC scans
int acScans = this.encoder.ProgressiveScans - 1;
int valuesPerScan = 64 / acScans;
for (int scan = 0; scan < acScans; scan++)
{
int start = Math.Max(1, scan * valuesPerScan);
int end = scan == acScans - 1 ? 64 : (scan + 1) * valuesPerScan;
for (int i = 0; i < components.Length; i++)
{
this.WriteStartOfScan(components.Slice(i, 1), buffer, (byte)start, (byte)(end - 1));
encoder.EncodeAcScan(frame.Components[i], start, end, cancellationToken);
}
}
}
/// <summary>
/// Writes the header for a marker with the given length.
/// </summary>

58
tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs

@ -160,6 +160,64 @@ public partial class JpegEncoderTests
TestJpegEncoderCore(provider, colorType, 100, comparer);
}
[Theory]
[WithFile(TestImages.Png.CalliphoraPartial, nameof(NonSubsampledEncodingSetups), PixelTypes.Rgb24)]
[WithFile(TestImages.Png.CalliphoraPartial, nameof(SubsampledEncodingSetups), PixelTypes.Rgb24)]
[WithFile(TestImages.Png.BikeGrayscale, nameof(LuminanceEncodingSetups), PixelTypes.L8)]
[WithFile(TestImages.Jpeg.Baseline.Cmyk, nameof(CmykEncodingSetups), PixelTypes.Rgb24)]
[WithFile(TestImages.Jpeg.Baseline.Ycck, nameof(YcckEncodingSetups), PixelTypes.Rgb24)]
public void EncodeProgressive_DefaultNumberOfScans<TPixel>(TestImageProvider<TPixel> provider, JpegColorType colorType, int quality, float tolerance)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
JpegEncoder encoder = new()
{
Quality = quality,
ColorType = colorType,
Progressive = true
};
string info = $"{colorType}-Q{quality}";
ImageComparer comparer = new TolerantImageComparer(tolerance);
// Does DebugSave & load reference CompareToReferenceInput():
image.VerifyEncoder(provider, "jpeg", info, encoder, comparer, referenceImageExtension: "jpg");
}
[Theory]
[WithFile(TestImages.Png.CalliphoraPartial, nameof(NonSubsampledEncodingSetups), PixelTypes.Rgb24)]
[WithFile(TestImages.Png.CalliphoraPartial, nameof(SubsampledEncodingSetups), PixelTypes.Rgb24)]
[WithFile(TestImages.Png.BikeGrayscale, nameof(LuminanceEncodingSetups), PixelTypes.L8)]
[WithFile(TestImages.Jpeg.Baseline.Cmyk, nameof(CmykEncodingSetups), PixelTypes.Rgb24)]
[WithFile(TestImages.Jpeg.Baseline.Ycck, nameof(YcckEncodingSetups), PixelTypes.Rgb24)]
public void EncodeProgressive_CustomNumberOfScans<TPixel>(TestImageProvider<TPixel> provider, JpegColorType colorType, int quality, float tolerance)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
JpegEncoder encoder = new()
{
Quality = quality,
ColorType = colorType,
Progressive = true,
ProgressiveScans = 4,
RestartInterval = 7
};
string info = $"{colorType}-Q{quality}";
using MemoryStream ms = new();
image.SaveAsJpeg(ms, encoder);
ms.Position = 0;
// TEMP: Save decoded output as PNG so we can do a pixel compare.
using Image<TPixel> image2 = Image.Load<TPixel>(ms);
image2.DebugSave(provider, testOutputDetails: info, extension: "png");
ImageComparer comparer = new TolerantImageComparer(tolerance);
image.VerifyEncoder(provider, "jpeg", info, encoder, comparer, referenceImageExtension: "jpg");
}
[Theory]
[InlineData(JpegColorType.YCbCrRatio420)]
[InlineData(JpegColorType.YCbCrRatio444)]

Loading…
Cancel
Save