static int Main(string[] args) { if (args.Length < 1) { Console.WriteLine("Usage: PSD2PAM.exe FILE.psd FILE.pam"); return(1); } // read the PSD file var photochop = new PSDFile(); using (var reading = new FileStream(args[0], FileMode.Open, FileAccess.Read, FileShare.Read)) { photochop.Read(reading); var versionInfo = photochop.ImageResources.FirstOrDefault(ir => ir.ID == 0x0421); if (versionInfo?.Data[4] == 0) { // does not have valid precomposed image Console.WriteLine("PSD2PAM requires that a valid precomposed image is part of the file."); return(1); } int width = photochop.Width; int height = photochop.Height; int channelCount = photochop.NumberOfChannels; int depth = photochop.Depth; if (depth == 1) { Console.WriteLine("PSD2PAM doesn't currently support 1-bit images."); return(1); } long maxValue; switch (depth) { case 8: maxValue = 0xFF; break; case 16: maxValue = 0xFFFF; break; case 32: maxValue = 0xFFFFFFFF; break; default: Console.WriteLine("Unsupported depth {0}.", depth); return(1); } int bytesPerColorComponent = depth / 8; string tupleTypeString = ""; switch (photochop.ColorMode) { case ColorMode.RGB: tupleTypeString = "TUPLTYPE RGB\n"; break; case ColorMode.CMYK: tupleTypeString = "TUPLTYPE CMYK\n"; break; case ColorMode.Grayscale: tupleTypeString = "TUPLTYPE GRAYSCALE\n"; break; } string header = string.Format( CultureInfo.InvariantCulture, "P7\nWIDTH {0}\nHEIGHT {1}\nDEPTH {2}\nMAXVAL {3}\n{4}ENDHDR\n", width, height, channelCount, maxValue, tupleTypeString ); reading.Seek(photochop.PrecomposedImageData.Offset, SeekOrigin.Begin); using (var tempFiles = new TemporaryFilesDeleteOnDispose(1 + channelCount)) { // get planar image using (var planarImageStorage = new FileStream(tempFiles.FileNames[0], FileMode.Create, FileAccess.Write, FileShare.None)) using (var zippage = new GZipStream(planarImageStorage, CompressionLevel.Fastest)) { switch (photochop.PrecomposedImageData.Compression) { case CompressionType.RawData: PixelDataDecoding.DecodeRawData(reading, zippage, null); break; case CompressionType.PackBits: int scanlineCount = height * channelCount; PixelDataDecoding.DecodePackBits(reading, zippage, scanlineCount, photochop.Version == 2); break; case CompressionType.ZipWithoutPrediction: PixelDataDecoding.DecodeZip(reading, zippage, null); break; case CompressionType.ZipWithPrediction: PixelDataDecoding.DecodeZipPredicted(reading, zippage, null, depth, width); break; } } // split planar image into plane files var buf = new byte[width * bytesPerColorComponent]; using (var reader = new FileStream(tempFiles.FileNames[0], FileMode.Open, FileAccess.Read, FileShare.Read)) using (var unzippage = new GZipStream(reader, CompressionMode.Decompress)) { for (int c = 0; c < channelCount; ++c) { using (var writer = new FileStream(tempFiles.FileNames[1 + c], FileMode.Create, FileAccess.Write, FileShare.None)) using (var zippage = new GZipStream(writer, CompressionLevel.Fastest)) { for (int y = 0; y < height; ++y) { if (y % 32 == 31) { Console.WriteLine("splitting channel {0}/{1} row {2}/{3}", c + 1, channelCount, y + 1, height); } int rd = unzippage.Read(buf, 0, buf.Length); if (rd < buf.Length) { throw new Exception("short read"); } zippage.Write(buf, 0, buf.Length); } } } } using (var streamDisposal = new DisposableStreamGroup()) { // reopen the planes for (int c = 0; c < channelCount; ++c) { var stream = new FileStream(tempFiles.FileNames[1 + c], FileMode.Open, FileAccess.Read, FileShare.Read); var unzipper = new GZipStream(stream, CompressionMode.Decompress, leaveOpen: false); streamDisposal.Streams.Add(unzipper); } // interleave the components var pixel = new byte[bytesPerColorComponent]; using (var writer = new FileStream(args[1], FileMode.Create, FileAccess.Write)) using (var zipper = new GZipStream(writer, CompressionLevel.Fastest)) using (var bufferer = new BufferedStream(zipper, 8 * 1024 * 1024)) { // write PAM header var pamHeaderBytes = header.Select(c => (byte)c).ToArray(); bufferer.Write(pamHeaderBytes, 0, pamHeaderBytes.Length); // read the pixels for (int y = 0; y < height; ++y) { if (y % 32 == 31) { Console.WriteLine("copying row {0}/{1}", y + 1, height); } for (int x = 0; x < width; ++x) { for (int c = 0; c < channelCount; ++c) { int r = streamDisposal.Streams[c].Read(pixel, 0, pixel.Length); if (r != bytesPerColorComponent) { throw new Exception("under-read"); } // invert color (Photoshop -> PAM) for (int i = 0; i < pixel.Length; ++i) { pixel[i] = (byte)(0xFF - pixel[i]); } bufferer.Write(pixel, 0, pixel.Length); } } } } } } } return(0); }
static void Main(string[] args) { var psd = new PSDFile(); using (var readStream = new FileStream(args[0], FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { psd.Read(readStream); } Console.WriteLine("Version: {0}", psd.Version); Console.WriteLine("Number of channels: {0}", psd.NumberOfChannels); Console.WriteLine("Width: {0}", psd.Width); Console.WriteLine("Height: {0}", psd.Height); Console.WriteLine("Depth: {0}", psd.Depth); Console.WriteLine("Color mode: {0}", psd.ColorMode); Console.WriteLine("Image resources:"); foreach (var res in psd.ImageResources) { Console.WriteLine(" Image resource ID: {0}", res.ID); //Console.WriteLine(" Name: {0}", res.Name); if (res.ID == ResolutionInfo.ResolutionInfoResourceID) { Console.WriteLine(" Binary data:"); for (int rowOffset = 0; rowOffset < res.Data.Length; rowOffset += 8) { Console.Write(" "); for (int i = 0; i < 8 && rowOffset + i < res.Data.Length; ++i) { Console.Write("{0:X2} ", res.Data[rowOffset + i]); } Console.WriteLine(); } Console.WriteLine(" Resolution info:"); var resInfo = ResolutionInfo.FromPSD(psd); Console.WriteLine(" Horizontal resolution: {0} dpi", resInfo.HorizontalResolutionDPI); Console.WriteLine(" Display unit: {0}", resInfo.HorizontalResolutionDisplayUnit); Console.WriteLine(" Width display unit: {0}", resInfo.WidthDisplayUnit); Console.WriteLine(" Vertical resolution: {0} dpi", resInfo.VerticalResolutionDPI); Console.WriteLine(" Display unit: {0}", resInfo.VerticalResolutionDisplayUnit); Console.WriteLine(" Height display unit: {0}", resInfo.HeightDisplayUnit); } else if (res.ID == VersionInfo.VersionInfoResourceID) { var verInfo = VersionInfo.FromPSD(psd); Console.WriteLine(" Version info:"); Console.WriteLine(" Version: {0}", verInfo.Version); Console.WriteLine(" {0} valid precomposed data", verInfo.HasValidPrecomposedData ? "Has" : "Does not have"); Console.WriteLine(" Writer: {0}", verInfo.WriterName); Console.WriteLine(" Reader: {0}", verInfo.ReaderName); Console.WriteLine(" File version: {0}", verInfo.FileVersion); } } Console.WriteLine("Layers: {0}", psd.Layers.Length); foreach (PSDLayer layer in psd.Layers) { Console.WriteLine(" Layer: {0}", layer.Name); Console.WriteLine(" Channels: {0}", layer.Channels.Length); foreach (PSDLayerChannel chan in layer.Channels) { Console.WriteLine(" {0} ({1} bytes {2} from 0x{3:x})", chan.ID, chan.Data.DataLength, chan.Data.Compression, chan.Data.Offset); } Console.WriteLine(" Additional layer information: {0} entries", layer.AdditionalInformation.Count); foreach (PSDAdditionalLayerInformation pali in layer.AdditionalInformation) { Console.WriteLine(" Key: {0}", pali.Key); Console.WriteLine(" Data length: {0}", pali.Data.Length); } } if (psd.GlobalLayerMask != null) { var glm = psd.GlobalLayerMask; Console.WriteLine("Global layer mask:"); Console.WriteLine(" Overlay color space: {0}", glm.OverlayColorSpace); Console.WriteLine(" Color components: {0}, {1}, {2}, {3}", glm.ColorComponent1, glm.ColorComponent2, glm.ColorComponent3, glm.ColorComponent4); Console.WriteLine(" Opacity: {0}", glm.Opacity); Console.WriteLine(" Kind: {0}", glm.Kind); } else { Console.WriteLine("No global layer mask"); } Console.WriteLine("Additional layer information: {0} entries", psd.AdditionalLayerInformation.Count); foreach (PSDAdditionalLayerInformation ali in psd.AdditionalLayerInformation) { Console.WriteLine(" Key: {0}", ali.Key); Console.WriteLine(" Data length: {0}", ali.Data.Length); } if (psd.PrecomposedImageData == null) { Console.WriteLine("No precomposed image data"); } else { Console.WriteLine("Precomposed image data: {0} from 0x{1:x}", psd.PrecomposedImageData.Compression, psd.PrecomposedImageData.Offset); } if (args.Length > 1 && args[1] == "-d") { // decode for (int l = 0; l < psd.Layers.Length; ++l) { PSDLayer layer = psd.Layers[l]; foreach (PSDLayerChannel chan in layer.Channels) { Console.WriteLine("extracting layer {0} channel {1} ({2})", l, chan.ID, chan.Data.Compression); string name = $"{args[0]}.l{l}c{chan.ID}.bin"; using (var reader = new FileStream(args[0], FileMode.Open, FileAccess.Read, FileShare.Read)) using (var writer = new FileStream(name, FileMode.Create, FileAccess.Write, FileShare.None)) { reader.Seek(chan.Data.Offset, SeekOrigin.Begin); switch (chan.Data.Compression) { case CompressionType.RawData: PixelDataDecoding.DecodeRawData( reader, writer, chan.Data.DataLength ); break; case CompressionType.PackBits: PixelDataDecoding.DecodePackBits( reader, writer, scanlineCount: layer.Bottom - layer.Top, fourByteLengths: psd.Version == 2 ); break; case CompressionType.ZipWithoutPrediction: PixelDataDecoding.DecodeZip( reader, writer, chan.Data.DataLength ); break; case CompressionType.ZipWithPrediction: PixelDataDecoding.DecodeZipPredicted( reader, writer, chan.Data.DataLength, psd.Depth, psd.Width ); break; } } } } if (psd.PrecomposedImageData != null) { string compositeName = $"{args[0]}.composite.bin"; using (var reader = new FileStream(args[0], FileMode.Open, FileAccess.Read, FileShare.Read)) using (var writer = new FileStream(compositeName, FileMode.Create, FileAccess.Write, FileShare.None)) { reader.Seek(psd.PrecomposedImageData.Offset, SeekOrigin.Begin); switch (psd.PrecomposedImageData.Compression) { case CompressionType.RawData: PixelDataDecoding.DecodeRawData( reader, writer, length: null ); break; case CompressionType.PackBits: PixelDataDecoding.DecodePackBits( reader, writer, scanlineCount: psd.Height * psd.NumberOfChannels, fourByteLengths: psd.Version == 2 ); break; case CompressionType.ZipWithoutPrediction: PixelDataDecoding.DecodeZip( reader, writer, length: null ); break; case CompressionType.ZipWithPrediction: PixelDataDecoding.DecodeZipPredicted( reader, writer, length: null, depth: psd.Depth, imageWidth: psd.Width ); break; } } } } }