-
-
Notifications
You must be signed in to change notification settings - Fork 893
Add support for OpenEXR image format #3096
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 76 commits
9dda678
a27696a
d3993f7
aaf2143
799dafe
4cf95a5
7e7472d
652b385
70cfcc9
3279762
85501f3
69c8cd8
fdc7b61
fd0c245
88a06e8
aabe561
440408b
ea1aaa7
ce21f1a
a9620ee
ec8163a
28dcdf2
94cd5b0
d0d5e86
7748467
fa52ab7
83dba58
b78402d
faf87fb
eaea042
490d4a0
d121039
1b97d8c
e4c22d8
1abfe69
fc40209
a0eaefb
b79ba21
51f3ab2
3b7aa4d
137ebbb
3ff9d24
0d51514
614a7f6
5ee52ef
f212af8
f9df911
a3349ef
9e80da9
d409a3b
d5f4e23
7cc73d9
6a92b0c
355d1bc
c0e3d28
3e7ef68
d29fe89
9ead3eb
aa0af92
53230af
a741e84
8c0c854
c8c3656
b956634
a9f6dbc
7939cc9
a1152c5
65df7c2
0c3ca3d
e6a9980
802e275
fccd5b5
84182c5
0aeaaf7
1853289
2916204
05c89f7
d1623fe
787ebb2
3fbf0ef
0ec523f
e611b1e
24dbf63
2abbedc
fb32977
51463a3
b4cd27f
5926455
0ceeba4
29f558a
b15625e
3722997
15701e5
af14463
1e69aa4
ce545b7
d98ffac
00db2e9
0390442
758896e
6b486e3
f96d287
dd395e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this correct?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no, but is also not needed. I made this similar to how the TiffCompressors are defined, but currently the compression property is not used. I have removed it because of that. |
||
|
|
||
| /// <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(); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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)?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@brianpopow actually, I don't understand this.
From32BitTo8Bit(uint.MaxValue) == 1. Are you sure this logic is correct? If it's incorrect, I wonder why tests didn't catch it? Wouldn't(byte)(component >> 16)do the job?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@antonfirsov Yes I think you are right, this does not look right.
There are no pixel conversion tests yet for the new pixel types. I will try to add tests for the new pixlel types
Rgb96andRgba128next.Yeah that's more like it, but I think it should be
(byte)(component >> 24)? Lets say we have for exampleInt.Max / 2, e.g. 2147483647. Converting this to 8 bit should be 127.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added tests for the pixel conversion methods for the new pixel types now. There was some issues with the conversions, I think they are fixed now for the most part (they need a review though).
The only thing I could not figure out yet is an issue with
FromVector4()in the new pixel types.Currently it looks like this (Similar to how ``Rgb48 does it`):
Note: Max is here uint.Max.
The issue is here with the edge case of the maximum values. For example when source is (1.0, 1.0, 1.0, 1.0)
Then source.X will become uint.Max as a float value and casting this back to
uintwill result in 0 with .net8.0.This seems to be changed with .net 9.0: fp-to-integer
Has anyone an idea howto do this correctly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a comment and an implementation for
From32BitTo8Bitand the current one truncates.I'll have a look at
FromVector4in the new pixel typesThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I've fixed it. Managed to spot the issue via the debugger. You can't use float at
uint.MaxValuescale for your constant as there is not enough precision. Switching to double made the tests pass on my machine (and hopefully the CI). I've added an explanatory comment to the code.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
great that you found and fixed the issue with it!