// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Editor.Sprites { #if UNITY_EDITOR using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using UnityEditor; using UnityEngine; using CustomEditors; using WallstopStudios.UnityHelpers.Core.Extension; using WallstopStudios.UnityHelpers.Editor.Utils; using WallstopStudios.UnityHelpers.Utils; using Object = UnityEngine.Object; /// /// Finds and crops single-sprite textures to their minimal bounding rectangle based on alpha /// coverage, with optional padding and output controls. Can overwrite originals or write to a /// separate folder, and optionally copy default platform import settings. /// /// /// /// Problems this solves: trimming transparent margins around sprites to reduce overdraw and /// improve packing; standardizing sprite bounds for consistent layout. /// /// /// How it works: scans provided folders for supported image extensions and single-sprite /// textures, computes an alpha-threshold-based tight rect, applies optional padding, and /// writes the cropped PNG. Provides a "Danger Zone" utility to replace references to originals /// with their Cropped_* counterparts across assets. /// /// /// Usage: /// /// /// Open via menu: Tools/Wallstop Studios/Unity Helpers/Sprite Cropper. /// Select input folders, optional name regex, and padding. /// Choose overwrite vs output directory, then Find/Process sprites. /// /// /// Pros: reduces texture waste, quick batch processing, preserves importer options when chosen. /// Caveats: Multi-sprite textures are skipped; overwriting is destructive—use VCS; reference /// replacement is potentially dangerous and should be reviewed carefully. /// /// public sealed class SpriteCropper : EditorWindow { private const string Name = "Sprite Cropper"; private const string CroppedPrefix = "Cropped_"; private static readonly string[] ImageFileExtensions = { ".png", ".jpg", ".jpeg", ".bmp", ".tga", ".psd", ".gif", }; private const float AlphaThreshold = 0.01f; internal enum OutputReadability { MirrorSource = 0, Readable = 1, NotReadable = 2, } [SerializeField] internal List _inputDirectories = new(); [SerializeField] internal string _spriteNameRegex = ".*"; [SerializeField] internal bool _onlyNecessary; [SerializeField] internal int _leftPadding; [SerializeField] internal int _rightPadding; [SerializeField] internal int _topPadding; [SerializeField] internal int _bottomPadding; [SerializeField] internal bool _overwriteOriginals; [SerializeField] internal Object _outputDirectory; [SerializeField] internal OutputReadability _outputReadability = OutputReadability.MirrorSource; [SerializeField] internal bool _copyDefaultPlatformSettings = true; internal List _filesToProcess; private SerializedObject _serializedObject; private SerializedProperty _inputDirectoriesProperty; private SerializedProperty _onlyNecessaryProperty; private SerializedProperty _leftPaddingProperty; private SerializedProperty _rightPaddingProperty; private SerializedProperty _topPaddingProperty; private SerializedProperty _bottomPaddingProperty; private SerializedProperty _spriteNameRegexProperty; private SerializedProperty _overwriteOriginalsProperty; private SerializedProperty _outputDirectoryProperty; private SerializedProperty _outputReadabilityProperty; private SerializedProperty _copyDefaultPlatformSettingsProperty; private Regex _regex; // Diagnostics for Multiple-sprite textures detected during search private readonly List _multiSpriteFiles = new(); // Danger zone acknowledgment for reference replacement private bool _ackDanger; [MenuItem("Tools/Wallstop Studios/Unity Helpers/" + Name)] private static void ShowWindow() => GetWindow(Name); private void OnEnable() { _serializedObject = new SerializedObject(this); _inputDirectoriesProperty = _serializedObject.FindProperty(nameof(_inputDirectories)); _onlyNecessaryProperty = _serializedObject.FindProperty(nameof(_onlyNecessary)); _leftPaddingProperty = _serializedObject.FindProperty(nameof(_leftPadding)); _rightPaddingProperty = _serializedObject.FindProperty(nameof(_rightPadding)); _topPaddingProperty = _serializedObject.FindProperty(nameof(_topPadding)); _bottomPaddingProperty = _serializedObject.FindProperty(nameof(_bottomPadding)); _spriteNameRegexProperty = _serializedObject.FindProperty(nameof(_spriteNameRegex)); _overwriteOriginalsProperty = _serializedObject.FindProperty( nameof(_overwriteOriginals) ); _outputDirectoryProperty = _serializedObject.FindProperty(nameof(_outputDirectory)); _outputReadabilityProperty = _serializedObject.FindProperty(nameof(_outputReadability)); _copyDefaultPlatformSettingsProperty = _serializedObject.FindProperty( nameof(_copyDefaultPlatformSettings) ); } private void OnGUI() { EditorGUILayout.LabelField("Input directories", EditorStyles.boldLabel); _serializedObject.Update(); PersistentDirectoryGUI.PathSelectorObjectArray( _inputDirectoriesProperty, nameof(SpriteCropper) ); EditorGUILayout.PropertyField(_spriteNameRegexProperty, true); EditorGUILayout.PropertyField(_onlyNecessaryProperty, true); EditorGUILayout.PropertyField(_leftPaddingProperty, true); EditorGUILayout.PropertyField(_rightPaddingProperty, true); EditorGUILayout.PropertyField(_topPaddingProperty, true); EditorGUILayout.PropertyField(_bottomPaddingProperty, true); EditorGUILayout.Space(); EditorGUILayout.LabelField("Output", EditorStyles.boldLabel); EditorGUILayout.PropertyField( _overwriteOriginalsProperty, new GUIContent("Overwrite Originals") ); using (new EditorGUI.DisabledScope(_overwriteOriginals)) { EditorGUILayout.PropertyField( _outputDirectoryProperty, new GUIContent("Output Directory (optional)") ); } EditorGUILayout.PropertyField( _outputReadabilityProperty, new GUIContent("Output Readability") ); EditorGUILayout.PropertyField( _copyDefaultPlatformSettingsProperty, new GUIContent("Copy Default Platform Settings") ); _serializedObject.ApplyModifiedProperties(); // Clamp paddings to non-negative _leftPadding = Mathf.Max(0, _leftPadding); _rightPadding = Mathf.Max(0, _rightPadding); _topPadding = Mathf.Max(0, _topPadding); _bottomPadding = Mathf.Max(0, _bottomPadding); if (GUILayout.Button("Find Sprites To Process")) { _regex = !string.IsNullOrWhiteSpace(_spriteNameRegex) ? new Regex(_spriteNameRegex) : null; _multiSpriteFiles.Clear(); FindFilesToProcess(); } if (_filesToProcess is { Count: > 0 }) { GUILayout.Label( $"Found {_filesToProcess.Count} sprites to process.", EditorStyles.boldLabel ); if (_multiSpriteFiles.Count > 0) { EditorGUILayout.HelpBox( $"Detected {_multiSpriteFiles.Count} textures with Sprite Import Mode = Multiple. SpriteCropper only supports Single sprites. These will be skipped.", MessageType.Warning ); if (GUILayout.Button("Log details of Multiple-sprite textures")) { foreach (string path in _multiSpriteFiles) { this.LogWarn($"Multiple-sprite texture detected (skipped): {path}"); } } } if (GUILayout.Button($"Process {_filesToProcess.Count} Sprites")) { ProcessFoundSprites(); _filesToProcess = null; } } else if (_filesToProcess != null) { GUILayout.Label( "No sprites found to process in the selected directories.", EditorStyles.label ); } EditorGUILayout.Space(); // Danger Zone: Reference Replacement using (new GUILayout.VerticalScope("box")) { Color prev = GUI.color; GUI.color = Color.red; GUILayout.Label( "Danger Zone: Replace references to originals with Cropped_* versions", EditorStyles.boldLabel ); GUI.color = prev; EditorGUILayout.HelpBox( "This will scan assets and replace Sprite references pointing to original textures with references to their Cropped_* counterparts. This is potentially destructive. Ensure you have backups/version control.", MessageType.Error ); _ackDanger = EditorGUILayout.ToggleLeft( "I understand the risks and want to proceed.", _ackDanger ); using (new EditorGUI.DisabledScope(!_ackDanger)) { if (GUILayout.Button("Replace Sprite References With Cropped_* Versions")) { ReplaceSpriteReferencesWithCropped(); } } } } internal void FindFilesToProcess() { _filesToProcess ??= new List(); _filesToProcess.Clear(); if (_inputDirectories is not { Count: > 0 }) { this.LogWarn($"No input directories selected."); return; } foreach (Object maybeDirectory in _inputDirectories.Where(d => d != null)) { string assetPath = AssetDatabase.GetAssetPath(maybeDirectory); if (!AssetDatabase.IsValidFolder(assetPath)) { this.LogWarn($"Skipping invalid path: {assetPath}"); continue; } IEnumerable files = Directory .GetFiles(assetPath, "*.*", SearchOption.AllDirectories) .Where(file => Array.Exists( ImageFileExtensions, extension => file.EndsWith(extension, StringComparison.OrdinalIgnoreCase) ) ); foreach (string file in files) { if (file.Contains(CroppedPrefix, StringComparison.OrdinalIgnoreCase)) { continue; } string fileName = Path.GetFileNameWithoutExtension(file); if (_regex != null && !_regex.IsMatch(fileName)) { continue; } // Skip and record textures with Multiple sprite import mode if (AssetImporter.GetAtPath(file) is TextureImporter ti) { if ( ti.textureType == TextureImporterType.Sprite && ti.spriteImportMode != SpriteImportMode.Single ) { _multiSpriteFiles.Add(file); continue; } } _filesToProcess.Add(file); } } Repaint(); } private void ReplaceSpriteReferencesWithCropped() { try { // Build mapping from original Sprite to Cropped_* Sprite based on input directories Dictionary mapping = new(); if (_inputDirectories is not { Count: > 0 }) { this.LogWarn( $"No input directories selected; cannot build replacement mapping." ); return; } foreach (Object maybeDirectory in _inputDirectories.Where(d => d != null)) { string dirPath = AssetDatabase.GetAssetPath(maybeDirectory); if (!AssetDatabase.IsValidFolder(dirPath)) { continue; } IEnumerable files = Directory .GetFiles(dirPath, "*.*", SearchOption.AllDirectories) .Where(file => Array.Exists( ImageFileExtensions, extension => file.EndsWith(extension, StringComparison.OrdinalIgnoreCase) ) ); foreach (string file in files) { if (file.Contains(CroppedPrefix, StringComparison.OrdinalIgnoreCase)) { continue; } string croppedPath = Path.Combine( Path.GetDirectoryName(file) ?? string.Empty, CroppedPrefix + Path.GetFileName(file) ); if (!File.Exists(croppedPath)) { continue; } Sprite original = AssetDatabase.LoadAssetAtPath(file); Sprite cropped = AssetDatabase.LoadAssetAtPath(croppedPath); if (original != null && cropped != null) { mapping[original] = cropped; } } } if (mapping.Count == 0) { this.LogWarn( $"No original→Cropped_* sprite pairs found. Aborting replacement." ); return; } string[] allAssets = AssetDatabase.GetAllAssetPaths(); string[] candidateExts = { ".prefab", ".unity", ".asset", ".mat", ".anim", ".overrideController", }; int modifiedAssets = 0; using (AssetDatabaseBatchHelper.BeginBatch(refreshOnDispose: true)) { try { for (int i = 0; i < allAssets.Length; ++i) { string path = allAssets[i]; if (!path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) { continue; } if ( !candidateExts.Any(ext => path.EndsWith(ext, StringComparison.OrdinalIgnoreCase) ) ) { continue; } if ( Utils.EditorUi.CancelableProgress( "Replacing Sprite References", $"Scanning {i + 1}/{allAssets.Length}: {Path.GetFileName(path)}", i / (float)allAssets.Length ) ) { this.LogWarn($"Reference replacement cancelled by user."); break; } bool assetModified = false; Object[] objs = AssetDatabase.LoadAllAssetsAtPath(path); foreach (Object o in objs) { if (o == null) { continue; } bool objectModified = false; SerializedObject so = new(o); SerializedProperty it = so.GetIterator(); bool enter = true; while (it.NextVisible(enter)) { enter = false; if (it.propertyType == SerializedPropertyType.ObjectReference) { Sprite s = it.objectReferenceValue as Sprite; if ( s != null && mapping.TryGetValue(s, out Sprite replacement) ) { it.objectReferenceValue = replacement; objectModified = true; } } } if (objectModified) { so.ApplyModifiedProperties(); EditorUtility.SetDirty(o); assetModified = true; } } if (assetModified) { modifiedAssets++; } } } finally { AssetDatabase.SaveAssets(); Utils.EditorUi.ClearProgress(); } } this.Log( $"Reference replacement complete. Modified assets: {modifiedAssets}. Mapped pairs: {mapping.Count}." ); } catch (Exception e) { this.LogError($"Error during reference replacement.", e); } } internal void ProcessFoundSprites() { if (_filesToProcess is not { Count: > 0 }) { this.LogWarn($"No files found or selected for processing."); return; } string lastProcessed = null; bool canceled = false; WallstopGenericPool> processedFilesPool = SetBuffers.GetHashSetPool(StringComparer.OrdinalIgnoreCase); using PooledResource> processedFilesLease = processedFilesPool.Get( out HashSet processedFiles ); using PooledResource> needReprocessingLease = Buffers.List.Get( out List needReprocessing ); WallstopGenericPool> originalReadablePool = DictionaryBuffer< string, bool >.GetDictionaryPool(StringComparer.OrdinalIgnoreCase); using PooledResource> originalReadableLease = originalReadablePool.Get(out Dictionary originalReadable); using PooledResource> newImportersLease = Buffers.List.Get(out List newImporters); { try { int total = _filesToProcess.Count; using (AssetDatabaseBatchHelper.BeginBatch(refreshOnDispose: true)) { for (int i = 0; i < _filesToProcess.Count; ++i) { string file = _filesToProcess[i]; lastProcessed = file; if ( Utils.EditorUi.CancelableProgress( Name, $"Pre-processing {i + 1}/{total}: {Path.GetFileName(file)}", i / (float)total ) ) { canceled = true; break; } CheckPreProcessNeeded(file, originalReadable); } AssetDatabase.SaveAssets(); } int totalSuccessfullyProcessed = 0; if (!canceled) { using (AssetDatabaseBatchHelper.BeginBatch(refreshOnDispose: true)) { for (int i = 0; i < _filesToProcess.Count; ++i) { string file = _filesToProcess[i]; if (!processedFiles.Add(file)) { continue; } lastProcessed = file; if ( Utils.EditorUi.CancelableProgress( Name, $"Processing {i + 1}/{total}: {Path.GetFileName(file)}", i / (float)total ) ) { canceled = true; break; } TextureImporter newImporter = ProcessSprite( file, out ProcessOutcome outcome, originalReadable ); switch (outcome) { case ProcessOutcome.Success: ++totalSuccessfullyProcessed; if (newImporter != null) { newImporters.Add(newImporter); } break; case ProcessOutcome.SkippedNoChange: // No-op break; case ProcessOutcome.RetryableError: needReprocessing.Add(file); break; case ProcessOutcome.FatalError: // Log already handled inside ProcessSprite; skip break; } } foreach (TextureImporter newImporter in newImporters) { newImporter.SaveAndReimport(); } AssetDatabase.SaveAssets(); } } if (!canceled && needReprocessing.Any()) { newImporters.Clear(); using (AssetDatabaseBatchHelper.BeginBatch(refreshOnDispose: true)) { foreach (string file in needReprocessing) { TextureImporter newImporter = ProcessSprite( file, out ProcessOutcome outcome, originalReadable ); if (outcome == ProcessOutcome.Success && newImporter != null) { ++totalSuccessfullyProcessed; newImporters.Add(newImporter); } } foreach (TextureImporter newImporter in newImporters) { newImporter.SaveAndReimport(); } AssetDatabase.SaveAssets(); } } // Restore readability to originals that we changed if (originalReadable.Count > 0 && !_overwriteOriginals) { using (AssetDatabaseBatchHelper.BeginBatch(refreshOnDispose: true)) { foreach ((string path, bool wasReadable) in originalReadable) { try { if ( AssetImporter.GetAtPath(path) is TextureImporter { textureType: TextureImporterType.Sprite } srcImporter ) { if (srcImporter.isReadable != wasReadable) { srcImporter.isReadable = wasReadable; srcImporter.SaveAndReimport(); } } } catch (Exception e) { this.LogError( $"Failed to restore readability for '{path}'.", e ); } } AssetDatabase.SaveAssets(); } } if (canceled) { this.LogWarn($"Sprite cropping canceled by user."); } else { int skipped = _filesToProcess.Count - totalSuccessfullyProcessed; this.Log( $"{totalSuccessfullyProcessed} sprites processed successfully. Skipped: {skipped}" ); } } catch (Exception e) { this.LogError( $"An error occurred during processing. Last processed: {lastProcessed}.", e ); } finally { Utils.EditorUi.ClearProgress(); } } } private static void CheckPreProcessNeeded( string assetPath, Dictionary originalReadable ) { string assetDirectory = Path.GetDirectoryName(assetPath); if (string.IsNullOrWhiteSpace(assetDirectory)) { return; } if ( AssetImporter.GetAtPath(assetPath) is not TextureImporter { textureType: TextureImporterType.Sprite } importer ) { return; } // Make readable if needed and remember original state to restore after processing if (!importer.isReadable) { originalReadable.TryAdd(assetPath, false); importer.isReadable = true; importer.SaveAndReimport(); } else { originalReadable.TryAdd(assetPath, true); } } private enum ProcessOutcome { Success, SkippedNoChange, RetryableError, FatalError, } private TextureImporter ProcessSprite( string assetPath, out ProcessOutcome outcome, Dictionary originalReadable ) { outcome = ProcessOutcome.FatalError; string assetDirectory = Path.GetDirectoryName(assetPath); if (string.IsNullOrWhiteSpace(assetDirectory)) { outcome = ProcessOutcome.FatalError; return null; } if ( AssetImporter.GetAtPath(assetPath) is not TextureImporter { textureType: TextureImporterType.Sprite } importer ) { outcome = ProcessOutcome.FatalError; return null; } if (importer.spriteImportMode != SpriteImportMode.Single) { this.LogWarn($"Skipping texture with Multiple sprite mode: {assetPath}"); outcome = ProcessOutcome.SkippedNoChange; return null; } Texture2D tex = AssetDatabase.LoadAssetAtPath(assetPath); if (tex == null) { outcome = ProcessOutcome.RetryableError; return null; } Color32[] pixels = tex.GetPixels32(); int width = tex.width; int height = tex.height; int minX = width; int minY = height; int maxX = 0; int maxY = 0; bool hasVisible = false; object lockObject = new(); byte alphaByteThreshold = (byte) Mathf.Clamp(Mathf.RoundToInt(AlphaThreshold * 255f), 0, 255); Parallel.For( 0, width * height, () => (minX: width, minY: height, maxX: 0, maxY: 0, hasVisible: false), (index, _, localState) => { int x = index % width; int y = index / width; byte a = pixels[index].a; if (a > alphaByteThreshold) { localState.hasVisible = true; localState.minX = Mathf.Min(localState.minX, x); localState.minY = Mathf.Min(localState.minY, y); localState.maxX = Mathf.Max(localState.maxX, x); localState.maxY = Mathf.Max(localState.maxY, y); } return localState; }, finalLocalState => { if (finalLocalState.hasVisible) { lock (lockObject) { hasVisible = true; minX = Mathf.Min(minX, finalLocalState.minX); minY = Mathf.Min(minY, finalLocalState.minY); maxX = Mathf.Max(maxX, finalLocalState.maxX); maxY = Mathf.Max(maxY, finalLocalState.maxY); } } } ); int visibleMinX = minX; int visibleMinY = minY; int visibleMaxX = maxX; int visibleMaxY = maxY; if (hasVisible) { visibleMinX -= _leftPadding; visibleMinY -= _bottomPadding; visibleMaxX += _rightPadding; visibleMaxY += _topPadding; } else { visibleMinX = visibleMinY = 0; visibleMaxX = visibleMaxY = 0; } int cropWidth = visibleMaxX - visibleMinX + 1; int cropHeight = visibleMaxY - visibleMinY + 1; if (_onlyNecessary && (!hasVisible || (cropWidth == width && cropHeight == height))) { outcome = ProcessOutcome.SkippedNoChange; return null; } Texture2D cropped = new(cropWidth, cropHeight, TextureFormat.RGBA32, false); int pixelCount = cropWidth * cropHeight; // Note: We need an exact-size array for SetPixels32, which requires the array length // to exactly match width * height. SystemArrayPool returns arrays that may be larger // than requested, so we must allocate an exact-size array for the Unity API call. Color32[] croppedPixels = new Color32[pixelCount]; int srcX0 = Mathf.Max(visibleMinX, 0); int srcY0 = Mathf.Max(visibleMinY, 0); int srcX1 = Mathf.Min(visibleMaxX, width - 1); int srcY1 = Mathf.Min(visibleMaxY, height - 1); Parallel.For( 0, cropHeight, y => { int destRow = y * cropWidth; int srcY = visibleMinY + y; if (srcY < 0 || srcY >= height || srcY < srcY0 || srcY > srcY1) { Array.Clear(croppedPixels, destRow, cropWidth); return; } int copyStartDestX = Mathf.Max(0, srcX0 - visibleMinX); int copyEndDestX = Mathf.Min(cropWidth - 1, srcX1 - visibleMinX); int leftClear = copyStartDestX; int rightClear = cropWidth - 1 - copyEndDestX; if (leftClear > 0) { Array.Clear(croppedPixels, destRow, leftClear); } if (copyEndDestX >= copyStartDestX) { int numToCopy = copyEndDestX - copyStartDestX + 1; int srcStartX = srcX0; int srcIndex = srcY * width + srcStartX; int destIndex = destRow + copyStartDestX; Array.Copy(pixels, srcIndex, croppedPixels, destIndex, numToCopy); } if (rightClear > 0) { Array.Clear(croppedPixels, destRow + (cropWidth - rightClear), rightClear); } } ); cropped.SetPixels32(croppedPixels); cropped.Apply(); // Determine output path and importer to modify string outputDirectory = assetDirectory; if (!_overwriteOriginals && _outputDirectory != null) { string dirPath = AssetDatabase.GetAssetPath(_outputDirectory); if (!string.IsNullOrWhiteSpace(dirPath) && AssetDatabase.IsValidFolder(dirPath)) { outputDirectory = dirPath; } } string outputFileName = _overwriteOriginals ? Path.GetFileName(assetPath) : CroppedPrefix + Path.GetFileName(assetPath); string newPath = Path.Combine(outputDirectory, outputFileName); byte[] pngBytes = cropped.EncodeToPNG(); File.WriteAllBytes(newPath, pngBytes); DestroyImmediate(cropped); AssetDatabase.ImportAsset(newPath); TextureImporter newImporter = AssetImporter.GetAtPath(newPath) as TextureImporter; if (newImporter == null) { outcome = ProcessOutcome.RetryableError; return null; } TextureImporterSettings newSettings = new(); importer.ReadTextureSettings(newSettings); Vector2 origPivot = GetSpritePivot(importer); Vector2 origCenter = new(width * origPivot.x, height * origPivot.y); Vector2 newPivotPixels = origCenter - new Vector2(visibleMinX, visibleMinY); Vector2 newPivotNorm = new( cropWidth > 0 ? newPivotPixels.x / cropWidth : 0.5f, cropHeight > 0 ? newPivotPixels.y / cropHeight : 0.5f ); if (!hasVisible) { newPivotNorm = new Vector2(0.5f, 0.5f); } // Adjust 9-slice borders based on trimming from edges Vector4 border = newSettings.spriteBorder; int deltaLeft = visibleMinX; // pixels trimmed from left of full image int deltaBottom = visibleMinY; // trimmed from bottom int deltaRight = width - 1 - visibleMaxX; // trimmed from right int deltaTop = height - 1 - visibleMaxY; // trimmed from top border.x = Mathf.Max(0, border.x - deltaLeft); border.y = Mathf.Max(0, border.y - deltaBottom); border.z = Mathf.Max(0, border.z - deltaRight); border.w = Mathf.Max(0, border.w - deltaTop); newSettings.spritePivot = newPivotNorm; newSettings.spriteAlignment = (int)SpriteAlignment.Custom; newSettings.spriteBorder = border; newImporter.SetTextureSettings(newSettings); // Always import the cropped output as Single unless we implement full metadata migration newImporter.spriteImportMode = SpriteImportMode.Single; newImporter.spritePivot = newPivotNorm; newImporter.textureType = importer.textureType; newImporter.filterMode = importer.filterMode; newImporter.textureCompression = importer.textureCompression; newImporter.wrapMode = importer.wrapMode; newImporter.mipmapEnabled = importer.mipmapEnabled; newImporter.spritePixelsPerUnit = importer.spritePixelsPerUnit; // Copy default platform settings if requested if (_copyDefaultPlatformSettings) { try { TextureImporterPlatformSettings srcDefault = importer.GetDefaultPlatformTextureSettings(); if (!string.IsNullOrWhiteSpace(srcDefault.name)) { newImporter.SetPlatformTextureSettings(srcDefault); } } catch (Exception e) { this.LogWarn($"Failed to copy default platform settings for '{assetPath}'.", e); } } // Set readability based on option bool srcOriginalReadable = originalReadable.TryGetValue(assetPath, out bool wasReadable) ? wasReadable : importer.isReadable; switch (_outputReadability) { case OutputReadability.MirrorSource: newImporter.isReadable = srcOriginalReadable; break; case OutputReadability.Readable: newImporter.isReadable = true; break; case OutputReadability.NotReadable: newImporter.isReadable = false; break; } newImporter.SaveAndReimport(); outcome = ProcessOutcome.Success; return newImporter; } private static Vector2 GetSpritePivot(TextureImporter importer) { if (importer.spriteImportMode == SpriteImportMode.Single) { return importer.spritePivot; } return new Vector2(0.5f, 0.5f); } } #endif }