Skip to content

Fix chlist attribute size and scan line offset table - #112

Merged
ds5678 merged 1 commit into
AssetRipper:masterfrom
ShiyumeMeguri:fix-exr-chlist-size-and-offset-table
Aug 14, 2026
Merged

Fix chlist attribute size and scan line offset table#112
ds5678 merged 1 commit into
AssetRipper:masterfrom
ShiyumeMeguri:fix-exr-chlist-size-and-offset-table

Conversation

@ShiyumeMeguri

Copy link
Copy Markdown
Contributor

Fix chlist attribute size and scan line offset table

Two off-by-N errors in ExrWriter produce files that violate the
OpenEXR file layout.
Both are single-line fixes. Pixel data is unaffected.

1. ExrChannelList.Size omits the terminating null byte

The spec defines a chlist value as "A sequence of channels followed by a null byte
(0x00)", and attribute size as "the size (in bytes) of the attribute value".
Size only summed the channels, while Write also emits the null byte, so the type
disagrees with itself: it reports 18 bytes for a one-channel list and writes 19.

libOpenEXR does not notice, because its chlist readValueFrom ignores size and
loops until it reads an empty channel name. A reader that skips attributes by their
declared size instead lands on that null byte, reads it as an empty attribute name,
and ends the header after the first attribute.

2. The scan line offset table skips the y coordinate

The spec defines a regular scan line block as "y coordinate | pixel data size | pixel
data", and each offset table entry as "the distance, in bytes, between the start of the
file and the start of the chunk". The table advanced by sizeof(int) + pixelData,
leaving out the y coordinate, so entry n points 4*n bytes past the start of its
block. This is the one that makes files fail to open.

Reproduction

Single file, no test assets, no project to add. Drop it in the repository root and run
dotnet run ExrWriterRepro.cs (requires the .NET 10 SDK's file-based app support).

#:project AssetRipper.TextureDecoder/AssetRipper.TextureDecoder.csproj

using AssetRipper.TextureDecoder.Exr;
using AssetRipper.TextureDecoder.Rgb.Formats;

public static class ExrWriterRepro
{
	private const int Width = 2;
	private const int Height = 2;
	private const int ChannelCount = 4;

	public static void Main()
	{
		ReportChannelListSize();
		ReportScanLineOffsets();
	}

	private static void ReportChannelListSize()
	{
		ExrChannel channel = new ExrChannel("R", ExrPixelType.Single, false, 1, 1);
		ExrChannelList channelList = new ExrChannelList(new ExrChannel[] { channel });

		MemoryStream stream = new MemoryStream();
		BinaryWriter writer = new BinaryWriter(stream);
		channelList.Write(writer);
		writer.Flush();

		Console.WriteLine($"chlist  : Size reports {channelList.Size}, Write emits {stream.Length}");
	}

	private static void ReportScanLineOffsets()
	{
		MemoryStream stream = new MemoryStream();
		ExrWriter.Write<ColorRGBA<float>, float>(stream, Width, Height, new ColorRGBA<float>[Width * Height]);
		byte[] exr = stream.ToArray();

		int scanLineDataSize = ChannelCount * sizeof(float) * Width;
		int scanLineBlockSize = sizeof(int) + sizeof(int) + scanLineDataSize;
		int offsetTablePosition = exr.Length - (Height * sizeof(long)) - (Height * scanLineBlockSize);

		long firstOffset = BitConverter.ToInt64(exr, offsetTablePosition);
		long secondOffset = BitConverter.ToInt64(exr, offsetTablePosition + sizeof(long));
		int yAtSecondOffset = BitConverter.ToInt32(exr, (int)secondOffset);

		Console.WriteLine($"offsets : table stride {secondOffset - firstOffset}, scan line block {scanLineBlockSize}");
		Console.WriteLine($"offsets : y stored at offset[1] is {yAtSecondOffset}, expected 1");
	}
}

Before:

chlist  : Size reports 18, Write emits 19
offsets : table stride 36, scan line block 40
offsets : y stored at offset[1] is 0, expected 1

After:

chlist  : Size reports 19, Write emits 19
offsets : table stride 40, scan line block 40
offsets : y stored at offset[1] is 1, expected 1

How I hit it

Exporting a Unity HDR cubemap (128x768, 4 channels, half, so 1024-byte scan lines) as
EXR. Blender refused to open the result: the offset table stride was 1028 where the
blocks are 1032 apart, and the chlist attribute declared 72 bytes where 73 are
written. Both reproduce with the generated image above, so nothing about the source
texture is involved.

OpenEXRFileLayout defines a chlist attribute value as "A sequence of channels
followed by a null byte (0x00)", and the attribute size as "the size (in bytes)
of the attribute value". Size left out the terminating null byte, so a reader
that trusts the declared size lands on that byte and reads it as an empty
attribute name, which terminates the header after the first attribute.

The same document defines a regular scan line block as "y coordinate | pixel
data size | pixel data", and an offset table entry as "the distance, in bytes,
between the start of the file and the start of the chunk". The table advanced
by one int plus the pixel data, leaving out the y coordinate, so every entry
after the first pointed four bytes past the start of its chunk.

https://openexr.com/en/latest/OpenEXRFileLayout.html
@ds5678
ds5678 merged commit 0fb5e24 into AssetRipper:master Aug 14, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants