Skip to content

Commit

Permalink
Revert "Optimize TexData (mostly QoiConverter) (UnderminersTeam#886)"
Browse files Browse the repository at this point in the history
This reverts commit b54a4a7.
  • Loading branch information
Miepee committed May 8, 2022
1 parent fafe41a commit 5006f05
Show file tree
Hide file tree
Showing 7 changed files with 58 additions and 104 deletions.
53 changes: 27 additions & 26 deletions UndertaleModLib/Models/UndertaleEmbeddedTexture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,27 +101,12 @@ public override string ToString()
/// </summary>
public class TexData : UndertaleObject, INotifyPropertyChanged
{
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);
}
private byte[] _TextureBlob;

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

public event PropertyChangedEventHandler PropertyChanged;

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

// 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);
// 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);
using MemoryStream input = new MemoryStream(data);
using MemoryStream output = new MemoryStream(1024);
BZip2.Compress(input, output, false, 9);
writer.Write(output);
writer.Write(output.ToArray());
bmp.Dispose();
}
else
{
writer.Write(QoiConverter.GetSpanFromImage(Image, writer.undertaleData.GM2022_3 ? 0 : 4));
// Encode the PNG data back to QOI
writer.Write(QoiConverter.GetArrayFromImage(TextureWorker.GetImageFromByteArray(TextureBlob),
writer.undertaleData.GM2022_3 ? 0 : 4));
}
}
else
Expand All @@ -172,22 +161,34 @@ 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);
Image = QoiConverter.GetImageFromSpan(result.GetBuffer());
Bitmap bmp = QoiConverter.GetImageFromStream(result);
using MemoryStream final = new MemoryStream();
bmp.Save(final, ImageFormat.Png);
TextureBlob = final.ToArray();
bmp.Dispose();
return;
}
else if (header.Take(4).SequenceEqual(QOIHeader))
{
reader.undertaleData.UseQoiFormat = true;
reader.undertaleData.UseBZipFormat = false;

Image = QoiConverter.GetImageFromSpan(reader.Buffer.AsSpan()[reader.Offset..], out int dataLength);
reader.Offset += dataLength;
// 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();
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.Image = embImage;
TexturePage.TextureData.TextureBlob = TextureWorker.GetImageBytes(embImage);
worker.Cleanup();
}

Expand Down
20 changes: 0 additions & 20 deletions UndertaleModLib/Util/BufferBinaryWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
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 @@ -82,24 +80,6 @@ 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: 17 additions & 52 deletions UndertaleModLib/Util/QoiConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,48 +25,24 @@ public static class QoiConverter
private const byte QOI_MASK_4 = 0xf0;

/// <summary>
/// Creates a <see cref="Bitmap"/> from a <see cref="Stream"/>.
/// Creates a 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 static Bitmap GetImageFromStream(Stream s)
public unsafe static Bitmap GetImageFromStream(Stream s)
{
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)
{
ReadOnlySpan<byte> header = bytes[..12];
byte[] header = new byte[12];
s.Read(header, 0, 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);
length = header[8] + (header[9] << 8) + (header[10] << 16) + (header[11] << 24);
int length = header[8] + (header[9] << 8) + (header[10] << 16) + (header[11] << 24);

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

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

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

bmp.UnlockBits(data);

length += header.Length;
return bmp;
}

/// <summary>
/// Creates a QOI image as a byte array 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 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"/>.
/// Creates a QOI image as a byte array from a Bitmap.
/// </summary>
/// <param name="bmp">The <see cref="Bitmap"/> to create the QOI image from.</param>
/// <param name="bmp">The 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
public unsafe static byte[] GetArrayFromImage(Bitmap bmp, int padding = 4)
{
byte[] res = new byte[(bmp.Width * bmp.Height * 4 * 12) + padding]; // default capacity
// Little-endian QOIF image magic
res[0] = (byte)'f';
res[1] = (byte)'i';
Expand All @@ -200,11 +165,11 @@ public unsafe static Span<byte> GetSpanFromImage(Bitmap bmp, int padding = 4) {
byte* bmpPtr = (byte*)data.Scan0;
byte* bmpEnd = bmpPtr + (4 * bmp.Width * bmp.Height);

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

// Write final length
int length = resPos - headerSize;
int length = resPos - 12;
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.AsSpan()[..resPos];
return res[..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] = embeddedTexture.TextureData.Image;
if (!embeddedDictionary.ContainsKey(embeddedTexture))
embeddedDictionary[embeddedTexture] = GetImageFromByteArray(embeddedTexture.TextureData.TextureBlob);
return embeddedDictionary[embeddedTexture];
}
}
Expand Down
8 changes: 6 additions & 2 deletions UndertaleModTool/Converters/UndertaleCachedImageLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,19 @@ 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);
g.DrawImage(texture.TexturePage.TextureData.Image, new Rectangle(x, y, rect.Width, rect.Height), rect, GraphicsUnit.Pixel);
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);
}

return spriteBMP;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ 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);
}

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

0 comments on commit 5006f05

Please sign in to comment.