///////////////////////////////////////////////////////////////////////////////// // // Photoshop PSD FileType Plugin for Paint.NET // http://psdplugin.codeplex.com/ // // This software is provided under the MIT License: // Copyright (c) 2006-2007 Frank Blumenberg // Copyright (c) 2010-2016 Tao Yue // // See LICENSE.txt for complete licensing and attribution information. // ///////////////////////////////////////////////////////////////////////////////// using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using PhotoshopFile; using UnityEngine; using PDNWrapper; using Unity.Collections; using Unity.Jobs; namespace PaintDotNet.Data.PhotoshopFileType { internal static class PsdLoad { public static PsdFile Load(System.IO.Stream input, ELoadFlag loadFlag) { var loadContext = new DocumentLoadContext(); return new PsdFile(input, loadContext, loadFlag); } public static Document Load(System.IO.Stream input) { // Load and decompress Photoshop file structures var loadContext = new DocumentLoadContext(); var psdFile = new PsdFile(input, loadContext); // Multichannel images are loaded by processing each channel as a // grayscale layer. if (psdFile.ColorMode == PsdColorMode.Multichannel) { CreateLayersFromChannels(psdFile); psdFile.ColorMode = PsdColorMode.Grayscale; } // Convert into Paint.NET internal representation var document = new Document(psdFile.ColumnCount, psdFile.RowCount); if (psdFile.Layers.Count == 0) { psdFile.BaseLayer.CreateMissingChannels(); var layer = PDNWrapper.Layer.CreateBackgroundLayer(psdFile.ColumnCount, psdFile.RowCount); ImageDecoderPdn.DecodeImage(layer, psdFile.BaseLayer); layer.Name = String.IsNullOrEmpty(psdFile.BaseLayer.Name)? "Background" : psdFile.BaseLayer.Name; layer.Opacity = psdFile.BaseLayer.Opacity; layer.Visible = psdFile.BaseLayer.Visible; layer.IsGroup = psdFile.BaseLayer.IsGroup; layer.LayerID = psdFile.BaseLayer.LayerID; layer.BlendMode = BlendModeMapping.FromPsdBlendMode(psdFile.BaseLayer.BlendModeKey); document.Layers.Add(layer); } else { psdFile.VerifyLayerSections(); ApplyLayerSections(psdFile.Layers); //var pdnLayers = psdFile.Layers.AsParallel().AsOrdered() // .Select(psdLayer => psdLayer.DecodeToPdnLayer()) // .ToList(); //document.Layers.AddRange(pdnLayers); /* foreach (var l in psdFile.Layers) { document.Layers.Add(l.DecodeToPdnLayer()); } */ BitmapLayer parent = null; JobHandle jobHandle = default(JobHandle); foreach (var l in Enumerable.Reverse(psdFile.Layers)) { if (l.IsEndGroupMarker) { parent = parent != null ? parent.ParentLayer : null; continue; } BitmapLayer b = null; jobHandle = l.DecodeToPdnLayer(jobHandle, out b); b.ParentLayer = parent; if (parent != null) parent.AddChildLayer(b); else document.Layers.Add(b); if (b.IsGroup) parent = b; } jobHandle.Complete(); } SetPdnResolutionInfo(psdFile, document); psdFile.Cleanup(); return document; } internal static JobHandle DecodeToPdnLayer(this PhotoshopFile.Layer psdLayer, JobHandle inputDeps, out BitmapLayer pdnLayer) { psdLayer.CreateMissingChannels(); pdnLayer = new BitmapLayer(psdLayer.Rect); pdnLayer.Name = psdLayer.Name; pdnLayer.Opacity = psdLayer.Opacity; pdnLayer.Visible = psdLayer.Visible; pdnLayer.IsGroup = psdLayer.IsGroup; pdnLayer.LayerID = psdLayer.LayerID; pdnLayer.BlendMode = BlendModeMapping.FromPsdBlendMode(psdLayer.BlendModeKey); return ImageDecoderPdn.DecodeImage(pdnLayer, psdLayer, inputDeps); } /// /// Creates a layer for each channel in a multichannel image. /// private static void CreateLayersFromChannels(PsdFile psdFile) { if (psdFile.ColorMode != PsdColorMode.Multichannel) throw new Exception("Not a multichannel image."); if (psdFile.Layers.Count > 0) throw new PsdInvalidException("Multichannel image should not have layers."); // Get alpha channel names, preferably in Unicode. var alphaChannelNames = (AlphaChannelNames)psdFile.ImageResources .Get(ResourceID.AlphaChannelNames); var unicodeAlphaNames = (UnicodeAlphaNames)psdFile.ImageResources .Get(ResourceID.UnicodeAlphaNames); if ((alphaChannelNames == null) && (unicodeAlphaNames == null)) throw new PsdInvalidException("No channel names found."); var channelNames = (unicodeAlphaNames != null) ? unicodeAlphaNames.ChannelNames : alphaChannelNames.ChannelNames; var channels = psdFile.BaseLayer.Channels; if (channels.Count > channelNames.Count) throw new PsdInvalidException("More channels than channel names."); // Channels are stored from top to bottom, but layers are stored from // bottom to top. for (int i = channels.Count - 1; i >= 0; i--) { var channel = channels[i]; var channelName = channelNames[i]; // Copy metadata over from base layer var layer = new PhotoshopFile.Layer(psdFile); layer.Rect = psdFile.BaseLayer.Rect; layer.Visible = true; layer.Masks = new MaskInfo(); layer.BlendingRangesData = new BlendingRanges(layer); // We do not attempt to reconstruct the appearance of the image, but // only to provide access to the channels image data. layer.Name = channelName; layer.BlendModeKey = PsdBlendMode.Darken; layer.Opacity = 255; // Copy channel image data into the new grayscale layer var layerChannel = new Channel(0, layer); layerChannel.ImageCompression = channel.ImageCompression; layerChannel.ImageData = new NativeArray(channel.ImageData, Allocator.Persistent); layer.Channels.Add(layerChannel); psdFile.Layers.Add(layer); } } /// /// Transform Photoshop's layer tree to Paint.NET's flat layer list. /// Indicate where layer sections begin and end, and hide all layers within /// hidden layer sections. /// private static void ApplyLayerSections(List layers) { // BUG: PsdPluginResources.GetString will always return English resource, // because Paint.NET does not set the CurrentUICulture when OnLoad is // called. This situation should be resolved with Paint.NET 4.0, which // will provide an alternative mechanism to retrieve the UI language. // Track the depth of the topmost hidden section. Any nested sections // will be hidden, whether or not they themselves have the flag set. int topHiddenSectionDepth = Int32.MaxValue; var layerSectionNames = new Stack(); // Layers are stored bottom-to-top, but layer sections are specified // top-to-bottom. foreach (var layer in Enumerable.Reverse(layers)) { // Leo: Since we are importing, we don't care if the group is collapsed // Apply to all layers within the layer section, as well as the // closing layer. //if (layerSectionNames.Count > topHiddenSectionDepth) // layer.Visible = false; var sectionInfo = (LayerSectionInfo)layer.AdditionalInfo .SingleOrDefault(x => x is LayerSectionInfo); if (sectionInfo == null) continue; switch (sectionInfo.SectionType) { case LayerSectionType.OpenFolder: case LayerSectionType.ClosedFolder: // Start a new layer section if ((!layer.Visible) && (topHiddenSectionDepth == Int32.MaxValue)) topHiddenSectionDepth = layerSectionNames.Count; layerSectionNames.Push(layer.Name); layer.IsGroup = true; //layer.Name = String.Format(beginSectionWrapper, layer.Name); break; case LayerSectionType.SectionDivider: // End the current layer section //var layerSectionName = layerSectionNames.Pop (); if (layerSectionNames.Count == topHiddenSectionDepth) topHiddenSectionDepth = Int32.MaxValue; layer.IsEndGroupMarker = true; //layer.Name = String.Format(endSectionWrapper, layerSectionName); break; } } } /// /// Set the resolution on the Paint.NET Document to match the PSD file. /// private static void SetPdnResolutionInfo(PsdFile psdFile, Document document) { if (psdFile.Resolution != null) { // PSD files always specify the resolution in DPI. When loading and // saving cm, we will have to round-trip the conversion, but doubles // have plenty of precision to spare vs. PSD's 16/16 fixed-point. if ((psdFile.Resolution.HResDisplayUnit == ResolutionInfo.ResUnit.PxPerCm) && (psdFile.Resolution.VResDisplayUnit == ResolutionInfo.ResUnit.PxPerCm)) { document.DpuUnit = MeasurementUnit.Centimeter; // HACK: Paint.NET truncates DpuX and DpuY to three decimal places, // so add 0.0005 to get a rounded value instead. document.DpuX = psdFile.Resolution.HDpi / 2.54 + 0.0005; document.DpuY = psdFile.Resolution.VDpi / 2.54 + 0.0005; } else { document.DpuUnit = MeasurementUnit.Inch; document.DpuX = psdFile.Resolution.HDpi; document.DpuY = psdFile.Resolution.VDpi; } } } /// /// Verify that the PSD file will fit into physical memory once loaded /// and converted to Paint.NET format. /// /// /// This check is necessary because layers in Paint.NET have the same /// dimensions as the canvas. Thus, PSD files that contain lots of /// tiny adjustment layers may blow up in size by several /// orders of magnitude. /// internal static void CheckSufficientMemory(PsdFile psdFile) { /* Remove memory check since we can't properly ensure there will be enough memory to import // Multichannel images have channels converted to layers var numLayers = (psdFile.ColorMode == PsdColorMode.Multichannel) ? psdFile.BaseLayer.Channels.Count : Math.Max(psdFile.Layers.Count, 1); // Paint.NET also requires a scratch layer and composite layer numLayers += 2; long numPixels = (long)psdFile.ColumnCount * psdFile.RowCount; ulong bytesRequired = (ulong)(checked(4 * numPixels * numLayers)); // Check that the file will fit entirely into physical memory, so that we // do not thrash and make the Paint.NET UI nonresponsive. We also have // to check against virtual memory address space because 32-bit processes // cannot access all 4 GB. //var computerInfo = new Microsoft.VisualBasic.Devices.ComputerInfo(); //var accessibleMemory = Math.Min(computerInfo.TotalPhysicalMemory, // computerInfo.TotalVirtualMemory); var accessibleMemory = (ulong)SystemInfo.systemMemorySize * 1024 * 1024; if (bytesRequired > accessibleMemory) { throw new OutOfMemoryException(); } */ } } }