diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
index fc11465444..a6334e2da4 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
@@ -14,6 +14,51 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
internal class HuffmanScanEncoder
{
+ ///
+ /// Maximum number of bytes encoded jpeg 8x8 block can occupy.
+ /// It's highly unlikely for block to occupy this much space - it's a theoretical limit.
+ ///
+ ///
+ /// Where 16 is maximum huffman code binary length according to itu
+ /// specs. 10 is maximum value binary length, value comes from discrete
+ /// cosine tranform with value range: [-1024..1023]. Block stores
+ /// 8x8 = 64 values thus multiplication by 64. Then divided by 8 to get
+ /// the number of bytes. This value is then multiplied by
+ /// for performance reasons.
+ ///
+ private const int MaxBytesPerBlock = (16 + 10) * 64 / 8 * MaxBytesPerBlockMultiplier;
+
+ ///
+ /// Multiplier used within cache buffers size calculation.
+ ///
+ ///
+ ///
+ /// Theoretically, bytes buffer can fit
+ /// exactly one minimal coding unit. In reality, coding blocks occupy much
+ /// less space than the theoretical maximum - this can be exploited.
+ /// If temporal buffer size is multiplied by at least 2, second half of
+ /// the resulting buffer will be used as an overflow 'guard' if next
+ /// block would occupy maximum number of bytes. While first half may fit
+ /// many blocks before needing to flush.
+ ///
+ ///
+ /// This is subject to change. This can be equal to 1 but recomended
+ /// value is 2 or even greater - futher benchmarking needed.
+ ///
+ ///
+ private const int MaxBytesPerBlockMultiplier = 2;
+
+ ///
+ /// size multiplier.
+ ///
+ ///
+ /// Jpeg specification requiers to insert 'stuff' bytes after each
+ /// 0xff byte value. Worst case scenarion is when all bytes are 0xff.
+ /// While it's highly unlikely (if not impossible) to get such
+ /// combination, it's theoretically possible so buffer size must be guarded.
+ ///
+ private const int OutputBufferLengthMultiplier = 2;
+
///
/// Compiled huffman tree to encode given values.
///
@@ -21,24 +66,23 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
private HuffmanLut[] huffmanTables;
///
- /// Number of bytes cached before being written to target stream via Stream.Write(byte[], offest, count).
+ /// Buffer for temporal storage of huffman rle encoding bit data.
///
///
- /// This is subject to change, 1024 seems to be the best value in terms of performance.
- /// expects it to be at least 8 (see comments in method body).
+ /// Encoding bits are assembled to 4 byte unsigned integers and then copied to this buffer.
+ /// This process does NOT include inserting stuff bytes.
///
- private const int EmitBufferSizeInBytes = 1024;
+ private readonly uint[] emitBuffer;
///
- /// A buffer for reducing the number of stream writes when emitting Huffman tables.
+ /// Buffer for temporal storage which is then written to the output stream.
///
- private readonly uint[] emitBuffer = new uint[EmitBufferSizeInBytes / 4];
-
- private readonly byte[] streamWriteBuffer = new byte[EmitBufferSizeInBytes * 2];
-
- private const int BytesPerCodingUnit = 256 * 3;
+ ///
+ /// Encoding bits from are copied to this byte buffer including stuff bytes.
+ ///
+ private readonly byte[] streamWriteBuffer;
- private int emitWriteIndex = (EmitBufferSizeInBytes / 4);
+ private int emitWriteIndex;
///
/// Emmited bits 'micro buffer' before being transfered to the .
@@ -58,11 +102,23 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
///
private readonly Stream target;
- public HuffmanScanEncoder(Stream outputStream)
+ public HuffmanScanEncoder(int componentCount, Stream outputStream)
{
+ int emitBufferByteLength = MaxBytesPerBlock * componentCount;
+ this.emitBuffer = new uint[emitBufferByteLength / sizeof(uint)];
+ this.emitWriteIndex = this.emitBuffer.Length;
+
+ this.streamWriteBuffer = new byte[emitBufferByteLength * OutputBufferLengthMultiplier];
+
this.target = outputStream;
}
+ private bool IsFlushNeeded
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => this.emitWriteIndex < this.emitBuffer.Length / 2;
+ }
+
///
/// Encodes the image with no subsampling.
///
@@ -117,14 +173,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
ref chrominanceQuantTable,
ref unzig);
- if (this.emitWriteIndex < this.emitBuffer.Length / 2)
+ if (this.IsFlushNeeded)
{
- this.WriteToStream();
+ this.FlushToStream();
}
}
}
- this.EmitFinalBits();
+ this.FlushRemainingBytes();
}
///
@@ -190,10 +246,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
ref pixelConverter.Cr,
ref chrominanceQuantTable,
ref unzig);
+
+ if (this.IsFlushNeeded)
+ {
+ this.FlushToStream();
+ }
}
}
- this.FlushInternalBuffer();
+ this.FlushRemainingBytes();
}
///
@@ -233,10 +294,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
ref pixelConverter.Y,
ref luminanceQuantTable,
ref unzig);
+
+ if (this.IsFlushNeeded)
+ {
+ this.FlushToStream();
+ }
}
}
- this.FlushInternalBuffer();
+ this.FlushRemainingBytes();
}
///
@@ -306,7 +372,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
}
[MethodImpl(InliningOptions.ShortMethod)]
- private void EmitFinalBits()
+ private void FlushRemainingBytes()
{
// Bytes count we want to write to the output stream
int valuableBytesCount = (int)Numerics.DivideCeil((uint)this.bitCount, 8);
@@ -317,7 +383,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
int writeIndex = this.emitWriteIndex;
this.emitBuffer[writeIndex - 1] = packedBytes;
- this.WriteToStream((writeIndex * 4) - valuableBytesCount);
+ this.FlushToStream((writeIndex * 4) - valuableBytesCount);
}
///
@@ -391,21 +457,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
this.Emit(prefix | (encodedValue >> prefixLen), prefixLen + valueLen);
}
- ///
- /// Writes remaining bytes from internal buffer to the target stream.
- ///
- /// Pads last byte with 1's if necessary
- private void FlushInternalBuffer()
- {
- // pad last byte with 1's
- //int padBitsCount = 8 - (this.bitCount % 8);
- //if (padBitsCount != 0)
- //{
- // this.Emit((1 << padBitsCount) - 1, padBitsCount);
- // this.target.Write(this.emitBuffer, 0, this.emitLen);
- //}
- }
-
///
/// Calculates how many minimum bits needed to store given value for Huffman jpeg encoding.
///
@@ -442,10 +493,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
}
[MethodImpl(InliningOptions.ShortMethod)]
- private void WriteToStream() => this.WriteToStream(this.emitWriteIndex * 4);
+ private void FlushToStream() => this.FlushToStream(this.emitWriteIndex * 4);
[MethodImpl(InliningOptions.ShortMethod)]
- private void WriteToStream(int endIndex)
+ private void FlushToStream(int endIndex)
{
Span emitBytes = MemoryMarshal.AsBytes(this.emitBuffer.AsSpan());
diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
index 88d96f554d..8c6726e65e 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
@@ -114,11 +114,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.WriteStartOfScan(image, componentCount, cancellationToken);
// Write the scan compressed data.
- var scanEncoder = new HuffmanScanEncoder(stream);
if (this.colorType == JpegColorType.Luminance)
{
// luminance quantization table only
- scanEncoder.EncodeGrayscale(image, ref luminanceQuantTable, cancellationToken);
+ new HuffmanScanEncoder(1, stream).EncodeGrayscale(image, ref luminanceQuantTable, cancellationToken);
}
else
{
@@ -126,10 +125,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
switch (this.subsample)
{
case JpegSubsample.Ratio444:
- scanEncoder.Encode444(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken);
+ new HuffmanScanEncoder(3, stream).Encode444(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken);
break;
case JpegSubsample.Ratio420:
- scanEncoder.Encode420(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken);
+ new HuffmanScanEncoder(6, stream).Encode420(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken);
break;
}
}