From b1b57b7e7f64e7ac7d060f47d8e2f9020ca76287 Mon Sep 17 00:00:00 2001 From: Diphome Date: Mon, 7 Sep 2026 22:01:51 +0200 Subject: [PATCH 1/5] FBX: guard null bone frame during mesh export (upstream #129) Port of aelurum/AssetStudioMod#129: when a bone's frame isn't present in the exported hierarchy, FindFrameByPath returns null and _frameToNode[frame] threw a NullReferenceException. Skip that bone with an empty cluster instead. Co-Authored-By: Clue Opus 4.8 --- UnityRiftFBXWrapper/FbxExporterContext.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/UnityRiftFBXWrapper/FbxExporterContext.cs b/UnityRiftFBXWrapper/FbxExporterContext.cs index 910319d..dc22eb4 100644 --- a/UnityRiftFBXWrapper/FbxExporterContext.cs +++ b/UnityRiftFBXWrapper/FbxExporterContext.cs @@ -228,19 +228,19 @@ private void ExportMesh(ImportedFrame rootFrame, List material foreach (var bone in boneList) { + var cluster = IntPtr.Zero; if (bone.Path != null) { var frame = rootFrame.FindFrameByPath(bone.Path); - var boneNode = _frameToNode[frame]; - - var cluster = AsFbxMeshCreateCluster(_pContext, boneNode); - - AsFbxMeshAddCluster(pClusterArray, cluster); - } - else - { - AsFbxMeshAddCluster(pClusterArray, IntPtr.Zero); + // The bone's frame may not exist in the exported hierarchy; skip it + // (add an empty cluster) instead of throwing a NullReferenceException. + if (frame != null) + { + var boneNode = _frameToNode[frame]; + cluster = AsFbxMeshCreateCluster(_pContext, boneNode); + } } + AsFbxMeshAddCluster(pClusterArray, cluster); } } From bdb47f75d171b1f16c58dc062deb41e66d8083f0 Mon Sep 17 00:00:00 2001 From: Diphome Date: Mon, 7 Sep 2026 22:01:51 +0200 Subject: [PATCH 2/5] deps: bump Kyaru.Texture2DDecoder native packages to 0.2.0 (upstream #130) Port of aelurum/AssetStudioMod#130: update the Linux/macOS/Windows native Texture2DDecoder runtime packages from 0.1.0 to 0.2.0. Restores and builds clean on net9. Co-Authored-By: Clue Opus 4.8 --- UnityRiftUtility/UnityRiftUtility.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/UnityRiftUtility/UnityRiftUtility.csproj b/UnityRiftUtility/UnityRiftUtility.csproj index 4ae0fab..395c4da 100644 --- a/UnityRiftUtility/UnityRiftUtility.csproj +++ b/UnityRiftUtility/UnityRiftUtility.csproj @@ -15,12 +15,12 @@ - - + + - + 0.17.0 From d2d7929bd8c224d93fc70d367743ba02c69f799f Mon Sep 17 00:00:00 2001 From: Diphome Date: Mon, 7 Sep 2026 22:01:51 +0200 Subject: [PATCH 3/5] CLI FBX: --fbx-ascii-format, load AnimationClips in animator mode (upstream #124) Port of aelurum/AssetStudioMod#124: - Add --fbx-ascii-format flag (our FBX wrapper already supports FbxFormat/ascii). - Load AnimationClip in animator mode so --fbx-animation all can bind clips. - Allow --fbx-uvs-as-diffuse in animator mode too (was split-objects only). Co-Authored-By: Clue Opus 4.8 --- UnityRiftCLI/Exporter.cs | 1 + UnityRiftCLI/Options/CLIOptions.cs | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/UnityRiftCLI/Exporter.cs b/UnityRiftCLI/Exporter.cs index 22953c2..94a73f1 100644 --- a/UnityRiftCLI/Exporter.cs +++ b/UnityRiftCLI/Exporter.cs @@ -337,6 +337,7 @@ private static void ExportFbx(IImported convert, string exportPath) ScaleFactor = CLIOptions.o_fbxScaleFactor.Value, ExportAllUvsAsDiffuseMaps = CLIOptions.f_fbxUvsAsDiffuseMaps.Value, ExportAnimations = CLIOptions.o_fbxAnimMode.Value != AnimationExportMode.Skip, + FbxFormat = CLIOptions.f_fbxAsciiFormat.Value ? 1 : 0, }; ModelExporter.ExportFbx(exportPath, convert, fbxSettings); } diff --git a/UnityRiftCLI/Options/CLIOptions.cs b/UnityRiftCLI/Options/CLIOptions.cs index a81bb78..2721f36 100644 --- a/UnityRiftCLI/Options/CLIOptions.cs +++ b/UnityRiftCLI/Options/CLIOptions.cs @@ -130,6 +130,7 @@ internal static class CLIOptions public static Option o_fbxBoneSize; public static Option o_fbxAnimMode; public static Option f_fbxUvsAsDiffuseMaps; + public static Option f_fbxAsciiFormat; //filter public static Option> o_filterByName; public static Option> o_filterByContainer; @@ -463,6 +464,15 @@ private static void InitOptions() optionExample: "", optionHelpGroup: HelpGroups.FBX ); + f_fbxAsciiFormat = new GroupedOption + ( + optionDefaultValue: false, + optionName: "--fbx-ascii-format", + optionDescription: "(Flag) If specified, Studio will export FBX in ASCII format.\n" + + "If not specified, Binary format will be used", + optionExample: "", + optionHelpGroup: HelpGroups.FBX + ); #endregion #region Init Filter Options @@ -889,6 +899,7 @@ public static void ParseArgs(string[] args) o_exportAssetTypes.Value = new List { ClassIDType.Animator, + ClassIDType.AnimationClip, // needed so --fbx-animation all can bind clips ClassIDType.Mesh, ClassIDType.Texture2D, }; @@ -948,7 +959,7 @@ public static void ParseArgs(string[] args) flagIndexes.Add(i); break; case "--fbx-uvs-as-diffuse": - if (o_workMode.Value != WorkMode.SplitObjects) + if (o_workMode.Value != WorkMode.SplitObjects && o_workMode.Value != WorkMode.Animator) { Console.WriteLine($"{"Error".Color(brightRed)} during parsing [{flag.Color(brightYellow)}] flag. This flag is not suitable for the current working mode [{o_workMode.Value}].\n"); ShowOptionDescription(o_workMode); @@ -957,6 +968,10 @@ public static void ParseArgs(string[] args) f_fbxUvsAsDiffuseMaps.Value = true; flagIndexes.Add(i); break; + case "--fbx-ascii-format": + f_fbxAsciiFormat.Value = true; + flagIndexes.Add(i); + break; case "--filter-with-regex": f_filterWithRegex.Value = true; flagIndexes.Add(i); From eafe8932021126a0ce297ebacdc4d0ff5352dae8 Mon Sep 17 00:00:00 2001 From: Diphome Date: Mon, 7 Sep 2026 22:05:55 +0200 Subject: [PATCH 4/5] CLI: add --filter-exclude-mode and --strip-path-prefix (upstream #113) Port of aelurum/AssetStudioMod#113: - --filter-exclude-mode inverts the filter (export everything that does NOT match). Clearer log wording than upstream (reports matched vs kept). - --strip-path-prefix strips a leading path from container paths when grouping by container / containerFull, validated against loaded containers. Co-Authored-By: Clue Opus 4.8 --- UnityRiftCLI/Options/CLIOptions.cs | 29 ++++++++++++++++++++++ UnityRiftCLI/Studio.cs | 40 +++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/UnityRiftCLI/Options/CLIOptions.cs b/UnityRiftCLI/Options/CLIOptions.cs index 2721f36..6c0e312 100644 --- a/UnityRiftCLI/Options/CLIOptions.cs +++ b/UnityRiftCLI/Options/CLIOptions.cs @@ -137,12 +137,14 @@ internal static class CLIOptions public static Option> o_filterByPathID; public static Option> o_filterByText; public static Option f_filterWithRegex; + public static Option f_filterExcludeMode; //advanced public static Option o_bundleBlockInfoCompression; public static Option o_bundleBlockCompression; public static Option o_maxParallelExportTasks; public static Option o_exportAssetList; public static Option o_assemblyPath; + public static Option o_stripPathPrefix; public static Option o_typeTreeDBPath; public static Option o_unityVersion; public static Option f_decompressToDisk; @@ -523,6 +525,16 @@ private static void InitOptions() optionHelpGroup: HelpGroups.Filter, isFlag: true ); + f_filterExcludeMode = new GroupedOption + ( + optionDefaultValue: false, + optionName: "--filter-exclude-mode", + optionDescription: "(Flag) If specified, the filter options will work as an exclusion\n" + + "(i.e. assets that match the filter conditions will be excluded)", + optionExample: "", + optionHelpGroup: HelpGroups.Filter, + isFlag: true + ); #endregion #region Init Advanced Options @@ -688,6 +700,15 @@ private static void InitOptions() optionExample: "", optionHelpGroup: HelpGroups.Advanced ); + o_stripPathPrefix = new GroupedOption + ( + optionDefaultValue: "", + optionName: "--strip-path-prefix ", + optionDescription: "Specify a path prefix to be stripped from exported asset container paths\n" + + "(applies to the ContainerPath / ContainerPathFull group options)\n", + optionExample: "Example: \"--strip-path-prefix assets/models/char/\"\n", + optionHelpGroup: HelpGroups.Advanced + ); o_typeTreeDBPath = new GroupedOption ( optionDefaultValue: "", @@ -976,6 +997,10 @@ public static void ParseArgs(string[] args) f_filterWithRegex.Value = true; flagIndexes.Add(i); break; + case "--filter-exclude-mode": + f_filterExcludeMode.Value = true; + flagIndexes.Add(i); + break; case "--il2cpp": f_il2cpp.Value = true; flagIndexes.Add(i); @@ -1561,6 +1586,10 @@ public static void ParseArgs(string[] args) return; } break; + case "--strip-path-prefix": + // Normalize to a directory-style prefix ending with a separator. + o_stripPathPrefix.Value = value.Replace('\\', '/').TrimEnd('/') + "/"; + break; case "--unity-version": try { diff --git a/UnityRiftCLI/Studio.cs b/UnityRiftCLI/Studio.cs index f329979..c7e471d 100644 --- a/UnityRiftCLI/Studio.cs +++ b/UnityRiftCLI/Studio.cs @@ -655,6 +655,14 @@ private static void FilterAssets() ); break; } + + if (CLIOptions.f_filterExcludeMode.Value) + { + var matchedCount = filteredAssets.Count; + filteredAssets = parsedAssetsList.Except(filteredAssets).ToList(); + Logger.Info($"Exclusion mode: dropping {matchedCount} matched asset(s), keeping {filteredAssets.Count}."); + } + parsedAssetsList.Clear(); parsedAssetsList = filteredAssets; } @@ -668,6 +676,26 @@ public static void ExportAssets() var groupOption = CLIOptions.o_groupAssetsBy.Value; var parallelExportCount = CLIOptions.o_maxParallelExportTasks.Value; var toExportAssetDict = new ConcurrentDictionary(); + + // Validate the strip-path-prefix against loaded containers; ignore it if any container + // doesn't start with it (upstream #113). + if (!string.IsNullOrEmpty(CLIOptions.o_stripPathPrefix.Value)) + { + foreach (var asset in parsedAssetsList) + { + if (string.IsNullOrEmpty(asset.Container)) + continue; + if (!asset.Container.StartsWith(CLIOptions.o_stripPathPrefix.Value)) + { + Logger.Warning($"Asset container path \"{asset.Container}\" does not start with the specified path prefix \"{CLIOptions.o_stripPathPrefix.Value}\""); + Logger.Warning("strip-path-prefix option will be ignored."); + CLIOptions.o_stripPathPrefix.Value = ""; + break; + } + } + if (!string.IsNullOrEmpty(CLIOptions.o_stripPathPrefix.Value)) + Logger.Info($"Asset container path prefix \"{CLIOptions.o_stripPathPrefix.Value}\" will be stripped off."); + } var toParallelExportAssetDict = new ConcurrentDictionary(); Parallel.ForEach(parsedAssetsList, asset => { @@ -681,7 +709,17 @@ public static void ExportAssets() case AssetGroupOption.ContainerPathFull: if (!string.IsNullOrEmpty(asset.Container)) { - exportPath = Path.Combine(savePath, Path.GetDirectoryName(asset.Container)); + var containerDir = Path.GetDirectoryName(asset.Container) ?? ""; + // Strip the configured prefix from the container path (upstream #113). + if (!string.IsNullOrEmpty(CLIOptions.o_stripPathPrefix.Value) + && asset.Container.StartsWith(CLIOptions.o_stripPathPrefix.Value)) + { + var stripped = asset.Container.Substring(CLIOptions.o_stripPathPrefix.Value.Length); + containerDir = Path.GetDirectoryName(stripped) ?? ""; + } + exportPath = string.IsNullOrEmpty(containerDir) + ? savePath + : Path.Combine(savePath, containerDir); if (groupOption == AssetGroupOption.ContainerPathFull) { exportPath = Path.Combine(exportPath, Path.GetFileNameWithoutExtension(asset.Container)); From 77b0cedc2d5612b9bf468e2cac99346b444ce86d Mon Sep 17 00:00:00 2001 From: Diphome Date: Mon, 7 Sep 2026 22:11:56 +0200 Subject: [PATCH 5/5] Sprite: optional full canvas-size export (upstream #115) Port of aelurum/AssetStudioMod#115: add an opt-in mode that places the cropped sprite onto a full-size canvas (the sprite's authored m_Rect) with transparent padding, so exports keep the original dimensions instead of the tight cropped region. Default off = unchanged (cropped) behavior. - SpriteHelper.GetImage gains a spriteWithCanvas param + CutImageWithCanvas. - CLI: --sprite-canvas flag. - GUI: "Export sprites at full canvas size" checkbox (Settings-backed). Co-Authored-By: Clue Opus 4.8 --- UnityRiftCLI/Options/CLIOptions.cs | 15 ++++++++++ UnityRiftCLI/ParallelExporter.cs | 2 +- UnityRiftGUI/ExportOptions.Designer.cs | 17 +++++++++-- UnityRiftGUI/ExportOptions.cs | 2 ++ UnityRiftGUI/ParallelExport.cs | 2 +- UnityRiftGUI/Properties/Settings.Designer.cs | 14 ++++++++- UnityRiftGUI/Properties/Settings.settings | 3 ++ UnityRiftUtility/SpriteHelper.cs | 30 ++++++++++++++++---- 8 files changed, 75 insertions(+), 10 deletions(-) diff --git a/UnityRiftCLI/Options/CLIOptions.cs b/UnityRiftCLI/Options/CLIOptions.cs index 6c0e312..138c9b6 100644 --- a/UnityRiftCLI/Options/CLIOptions.cs +++ b/UnityRiftCLI/Options/CLIOptions.cs @@ -118,6 +118,7 @@ internal static class CLIOptions public static bool convertTexture; public static Option o_imageFormat; public static Option o_audioFormat; + public static Option f_spriteExportWithCanvas; //live2d public static Option o_l2dGroupOption; public static Option f_l2dAssetSearchByFilename; @@ -358,6 +359,16 @@ private static void InitOptions() optionExample: "Example: \"--audio-format wav\"", optionHelpGroup: HelpGroups.Convert ); + f_spriteExportWithCanvas = new GroupedOption + ( + optionDefaultValue: false, + optionName: "--sprite-canvas", + optionDescription: "(Flag) If specified, sprites are exported at their full authored\n" + + "size (m_Rect) with transparent padding, instead of just the cropped region", + optionExample: "", + optionHelpGroup: HelpGroups.Convert, + isFlag: true + ); #endregion #region Init Cubism Live2D Options @@ -1001,6 +1012,10 @@ public static void ParseArgs(string[] args) f_filterExcludeMode.Value = true; flagIndexes.Add(i); break; + case "--sprite-canvas": + f_spriteExportWithCanvas.Value = true; + flagIndexes.Add(i); + break; case "--il2cpp": f_il2cpp.Value = true; flagIndexes.Add(i); diff --git a/UnityRiftCLI/ParallelExporter.cs b/UnityRiftCLI/ParallelExporter.cs index 7e55d50..a4c6307 100644 --- a/UnityRiftCLI/ParallelExporter.cs +++ b/UnityRiftCLI/ParallelExporter.cs @@ -78,7 +78,7 @@ public static bool ExportSprite(AssetItem item, string exportPath, out string de var alphaMask = SpriteMaskMode.On; if (!TryExportFile(exportPath, item, "." + type.ToString().ToLower(), out var exportFullPath)) return false; - var image = ((Sprite)item.Asset).GetImage(alphaMask); + var image = ((Sprite)item.Asset).GetImage(alphaMask, CLIOptions.f_spriteExportWithCanvas.Value); if (image != null) { using (image) diff --git a/UnityRiftGUI/ExportOptions.Designer.cs b/UnityRiftGUI/ExportOptions.Designer.cs index cdbefec..d48c9ad 100644 --- a/UnityRiftGUI/ExportOptions.Designer.cs +++ b/UnityRiftGUI/ExportOptions.Designer.cs @@ -40,6 +40,7 @@ private void InitializeComponent() this.filenameFormatLabel = new System.Windows.Forms.Label(); this.filenameFormatComboBox = new System.Windows.Forms.ComboBox(); this.exportSpriteWithAlphaMask = new System.Windows.Forms.CheckBox(); + this.exportSpriteWithCanvas = new System.Windows.Forms.CheckBox(); this.openAfterExport = new System.Windows.Forms.CheckBox(); this.restoreExtensionName = new System.Windows.Forms.CheckBox(); this.assetGroupOptions = new System.Windows.Forms.ComboBox(); @@ -134,6 +135,7 @@ private void InitializeComponent() this.groupBox1.Controls.Add(this.filenameFormatLabel); this.groupBox1.Controls.Add(this.filenameFormatComboBox); this.groupBox1.Controls.Add(this.exportSpriteWithAlphaMask); + this.groupBox1.Controls.Add(this.exportSpriteWithCanvas); this.groupBox1.Controls.Add(this.openAfterExport); this.groupBox1.Controls.Add(this.restoreExtensionName); this.groupBox1.Controls.Add(this.assetGroupOptions); @@ -143,7 +145,7 @@ private void InitializeComponent() this.groupBox1.Controls.Add(this.converttexture); this.groupBox1.Location = new System.Drawing.Point(12, 13); this.groupBox1.Name = "groupBox1"; - this.groupBox1.Size = new System.Drawing.Size(316, 303); + this.groupBox1.Size = new System.Drawing.Size(316, 340); this.groupBox1.TabIndex = 1; this.groupBox1.TabStop = false; this.groupBox1.Text = "Export"; @@ -248,7 +250,17 @@ private void InitializeComponent() this.exportSpriteWithAlphaMask.TabIndex = 9; this.exportSpriteWithAlphaMask.Text = "Export sprites with alpha mask applied"; this.exportSpriteWithAlphaMask.UseVisualStyleBackColor = true; - // + // + // exportSpriteWithCanvas + // + this.exportSpriteWithCanvas.AutoSize = true; + this.exportSpriteWithCanvas.Location = new System.Drawing.Point(6, 314); + this.exportSpriteWithCanvas.Name = "exportSpriteWithCanvas"; + this.exportSpriteWithCanvas.Size = new System.Drawing.Size(205, 17); + this.exportSpriteWithCanvas.TabIndex = 16; + this.exportSpriteWithCanvas.Text = "Export sprites at full canvas size"; + this.exportSpriteWithCanvas.UseVisualStyleBackColor = true; + // // openAfterExport // this.openAfterExport.AutoSize = true; @@ -889,6 +901,7 @@ private void InitializeComponent() private System.Windows.Forms.CheckBox exportAllUvsAsDiffuseMaps; private System.Windows.Forms.ToolTip optionTooltip; private System.Windows.Forms.CheckBox exportSpriteWithAlphaMask; + private System.Windows.Forms.CheckBox exportSpriteWithCanvas; private System.Windows.Forms.RadioButton towebp; private System.Windows.Forms.GroupBox l2dGroupBox; private System.Windows.Forms.CheckBox l2dForceBezierCheckBox; diff --git a/UnityRiftGUI/ExportOptions.cs b/UnityRiftGUI/ExportOptions.cs index 8330a46..3ce226f 100644 --- a/UnityRiftGUI/ExportOptions.cs +++ b/UnityRiftGUI/ExportOptions.cs @@ -20,6 +20,7 @@ public ExportOptions() restoreExtensionName.Checked = Properties.Settings.Default.restoreExtensionName; converttexture.Checked = Properties.Settings.Default.convertTexture; exportSpriteWithAlphaMask.Checked = Properties.Settings.Default.exportSpriteWithMask; + exportSpriteWithCanvas.Checked = Properties.Settings.Default.spriteExportWithCanvas; convertAudio.Checked = Properties.Settings.Default.convertAudio; var defaultImageType = Properties.Settings.Default.convertType.ToString(); ((RadioButton)panel1.Controls.Cast().First(x => x.Text == defaultImageType)).Checked = true; @@ -50,6 +51,7 @@ private void OKbutton_Click(object sender, EventArgs e) Properties.Settings.Default.restoreExtensionName = restoreExtensionName.Checked; Properties.Settings.Default.convertTexture = converttexture.Checked; Properties.Settings.Default.exportSpriteWithMask = exportSpriteWithAlphaMask.Checked; + Properties.Settings.Default.spriteExportWithCanvas = exportSpriteWithCanvas.Checked; Properties.Settings.Default.convertAudio = convertAudio.Checked; var checkedImageType = (RadioButton)panel1.Controls.Cast().First(x => ((RadioButton)x).Checked); Properties.Settings.Default.convertType = (ImageFormat)Enum.Parse(typeof(ImageFormat), checkedImageType.Text); diff --git a/UnityRiftGUI/ParallelExport.cs b/UnityRiftGUI/ParallelExport.cs index ae85c8f..0188061 100644 --- a/UnityRiftGUI/ParallelExport.cs +++ b/UnityRiftGUI/ParallelExport.cs @@ -77,7 +77,7 @@ public static bool ExportSprite(AssetItem item, string exportPath, out string de var spriteMaskMode = Properties.Settings.Default.exportSpriteWithMask ? SpriteMaskMode.Export : SpriteMaskMode.Off; if (!TryExportFile(exportPath, item, "." + type.ToString().ToLower(), out var exportFullPath)) return false; - var image = ((Sprite)item.Asset).GetImage(spriteMaskMode: spriteMaskMode); + var image = ((Sprite)item.Asset).GetImage(spriteMaskMode: spriteMaskMode, spriteWithCanvas: Properties.Settings.Default.spriteExportWithCanvas); if (image != null) { using (image) diff --git a/UnityRiftGUI/Properties/Settings.Designer.cs b/UnityRiftGUI/Properties/Settings.Designer.cs index 800dfab..01d15f4 100644 --- a/UnityRiftGUI/Properties/Settings.Designer.cs +++ b/UnityRiftGUI/Properties/Settings.Designer.cs @@ -154,7 +154,19 @@ public bool exportSpriteWithMask { this["exportSpriteWithMask"] = value; } } - + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool spriteExportWithCanvas { + get { + return ((bool)(this["spriteExportWithCanvas"])); + } + set { + this["spriteExportWithCanvas"] = value; + } + } + [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("MonoBehaviour")] diff --git a/UnityRiftGUI/Properties/Settings.settings b/UnityRiftGUI/Properties/Settings.settings index 1e1c61e..ea20730 100644 --- a/UnityRiftGUI/Properties/Settings.settings +++ b/UnityRiftGUI/Properties/Settings.settings @@ -35,6 +35,9 @@ True + + False + MonoBehaviour diff --git a/UnityRiftUtility/SpriteHelper.cs b/UnityRiftUtility/SpriteHelper.cs index 3681eed..9a4d35a 100644 --- a/UnityRiftUtility/SpriteHelper.cs +++ b/UnityRiftUtility/SpriteHelper.cs @@ -21,13 +21,13 @@ public enum SpriteMaskMode public static class SpriteHelper { - public static Image GetImage(this Sprite m_Sprite, SpriteMaskMode spriteMaskMode = SpriteMaskMode.On) + public static Image GetImage(this Sprite m_Sprite, SpriteMaskMode spriteMaskMode = SpriteMaskMode.On, bool spriteWithCanvas = false) { if (m_Sprite.m_SpriteAtlas != null && m_Sprite.m_SpriteAtlas.TryGet(out var m_SpriteAtlas)) { if (m_SpriteAtlas.m_RenderDataMap.TryGetValue(m_Sprite.m_RenderDataKey, out var spriteAtlasData) && spriteAtlasData.texture.TryGet(out var m_Texture2D)) { - return CutImage(m_Sprite, m_Texture2D, spriteAtlasData.textureRect, spriteAtlasData.textureRectOffset, spriteAtlasData.downscaleMultiplier, spriteAtlasData.settingsRaw); + return CutImageWithCanvas(m_Sprite, m_Texture2D, spriteAtlasData.textureRect, spriteAtlasData.textureRectOffset, spriteAtlasData.downscaleMultiplier, spriteAtlasData.settingsRaw, spriteWithCanvas); } } else @@ -37,9 +37,9 @@ public static Image GetImage(this Sprite m_Sprite, SpriteMaskMode sprite Image tex = null; if (spriteMaskMode != SpriteMaskMode.MaskOnly) { - tex = CutImage(m_Sprite, m_Texture2D, m_Sprite.m_RD.textureRect, m_Sprite.m_RD.textureRectOffset, m_Sprite.m_RD.downscaleMultiplier, m_Sprite.m_RD.settingsRaw); + tex = CutImageWithCanvas(m_Sprite, m_Texture2D, m_Sprite.m_RD.textureRect, m_Sprite.m_RD.textureRectOffset, m_Sprite.m_RD.downscaleMultiplier, m_Sprite.m_RD.settingsRaw, spriteWithCanvas); } - var alphaTex = CutImage(m_Sprite, m_AlphaTexture2D, m_Sprite.m_RD.textureRect, m_Sprite.m_RD.textureRectOffset, m_Sprite.m_RD.downscaleMultiplier, m_Sprite.m_RD.settingsRaw); + var alphaTex = CutImageWithCanvas(m_Sprite, m_AlphaTexture2D, m_Sprite.m_RD.textureRect, m_Sprite.m_RD.textureRectOffset, m_Sprite.m_RD.downscaleMultiplier, m_Sprite.m_RD.settingsRaw, spriteWithCanvas); switch (spriteMaskMode) { @@ -55,12 +55,32 @@ public static Image GetImage(this Sprite m_Sprite, SpriteMaskMode sprite } else if (m_Sprite.m_RD.texture.TryGet(out m_Texture2D)) { - return CutImage(m_Sprite, m_Texture2D, m_Sprite.m_RD.textureRect, m_Sprite.m_RD.textureRectOffset, m_Sprite.m_RD.downscaleMultiplier, m_Sprite.m_RD.settingsRaw); + return CutImageWithCanvas(m_Sprite, m_Texture2D, m_Sprite.m_RD.textureRect, m_Sprite.m_RD.textureRectOffset, m_Sprite.m_RD.downscaleMultiplier, m_Sprite.m_RD.settingsRaw, spriteWithCanvas); } } return null; } + // Optionally place the cropped sprite onto a full-size canvas (the sprite's authored + // m_Rect), so the export keeps the sprite's original dimensions with transparent padding + // instead of the tight cropped region (upstream #115). Default off = cropped behavior. + private static Image CutImageWithCanvas(Sprite m_Sprite, Texture2D m_Texture2D, Rectf textureRect, Vector2 textureRectOffset, float downscaleMultiplier, SpriteSettings settingsRaw, bool spriteWithCanvas = false) + { + var cropped = CutImage(m_Sprite, m_Texture2D, textureRect, textureRectOffset, downscaleMultiplier, settingsRaw); + if (cropped == null) + return null; + if (!spriteWithCanvas) + return cropped; + + var canvas = new Image((int)MathF.Floor(m_Sprite.m_Rect.width), (int)MathF.Floor(m_Sprite.m_Rect.height)); + canvas.Mutate(ctx => ctx.DrawImage( + cropped, + new Point((int)MathF.Floor(textureRectOffset.X), (int)MathF.Floor(m_Sprite.m_Rect.height - textureRectOffset.Y - cropped.Height)), + 1f)); + cropped.Dispose(); + return canvas; + } + private static void ApplyRGBMask(this Image tex, Image texMask, bool isPreview = false) { using (texMask)