using System; using System.Collections.Generic; using UnityEngine; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Jobs; using Unity.Mathematics; using Unity.Burst; namespace UnityEditor.U2D.PSD { internal static class ExtractLayerTask { struct LayerGroupData { public int startIndex { get; set; } public int endIndex { get; set; } /// /// The layer's bounding box in document space. /// public int4 documentRect { get; set; } } [BurstCompile] struct ConvertBufferJob : IJobParallelFor { [ReadOnly, DeallocateOnJobCompletion] public NativeArray inputTextureBufferSizes; [ReadOnly, DeallocateOnJobCompletion] public NativeArray inputTextures; [ReadOnly, DeallocateOnJobCompletion] public NativeArray inputLayerRects; [ReadOnly, DeallocateOnJobCompletion] public NativeArray layerGroupDataData; [ReadOnly, DeallocateOnJobCompletion] public NativeArray outputLayerRect; [DeallocateOnJobCompletion] public NativeArray outputTextures; public unsafe void Execute(int groupIndex) { var outputColor = (Color32*)outputTextures[groupIndex]; var groupStartIndex = layerGroupDataData[groupIndex].startIndex; var groupEndIndex = layerGroupDataData[groupIndex].endIndex; var outStartX = outputLayerRect[groupIndex].x; var outStartY = outputLayerRect[groupIndex].y; var outWidth = outputLayerRect[groupIndex].z; var outHeight = outputLayerRect[groupIndex].w; for (var layerIndex = groupEndIndex; layerIndex >= groupStartIndex; --layerIndex) { if (inputTextures[layerIndex] == IntPtr.Zero) continue; var inputColor = (Color32*)inputTextures[layerIndex]; var inX = inputLayerRects[layerIndex].x; var inY = inputLayerRects[layerIndex].y; var inWidth = inputLayerRects[layerIndex].z; var inHeight = inputLayerRects[layerIndex].w; for (var y = 0; y < inHeight; ++y) { var outPosY = (y + inY) - outStartY; // If pixel is outside of output texture's Y, move to the next pixel. if (outPosY < 0 || outPosY >= outHeight) continue; // Flip Y position on the input texture, because // PSDs textures are stored "upside-down" var inRow = ((inHeight - 1) - y) * inWidth; var outRow = outPosY * outWidth; for (var x = 0; x < inWidth; ++x) { var outPosX = (x + inX) - outStartX; // If pixel is outside of output texture's X, move to the next pixel. if (outPosX < 0 || outPosX >= outWidth) continue; var inBufferIndex = inRow + x; var outBufferIndex = outRow + outPosX; if (outBufferIndex < 0 || outBufferIndex > (outWidth * outHeight)) continue; Color inColor = inputColor[inBufferIndex]; Color prevOutColor = outputColor[outBufferIndex]; var outColor = new Color(); var destAlpha = prevOutColor.a * (1 - inColor.a); outColor.a = inColor.a + prevOutColor.a * (1 - inColor.a); var premultiplyAlpha = outColor.a > 0.0f ? 1 / outColor.a : 1f; outColor.r = (inColor.r * inColor.a + prevOutColor.r * destAlpha) * premultiplyAlpha; outColor.g = (inColor.g * inColor.a + prevOutColor.g * destAlpha) * premultiplyAlpha; outColor.b = (inColor.b * inColor.a + prevOutColor.b * destAlpha) * premultiplyAlpha; outputColor[outBufferIndex] = outColor; } } } } } public static unsafe void Execute(in PSDExtractLayerData[] psdExtractLayerData, out List outputLayers, bool importHiddenLayer, Vector2Int canvasSize) { outputLayers = new List(); UnityEngine.Profiling.Profiler.BeginSample("ExtractLayer_PrepareJob"); var inputLayers = new List(); ExtractLayerData(in psdExtractLayerData, ref inputLayers, importHiddenLayer, false, true, canvasSize); var layerGroupData = new List(); GenerateOutputLayers(in inputLayers, ref outputLayers, ref layerGroupData, false, canvasSize); if (layerGroupData.Count == 0) { foreach (var layer in outputLayers) layer.texture = default; return; } var job = new ConvertBufferJob(); job.inputTextureBufferSizes = new NativeArray(inputLayers.Count, Allocator.TempJob); job.inputTextures = new NativeArray(inputLayers.Count, Allocator.TempJob); job.inputLayerRects = new NativeArray(inputLayers.Count, Allocator.TempJob); job.outputLayerRect = new NativeArray(layerGroupData.Count, Allocator.TempJob); job.outputTextures = new NativeArray(layerGroupData.Count, Allocator.TempJob); for (int i = 0, groupIndex = 0; i < inputLayers.Count; ++i) { var inputLayer = inputLayers[i]; var outputLayer = outputLayers[i]; job.inputTextures[i] = inputLayer.texture.IsCreated ? new IntPtr(inputLayer.texture.GetUnsafePtr()) : IntPtr.Zero; var isGroupOwner = groupIndex < layerGroupData.Count && layerGroupData[groupIndex].startIndex == i; if (isGroupOwner) { outputLayer.texture = new NativeArray(outputLayer.width * outputLayer.height, Allocator.Persistent); job.outputLayerRect[groupIndex] = new int4((int)outputLayer.layerPosition.x, (int)outputLayer.layerPosition.y, outputLayer.width, outputLayer.height); job.outputTextures[groupIndex] = outputLayer.texture.IsCreated ? new IntPtr(outputLayer.texture.GetUnsafePtr()) : IntPtr.Zero; job.inputTextureBufferSizes[i] = inputLayer.texture.IsCreated ? inputLayer.texture.Length : -1; job.inputLayerRects[i] = layerGroupData[groupIndex].documentRect; ++groupIndex; } else { job.inputTextureBufferSizes[i] = inputLayer.texture.IsCreated ? inputLayer.texture.Length : -1; job.inputLayerRects[i] = new int4((int)inputLayer.layerPosition.x, (int)inputLayer.layerPosition.y, inputLayer.width, inputLayer.height); outputLayer.texture = default; } } job.layerGroupDataData = new NativeArray(layerGroupData.ToArray(), Allocator.TempJob); var jobsPerThread = layerGroupData.Count / (SystemInfo.processorCount == 0 ? 8 : SystemInfo.processorCount); jobsPerThread = Mathf.Max(jobsPerThread, 1); var handle = job.Schedule(layerGroupData.Count, jobsPerThread); UnityEngine.Profiling.Profiler.EndSample(); handle.Complete(); } static void ExtractLayerData(in PSDExtractLayerData[] inputLayers, ref List extractedLayers, bool importHiddenLayer, bool flatten, bool parentGroupVisible, Vector2Int canvasSize) { var parentGroupIndex = extractedLayers.Count - 1; foreach (var inputLayer in inputLayers) { var bitmapLayer = inputLayer.bitmapLayer; var importSettings = inputLayer.importSetting; var layerVisible = bitmapLayer.Visible && parentGroupVisible; var layerRect = new RectInt(bitmapLayer.documentRect.X, bitmapLayer.documentRect.Y, bitmapLayer.Surface.width, bitmapLayer.Surface.height); if (!bitmapLayer.IsGroup) layerRect.y = (canvasSize.y - layerRect.y - layerRect.height); NativeArray surface = default; if ((importHiddenLayer || bitmapLayer.Visible) && bitmapLayer.Surface.color.IsCreated && bitmapLayer.Surface.color.Length > 0) surface = bitmapLayer.Surface.color; var extractedLayer = new PSDLayer(surface, parentGroupIndex, bitmapLayer.IsGroup, bitmapLayer.Name, layerRect.width, layerRect.height, bitmapLayer.LayerID, bitmapLayer.Visible) { spriteID = inputLayer.importSetting.spriteId, flatten = bitmapLayer.IsGroup && inputLayer.importSetting.flatten, layerPosition = bitmapLayer.IsGroup ? Vector2.zero : layerRect.position }; extractedLayer.isImported = (importHiddenLayer || layerVisible) && !flatten; if (extractedLayer.isGroup) extractedLayer.isImported = extractedLayer.isImported && extractedLayer.flatten; extractedLayers.Add(extractedLayer); if (inputLayer.children.Length > 0) ExtractLayerData(in inputLayer.children, ref extractedLayers, importHiddenLayer, flatten || extractedLayer.flatten, layerVisible, canvasSize); } } static void GenerateOutputLayers(in List inputLayers, ref List outputLayers, ref List layerGroupData, bool flatten, Vector2Int canvasSize) { var canvasRect = new RectInt(Vector2Int.zero, canvasSize); for (var i = 0; i < inputLayers.Count; ++i) { var inputLayer = inputLayers[i]; var outputLayer = new PSDLayer(inputLayer); var outputRect = new RectInt((int)outputLayer.layerPosition.x, (int)outputLayer.layerPosition.y, outputLayer.width, outputLayer.height); if (inputLayer.isGroup) { var childIndices = FindAllChildrenOfParent(i, in inputLayers); childIndices.Sort(); var startIndex = i; var endIndex = i + childIndices.Count; if (flatten == false && inputLayer.flatten && startIndex < endIndex) { var groupBoundingBox = CalculateLayerRectInChildren(in inputLayers, in childIndices); layerGroupData.Add(new LayerGroupData() { startIndex = startIndex, endIndex = endIndex, documentRect = new int4(groupBoundingBox.x, groupBoundingBox.y, groupBoundingBox.width, groupBoundingBox.height) }); outputLayer.texture = default; outputRect = groupBoundingBox; } } else if(!inputLayer.isGroup && inputLayer.isImported) { var inputRect = new int4((int)inputLayer.layerPosition.x, (int)inputLayer.layerPosition.y, inputLayer.width, inputLayer.height); layerGroupData.Add(new LayerGroupData() { startIndex = i, endIndex = i, documentRect = inputRect }); } CropRect(ref outputRect, canvasRect); outputLayer.layerPosition = outputRect.position; outputLayer.width = outputRect.width; outputLayer.height = outputRect.height; outputLayers.Add(outputLayer); } } static List FindAllChildrenOfParent(int parentIndex, in List layers) { var childIndices = new List(); for (var i = parentIndex + 1; i < layers.Count; ++i) { if (layers[i].parentIndex == parentIndex) { childIndices.Add(i); if (layers[i].isGroup) childIndices.AddRange(FindAllChildrenOfParent(i, in layers)); } } return childIndices; } static RectInt CalculateLayerRectInChildren(in List inputLayers, in List childIndices) { var groupBoundingBox = default(RectInt); for (var m = 0; m < childIndices.Count; ++m) { var childLayer = inputLayers[childIndices[m]]; if (childLayer.isGroup) continue; var layerRect = new RectInt((int) childLayer.layerPosition.x, (int) childLayer.layerPosition.y, childLayer.width, childLayer.height); if (IsRectIntDefault(groupBoundingBox)) groupBoundingBox = layerRect; else FitRectInsideRect(ref groupBoundingBox, in layerRect); } return groupBoundingBox; } static bool IsRectIntDefault(RectInt rectInt) { return rectInt.x == 0 && rectInt.y == 0 && rectInt.width == 0 && rectInt.height == 0; } static void CropRect(ref RectInt baseRect, in RectInt cropArea) { if (baseRect.x < cropArea.x) { baseRect.width = Mathf.Max(baseRect.width - (cropArea.x - baseRect.x), 0); baseRect.x = cropArea.x; } if (baseRect.xMax > cropArea.xMax) { baseRect.x = Mathf.Min(baseRect.x, cropArea.xMax); baseRect.width = Mathf.Max(cropArea.xMax - baseRect.x, 0); } if (baseRect.y < cropArea.y) { baseRect.height = Mathf.Max(baseRect.height - (cropArea.y - baseRect.y), 0); baseRect.y = cropArea.y; } if (baseRect.yMax > cropArea.yMax) { baseRect.y = Mathf.Min(baseRect.y, cropArea.yMax); baseRect.height = Mathf.Max(cropArea.yMax - baseRect.y, 0); } } static void FitRectInsideRect(ref RectInt baseRect, in RectInt rectToFitIn) { if (baseRect.xMin > rectToFitIn.xMin) baseRect.xMin = rectToFitIn.xMin; if (baseRect.yMin > rectToFitIn.yMin) baseRect.yMin = rectToFitIn.yMin; if (baseRect.xMax < rectToFitIn.xMax) baseRect.xMax = rectToFitIn.xMax; if (baseRect.yMax < rectToFitIn.yMax) baseRect.yMax = rectToFitIn.yMax; } } }