From 0d515147d019f6539f453b10f9bfbedcc4331195 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 21 Mar 2026 19:57:10 +0100 Subject: [PATCH] Use compressor when writing pixel row data with exr encoder --- .../Compressors/NoneExrCompressor.cs | 6 +- .../Compressors/ZipExrCompressor.cs | 3 +- .../Exr/Compression/ExrCompressorFactory.cs | 2 + .../Formats/Exr/ExrBaseCompressor.cs | 5 +- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 2 +- src/ImageSharp/Formats/Exr/ExrEncoderCore.cs | 179 ++++++++++-------- 6 files changed, 110 insertions(+), 87 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs index e8f3a0686f..40ee81cf03 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs @@ -22,7 +22,11 @@ internal class NoneExrCompressor : ExrBaseCompressor } /// - public override void CompressStrip(Span rows, int height) => this.Output.Write(rows); + public override uint CompressRowBlock(Span rows, int height) + { + this.Output.Write(rows); + return (uint)rows.Length; + } /// protected override void Dispose(bool disposing) diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs index af93664f33..e7b695df0c 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs @@ -26,7 +26,7 @@ internal class ZipExrCompressor : ExrBaseCompressor } /// - public override void CompressStrip(Span rows, int height) + public override uint CompressRowBlock(Span rows, int height) { this.memoryStream.Seek(0, SeekOrigin.Begin); using (ZlibDeflateStream stream = new(this.Allocator, this.memoryStream, this.compressionLevel)) @@ -38,6 +38,7 @@ internal class ZipExrCompressor : ExrBaseCompressor int size = (int)this.memoryStream.Position; byte[] buffer = this.memoryStream.GetBuffer(); this.Output.Write(buffer, 0, size); + return (uint)buffer.Length; } /// diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs b/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs index 4482ca7633..e8e7af4712 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs @@ -25,6 +25,8 @@ internal static class ExrCompressorFactory { case ExrCompression.None: return new NoneExrCompressor(output, allocator, bytesPerBlock); + case ExrCompression.Zip: + return new ZipExrCompressor(output, allocator, bytesPerBlock, compressionLevel); default: throw ExrThrowHelper.NotSupportedCompressor(method.ToString()); diff --git a/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs b/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs index ee21acb703..cf547eddb9 100644 --- a/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs +++ b/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs @@ -35,9 +35,10 @@ internal abstract class ExrBaseCompressor : ExrBaseCompression public abstract void Initialize(int rowsPerBlock); /// - /// Compresses a strip of the image. + /// Compresses a block of rows of the image. /// /// Image rows to compress. /// Image height. - public abstract void CompressStrip(Span rows, int height); + /// Number of bytes of of the compressed data. + public abstract uint CompressRowBlock(Span rows, int height); } diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index c7c05a9c54..7165720d30 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -53,7 +53,7 @@ internal sealed class ExrDecoderCore : ImageDecoderCore : base(options.GeneralOptions) { this.configuration = options.GeneralOptions.Configuration; - this.memoryAllocator = this.configuration.MemoryAllocator; + this.memoryAllocator = this.configuration.MemoryAllocator; } /// diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs index 7151a642ab..9e63413445 100644 --- a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.Buffers.Binary; using System.Numerics; using System.Runtime.CompilerServices; +using System.Threading.Channels; using SixLabors.ImageSharp.Formats.Exr.Compression; using SixLabors.ImageSharp.Formats.Exr.Constants; using SixLabors.ImageSharp.Memory; @@ -54,8 +55,14 @@ internal sealed class ExrEncoderCore this.configuration = configuration; this.encoder = encoder; this.memoryAllocator = memoryAllocator; + this.Compression = encoder.Compression ?? ExrCompression.None; } + /// + /// Gets or sets the compression implementation to use when encoding the image. + /// + internal ExrCompression Compression { get; set; } + /// /// Encodes the image to the specified stream from the . /// @@ -76,7 +83,6 @@ internal sealed class ExrEncoderCore this.pixelType ??= exrMetadata.PixelType; int width = image.Width; int height = image.Height; - ExrCompression compression = ExrCompression.None; float aspectRatio = 1.0f; ExrBox2i dataWindow = new(0, 0, width - 1, height - 1); ExrBox2i displayWindow = new(0, 0, width - 1, height - 1); @@ -91,7 +97,7 @@ internal sealed class ExrEncoderCore ]; ExrHeaderAttributes header = new( channels, - compression, + this.Compression, dataWindow, displayWindow, lineOrder, @@ -115,31 +121,40 @@ internal sealed class ExrEncoderCore // Write EXR header. this.WriteHeader(stream, header); - // Write offsets to each pixel row. + // Next is offsets table to each pixel row, which will be written after the pixel data was written. int bytesPerChannel = this.pixelType == ExrPixelType.Half ? 2 : 4; int numberOfChannels = 3; uint rowSizeBytes = (uint)(width * numberOfChannels * bytesPerChannel); - this.WriteRowOffsets(stream, height, rowSizeBytes); + ulong startOfRowOffsetData = (ulong)stream.Position; + stream.Position += 8 * height; // Write pixel data. switch (this.pixelType) { case ExrPixelType.Half: case ExrPixelType.Float: - this.EncodeFloatingPointPixelData(stream, pixels, width, height, rowSizeBytes, channels, compression); + { + ulong[] rowOffsets = this.EncodeFloatingPointPixelData(stream, pixels, width, height, channels, this.Compression); + stream.Position = (long)startOfRowOffsetData; + this.WriteRowOffsets(stream, height, rowOffsets); break; + } + case ExrPixelType.UnsignedInt: - this.EncodeUnsignedIntPixelData(stream, pixels, width, height, rowSizeBytes); + { + ulong[] rowOffsets = this.EncodeUnsignedIntPixelData(stream, pixels, width, height, channels, this.Compression); + stream.Position = (long)startOfRowOffsetData; + this.WriteRowOffsets(stream, height, rowOffsets); break; + } } } - private void EncodeFloatingPointPixelData( + private ulong[] EncodeFloatingPointPixelData( Stream stream, Buffer2D pixels, int width, int height, - uint rowSizeBytes, List channels, ExrCompression compression) where TPixel : unmanaged, IPixel @@ -150,24 +165,26 @@ internal sealed class ExrEncoderCore int channelCount = channels.Count; using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 3, AllocationOptions.Clean); - using IMemoryOwner blockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean); + using IMemoryOwner rowBlockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean); Span redBuffer = rgbBuffer.GetSpan().Slice(0, width); Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width); Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, width, height, bytesPerBlock, rowsPerBlock, channelCount); + ulong[] rowOffsets = new ulong[height]; for (int y = 0; y < height; y++) { + rowOffsets[y] = (ulong)stream.Position; + // Write row index. BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)y); stream.Write(this.buffer.AsSpan(0, 4)); - // Write pixel row data size. - BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, rowSizeBytes); - stream.Write(this.buffer.AsSpan(0, 4)); - + // At this point, it is not yet known how mcuh bytes the compressed data will take up, keep stream position. + long pixelDataSizePos = stream.Position; Span pixelRowSpan = pixels.DangerousGetRowSpan(y); + stream.Position = pixelDataSizePos + 4; for (int x = 0; x < width; x++) { @@ -177,32 +194,67 @@ internal sealed class ExrEncoderCore blueBuffer[x] = vector4.Z; } + // Write pixel data to buffer. switch (this.pixelType) { case ExrPixelType.Float: - this.WriteSingleRow(blockBuffer.GetSpan(), width, blueBuffer, greenBuffer, redBuffer); + this.WriteSingleRow(rowBlockBuffer.GetSpan(), width, blueBuffer, greenBuffer, redBuffer); break; case ExrPixelType.Half: - this.WriteHalfSingleRow(blockBuffer.GetSpan(), width, blueBuffer, greenBuffer, redBuffer); + this.WriteHalfSingleRow(rowBlockBuffer.GetSpan(), width, blueBuffer, greenBuffer, redBuffer); break; } - compressor.CompressStrip(blockBuffer.GetSpan(), 1); + // Write compressed pixel row data to stream. + uint compressedBytes = compressor.CompressRowBlock(rowBlockBuffer.GetSpan(), 1); + long positionAfterPixelData = stream.Position; + + // Write pixel row data size. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, compressedBytes); + stream.Position = pixelDataSizePos; + stream.Write(this.buffer.AsSpan(0, 4)); + stream.Position = positionAfterPixelData; } + + return rowOffsets; } - private void EncodeUnsignedIntPixelData(Stream stream, Buffer2D pixels, int width, int height, uint rowSizeBytes) + private ulong[] EncodeUnsignedIntPixelData( + Stream stream, + Buffer2D pixels, + int width, + int height, + List channels, + ExrCompression compression) where TPixel : unmanaged, IPixel { - using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 3); + uint bytesPerRow = this.CalculateBytesPerRow(channels, (uint)width); + uint rowsPerBlock = this.RowsPerBlock(compression); + uint bytesPerBlock = bytesPerRow * rowsPerBlock; + int channelCount = channels.Count; + + using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 3, AllocationOptions.Clean); + using IMemoryOwner rowBlockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean); Span redBuffer = rgbBuffer.GetSpan().Slice(0, width); Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width); Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); + using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, width, height, bytesPerBlock, rowsPerBlock, channelCount); + Rgb96 rgb = default; + ulong[] rowOffsets = new ulong[height]; for (int y = 0; y < height; y++) { + rowOffsets[y] = (ulong)stream.Position; + + // Write row index. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)y); + stream.Write(this.buffer.AsSpan(0, 4)); + + // At this point, it is not yet known how mcuh bytes the compressed data will take up, keep stream position. + long pixelDataSizePos = stream.Position; Span pixelRowSpan = pixels.DangerousGetRowSpan(y); + stream.Position = pixelDataSizePos + 4; for (int x = 0; x < width; x++) { @@ -214,16 +266,21 @@ internal sealed class ExrEncoderCore blueBuffer[x] = rgb.B; } - // Write row index. - BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)y); - stream.Write(this.buffer.AsSpan(0, 4)); + // Write row data to buffer. + this.WriteUnsignedIntRow(rowBlockBuffer.GetSpan(), width, blueBuffer, greenBuffer, redBuffer); + + // Write pixel row data compressed to the stream. + uint compressedBytes = compressor.CompressRowBlock(rowBlockBuffer.GetSpan(), 1); + long positionAfterPixelData = stream.Position; // Write pixel row data size. - BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, rowSizeBytes); + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, compressedBytes); + stream.Position = pixelDataSizePos; stream.Write(this.buffer.AsSpan(0, 4)); - - this.WriteUnsignedIntRow(stream, width, blueBuffer, greenBuffer, redBuffer); + stream.Position = positionAfterPixelData; } + + return rowOffsets; } private void WriteHeader(Stream stream, ExrHeaderAttributes header) @@ -239,24 +296,6 @@ internal sealed class ExrEncoderCore stream.WriteByte(0); } - private void WriteSingleRow(Stream stream, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) - { - for (int x = 0; x < width; x++) - { - this.WriteSingle(stream, blueBuffer[x]); - } - - for (int x = 0; x < width; x++) - { - this.WriteSingle(stream, greenBuffer[x]); - } - - for (int x = 0; x < width; x++) - { - this.WriteSingle(stream, redBuffer[x]); - } - } - private void WriteSingleRow(Span buffer, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) { int offset = 0; @@ -301,51 +340,36 @@ internal sealed class ExrEncoderCore } } - private void WriteHalfSingleRow(Stream stream, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) - { - for (int x = 0; x < width; x++) - { - this.WriteHalfSingle(stream, blueBuffer[x]); - } - - for (int x = 0; x < width; x++) - { - this.WriteHalfSingle(stream, greenBuffer[x]); - } - - for (int x = 0; x < width; x++) - { - this.WriteHalfSingle(stream, redBuffer[x]); - } - } - - private void WriteUnsignedIntRow(Stream stream, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) + private void WriteUnsignedIntRow(Span buffer, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) { + int offset = 0; for (int x = 0; x < width; x++) { - this.WriteUnsignedInt(stream, blueBuffer[x]); + this.WriteUnsignedInt(buffer.Slice(offset), blueBuffer[x]); + offset += 4; } for (int x = 0; x < width; x++) { - this.WriteUnsignedInt(stream, greenBuffer[x]); + this.WriteUnsignedInt(buffer.Slice(offset), greenBuffer[x]); + offset += 4; } for (int x = 0; x < width; x++) { - this.WriteUnsignedInt(stream, redBuffer[x]); + this.WriteUnsignedInt(buffer.Slice(offset), redBuffer[x]); + offset += 4; } } - private void WriteRowOffsets(Stream stream, int height, uint rowSizeBytes) + private void WriteRowOffsets(Stream stream, int height, ulong[] rowOffsets) { - ulong startOfPixelData = (ulong)stream.Position + (8 * (ulong)height); - ulong offset = startOfPixelData; + ulong startOfRowOffsetData = (ulong)stream.Position; + ulong offset = startOfRowOffsetData; for (int i = 0; i < height; i++) { - BinaryPrimitives.WriteUInt64LittleEndian(this.buffer, offset); + BinaryPrimitives.WriteUInt64LittleEndian(this.buffer, rowOffsets[i]); stream.Write(this.buffer); - offset += 4 + 4 + rowSizeBytes; } } @@ -482,19 +506,7 @@ internal sealed class ExrEncoderCore } [MethodImpl(InliningOptions.ShortMethod)] - private unsafe void WriteSingle(Span buffer, float value) - { - BinaryPrimitives.WriteInt32LittleEndian(buffer, *(int*)&value); - } - - [MethodImpl(InliningOptions.ShortMethod)] - private void WriteHalfSingle(Stream stream, float value) - { - ushort valueAsShort = HalfTypeHelper.Pack(value); - BinaryPrimitives.WriteUInt16LittleEndian(this.buffer, valueAsShort); - stream.Write(this.buffer.AsSpan(0, 2)); - } - + private unsafe void WriteSingle(Span buffer, float value) => BinaryPrimitives.WriteInt32LittleEndian(buffer, *(int*)&value); [MethodImpl(InliningOptions.ShortMethod)] private void WriteHalfSingle(Span buffer, float value) @@ -510,6 +522,9 @@ internal sealed class ExrEncoderCore stream.Write(this.buffer.AsSpan(0, 4)); } + [MethodImpl(InliningOptions.ShortMethod)] + private void WriteUnsignedInt(Span buffer, uint value) => BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); + // TODO: avoid code duplication: This code is duplicate in the decoder. private uint CalculateBytesPerRow(List channels, uint width) {