Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
9dda678
Parse open EXR header
brianpopow Dec 26, 2021
a27696a
Decode uncompressed EXR images with 16 bit pixel data for each channel
brianpopow Dec 28, 2021
d3993f7
Decode EXR images with 32 bit float pixel data
brianpopow Dec 28, 2021
aaf2143
Decode EXR images with unsigned int pixel data
brianpopow Dec 29, 2021
799dafe
Encode EXR image with float pixel data
brianpopow Dec 31, 2021
4cf95a5
Encode EXR image with HalfSingle pixel data
brianpopow Jan 1, 2022
7e7472d
Encode EXR image with usigned int pixel data
brianpopow Jan 1, 2022
652b385
Update tests for EXR images
brianpopow Jan 2, 2022
70cfcc9
Add support for decoding EXR images with alpha channel
brianpopow Jan 3, 2022
3279762
Add support for decoding exr with zips compression
brianpopow Jan 21, 2022
85501f3
Add support for decoding RLE exr images
brianpopow Jan 21, 2022
69c8cd8
Add support for ZIP compressed exr images
brianpopow Jan 22, 2022
fdc7b61
Add support for decoding gray exr images
brianpopow Jan 23, 2022
fd0c245
Refactor
brianpopow Jan 27, 2022
88a06e8
Merge remote-tracking branch 'origin/main' into bp/openExr
brianpopow Feb 17, 2022
aabe561
Merge remote-tracking branch 'origin/main' into bp/openExr
brianpopow Aug 10, 2022
440408b
Merge remote-tracking branch 'origin/main' into bp/openExr
brianpopow Sep 17, 2022
ea1aaa7
Fix build errors
brianpopow Sep 18, 2022
ce21f1a
Add exr decoder tests
brianpopow Sep 20, 2022
a9620ee
Add support for decoding b44 compressed exr files
brianpopow Sep 23, 2022
ec8163a
Fix warnings
brianpopow Sep 23, 2022
28dcdf2
Add DecodeTiledFloatingPointPixelData
brianpopow Apr 7, 2023
94cd5b0
Merge remote-tracking branch 'origin/main' into bp/openExr
brianpopow Apr 7, 2023
d0d5e86
Merge remote-tracking branch 'origin/bp/openExr' into bp/openExr
brianpopow Apr 7, 2023
7748467
Merge branch 'main' into bp/openExr
brianpopow Mar 12, 2026
fa52ab7
Adjust exr decoder / encoder to ImageSharp changes
brianpopow Mar 14, 2026
83dba58
Change exr name space to SixLabors.ImageSharp.Formats.Exr
brianpopow Mar 14, 2026
b78402d
Fix issue in ExrImageFormatDetector.cs returning wrong format
brianpopow Mar 14, 2026
faf87fb
Use MagickReference decoder for exr decoding tests
brianpopow Mar 14, 2026
eaea042
Fix issue assigning pixel value from HalfVector4
brianpopow Mar 15, 2026
490d4a0
Fix mistake in Identify
brianpopow Mar 15, 2026
d121039
Add test for Identify
brianpopow Mar 15, 2026
1b97d8c
Smaller test files
brianpopow Mar 15, 2026
e4c22d8
Add tests for decode RGB and gray exr files
brianpopow Mar 15, 2026
1abfe69
Add test image for decoding exr with float piyel type
brianpopow Mar 16, 2026
fc40209
Test case for pixel type uint
brianpopow Mar 16, 2026
a0eaefb
Try fix decode unsigned int pixel data
brianpopow Mar 17, 2026
b79ba21
Fix not using offset when accessing decompressedPixelData
brianpopow Mar 17, 2026
51f3ab2
Fix test image for float32: it is now actually uncompressed
brianpopow Mar 17, 2026
3b7aa4d
Move constants to own folder
brianpopow Mar 19, 2026
137ebbb
Add Exr compressor factory
brianpopow Mar 19, 2026
3ff9d24
Refactor exr encoder for compression
brianpopow Mar 21, 2026
0d51514
Use compressor when writing pixel row data with exr encoder
brianpopow Mar 21, 2026
614a7f6
Re-arrange pixeldata and add predictor for zip compressed data
brianpopow Mar 22, 2026
5ee52ef
Fix issue in re-ordering pixel value, does not work inplace
brianpopow Mar 22, 2026
f212af8
Cleanup Compressor/Decompressors: Remove not needed fields
brianpopow Mar 23, 2026
f9df911
Add support for writing exr with zip compression with 16 rows per block
brianpopow Mar 23, 2026
a3349ef
Add ExrEncoder tests
brianpopow Mar 24, 2026
9e80da9
Try fix issue with undisposed buffers
brianpopow Mar 24, 2026
d409a3b
Use magick Reference decoder
brianpopow Mar 24, 2026
d5f4e23
Avoid code duplication for CalculateBytesPerRow and RowsPerBlock
brianpopow Mar 25, 2026
7cc73d9
Fix issue decoding last row block with zip compression
brianpopow Mar 25, 2026
6a92b0c
Implement missing Rgb96 methods
brianpopow Mar 26, 2026
355d1bc
Implement missing methods for Rgba128
brianpopow Mar 26, 2026
c0e3d28
Use cancellation token, when decoding exr images
brianpopow Mar 26, 2026
3e7ef68
Use cancellation token when encoding exr images
brianpopow Mar 26, 2026
d29fe89
Add benchmark for decoding exr image
brianpopow Mar 27, 2026
9ead3eb
Update Magick.NET-Q16 to version 14.11.1 for fix to decoding zip comp…
brianpopow Mar 27, 2026
aa0af92
Use CompareToReferenceOutput for int pixel type
brianpopow Mar 27, 2026
53230af
Use tolerant comparer for B44 compression
brianpopow Mar 27, 2026
a741e84
Use exact comparer as default for exr decoder tests
brianpopow Mar 27, 2026
8c0c854
Set exr pixel type when decoding an image
brianpopow Mar 27, 2026
c8c3656
Implement GetPixelTypeInfo() for EXR
brianpopow Mar 28, 2026
b956634
Add tests for exr metadata
brianpopow Mar 29, 2026
a9f6dbc
Add compression to exr metadata
brianpopow Mar 29, 2026
7939cc9
Throw InvalidImageContentException when requiered header fields are m…
brianpopow Mar 29, 2026
a1152c5
Add some more comments
brianpopow Mar 29, 2026
65df7c2
Merge branch 'main' into bp/openExr
brianpopow Mar 29, 2026
0c3ca3d
Fix failing bmp tests
brianpopow Mar 30, 2026
e6a9980
Change expectedDefaultConfigurationCount to 12
brianpopow Mar 30, 2026
802e275
Use CompareToReferenceOutput for failing bmp tests on linux
brianpopow Mar 30, 2026
fccd5b5
Remove unused variables
brianpopow Mar 30, 2026
84182c5
Remove more unused variables
brianpopow Mar 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ImageSharp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{815C0625-CD3
ProjectSection(SolutionItems) = preProject
src\Directory.Build.props = src\Directory.Build.props
src\Directory.Build.targets = src\Directory.Build.targets
src\README.md = src\README.md
src\ImageSharp.ruleset = src\ImageSharp.ruleset
src\README.md = src\README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp", "src\ImageSharp\ImageSharp.csproj", "{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}"
Expand Down Expand Up @@ -215,6 +215,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "issues", "issues", "{5C9B68
ProjectSection(SolutionItems) = preProject
tests\Images\Input\Jpg\issues\issue-1076-invalid-subsampling.jpg = tests\Images\Input\Jpg\issues\issue-1076-invalid-subsampling.jpg
tests\Images\Input\Jpg\issues\issue-1221-identify-multi-frame.jpg = tests\Images\Input\Jpg\issues\issue-1221-identify-multi-frame.jpg
tests\Images\Input\Jpg\issues\issue-2067-comment.jpg = tests\Images\Input\Jpg\issues\issue-2067-comment.jpg
tests\Images\Input\Jpg\issues\issue1006-incorrect-resize.jpg = tests\Images\Input\Jpg\issues\issue1006-incorrect-resize.jpg
tests\Images\Input\Jpg\issues\issue1049-exif-resize.jpg = tests\Images\Input\Jpg\issues\issue1049-exif-resize.jpg
tests\Images\Input\Jpg\issues\Issue159-MissingFF00-Progressive-Bedroom.jpg = tests\Images\Input\Jpg\issues\Issue159-MissingFF00-Progressive-Bedroom.jpg
Expand All @@ -238,7 +239,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "issues", "issues", "{5C9B68
tests\Images\Input\Jpg\issues\issue750-exif-tranform.jpg = tests\Images\Input\Jpg\issues\issue750-exif-tranform.jpg
tests\Images\Input\Jpg\issues\Issue845-Incorrect-Quality99.jpg = tests\Images\Input\Jpg\issues\Issue845-Incorrect-Quality99.jpg
tests\Images\Input\Jpg\issues\issue855-incorrect-colorspace.jpg = tests\Images\Input\Jpg\issues\issue855-incorrect-colorspace.jpg
tests\Images\Input\Jpg\issues\issue-2067-comment.jpg = tests\Images\Input\Jpg\issues\issue-2067-comment.jpg
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "fuzz", "fuzz", "{516A3532-6AC2-417B-AD79-9BD5D0D378A0}"
Expand Down
11 changes: 11 additions & 0 deletions src/ImageSharp/Common/Helpers/ColorNumerics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ namespace SixLabors.ImageSharp;
/// </summary>
internal static class ColorNumerics
{
private const float Scale32Bit = 1f / 0xFFFFFFFF;

/// <summary>
/// Vector for converting pixel to gray value as specified by
/// ITU-R Recommendation BT.709.
Expand Down Expand Up @@ -132,6 +134,15 @@ public static byte From16BitTo8Bit(ushort component) =>
// (V * 255 + 32895) >> 16
(byte)(((component * 255) + 32895) >> 16);

/// <summary>
/// Scales a value from an 32 bit <see cref="uint"/> to
/// an 8 bit <see cref="byte"/> equivalent.
/// </summary>
/// <param name="component">The 32 bit component value.</param>
/// <returns>The <see cref="byte"/> value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte From32BitTo8Bit(uint component) => (byte)(component * Scale32Bit);
Copy link
Copy Markdown
Member

@antonfirsov antonfirsov Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perf is maybe not a primary concern at this point but I wonder if there is a cheaper way to correctly calculate this that avoids FP conversion? How does the reference implementation calculate this (if they care about decoding into lower res format)?


/// <summary>
/// Scales a value from an 8 bit <see cref="byte"/> to
/// an 16 bit <see cref="ushort"/> equivalent.
Expand Down
3 changes: 3 additions & 0 deletions src/ImageSharp/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Exr;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.Formats.Jpeg;
Expand Down Expand Up @@ -212,6 +213,7 @@ public void Configure(IImageFormatConfigurationModule configuration)
/// <see cref="TgaConfigurationModule"/>.
/// <see cref="TiffConfigurationModule"/>.
/// <see cref="WebpConfigurationModule"/>.
/// <see cref="ExrConfigurationModule"/>.
/// <see cref="QoiConfigurationModule"/>.
/// </summary>
/// <returns>The default configuration of <see cref="Configuration"/>.</returns>
Expand All @@ -224,6 +226,7 @@ public void Configure(IImageFormatConfigurationModule configuration)
new TgaConfigurationModule(),
new TiffConfigurationModule(),
new WebpConfigurationModule(),
new ExrConfigurationModule(),
new QoiConfigurationModule(),
new IcoConfigurationModule(),
new CurConfigurationModule());
Expand Down
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/Bmp/BmpConstants.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

namespace SixLabors.ImageSharp.Formats.Bmp;
Expand Down
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/Bmp/BmpFormat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@ private BmpFormat()
public IEnumerable<string> FileExtensions => BmpConstants.FileExtensions;

/// <inheritdoc/>
public BmpMetadata CreateDefaultFormatMetadata() => new();
public BmpMetadata CreateDefaultFormatMetadata() => new BmpMetadata();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using SixLabors.ImageSharp.Formats.Exr.Constants;
using SixLabors.ImageSharp.Memory;

namespace SixLabors.ImageSharp.Formats.Exr.Compression.Compressors;

internal class NoneExrCompressor : ExrBaseCompressor
{
public NoneExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
: base(output, allocator, bytesPerBlock, bytesPerRow)
{
}

/// <inheritdoc/>
public override ExrCompression Method => ExrCompression.Zip;

/// <inheritdoc/>
public override uint CompressRowBlock(Span<byte> rows, int rowCount)
{
this.Output.Write(rows);
return (uint)rows.Length;
}

/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Exr.Constants;
using SixLabors.ImageSharp.Memory;

namespace SixLabors.ImageSharp.Formats.Exr.Compression.Compressors;

internal class ZipExrCompressor : ExrBaseCompressor
{
private readonly DeflateCompressionLevel compressionLevel;

private readonly MemoryStream memoryStream;

private readonly System.Buffers.IMemoryOwner<byte> buffer;

public ZipExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, DeflateCompressionLevel compressionLevel)
: base(output, allocator, bytesPerBlock, bytesPerRow)
{
this.compressionLevel = compressionLevel;
this.buffer = allocator.Allocate<byte>((int)bytesPerBlock);
this.memoryStream = new();
}

/// <inheritdoc/>
public override ExrCompression Method => ExrCompression.Zip;

/// <inheritdoc/>
public override uint CompressRowBlock(Span<byte> rows, int rowCount)
{
// Re-oder pixel values.
Span<byte> reordered = this.buffer.GetSpan()[..(int)(rowCount * this.BytesPerRow)];
int n = reordered.Length;
int t1 = 0;
int t2 = (n + 1) >> 1;
for (int i = 0; i < n; i++)
{
bool isOdd = (i & 1) == 1;
reordered[isOdd ? t2++ : t1++] = rows[i];
}

// Predictor.
Span<byte> predicted = reordered;
byte p = predicted[0];
for (int i = 1; i < predicted.Length; i++)
{
int d = (predicted[i] - p + 128 + 256) & 255;
p = predicted[i];
predicted[i] = (byte)d;
}

this.memoryStream.Seek(0, SeekOrigin.Begin);
using (ZlibDeflateStream stream = new(this.Allocator, this.memoryStream, this.compressionLevel))
{
stream.Write(predicted);
stream.Flush();
}

int size = (int)this.memoryStream.Position;
byte[] buffer = this.memoryStream.GetBuffer();
this.Output.Write(buffer, 0, size);

// Reset memory stream for next pixel row.
this.memoryStream.Seek(0, SeekOrigin.Begin);
this.memoryStream.SetLength(0);

return (uint)size;
}

/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
this.buffer.Dispose();
this.memoryStream?.Dispose();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Buffers;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;

namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors;

internal class B44ExrCompression : ExrBaseDecompressor
{
private readonly int width;

private readonly uint rowsPerBlock;

private readonly int channelCount;

private readonly byte[] scratch = new byte[14];

private readonly ushort[] s = new ushort[16];

private readonly IMemoryOwner<ushort> tmpBuffer;

public B44ExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, int channelCount)
: base(allocator, bytesPerBlock, bytesPerRow)
{
this.width = width;
this.rowsPerBlock = rowsPerBlock;
this.channelCount = channelCount;
this.tmpBuffer = allocator.Allocate<ushort>((int)(width * rowsPerBlock * channelCount));
}

/// <inheritdoc/>
public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer)
{
Span<ushort> outputBuffer = MemoryMarshal.Cast<byte, ushort>(buffer);
Span<ushort> decompressed = this.tmpBuffer.GetSpan();
int outputOffset = 0;
int bytesLeft = (int)compressedBytes;
for (int i = 0; i < this.channelCount && bytesLeft > 0; i++)
{
for (int y = 0; y < this.rowsPerBlock; y += 4)
{
Span<ushort> row0 = decompressed.Slice(outputOffset, this.width);
outputOffset += this.width;
Span<ushort> row1 = decompressed.Slice(outputOffset, this.width);
outputOffset += this.width;
Span<ushort> row2 = decompressed.Slice(outputOffset, this.width);
outputOffset += this.width;
Span<ushort> row3 = decompressed.Slice(outputOffset, this.width);
outputOffset += this.width;

int rowOffset = 0;
for (int x = 0; x < this.width && bytesLeft > 0; x += 4)
{
int bytesRead = stream.Read(this.scratch, 0, 3);
if (bytesRead == 0)
{
ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream");
}

if (this.scratch[2] >= 13 << 2)
{
Unpack3(this.scratch, this.s);
bytesLeft -= 3;
}
else
{
bytesRead = stream.Read(this.scratch, 3, 11);
if (bytesRead == 0)
{
ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream");
}

Unpack14(this.scratch, this.s);
bytesLeft -= 14;
}

int n = x + 3 < this.width ? 4 : this.width - x;
if (y + 3 < this.rowsPerBlock)
{
this.s.AsSpan(0, n).CopyTo(row0.Slice(rowOffset));
this.s.AsSpan(4, n).CopyTo(row1.Slice(rowOffset));
this.s.AsSpan(8, n).CopyTo(row2.Slice(rowOffset));
this.s.AsSpan(12, n).CopyTo(row3.Slice(rowOffset));
}
else
{
this.s.AsSpan(0, n).CopyTo(row0.Slice(rowOffset));
if (y + 1 < this.rowsPerBlock)
{
this.s.AsSpan(4, n).CopyTo(row1.Slice(rowOffset));
}

if (y + 2 < this.rowsPerBlock)
{
this.s.AsSpan(8, n).CopyTo(row2.Slice(rowOffset));
}
}

rowOffset += 4;
}

if (bytesLeft <= 0)
{
break;
}
}
}

// Rearrange the decompressed data such that the data for each scan line form a contiguous block.
int offsetDecompressed = 0;
int offsetOutput = 0;
int blockSize = (int)(this.width * this.rowsPerBlock);
for (int y = 0; y < this.rowsPerBlock; y++)
{
for (int i = 0; i < this.channelCount; i++)
{
decompressed.Slice(offsetDecompressed + (i * blockSize), this.width).CopyTo(outputBuffer.Slice(offsetOutput));
offsetOutput += this.width;
}

offsetDecompressed += this.width;
}
}

// Unpack a 14-byte block into 4 by 4 16-bit pixels.
private static void Unpack14(Span<byte> b, Span<ushort> s)
{
s[0] = (ushort)((b[0] << 8) | b[1]);

ushort shift = (ushort)(b[2] >> 2);
ushort bias = (ushort)(0x20u << shift);

s[4] = (ushort)(s[0] + ((((b[2] << 4) | (b[3] >> 4)) & 0x3fu) << shift) - bias);
s[8] = (ushort)(s[4] + ((((b[3] << 2) | (b[4] >> 6)) & 0x3fu) << shift) - bias);
s[12] = (ushort)(s[8] + ((b[4] & 0x3fu) << shift) - bias);

s[1] = (ushort)(s[0] + ((uint)(b[5] >> 2) << shift) - bias);
s[5] = (ushort)(s[4] + ((((b[5] << 4) | (b[6] >> 4)) & 0x3fu) << shift) - bias);
s[9] = (ushort)(s[8] + ((((b[6] << 2) | (b[7] >> 6)) & 0x3fu) << shift) - bias);
s[13] = (ushort)(s[12] + ((b[7] & 0x3fu) << shift) - bias);

s[2] = (ushort)(s[1] + ((uint)(b[8] >> 2) << shift) - bias);
s[6] = (ushort)(s[5] + ((((b[8] << 4) | (b[9] >> 4)) & 0x3fu) << shift) - bias);
s[10] = (ushort)(s[9] + ((((b[9] << 2) | (b[10] >> 6)) & 0x3fu) << shift) - bias);
s[14] = (ushort)(s[13] + ((b[10] & 0x3fu) << shift) - bias);

s[3] = (ushort)(s[2] + ((uint)(b[11] >> 2) << shift) - bias);
s[7] = (ushort)(s[6] + ((((b[11] << 4) | (b[12] >> 4)) & 0x3fu) << shift) - bias);
s[11] = (ushort)(s[10] + ((((b[12] << 2) | (b[13] >> 6)) & 0x3fu) << shift) - bias);
s[15] = (ushort)(s[14] + ((b[13] & 0x3fu) << shift) - bias);

for (int i = 0; i < 16; ++i)
{
if ((s[i] & 0x8000) != 0)
{
s[i] &= 0x7fff;
}
else
{
s[i] = (ushort)~s[i];
}
}
}

// Unpack a 3-byte block into 4 by 4 identical 16-bit pixels.
private static void Unpack3(Span<byte> b, Span<ushort> s)
{
s[0] = (ushort)((b[0] << 8) | b[1]);

if ((s[0] & 0x8000) != 0)
{
s[0] &= 0x7fff;
}
else
{
s[0] = (ushort)~s[0];
}

for (int i = 1; i < 16; ++i)
{
s[i] = s[0];
}
}

/// <inheritdoc/>
protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose();
}
Loading
Loading