OpenEXR writing

This commit is contained in:
Jeremy Pritts committed 2024-03-24 17:00:42 -07:00
1 parent 1957784de4
commit 8e82e702a7
21 files changed
+645 -12

No files matched your search

@@ -1,3 +1,4 @@
using AssetRipper.TextureDecoder.Exr;
using AssetRipper.TextureDecoder.Rgb;
using AssetRipper.TextureDecoder.Rgb.Channels;
using AssetRipper.TextureDecoder.Rgb.Formats;
@@ -55,6 +56,12 @@ public readonly struct DirectBitmap<TColor, TColorArg>
}
}
public void SaveAsExr(string path)
{
using Stream stream = File.OpenWrite(path);
ExrWriter.Write<TColor, TColorArg>(stream, Width, Height, Pixels);
}
public void SaveAsHdr(string path)
{
GetDataAndComponentsForSaving(out byte[] data, out ColorComponents components);
@@ -63,18 +63,32 @@ internal static partial class Program
switch (outputType)
{
case "png":
{
string newPath = Path.Combine(dirPath, name + ".png");
bitmap.FlipY();
bitmap.SaveAsPng(newPath);
}
break;
case "bgra":
{
string newPath = Path.Combine(dirPath, name + ".bgra");
WriteAllBytes(newPath, bitmap.Bits);
}
WriteAllBytes(Path.Combine(dirPath, name + ".bgra"), bitmap.Bits);
break;
case "bmp":
bitmap.FlipY();
bitmap.SaveAsBmp(Path.Combine(dirPath, name + ".bmp"));
break;
case "exr":
bitmap.FlipY();
bitmap.SaveAsExr(Path.Combine(dirPath, name + ".exr"));
break;
case "hdr":
bitmap.FlipY();
bitmap.SaveAsHdr(Path.Combine(dirPath, name + ".hdr"));
break;
case "jpg":
bitmap.FlipY();
bitmap.SaveAsJpg(Path.Combine(dirPath, name + ".jpg"));
break;
case "png":
bitmap.FlipY();
bitmap.SaveAsPng(Path.Combine(dirPath, name + ".png"));
break;
case "tga":
bitmap.FlipY();
bitmap.SaveAsTga(Path.Combine(dirPath, name + ".tga"));
break;
default:
throw new NotSupportedException(outputType);
@@ -239,4 +253,4 @@ internal static partial class Program
using Microsoft.Win32.SafeHandles.SafeFileHandle sfh = File.OpenHandle(path, FileMode.Create, FileAccess.Write, FileShare.Read);
RandomAccess.Write(sfh, bytes, 0);
}
}
}
@@ -0,0 +1,112 @@
using System.Buffers;
using System.Text;
namespace AssetRipper.TextureDecoder.Exr;
internal static class BinaryWriterExtensions
{
public static void WriteLengthPrefixedString(this BinaryWriter writer, string value)
{
int length = Encoding.UTF8.GetByteCount(value);
writer.Write(length);
byte[]? rentedArray;
scoped Span<byte> buffer;
if (length <= 1024)
{
rentedArray = null;
buffer = stackalloc byte[length];
}
else
{
rentedArray = ArrayPool<byte>.Shared.Rent(length);
buffer = rentedArray.AsSpan(0, length);
}
Encoding.UTF8.GetBytes(value, buffer);
writer.Write(buffer);
if (rentedArray is not null)
{
ArrayPool<byte>.Shared.Return(rentedArray);
}
}
public static void WriteNullTerminatedString(this BinaryWriter writer, string value)
{
int length = Encoding.UTF8.GetByteCount(value);
byte[]? rentedArray;
scoped Span<byte> buffer;
if (length <= 1024)
{
rentedArray = null;
buffer = stackalloc byte[length];
}
else
{
rentedArray = ArrayPool<byte>.Shared.Rent(length);
buffer = rentedArray.AsSpan(0, length);
}
Encoding.UTF8.GetBytes(value, buffer);
writer.Write(buffer);
if (rentedArray is not null)
{
ArrayPool<byte>.Shared.Return(rentedArray);
}
writer.WriteNullByte();
}
public static void WriteNullTerminatedString(this BinaryWriter writer, ReadOnlySpan<byte> utf8String)
{
writer.Write(utf8String);
writer.WriteNullByte();
}
public static void WriteNullByte(this BinaryWriter writer)
{
writer.Write(default(byte));
}
public static void Write<T>(this BinaryWriter writer, T value) where T : unmanaged, Enum
{
switch (Unsafe.SizeOf<T>())
{
case sizeof(byte):
writer.Write(Unsafe.As<T, byte>(ref value));
break;
case sizeof(ushort):
writer.Write(Unsafe.As<T, ushort>(ref value));
break;
case sizeof(uint):
writer.Write(Unsafe.As<T, uint>(ref value));
break;
case sizeof(ulong):
writer.Write(Unsafe.As<T, ulong>(ref value));
break;
}
}
public static void WriteAttribute<T>(this BinaryWriter writer, ReadOnlySpan<byte> attributeName, T attributeValue) where T : IExrDataType
{
writer.Write(attributeName);
writer.WriteNullByte();
writer.Write(T.TypeName);
writer.WriteNullByte();
writer.Write(attributeValue.Size);
attributeValue.Write(writer);
}
public static void WriteAttribute<T>(this BinaryWriter writer, ReadOnlySpan<byte> attributeName, ReadOnlySpan<byte> attributeType, T attributeValue) where T : unmanaged, Enum
{
writer.Write(attributeName);
writer.WriteNullByte();
writer.Write(attributeType);
writer.WriteNullByte();
writer.Write(Unsafe.SizeOf<T>());
writer.Write(attributeValue);
}
}
@@ -0,0 +1,22 @@
namespace AssetRipper.TextureDecoder.Exr;
public static class ExrAttributes
{
public static ReadOnlySpan<byte> Channels => "channels"u8;
public static ReadOnlySpan<byte> Compression => "compression"u8;
public static ReadOnlySpan<byte> DataWindow => "dataWindow"u8;
public static ReadOnlySpan<byte> DisplayWindow => "displayWindow"u8;
public static ReadOnlySpan<byte> LineOrder => "lineOrder"u8;
public static ReadOnlySpan<byte> PixelAspectRatio => "pixelAspectRatio"u8;
public static ReadOnlySpan<byte> ScreenWindowCenter => "screenWindowCenter"u8;
public static ReadOnlySpan<byte> ScreenWindowWidth => "screenWindowWidth"u8;
public static ReadOnlySpan<byte> Tiles => "tiles"u8;
}
@@ -0,0 +1,16 @@
namespace AssetRipper.TextureDecoder.Exr;
public readonly record struct ExrBox2F(float XMin, float YMin, float XMax, float YMax) : IExrDataType
{
public static ReadOnlySpan<byte> TypeName => "box2f"u8;
public int Size => sizeof(float) + sizeof(float) + sizeof(float) + sizeof(float);
public void Write(BinaryWriter writer)
{
writer.Write(XMin);
writer.Write(YMin);
writer.Write(XMax);
writer.Write(YMax);
}
}
@@ -0,0 +1,16 @@
namespace AssetRipper.TextureDecoder.Exr;
public readonly record struct ExrBox2I(int XMin, int YMin, int XMax, int YMax) : IExrDataType
{
public static ReadOnlySpan<byte> TypeName => "box2i"u8;
public int Size => sizeof(int) + sizeof(int) + sizeof(int) + sizeof(int);
public void Write(BinaryWriter writer)
{
writer.Write(XMin);
writer.Write(YMin);
writer.Write(XMax);
writer.Write(YMax);
}
}
@@ -0,0 +1,32 @@
using System.Text;
namespace AssetRipper.TextureDecoder.Exr;
public readonly record struct ExrChannel(string Name, ExrPixelType PixelType, bool PLinear, int XSampling, int YSampling)
{
public int Size
{
get
{
return Encoding.UTF8.GetByteCount(Name)
+ sizeof(byte)//Null terminator
+ sizeof(ExrPixelType)
+ sizeof(bool)
+ 3 * sizeof(byte)//Reserved bytes
+ sizeof(int)
+ sizeof(int);
}
}
public void Write(BinaryWriter writer)
{
writer.WriteNullTerminatedString(Name);
writer.Write(PixelType);
writer.Write(PLinear);
writer.WriteNullByte();
writer.WriteNullByte();
writer.WriteNullByte();
writer.Write(XSampling);
writer.Write(YSampling);
}
}
@@ -0,0 +1,17 @@
namespace AssetRipper.TextureDecoder.Exr;
public readonly record struct ExrChannelList(ExrChannel[] Channels) : IExrDataType
{
public static ReadOnlySpan<byte> TypeName => "chlist"u8;
public int Size => Channels.Sum(c => c.Size);
public void Write(BinaryWriter writer)
{
foreach (ExrChannel channel in Channels)
{
channel.Write(writer);
}
writer.WriteNullByte();
}
}
@@ -0,0 +1,13 @@
namespace AssetRipper.TextureDecoder.Exr;
public enum ExrCompression : byte
{
None = 0,
RLE = 1,
ZIPS = 2,
ZIP = 3,
PIZ = 4,
PXR24 = 5,
B44 = 6,
B44A = 7,
}
@@ -0,0 +1,17 @@
namespace AssetRipper.TextureDecoder.Exr;
public readonly record struct ExrDouble(double Value) : IExrDataType
{
public static ReadOnlySpan<byte> TypeName => "double"u8;
public int Size => sizeof(double);
public void Write(BinaryWriter writer)
{
writer.Write(Value);
}
public static implicit operator double(ExrDouble value) => value.Value;
public static implicit operator ExrDouble(double value) => new(value);
}
@@ -0,0 +1,14 @@
namespace AssetRipper.TextureDecoder.Exr;
public enum ExrImageFormat
{
ScanLines,
Tiles,
}
public static class ExrImageFormatExtensions
{
public static byte ToVersionByte(this ExrImageFormat format) => format switch
{
ExrImageFormat.Tiles => 0x02,
_ => 0x00,
};
}
@@ -0,0 +1,17 @@
namespace AssetRipper.TextureDecoder.Exr;
public readonly record struct ExrInt32(int Value) : IExrDataType
{
public static ReadOnlySpan<byte> TypeName => "int"u8;
public int Size => sizeof(int);
public void Write(BinaryWriter writer)
{
writer.Write(Value);
}
public static implicit operator int(ExrInt32 value) => value.Value;
public static implicit operator ExrInt32(int value) => new(value);
}
@@ -0,0 +1,11 @@
namespace AssetRipper.TextureDecoder.Exr;
[Flags]
public enum ExrLevelMode : byte
{
OneLevel = 0,
MipMapLevels = 1,
RipMapLevels = 2,
RoundDown = 0,
RoundUp = 16,
}
@@ -0,0 +1,8 @@
namespace AssetRipper.TextureDecoder.Exr;
public enum ExrLineOrder : byte
{
IncreasingY = 0,
DecreasingY = 1,
RandomY = 2,
}
@@ -0,0 +1,8 @@
namespace AssetRipper.TextureDecoder.Exr;
public enum ExrPixelType : int
{
UInt = 0,
Half = 1,
Single = 2,
}
@@ -0,0 +1,17 @@
namespace AssetRipper.TextureDecoder.Exr;
public readonly record struct ExrSingle(float Value) : IExrDataType
{
public static ReadOnlySpan<byte> TypeName => "float"u8;
public int Size => sizeof(float);
public void Write(BinaryWriter writer)
{
writer.Write(Value);
}
public static implicit operator float(ExrSingle value) => value.Value;
public static implicit operator ExrSingle(float value) => new(value);
}
@@ -0,0 +1,16 @@
using System.Text;
namespace AssetRipper.TextureDecoder.Exr;
public readonly record struct ExrString(string Value) : IExrDataType
{
public static ReadOnlySpan<byte> TypeName => "string"u8;
public int Size => sizeof(int) + Encoding.UTF8.GetByteCount(Value);
public void Write(BinaryWriter writer) => writer.WriteLengthPrefixedString(Value);
public static implicit operator string(ExrString value) => value.Value;
public static implicit operator ExrString(string value) => new(value);
}
@@ -0,0 +1,26 @@
namespace AssetRipper.TextureDecoder.Exr;
public readonly record struct ExrVector2(Vector2 Value) : IExrDataType
{
public ExrVector2(float x, float y) : this(new Vector2(x, y))
{
}
public int Size => sizeof(float) + sizeof(float);
public float X => Value.X;
public float Y => Value.Y;
public static ReadOnlySpan<byte> TypeName => "v2f"u8;
public void Write(BinaryWriter writer)
{
writer.Write(X);
writer.Write(Y);
}
public static implicit operator Vector2(ExrVector2 value) => value.Value;
public static implicit operator ExrVector2(Vector2 value) => new(value);
}
+225
View File
@@ -0,0 +1,225 @@
using AssetRipper.TextureDecoder.Rgb;
namespace AssetRipper.TextureDecoder.Exr;
public static class ExrWriter
{
public static ReadOnlySpan<byte> Magic => [0x76, 0x2f, 0x31, 0x01];
public static void Write<TColor, TChannelValue>(Stream stream, int width, int height, ReadOnlySpan<TColor> pixels)
where TChannelValue : unmanaged
where TColor : unmanaged, IColor<TChannelValue>
{
int ChannelCount = Color.GetChannelCount<TColor>();
ExrPixelType PixelType = Unsafe.SizeOf<TChannelValue>() <= sizeof(ushort) ? ExrPixelType.Half : ExrPixelType.Single;
int ChannelSize = PixelType is ExrPixelType.Half ? Unsafe.SizeOf<Half>() : sizeof(float);
using BinaryWriter writer = new BinaryWriter(stream);
//Magic bytes
writer.Write(Magic);
//Version bytes
writer.Write((byte)2);//OpenEXR version 2
writer.Write(ExrImageFormat.ScanLines.ToVersionByte());
writer.WriteNullByte();
writer.WriteNullByte();
ExrChannel[] channels;
{
Span<ExrChannel> buffer = [default, default, default, default];
//Stackalloc doesn't work for managed types.
//This can be reworked when ref structs can be used in generics.
int i = 0;
//The order doesn't really matter here because the format sorts channels alphabetically
//when reading reading scan lines, but this makes that more clear.
if (TColor.HasAlphaChannel)
{
buffer[i] = new ExrChannel("A", PixelType, false, 1, 1);
i++;
}
if (TColor.HasBlueChannel)
{
buffer[i] = new ExrChannel("B", PixelType, false, 1, 1);
i++;
}
if (TColor.HasGreenChannel)
{
buffer[i] = new ExrChannel("G", PixelType, false, 1, 1);
i++;
}
if (TColor.HasRedChannel)
{
buffer[i] = new ExrChannel("R", PixelType, false, 1, 1);
i++;
}
channels = buffer[..i].ToArray();
}
//Attributes
writer.WriteAttribute(ExrAttributes.Channels, new ExrChannelList(channels));
writer.WriteAttribute(ExrAttributes.Compression, "compression"u8, ExrCompression.None);
writer.WriteAttribute(ExrAttributes.DataWindow, new ExrBox2I(0, 0, width - 1, height - 1));
writer.WriteAttribute(ExrAttributes.DisplayWindow, new ExrBox2I(0, 0, width - 1, height - 1));
writer.WriteAttribute(ExrAttributes.LineOrder, "lineOrder"u8, ExrLineOrder.IncreasingY);
writer.WriteAttribute(ExrAttributes.PixelAspectRatio, new ExrSingle(1f));
writer.WriteAttribute(ExrAttributes.ScreenWindowCenter, new ExrVector2(0f, 0f));
writer.WriteAttribute(ExrAttributes.ScreenWindowWidth, new ExrSingle(1f));
writer.WriteNullByte();//End of header
//Scan line offset table
long offset = writer.BaseStream.Position + height * sizeof(ulong);
for (int i = 0; i < height; i++)
{
writer.Write(offset);
offset += sizeof(int) + ChannelCount * ChannelSize * width;
}
//Scan lines
if (PixelType is ExrPixelType.Half)
{
for (int i = 0; i < height; i++)
{
writer.Write(i);
writer.Write(ChannelCount * ChannelSize * width);
if (TColor.HasAlphaChannel)
{
for (int j = 0; j < width; j++)
{
Half alpha = NumericConversion.Convert<TChannelValue, Half>(pixels[i * width + j].A);
writer.Write(alpha);
}
}
if (TColor.HasBlueChannel)
{
for (int j = 0; j < width; j++)
{
float blue = NumericConversion.Convert<TChannelValue, float>(pixels[i * width + j].B);
if (TColor.HasAlphaChannel)
{
float alpha = NumericConversion.Convert<TChannelValue, float>(pixels[i * width + j].A);
writer.Write((Half)ConvertSRGB(blue * alpha));
}
else
{
writer.Write((Half)ConvertSRGB(blue));
}
}
}
if (TColor.HasGreenChannel)
{
for (int j = 0; j < width; j++)
{
float green = NumericConversion.Convert<TChannelValue, float>(pixels[i * width + j].G);
if (TColor.HasAlphaChannel)
{
float alpha = NumericConversion.Convert<TChannelValue, float>(pixels[i * width + j].A);
writer.Write((Half)ConvertSRGB(green * alpha));
}
else
{
writer.Write((Half)ConvertSRGB(green));
}
}
}
if (TColor.HasRedChannel)
{
for (int j = 0; j < width; j++)
{
float red = NumericConversion.Convert<TChannelValue, float>(pixels[i * width + j].R);
if (TColor.HasAlphaChannel)
{
float alpha = NumericConversion.Convert<TChannelValue, float>(pixels[i * width + j].A);
writer.Write((Half)ConvertSRGB(red * alpha));
}
else
{
writer.Write((Half)ConvertSRGB(red));
}
}
}
}
}
else
{
for (int i = 0; i < height; i++)
{
writer.Write(i);
writer.Write(ChannelCount * ChannelSize * width);
if (TColor.HasAlphaChannel)
{
for (int j = 0; j < width; j++)
{
float alpha = NumericConversion.Convert<TChannelValue, float>(pixels[i * width + j].A);
writer.Write(alpha);
}
}
if (TColor.HasBlueChannel)
{
for (int j = 0; j < width; j++)
{
float blue = NumericConversion.Convert<TChannelValue, float>(pixels[i * width + j].B);
if (TColor.HasAlphaChannel)
{
float alpha = NumericConversion.Convert<TChannelValue, float>(pixels[i * width + j].A);
writer.Write(ConvertSRGB(blue * alpha));
}
else
{
writer.Write(ConvertSRGB(blue));
}
}
}
if (TColor.HasGreenChannel)
{
for (int j = 0; j < width; j++)
{
float green = NumericConversion.Convert<TChannelValue, float>(pixels[i * width + j].G);
if (TColor.HasAlphaChannel)
{
float alpha = NumericConversion.Convert<TChannelValue, float>(pixels[i * width + j].A);
writer.Write(ConvertSRGB(green * alpha));
}
else
{
writer.Write(ConvertSRGB(green));
}
}
}
if (TColor.HasRedChannel)
{
for (int j = 0; j < width; j++)
{
float red = NumericConversion.Convert<TChannelValue, float>(pixels[i * width + j].R);
if (TColor.HasAlphaChannel)
{
float alpha = NumericConversion.Convert<TChannelValue, float>(pixels[i * width + j].A);
writer.Write(ConvertSRGB(red * alpha));
}
else
{
writer.Write(ConvertSRGB(red));
}
}
}
}
}
}
private static float ConvertSRGB(float value)
{
//https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ
//https://en.wikipedia.org/wiki/Gamma_correction#Computer_displays
if (value < 0.04045)
{
return value / 12.92f;
}
else
{
return float.Pow((value + 0.055f) / 1.055f, 2.4f);
}
}
}
@@ -0,0 +1,10 @@
namespace AssetRipper.TextureDecoder.Exr;
public interface IExrDataType
{
static abstract ReadOnlySpan<byte> TypeName { get; }
int Size { get; }
void Write(BinaryWriter writer);
}
@@ -0,0 +1,15 @@
namespace AssetRipper.TextureDecoder.Exr;
public readonly record struct TileDescription(int XSize, int YSize, ExrLevelMode Mode) : IExrDataType
{
public static ReadOnlySpan<byte> TypeName => "tileDesc"u8;
public int Size => sizeof(int) + sizeof(int) + sizeof(ExrLevelMode);
public void Write(BinaryWriter writer)
{
writer.Write(XSize);
writer.Write(YSize);
writer.Write(Mode);
}
}