From 598d0c1d3cfb769532f089cf4adc890c10e0cabc Mon Sep 17 00:00:00 2001 From: luizzeroxis Date: Thu, 26 Sep 2024 21:42:03 -0300 Subject: [PATCH 1/2] Add support for games that use DDS textures (cherry picked from commit de9476cc2512e18dcfbaa983a7dab354e0d81733) --- UndertaleModLib/Util/GMImage.cs | 109 ++++++++++++++++-- .../UndertaleEmbeddedTextureEditor.xaml.cs | 30 ++--- .../UndertaleTexturePageItemEditor.xaml.cs | 27 +++-- 3 files changed, 137 insertions(+), 29 deletions(-) diff --git a/UndertaleModLib/Util/GMImage.cs b/UndertaleModLib/Util/GMImage.cs index eef1eb7a7..d5041aa61 100644 --- a/UndertaleModLib/Util/GMImage.cs +++ b/UndertaleModLib/Util/GMImage.cs @@ -1,8 +1,8 @@ -using ICSharpCode.SharpZipLib.BZip2; -using ImageMagick; -using System; +using System; using System.Buffers.Binary; using System.IO; +using ICSharpCode.SharpZipLib.BZip2; +using ImageMagick; namespace UndertaleModLib.Util; @@ -34,7 +34,12 @@ public enum ImageFormat /// /// BZip2 compression applied on top of GameMaker's custom variant of the QOI image file format. /// - Bz2Qoi + Bz2Qoi, + + /// + /// DDS file format. + /// + Dds, } /// @@ -77,6 +82,11 @@ public enum ImageFormat /// private static ReadOnlySpan MagicBz2Footer => new byte[] { 0x17, 0x72, 0x45, 0x38, 0x50, 0x90 }; + /// + /// DDS file format magic. + /// + private static ReadOnlySpan MagicDds => "DDS "u8; + /// /// Backing data for the image, whether compressed or not. /// @@ -335,6 +345,14 @@ public static GMImage FromBinaryReader(IBinaryReader reader, long maxEndOfStream return FromQoi(reader.ReadBytes(12 + (int)compressedLength)); } + // DDS + if (header.StartsWith(MagicDds)) + { + // Read entire image + reader.Position = startAddress; + return FromDds(reader.ReadBytes((int)(maxEndOfStreamPosition - startAddress))); + } + throw new IOException("Failed to recognize any known image header"); } @@ -452,6 +470,31 @@ public static GMImage FromQoi(byte[] data) return new GMImage(ImageFormat.Qoi, width, height, data); } + /// + /// Creates a of DDS format, wrapping around the provided byte array containing DDS data. + /// + /// Byte array of DDS data. + public static GMImage FromDds(byte[] data) + { + ArgumentNullException.ThrowIfNull(data); + + ReadOnlySpan span = data.AsSpan(); + + // Get height and width + int height = (int)BinaryPrimitives.ReadUInt32LittleEndian(span[12..16]); + int width = (int)BinaryPrimitives.ReadUInt32LittleEndian(span[16..20]); + + // Create wrapper image + return new GMImage(ImageFormat.Dds, width, height, data); + } + + private void AddMagickToPngSettings(MagickReadSettings settings) + { + settings.SetDefine(MagickFormat.Png32, "compression-level", 4); + settings.SetDefine(MagickFormat.Png32, "compression-filter", 5); + settings.SetDefine(MagickFormat.Png32, "compression-strategy", 2); + } + // Settings to be used for raw data, and when encoding a PNG private MagickReadSettings GetMagickRawToPngSettings() { @@ -462,9 +505,18 @@ private MagickReadSettings GetMagickRawToPngSettings() Format = MagickFormat.Bgra, Compression = CompressionMethod.NoCompression }; - settings.SetDefine(MagickFormat.Png32, "compression-level", 4); - settings.SetDefine(MagickFormat.Png32, "compression-filter", 5); - settings.SetDefine(MagickFormat.Png32, "compression-strategy", 2); + AddMagickToPngSettings(settings); + return settings; + } + + // Settings to be used for decoding DDS, and when encoding a PNG + private MagickReadSettings GetMagickDdsToPngSettings() + { + var settings = new MagickReadSettings() + { + Format = MagickFormat.Dds, + }; + AddMagickToPngSettings(settings); return settings; } @@ -518,6 +570,15 @@ public void SavePng(Stream stream) rawImage.SavePng(stream); break; } + case ImageFormat.Dds: + { + // Create image using ImageMagick, and save it as PNG format + using var image = new MagickImage(_data, GetMagickDdsToPngSettings()); + image.Alpha(AlphaOption.Set); + image.Format = MagickFormat.Png32; + image.Write(stream); + break; + } default: throw new InvalidOperationException($"Unknown format {Format}"); } @@ -536,6 +597,7 @@ public GMImage ConvertToFormat(ImageFormat format, MemoryStream sharedStream = n ImageFormat.Png => ConvertToPng(), ImageFormat.Qoi => ConvertToQoi(), ImageFormat.Bz2Qoi => ConvertToBz2Qoi(sharedStream), + ImageFormat.Dds => ConvertToDds(), _ => throw new ArgumentOutOfRangeException(nameof(format)), }; } @@ -553,6 +615,7 @@ public GMImage ConvertToRawBgra() return this; } case ImageFormat.Png: + case ImageFormat.Dds: { // Convert image to raw byte array var image = new MagickImage(_data); @@ -632,6 +695,14 @@ public GMImage ConvertToPng() // Convert raw image to PNG return rawImage.ConvertToPng(); } + case ImageFormat.Dds: + { + // Create image using ImageMagick, and convert it to PNG format + using var image = new MagickImage(_data, GetMagickDdsToPngSettings()); + image.Alpha(AlphaOption.Set); + image.Format = MagickFormat.Png32; + return new GMImage(ImageFormat.Png, Width, Height, image.ToByteArray()); + } } throw new InvalidOperationException($"Unknown source format {Format}"); @@ -647,6 +718,7 @@ public GMImage ConvertToQoi() case ImageFormat.RawBgra: case ImageFormat.Png: case ImageFormat.Bz2Qoi: + case ImageFormat.Dds: { // Encode image as QOI return new GMImage(ImageFormat.Qoi, Width, Height, QoiConverter.GetArrayFromImage(this, false)); @@ -706,6 +778,7 @@ public GMImage ConvertToBz2Qoi(MemoryStream sharedStream = null) { case ImageFormat.RawBgra: case ImageFormat.Png: + case ImageFormat.Dds: { // Encode image as QOI, first byte[] data = QoiConverter.GetArrayFromImage(this, false); @@ -726,6 +799,17 @@ public GMImage ConvertToBz2Qoi(MemoryStream sharedStream = null) throw new InvalidOperationException($"Unknown source format {Format}"); } + /// + /// Same as . + /// + /// This is supposd to return the image converted to format, but that's not implemented yet. + /// + public GMImage ConvertToDds() + { + // TODO: Actually convert to DDS + return ConvertToPng(); + } + /// /// Returns the raw BGRA32 pixel data of this image, which can be modified. /// @@ -756,6 +840,7 @@ public void WriteToBinaryWriter(BinaryWriter writer, bool gm2022_5) case ImageFormat.RawBgra: case ImageFormat.Png: case ImageFormat.Qoi: + case ImageFormat.Dds: // Data is stored identically to file format, so write it verbatim writer.Write(_data); break; @@ -843,6 +928,16 @@ public MagickImage GetMagickImage() case ImageFormat.Bz2Qoi: // Convert to raw data, then parse that return ConvertToRawBgra().GetMagickImage(); + case ImageFormat.Dds: + { + // Parse the DDS data + MagickReadSettings settings = new() + { + Format = MagickFormat.Dds + }; + MagickImage image = new(_data, settings); + return image; + } } throw new InvalidOperationException($"Unknown format {Format}"); diff --git a/UndertaleModTool/Editors/UndertaleEmbeddedTextureEditor.xaml.cs b/UndertaleModTool/Editors/UndertaleEmbeddedTextureEditor.xaml.cs index 8950e0885..631e36e96 100644 --- a/UndertaleModTool/Editors/UndertaleEmbeddedTextureEditor.xaml.cs +++ b/UndertaleModTool/Editors/UndertaleEmbeddedTextureEditor.xaml.cs @@ -1,28 +1,20 @@ -using Microsoft.Win32; -using System; -using System.Collections.Generic; +using System; +using System.ComponentModel; +using System.Globalization; using System.IO; using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; -using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; -using System.Drawing; +using System.Windows.Threading; +using ImageMagick; +using Microsoft.Win32; using UndertaleModLib.Models; using UndertaleModLib.Util; -using System.Globalization; -using UndertaleModLib; using UndertaleModTool.Windows; -using System.Windows.Threading; -using ImageMagick; -using System.ComponentModel; namespace UndertaleModTool { @@ -252,9 +244,19 @@ private void Import_Click(object sender, RoutedEventArgs e) mainWindow.ShowWarning("WARNING: Texture page dimensions are not powers of 2. Sprite blurring is very likely in-game.", "Unexpected texture dimensions"); } + var previousFormat = target.TextureData.Image.Format; + // Import image target.TextureData.Image = image; + var currentFormat = target.TextureData.Image.Format; + + // If texture was DDS, warn user that texture has been converted to PNG + if (previousFormat == GMImage.ImageFormat.Dds && currentFormat == GMImage.ImageFormat.Png) + { + mainWindow.ShowMessage($"{target} was converted into PNG format since we don't support converting images into DDS format. This might have performance issues in the game."); + } + // Update width/height properties in the UI TexWidth.GetBindingExpression(TextBox.TextProperty)?.UpdateTarget(); TexHeight.GetBindingExpression(TextBox.TextProperty)?.UpdateTarget(); diff --git a/UndertaleModTool/Editors/UndertaleTexturePageItemEditor.xaml.cs b/UndertaleModTool/Editors/UndertaleTexturePageItemEditor.xaml.cs index 1724def29..102598fa6 100644 --- a/UndertaleModTool/Editors/UndertaleTexturePageItemEditor.xaml.cs +++ b/UndertaleModTool/Editors/UndertaleTexturePageItemEditor.xaml.cs @@ -1,15 +1,15 @@ -using Microsoft.Win32; -using System; +using System; +using System.ComponentModel; using System.Windows; -using UndertaleModLib.Models; -using UndertaleModLib.Util; using System.Windows.Controls; -using System.Windows.Media; using System.Windows.Data; -using UndertaleModTool.Windows; -using ImageMagick; +using System.Windows.Media; using System.Windows.Media.Imaging; -using System.ComponentModel; +using ImageMagick; +using Microsoft.Win32; +using UndertaleModLib.Models; +using UndertaleModLib.Util; +using UndertaleModTool.Windows; namespace UndertaleModTool { @@ -150,8 +150,19 @@ private void Import_Click(object sender, RoutedEventArgs e) { using MagickImage image = TextureWorker.ReadBGRAImageFromFile(dlg.FileName); UndertaleTexturePageItem item = DataContext as UndertaleTexturePageItem; + + var previousFormat = item.TexturePage.TextureData.Image.Format; + item.ReplaceTexture(image); + var currentFormat = item.TexturePage.TextureData.Image.Format; + + // If texture was DDS, warn user that texture has been converted to PNG + if (previousFormat == GMImage.ImageFormat.Dds && currentFormat == GMImage.ImageFormat.Png) + { + mainWindow.ShowMessage($"{item.TexturePage} was converted into PNG format since we don't support converting images into DDS format. This might have performance issues in the game."); + } + // Refresh the image of "ItemDisplay" if (ItemDisplay.FindName("RenderAreaBorder") is not Border border) return; From a0df1dfe6987d2ca30d06ba57435f2e78c22158d Mon Sep 17 00:00:00 2001 From: luizzeroxis Date: Fri, 27 Sep 2024 21:01:17 -0300 Subject: [PATCH 2/2] Check size of DDS file to not include padding --- UndertaleModLib/Util/GMImage.cs | 53 +++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/UndertaleModLib/Util/GMImage.cs b/UndertaleModLib/Util/GMImage.cs index d5041aa61..87bf450f9 100644 --- a/UndertaleModLib/Util/GMImage.cs +++ b/UndertaleModLib/Util/GMImage.cs @@ -348,9 +348,56 @@ public static GMImage FromBinaryReader(IBinaryReader reader, long maxEndOfStream // DDS if (header.StartsWith(MagicDds)) { - // Read entire image + // Size int skipped because 8 bytes were already read + uint flags = reader.ReadUInt32(); + uint height = reader.ReadUInt32(); + + //uint width = reader.ReadUInt32(); + reader.Position += 4; + + uint pitchOrLinearSize = reader.ReadUInt32(); + + //uint depth = reader.ReadUInt32(); + //uint mipMapCount = reader.ReadUInt32(); + //byte[] reserved1 = reader.ReadBytes(4 * 11); + //uint pixelFormatSize = reader.ReadUInt32(); + reader.Position += 56; + + uint pixelFormatFlags = reader.ReadUInt32(); + uint pixelFormatFourCC = reader.ReadUInt32(); + + // TODO: Check caps for DDSCAPS_COMPLEX (when there's extra data afterwards) + + // Skip to end of header + reader.Position += 40; + + // Check if DX10 header is present and skip it + // DDPF_FOURCC == 0x4 + // DX10 == 0x30315844 + if ((pixelFormatFlags & 0x4) != 0 && pixelFormatFourCC == 0x30315844) + reader.Position += 20; + + // Check if that int is the size or pitch + // DDSD_LINEARSIZE == 0x80000 + int size = (int)(reader.Position - startAddress); + if ((flags & 0x80000) != 0) + size += (int)pitchOrLinearSize; + else + size += (int)(pitchOrLinearSize * height); + + // Read entire data reader.Position = startAddress; - return FromDds(reader.ReadBytes((int)(maxEndOfStreamPosition - startAddress))); + byte[] bytes = reader.ReadBytes(size); + + // Check if rest of bytes are 0x00 padded + byte[] paddingBytes = reader.ReadBytes((int)(maxEndOfStreamPosition - reader.Position)); + for (int i=0; i