/// <summary> /// Reads version information from a PSD file object. /// </summary> /// <param name="psd">The PSD file object from which to obtain version information.</param> /// <returns> /// The version information obtained from the PSD file, or <c>null</c> if the file contains no version /// information. /// </returns> public static VersionInfo FromPSD(PSDFile psd) { var resource = psd.ImageResources.FirstOrDefault(ir => ir.ID == VersionInfoResourceID); if (resource == null) { return(null); } var verInfo = new VersionInfo(); using (var ms = new MemoryStream(resource.Data, writable: false)) { verInfo.Version = ms.ReadBigEndianInt32(); byte hasRealMergedData = ms.ReadByteOrThrow(); if (hasRealMergedData > 1) { throw new PSDFormatException($"got hasRealMergedData value {hasRealMergedData}, expected 0 or 1"); } verInfo.HasValidPrecomposedData = (hasRealMergedData == 1); verInfo.WriterName = ms.ReadUnicodeString(); verInfo.ReaderName = ms.ReadUnicodeString(); verInfo.FileVersion = ms.ReadBigEndianInt32(); } return(verInfo); }
/// <summary> /// Obtains the precomposed image data of a PSD image by decoding rows of PackBits-encoded data from a source /// stream and writing the decoded data to a target stream. /// </summary> /// <remarks><paramref name="source"/> must already be at the correct position.</remarks> /// <param name="source">The stream from which to copy the data.</param> /// <param name="dest">The stream to which to copy the data.</param> /// <param name="psd">The PSD file from which to obtain the number of PackBits-encoded rows.</param> /// <param name="cancelToken"> /// A cancellation token which allows copying to be cancelled before it completes. /// </param> public static void DecodePackBits(Stream source, Stream dest, PSDFile psd, CancellationToken cancelToken = default(CancellationToken)) { int scanlineCount = psd.Height * psd.NumberOfChannels; DecodePackBits(source, dest, scanlineCount, psd.Version == 2, cancelToken); }
/// <summary> /// Reads metadata about a PSD file's precomposed image data and populates a <see cref="PSDFile"/> with it. /// </summary> /// <param name="psd">The PSD file object to populate.</param> /// <param name="stream">The stream from which to read the precomposed image data metadata.</param> public static void CreateImageDataPlaceholder(PSDFile psd, Stream stream) { var placeholder = new PSDImageDataPlaceholder(); // read compression method short compressionValue = stream.ReadBigEndianInt16(); if (!Enum.IsDefined(typeof(CompressionType), compressionValue)) { throw new PSDFormatException($"unexpected compression type {compressionValue}, expected one of {string.Join(", ", EnumUtils.GetUnderlyingValues<CompressionType, short>())}"); } placeholder.Compression = (CompressionType)compressionValue; placeholder.Offset = stream.Position; psd.PrecomposedImageData = placeholder; }
/// <summary> /// Reads PSD color mode data from a stream and populates a <see cref="PSDFile"/> with the information. /// </summary> /// <param name="psd">The PSD file object to populate.</param> /// <param name="stream">The stream from which to read the PSD color mode data.</param> public static void ReadColorModeData(PSDFile psd, Stream stream) { int colorModeDataLength = stream.ReadBigEndianInt32(); if (colorModeDataLength < 0) { throw new PSDFormatException($"color mode data length is {colorModeDataLength}, expected at least 0"); } bool requiresColorModeData = (psd.ColorMode == ColorMode.Indexed || psd.ColorMode == ColorMode.Duotone); bool hasColorModeData = (colorModeDataLength > 0); if (hasColorModeData != requiresColorModeData) { throw new PSDFormatException($"color mode is {psd.ColorMode} but color mode data length is 0"); } psd.ColorModeData = stream.ReadBytes(colorModeDataLength); }
/// <summary> /// Reads the global mask information subsection of a PSD layer and mask information section from a stream /// and populates a <see cref="PSDFile"/> with the information. /// </summary> /// <param name="psd">The PSD file object to populate.</param> /// <param name="stream">The stream from which to read the layer information subsection.</param> public static void ReadGlobalMaskInformation(PSDFile psd, Stream stream) { int length = stream.ReadBigEndianInt32(); if (length == 0) { // no global mask info psd.GlobalLayerMask = null; return; } var mask = new PSDGlobalLayerMask(); mask.OverlayColorSpace = stream.ReadBigEndianInt16(); mask.ColorComponent1 = stream.ReadBigEndianInt16(); mask.ColorComponent2 = stream.ReadBigEndianInt16(); mask.ColorComponent3 = stream.ReadBigEndianInt16(); mask.ColorComponent4 = stream.ReadBigEndianInt16(); short opacity = stream.ReadBigEndianInt16(); if (opacity < 0 || opacity > 100) { throw new PSDFormatException($"global mask opacity must be at least 0 and at most 100, got {opacity}"); } mask.Opacity = opacity; byte kind = stream.ReadByteOrThrow(); if (!Enum.IsDefined(typeof(LayerMaskKind), kind)) { throw new PSDFormatException($"unknown layer mask kind {kind}, expected one of {string.Join(", ", EnumUtils.GetUnderlyingValues<LayerMaskKind, byte>())}"); } mask.Kind = (LayerMaskKind)kind; int paddingByteCount = length - 13; stream.ReadBytes(paddingByteCount); psd.GlobalLayerMask = mask; }
/// <summary> /// Reads PSD layer and mask information from a stream and populates a <see cref="PSDFile"/> with the /// information. /// </summary> /// <param name="psd">The PSD file object to populate.</param> /// <param name="stream">The stream from which to read the PSD layer and mask information.</param> public static void ReadLayerAndMaskInformation(PSDFile psd, Stream stream) { long layerMaskInfoLength = (psd.Version == 2) ? stream.ReadBigEndianInt64() : stream.ReadBigEndianInt32(); long layerStartPosition = stream.Position; ReadLayerInformation(psd, stream); ReadGlobalMaskInformation(psd, stream); psd.AdditionalLayerInformation = new List <PSDAdditionalLayerInformation>(); while (stream.Position - layerStartPosition < layerMaskInfoLength) { var additionalLayerInfo = ReadAdditionalLayerInformation(psd, stream); if (additionalLayerInfo != null) { psd.AdditionalLayerInformation.Add(additionalLayerInfo); } } }
/// <summary> /// Reads the PSD image resources from a stream and populates a <see cref="PSDFile"/> with the information. /// </summary> /// <param name="psd">The PSD file object to populate.</param> /// <param name="stream">The stream from which to read the PSD image resources.</param> public static void ReadImageResources(PSDFile psd, Stream stream) { int imageResourceSectionLength = stream.ReadBigEndianInt32(); if (imageResourceSectionLength < 0) { throw new PSDFormatException( $"image resource section length is {imageResourceSectionLength}, expected at least 0"); } psd.ImageResources = new List <PSDImageResource>(); long endPosition = stream.Position + imageResourceSectionLength; while (stream.Position < endPosition) { var resource = new PSDImageResource(); PSDImageResource.Reading.ReadResourceBlock(resource, stream); psd.ImageResources.Add(resource); } }
/// <summary> /// Reads a layer record from a stream and populates a PSD layer object with the information. /// </summary> /// <param name="psd"> /// The PSD file being read. Used to obtain the file format version and to populate the /// <see cref="PSDFile.Layers"/> array if general layer information has been "outsourced" to an additional /// layer information block. /// </param> /// <param name="layer">The layer to populate with information read from the stream.</param> /// <param name="stream">The stream from which to read layer information.</param> public static void ReadLayerRecord(PSDFile psd, PSDLayer layer, Stream stream) { layer.Top = stream.ReadBigEndianInt32(); layer.Left = stream.ReadBigEndianInt32(); layer.Bottom = stream.ReadBigEndianInt32(); layer.Right = stream.ReadBigEndianInt32(); short numberChannels = stream.ReadBigEndianInt16(); layer.Channels = new PSDLayerChannel[numberChannels]; for (int i = 0; i < numberChannels; ++i) { var channel = new PSDLayerChannel(); channel.ID = stream.ReadBigEndianInt16(); channel.DataLength = (psd.Version == 2) ? stream.ReadBigEndianInt64() : stream.ReadBigEndianInt32(); layer.Channels[i] = channel; } string blendModeSignature = stream.ReadUsAsciiString(4); if (blendModeSignature != BlendModeSignature) { throw new PSDFormatException($"expected blend mode signature \"{BlendModeSignature}\", got \"{blendModeSignature}\""); } int blendModeValue = stream.ReadBigEndianInt32(); if (!Enum.IsDefined(typeof(BlendMode), blendModeValue)) { char[] blendModeChars = { (char)((blendModeValue >> 24) & 0xFF), (char)((blendModeValue >> 16) & 0xFF), (char)((blendModeValue >> 8) & 0xFF), (char)((blendModeValue >> 0) & 0xFF) }; string blendModeString = new string(blendModeChars); throw new PSDFormatException($"invalid blend mode {blendModeValue} (\"{blendModeString}\")"); } layer.BlendMode = (BlendMode)blendModeValue; byte opacity = stream.ReadByteOrThrow(); layer.Opacity = opacity; byte clipping = stream.ReadByteOrThrow(); if (clipping > 1) { throw new PSDFormatException($"invalid clipping value {clipping}; expected 0 (base) or 1 (non-base)"); } layer.NonBaseClipping = (clipping == 1); byte flags = stream.ReadByteOrThrow(); if ((flags & 0x01) != 0) { layer.TransparencyProtected = true; } if ((flags & 0x02) != 0) { layer.Visible = true; } if ((flags & 0x04) != 0) { layer.Obsolete = true; } if ((flags & 0x18) == 0x18) { // (both bits 3 and 4 must be set) layer.PixelDataIrrelevantToDocumentAppearance = true; } // filler stream.ReadByteOrThrow(); int extraDataLength = stream.ReadBigEndianInt32(); long extraDataStart = stream.Position; ReadLayerMask(layer, stream); ReadLayerBlendingRanges(layer, stream); byte layerNameLength = stream.ReadByteOrThrow(); layer.Name = stream.ReadWindows1252String(layerNameLength); int padTo4Bytes = 4 - ((layerNameLength + 1) % 4); if (padTo4Bytes != 4) // there is zero padding if == 4 { stream.ReadBytes(padTo4Bytes); } layer.AdditionalInformation = new List <PSDAdditionalLayerInformation>(); while (stream.Position < extraDataStart + extraDataLength) { PSDAdditionalLayerInformation pali = PSDFile.Reading.ReadAdditionalLayerInformation(psd, stream); layer.AdditionalInformation.Add(pali); } }
/// <summary> /// Reads resolution information from a PSD file object. /// </summary> /// <param name="psd">The PSD file object from which to obtain resolution information.</param> /// <returns> /// The resolution information obtained from the PSD file, or <c>null</c> if the file contains no resolution /// information. /// </returns> public static ResolutionInfo FromPSD(PSDFile psd) { var resource = psd.ImageResources.FirstOrDefault(ir => ir.ID == ResolutionInfoResourceID); if (resource == null) { return(null); } var resInfo = new ResolutionInfo(); using (var ms = new MemoryStream(resource.Data, writable: false)) { int horizontalResolutionDPIFixedPoint = ms.ReadBigEndianInt32(); resInfo.HorizontalResolutionDPI = horizontalResolutionDPIFixedPoint / FixedPointDivisor; if (resInfo.HorizontalResolutionDPI <= 0.0) { throw new PSDFormatException($"horizontal resolution is {resInfo.HorizontalResolutionDPI}, expected more than 0.0"); } resInfo.HorizontalResolutionDisplayUnit = (ResolutionDisplayUnit)ms.ReadBigEndianInt16(); if (!Enum.IsDefined(typeof(ResolutionDisplayUnit), resInfo.HorizontalResolutionDisplayUnit)) { throw new PSDFormatException( $"horizontal resolution display unit is {resInfo.HorizontalResolutionDisplayUnit}, expected one of {string.Join(", ", EnumUtils.GetUnderlyingValues<ResolutionDisplayUnit, short>())}" ); } resInfo.WidthDisplayUnit = (SizeDisplayUnit)ms.ReadBigEndianInt16(); if (!Enum.IsDefined(typeof(SizeDisplayUnit), resInfo.WidthDisplayUnit)) { throw new PSDFormatException( $"width display unit is {resInfo.WidthDisplayUnit}, expected one of {string.Join(", ", EnumUtils.GetUnderlyingValues<SizeDisplayUnit, short>())}" ); } int verticalResolutionDPIFixedPoint = ms.ReadBigEndianInt32(); resInfo.VerticalResolutionDPI = verticalResolutionDPIFixedPoint / FixedPointDivisor; if (resInfo.VerticalResolutionDPI <= 0.0) { throw new PSDFormatException($"vertical resolution is {resInfo.VerticalResolutionDPI}, expected more than 0.0"); } resInfo.VerticalResolutionDisplayUnit = (ResolutionDisplayUnit)ms.ReadBigEndianInt16(); if (!Enum.IsDefined(typeof(ResolutionDisplayUnit), resInfo.VerticalResolutionDisplayUnit)) { throw new PSDFormatException( $"vertical resolution display unit is {resInfo.VerticalResolutionDisplayUnit}, expected one of {string.Join(", ", EnumUtils.GetUnderlyingValues<ResolutionDisplayUnit, short>())}" ); } resInfo.HeightDisplayUnit = (SizeDisplayUnit)ms.ReadBigEndianInt16(); if (!Enum.IsDefined(typeof(SizeDisplayUnit), resInfo.HeightDisplayUnit)) { throw new PSDFormatException( $"height display unit is {resInfo.HeightDisplayUnit}, expected one of {string.Join(", ", EnumUtils.GetUnderlyingValues<SizeDisplayUnit, short>())}" ); } } return(resInfo); }
/// <summary> /// Reads an item of additional layer information within the additional layer information subsection of a /// PSD layer and mask information section from a stream and either returns a /// <see cref="PSDAdditionalLayerInformation"/> object containing the additional information, or (if this /// is an "out-of-band" layer information block) populates a <see cref="PSDFile"/> with the layer /// information within. /// </summary> /// <param name="psd"> /// The PSD file object to populate if this is an "out-of-band" layer information block. /// </param> /// <param name="stream">The stream from which to read the layer information subsection.</param> /// <returns> /// The additional layer information, or <c>null</c> if the encountered block is an "out-of-band" layer /// information block. /// </returns> public static PSDAdditionalLayerInformation ReadAdditionalLayerInformation(PSDFile psd, Stream stream) { var ali = new PSDAdditionalLayerInformation(); string signature = stream.ReadUsAsciiString(4); if (signature != AdditionalLayerInfoSignature && signature != AdditionalLayerInfoSignature2) { throw new PSDFormatException($"unknown additional layer information signature \"{signature}\", expected \"{AdditionalLayerInfoSignature}\" or \"{AdditionalLayerInfoSignature2}\""); } string key = stream.ReadUsAsciiString(4); bool length64Bit = false; if (key == "Layr" || key == "Lr16" || key == "Lr32") { // layer info has been shunted in here instead of where it belongs ReadLayerInformation(psd, stream, roundToFourBytes: true); return(null); } if (psd.Version == 2) { // some keys have 64-bit lengths length64Bit = (key == "LMsk") || (key == "Mt16") || (key == "Mt32") || (key == "Mtrn") || (key == "Alph") || (key == "FMsk") || (key == "Ink2") || (key == "FEid") || (key == "FXid") || (key == "PxSD") ; } long length = length64Bit ? stream.ReadBigEndianInt64() : stream.ReadBigEndianInt32(); byte[] data = stream.ReadBytes((int)length); ali.Key = key; ali.Data = data; // round up to 4 bytes // WARNING: spec is a liar; says "rounded up to an even byte count" if (length % 4 != 0) { var skipCount = (int)(4 - (length % 4)); stream.ReadBytes(skipCount); } return(ali); }
/// <summary> /// Reads the PSD header from a stream and populates a <see cref="PSDFile"/> with the information. /// </summary> /// <param name="psd">The PSD file object to populate.</param> /// <param name="stream">The stream from which to read the PSD header.</param> public static void ReadHeader(PSDFile psd, Stream stream) { string magic = stream.ReadUsAsciiString(4); if (!string.Equals(magic, Magic, StringComparison.Ordinal)) { throw new PSDFormatException($"unexpected magic value (is \"{magic}\", expected \"{Magic}\")"); } short version = stream.ReadBigEndianInt16(); if (version < 1 || version > 2) { throw new PSDFormatException($"version is {version}, expected 1 or 2"); } psd.Version = version; byte[] reserved = stream.ReadBytes(6); if (reserved.Any(b => b != 0x00)) { throw new PSDFormatException("nonzero byte in reserved 6-byte value"); } short numberOfChannels = stream.ReadBigEndianInt16(); if (numberOfChannels < MinChannels || numberOfChannels > MaxChannels) { throw new PSDFormatException( $"number of channels is is {numberOfChannels}, expected at least {MinChannels} and at most {MaxChannels}"); } psd.NumberOfChannels = numberOfChannels; int height = stream.ReadBigEndianInt32(); if (height < 1) { throw new PSDFormatException($"height is {height}, expected at least 1"); } if (version == 1 && height > Version1MaxDimension) { throw new PSDFormatException( $"height is {height} and this is a version 1 document, expected at most {Version1MaxDimension}"); } if (version == 2 && height > Version2MaxDimension) { throw new PSDFormatException( $"height is {height} and this is a version 2 document, expected at most {Version2MaxDimension}"); } psd.Height = height; int width = stream.ReadBigEndianInt32(); if (width < 1) { throw new PSDFormatException($"width is {width}, expected at least 1"); } if (version == 1 && width > Version1MaxDimension) { throw new PSDFormatException( $"width is {width} and this is a version 1 document, expected at most {Version1MaxDimension}"); } if (version == 2 && width > Version2MaxDimension) { throw new PSDFormatException( $"width is {width} and this is a version 2 document, expected at most {Version2MaxDimension}"); } psd.Width = width; short depth = stream.ReadBigEndianInt16(); if (depth != 1 && depth != 8 && depth != 16 && depth != 32) { throw new PSDFormatException($"depth is {depth}, expected 1 or 8 or 16 or 32"); } psd.Depth = depth; short colorMode = stream.ReadBigEndianInt16(); if (!Enum.IsDefined(typeof(ColorMode), colorMode)) { throw new PSDFormatException( $"color mode is {colorMode}, expected one of {string.Join(", ", EnumUtils.GetUnderlyingValues<ColorMode, short>())}"); } psd.ColorMode = (ColorMode)colorMode; }
/// <summary> /// Reads the layer information subsection of a PSD layer and mask information section from a stream and /// populates a <see cref="PSDFile"/> with the information. /// </summary> /// <param name="psd">The PSD file object to populate.</param> /// <param name="stream">The stream from which to read the layer information subsection.</param> /// <param name="roundToFourBytes"> /// Whether the layer information block is padded to a multiple of 4 bytes. /// </param> public static void ReadLayerInformation(PSDFile psd, Stream stream, bool roundToFourBytes = false) { long layerInfoLength = (psd.Version == 2) ? stream.ReadBigEndianInt64() : stream.ReadBigEndianInt32(); if (layerInfoLength == 0) { // no layers at all psd.Layers = new PSDLayer[0]; return; } long layerInfoStart = stream.Position; short layerCount = stream.ReadBigEndianInt16(); if (layerCount < 0) { // FIXME: "If it is a negative number, its absolute value is the number of layers and the first alpha channel contains the transparency data for the merged result." layerCount = (short)-layerCount; } psd.Layers = new PSDLayer[layerCount]; for (int i = 0; i < layerCount; ++i) { psd.Layers[i] = new PSDLayer(); PSDLayer.Reading.ReadLayerRecord(psd, psd.Layers[i], stream); } long totalDataLength = 0; for (int l = 0; l < layerCount; ++l) { PSDLayer layer = psd.Layers[l]; for (int c = 0; c < layer.Channels.Length; ++c) { PSDLayerChannel channel = layer.Channels[c]; // get compression type short compressionValue = stream.ReadBigEndianInt16(); if (!Enum.IsDefined(typeof(CompressionType), compressionValue)) { throw new PSDFormatException($"unknown layer data compression type {compressionValue}"); } var compression = (CompressionType)compressionValue; channel.Data = new PSDLayerChannelDataPlaceholder { Compression = compression, Offset = stream.Position, DataLength = channel.DataLength - 2 }; long dataLengthWithCompressionInfo = channel.DataLength; totalDataLength += dataLengthWithCompressionInfo; // skip stream.Seek(channel.Data.DataLength, SeekOrigin.Current); } } // skip padding if any if (stream.Position < layerInfoStart + layerInfoLength) { // padding; skip int skipBytes = (int)((layerInfoStart + layerInfoLength) - stream.Position); stream.ReadBytes(skipBytes); } // additionally round up to 4 bytes if requested (part of Layr/Lr16/Lr32) // WARNING: spec is a liar; says "rounded up to an even byte count" if (roundToFourBytes && layerInfoLength % 4 != 0) { var skipCount = (int)(4 - (layerInfoLength % 4)); stream.ReadBytes(skipCount); } }
/// <summary> /// Obtains the precomposed image data of a PSD image by decoding per-row delta-encoded and then /// Deflate-compressed data from a source stream and writing the decoded data to a target stream. /// </summary> /// <remarks><paramref name="source"/> must already be at the correct position.</remarks> /// <param name="source">The stream from which to copy the data.</param> /// <param name="dest">The stream to which to copy the data.</param> /// <param name="psd">The PSD file from which to obtain the image depth and width.</param> /// <param name="cancelToken"> /// A cancellation token which allows copying to be cancelled before it completes. /// </param> public static void DecodeZipPredicted(Stream source, Stream dest, PSDFile psd, CancellationToken cancelToken = default(CancellationToken)) { DecodeZipPredicted(source, dest, null, psd.Depth, psd.Width, cancelToken); }