Skip to content
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

Optimize TexData (mostly QoiConverter) #886

Merged
merged 7 commits into from
May 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
53 changes: 26 additions & 27 deletions UndertaleModLib/Models/UndertaleEmbeddedTexture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,27 @@ public override string ToString()
/// </summary>
public class TexData : UndertaleObject, INotifyPropertyChanged
{
private byte[] _TextureBlob;
private Bitmap _Image;

/// <summary>
/// The PNG image data of the texture.
/// </summary>
[Obsolete($"{nameof(TextureBlob)} is obsolete. Use {nameof(Image)} instead.", false)]
public byte[] TextureBlob
{
get
{
using MemoryStream final = new();
Image.Save(final, ImageFormat.Png);
return final.ToArray();
}
set => Image = TextureWorker.GetImageFromByteArray(value);
}

/// <summary>
/// The image data of the texture.
/// </summary>
public byte[] TextureBlob { get => _TextureBlob; set { _TextureBlob = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TextureBlob))); } }
public Bitmap Image { get => _Image; set { _Image = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TextureBlob))); } }

public event PropertyChangedEventHandler PropertyChanged;

Expand All @@ -122,22 +137,18 @@ public void Serialize(UndertaleWriter writer)
{
writer.Write(QOIandBZipHeader);

// Encode the PNG data back to QOI+BZip2
Bitmap bmp = TextureWorker.GetImageFromByteArray(TextureBlob);
writer.Write((short)bmp.Width);
writer.Write((short)bmp.Height);
byte[] data = QoiConverter.GetArrayFromImage(bmp, writer.undertaleData.GM2022_3 ? 0 : 4);
// Encode the bitmap back to QOI+BZip2
writer.Write((short)Image.Width);
writer.Write((short)Image.Height);
byte[] data = QoiConverter.GetArrayFromImage(Image, writer.undertaleData.GM2022_3 ? 0 : 4);
using MemoryStream input = new MemoryStream(data);
using MemoryStream output = new MemoryStream(1024);
BZip2.Compress(input, output, false, 9);
writer.Write(output.ToArray());
bmp.Dispose();
writer.Write(output);
}
else
{
// Encode the PNG data back to QOI
writer.Write(QoiConverter.GetArrayFromImage(TextureWorker.GetImageFromByteArray(TextureBlob),
writer.undertaleData.GM2022_3 ? 0 : 4));
writer.Write(QoiConverter.GetSpanFromImage(Image, writer.undertaleData.GM2022_3 ? 0 : 4));
}
}
else
Expand All @@ -161,34 +172,22 @@ public void Unserialize(UndertaleReader reader)
// Don't really care about the width/height, so skip them, as well as header
reader.Position += 8;

// Need to fully decompress and convert the QOI data to PNG for compatibility purposes (at least for now)
using MemoryStream bufferWrapper = new MemoryStream(reader.Buffer);
bufferWrapper.Seek(reader.Offset, SeekOrigin.Begin);
using MemoryStream result = new MemoryStream(1024);
BZip2.Decompress(bufferWrapper, result, false);
reader.Position = (uint)bufferWrapper.Position;
result.Seek(0, SeekOrigin.Begin);
Bitmap bmp = QoiConverter.GetImageFromStream(result);
using MemoryStream final = new MemoryStream();
bmp.Save(final, ImageFormat.Png);
TextureBlob = final.ToArray();
bmp.Dispose();
Image = QoiConverter.GetImageFromSpan(result.GetBuffer());
return;
}
else if (header.Take(4).SequenceEqual(QOIHeader))
{
reader.undertaleData.UseQoiFormat = true;
reader.undertaleData.UseBZipFormat = false;

// Need to convert the QOI data to PNG for compatibility purposes (at least for now)
using MemoryStream ms = new MemoryStream(reader.Buffer);
ms.Seek(reader.Offset, SeekOrigin.Begin);
Bitmap bmp = QoiConverter.GetImageFromStream(ms);
reader.Offset = (int)ms.Position;
using MemoryStream final = new MemoryStream();
bmp.Save(final, ImageFormat.Png);
TextureBlob = final.ToArray();
bmp.Dispose();
Image = QoiConverter.GetImageFromSpan(reader.Buffer.AsSpan()[reader.Offset..], out int dataLength);
reader.Offset += dataLength;
return;
}
else
Expand Down
2 changes: 1 addition & 1 deletion UndertaleModLib/Models/UndertaleTexturePageItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ public void ReplaceTexture(Image replaceImage, bool disposeImage = true)
g.DrawImage(finalImage, SourceX, SourceY);
g.Dispose();

TexturePage.TextureData.TextureBlob = TextureWorker.GetImageBytes(embImage);
TexturePage.TextureData.Image = embImage;
worker.Cleanup();
}

Expand Down
20 changes: 20 additions & 0 deletions UndertaleModLib/Util/BufferBinaryWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

Expand Down Expand Up @@ -80,6 +82,24 @@ public void Write(byte[] value)
offset += value.Length;
}

public void Write(MemoryStream value)
{
ResizeToFit((int)(offset + value.Length));
Buffer.BlockCopy(value.GetBuffer(), 0, buffer, (int)offset, (int)value.Length);
offset += value.Length;
}

public void Write(Span<byte> value)
{
ResizeToFit((int)offset + value.Length);
// basically reimplements Buffer.BlockCopy but using Span as the source
ref byte valueRef = ref MemoryMarshal.GetReference(value);
ref byte bufferRef =
ref Unsafe.AddByteOffset(ref MemoryMarshal.GetArrayDataReference(buffer), (nuint)offset);
Unsafe.CopyBlock(ref bufferRef, ref valueRef, (uint)value.Length);
offset += value.Length;
}

public void Write(char[] value)
{
ResizeToFit((int)offset + value.Length);
Expand Down
69 changes: 52 additions & 17 deletions UndertaleModLib/Util/QoiConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,48 @@ public static class QoiConverter
private const byte QOI_MASK_4 = 0xf0;

/// <summary>
/// Creates a Bitmap from a <see cref="Stream"/>.
/// Creates a <see cref="Bitmap"/> from a <see cref="Stream"/>.
/// </summary>
/// <param name="s">The stream to create the PNG image from.</param>
/// <returns>The QOI image as a PNG.</returns>
/// <exception cref="Exception">If there is an invalid QOIF magic header or there was an error with stride width.</exception>
public unsafe static Bitmap GetImageFromStream(Stream s)
public static Bitmap GetImageFromStream(Stream s)
{
byte[] header = new byte[12];
s.Read(header, 0, 12);
Span<byte> header = stackalloc byte[12];
s.Read(header);
int length = header[8] + (header[9] << 8) + (header[10] << 16) + (header[11] << 24);
byte[] bytes = new byte[12 + length];
s.Position -= 12;
s.Read(bytes, 0, bytes.Length);
return GetImageFromSpan(bytes);
}

/// <summary>
/// Creates a <see cref="Bitmap"/> from a <see cref="ReadOnlySpan"/>.
/// </summary>
/// <param name="bytes">The <see cref="Span"/> to create the PNG image from.</param>
/// <returns>The QOI image as a PNG.</returns>
/// <exception cref="Exception">If there is an invalid QOIF magic header or there was an error with stride width.</exception>
public static Bitmap GetImageFromSpan(ReadOnlySpan<byte> bytes) => GetImageFromSpan(bytes, out _);

/// <summary>
/// Creates a <see cref="Bitmap"/> from a <see cref="ReadOnlySpan"/>.
/// </summary>
/// <param name="bytes">The <see cref="Span"/> to create the PNG image from.</param>
/// <param name="length">The total amount of data read from the <see cref="Span"/>.</param>
/// <returns>The QOI image as a PNG.</returns>
/// <exception cref="Exception">If there is an invalid QOIF magic header or there was an error with stride width.</exception>
public unsafe static Bitmap GetImageFromSpan(ReadOnlySpan<byte> bytes, out int length)
cgytrus marked this conversation as resolved.
Show resolved Hide resolved
{
ReadOnlySpan<byte> header = bytes[..12];
if (header[0] != (byte)'f' || header[1] != (byte)'i' || header[2] != (byte)'o' || header[3] != (byte)'q')
throw new Exception("Invalid little-endian QOIF image magic");

int width = header[4] + (header[5] << 8);
int height = header[6] + (header[7] << 8);
int length = header[8] + (header[9] << 8) + (header[10] << 16) + (header[11] << 24);
length = header[8] + (header[9] << 8) + (header[10] << 16) + (header[11] << 24);

byte[] pixelData = new byte[length];
s.Read(pixelData, 0, length);
ReadOnlySpan<byte> pixelData = bytes.Slice(12, length);

Bitmap bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb);

Expand All @@ -56,7 +80,7 @@ public unsafe static Bitmap GetImageFromStream(Stream s)
int pos = 0;
int run = 0;
byte r = 0, g = 0, b = 0, a = 255;
byte[] index = new byte[64 * 4];
Span<byte> index = stackalloc byte[64 * 4];
while (bmpPtr < bmpEnd)
{
if (run > 0)
Expand Down Expand Up @@ -135,19 +159,30 @@ public unsafe static Bitmap GetImageFromStream(Stream s)

bmp.UnlockBits(data);

length += header.Length;
return bmp;
}

/// <summary>
/// Creates a QOI image as a byte array from a Bitmap.
/// Creates a QOI image as a byte array from a <see cref="Bitmap"/>.
/// </summary>
/// <param name="bmp">The bitmap to create the QOI image from.</param>
/// <param name="bmp">The <see cref="Bitmap"/> to create the QOI image from.</param>
/// <param name="padding">The amount of bytes of padding that should be used.</param>
/// <returns>A QOI Image as a byte array.</returns>
/// <exception cref="Exception">If there was an error with stride width.</exception>
public unsafe static byte[] GetArrayFromImage(Bitmap bmp, int padding = 4)
{
byte[] res = new byte[(bmp.Width * bmp.Height * 4 * 12) + padding]; // default capacity
public static byte[] GetArrayFromImage(Bitmap bmp, int padding = 4) => GetSpanFromImage(bmp, padding).ToArray();

/// <summary>
/// Creates a QOI image as a <see cref="Span"/> from a <see cref="Bitmap"/>.
/// </summary>
/// <param name="bmp">The <see cref="Bitmap"/> to create the QOI image from.</param>
/// <param name="padding">The amount of bytes of padding that should be used.</param>
/// <returns>A QOI Image as a byte array.</returns>
/// <exception cref="Exception">If there was an error with stride width.</exception>
public unsafe static Span<byte> GetSpanFromImage(Bitmap bmp, int padding = 4) {
const int maxChunkSize = 5; // according to the QOI spec: https://qoiformat.org/qoi-specification.pdf
const int headerSize = 12;
byte[] res = new byte[bmp.Width * bmp.Height * maxChunkSize + headerSize + padding]; // default capacity
// Little-endian QOIF image magic
res[0] = (byte)'f';
res[1] = (byte)'i';
Expand All @@ -165,11 +200,11 @@ public unsafe static byte[] GetArrayFromImage(Bitmap bmp, int padding = 4)
byte* bmpPtr = (byte*)data.Scan0;
byte* bmpEnd = bmpPtr + (4 * bmp.Width * bmp.Height);

int resPos = 12;
int resPos = headerSize;
byte r = 0, g = 0, b = 0, a = 255;
int run = 0;
int v = 0, vPrev = 0xff;
int[] index = new int[64];
Span<int> index = stackalloc int[64];
while (bmpPtr < bmpEnd)
{
b = *bmpPtr;
Expand Down Expand Up @@ -261,13 +296,13 @@ public unsafe static byte[] GetArrayFromImage(Bitmap bmp, int padding = 4)
resPos += padding;

// Write final length
int length = resPos - 12;
int length = resPos - headerSize;
res[8] = (byte)(length & 0xff);
res[9] = (byte)((length >> 8) & 0xff);
res[10] = (byte)((length >> 16) & 0xff);
res[11] = (byte)((length >> 24) & 0xff);

return res[..resPos];
return res.AsSpan()[..resPos];
}
}
}
4 changes: 2 additions & 2 deletions UndertaleModLib/Util/TextureWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ public Bitmap GetEmbeddedTexture(UndertaleEmbeddedTexture embeddedTexture)
{
lock (embeddedDictionary)
{
if (!embeddedDictionary.ContainsKey(embeddedTexture))
embeddedDictionary[embeddedTexture] = GetImageFromByteArray(embeddedTexture.TextureData.TextureBlob);
if(!embeddedDictionary.ContainsKey(embeddedTexture))
embeddedDictionary[embeddedTexture] = embeddedTexture.TextureData.Image;
return embeddedDictionary[embeddedTexture];
}
}
Expand Down
8 changes: 2 additions & 6 deletions UndertaleModTool/Converters/UndertaleCachedImageLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,19 +158,15 @@ public static void Reset()

public static Bitmap CreateSpriteBitmap(Rectangle rect, in UndertaleTexturePageItem texture, int diffW = 0, int diffH = 0, bool isTile = false)
{
using MemoryStream stream = new(texture.TexturePage.TextureData.TextureBlob);
Bitmap spriteBMP = new(rect.Width, rect.Height);

rect.Width -= (diffW > 0) ? diffW : 0;
rect.Height -= (diffH > 0) ? diffH : 0;
int x = isTile ? texture.TargetX : 0;
int y = isTile ? texture.TargetY : 0;

using (Graphics g = Graphics.FromImage(spriteBMP))
{
using Image img = Image.FromStream(stream); // "ImageConverter.ConvertFrom()" does the same, except it doesn't explicitly dispose MemoryStream
g.DrawImage(img, new Rectangle(x, y, rect.Width, rect.Height), rect, GraphicsUnit.Pixel);
}
using Graphics g = Graphics.FromImage(spriteBMP);
g.DrawImage(texture.TexturePage.TextureData.Image, new Rectangle(x, y, rect.Width, rect.Height), rect, GraphicsUnit.Pixel);

return spriteBMP;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,7 @@ private void Import_Click(object sender, RoutedEventArgs e)
MessageBox.Show("WARNING: texture page dimensions are not powers of 2. Sprite blurring is very likely in game.", "Unexpected texture dimensions", MessageBoxButton.OK, MessageBoxImage.Warning);
}

using (var stream = new MemoryStream())
{
bmp.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
target.TextureData.TextureBlob = stream.ToArray();
}
target.TextureData.Image = bmp;
}
catch (Exception ex)
{
Expand Down