{ Vampyre Imaging Library by Marek Mauder http://imaginglib.sourceforge.net The contents of this file are used with permission, subject to the Mozilla Public License Version 1.1 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.mozilla.org/MPL/MPL-1.1.html Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License. Alternatively, the contents of this file may be used under the terms of the GNU Lesser General Public License (the "LGPL License"), in which case the provisions of the LGPL License are applicable instead of those above. If you wish to allow use of your version of this file only under the terms of the LGPL License and not to allow others to use your version of this file under the MPL, indicate your decision by deleting the provisions above and replace them with the notice and other provisions required by the LGPL License. If you do not delete the provisions above, a recipient may use your version of this file under either the MPL or the LGPL License. For more information about the LGPL: http://www.gnu.org/copyleft/lesser.html } { This unit contains image format loader/saver for Photoshop PSD image format.} unit ImagingPsd; {$I ImagingOptions.inc} interface uses SysUtils, ImagingTypes, Imaging, ImagingColors, ImagingUtility; type { Class for loading and saving Adobe Photoshop PSD images. Loading and saving of indexed, grayscale, RGB(A), HDR (FP32), and CMYK (auto converted to RGB) images is supported. Non-HDR gray, RGB, and CMYK images can have 8bit or 16bit color channels. There is no support for loading mono images, duotone images are treated like grayscale images, and multichannel and CIE Lab images are loaded as RGB images but without actual conversion to RGB color space. Also no layer information is loaded.} TPSDFileFormat = class(TImageFileFormat) private FSaveAsLayer: LongBool; protected procedure Define; override; function LoadData(Handle: TImagingHandle; var Images: TDynImageDataArray; OnlyFirstLevel: Boolean): Boolean; override; function SaveData(Handle: TImagingHandle; const Images: TDynImageDataArray; Index: LongInt): Boolean; override; procedure ConvertToSupported(var Image: TImageData; const Info: TImageFormatInfo); override; public function TestFormat(Handle: TImagingHandle): Boolean; override; published property SaveAsLayer: LongBool read FSaveAsLayer write FSaveAsLayer; end; implementation uses ImagingExtras; const SPSDFormatName = 'Photoshop Image'; SPSDMasks = '*.psd,*.pdd'; PSDSupportedFormats: TImageFormats = [ifIndex8, ifGray8, ifA8Gray8, ifR8G8B8, ifA8R8G8B8, ifGray16, ifA16Gray16, ifR16G16B16, ifA16R16G16B16, ifR32F, ifR32G32B32F, ifA32R32G32B32F]; PSDDefaultSaveAsLayer = True; const SPSDMagic = '8BPS'; CompressionNone: Word = 0; CompressionRLE: Word = 1; type {$MINENUMSIZE 2} { PSD Image color mode.} TPSDColorMode = ( cmMono = 0, cmGrayscale = 1, cmIndexed = 2, cmRGB = 3, cmCMYK = 4, cmMultiChannel = 7, cmDuoTone = 8, cmLab = 9 ); { PSD image main header.} TPSDHeader = packed record Signature: TChar4; // Format ID '8BPS' Version: Word; // Always 1 Reserved: array[0..5] of Byte; // Reserved, all zero Channels: Word; // Number of color channels (1-24) including alpha channels Rows : LongWord; // Height of image in pixels (1-30000) Columns: LongWord; // Width of image in pixels (1-30000) Depth: Word; // Number of bits per channel (1, 8, and 16) Mode: TPSDColorMode; // Color mode end; TPSDChannelInfo = packed record ChannelID: Word; // 0 = Red, 1 = Green, 2 = Blue etc., -1 = Transparency mask, -2 = User mask Size: LongWord; // Size of channel data. end; procedure SwapHeader(var Header: TPSDHeader); begin Header.Version := SwapEndianWord(Header.Version); Header.Channels := SwapEndianWord(Header.Channels); Header.Depth := SwapEndianWord(Header.Depth); Header.Rows := SwapEndianLongWord(Header.Rows); Header.Columns := SwapEndianLongWord(Header.Columns); Header.Mode := TPSDColorMode(SwapEndianWord(Word(Header.Mode))); end; { TPSDFileFormat class implementation } procedure TPSDFileFormat.Define; begin inherited; FName := SPSDFormatName; FFeatures := [ffLoad, ffSave]; FSupportedFormats := PSDSupportedFormats; AddMasks(SPSDMasks); FSaveAsLayer := PSDDefaultSaveAsLayer; RegisterOption(ImagingPSDSaveAsLayer, @FSaveAsLayer); end; function TPSDFileFormat.LoadData(Handle: TImagingHandle; var Images: TDynImageDataArray; OnlyFirstLevel: Boolean): Boolean; var Header: TPSDHeader; ByteCount: LongWord; RawPal: array[0..767] of Byte; Compression, PackedSize: Word; LineSize, ChannelPixelSize, WidthBytes, CurrChannel, MaxRLESize, I, Y, X: LongInt; Info: TImageFormatInfo; PackedLine, LineBuffer: PByte; RLELineSizes: array of Word; Col32: TColor32Rec; Col64: TColor64Rec; PCol32: PColor32Rec; PCol64: PColor64Rec; { PackBits RLE decode code from Mike Lischke's GraphicEx library.} procedure DecodeRLE(Source, Dest: PByte; PackedSize, UnpackedSize: LongInt); var Count: LongInt; begin while (UnpackedSize > 0) and (PackedSize > 0) do begin Count := ShortInt(Source^); Inc(Source); Dec(PackedSize); if Count < 0 then begin // Replicate next byte -Count + 1 times if Count = -128 then Continue; Count := -Count + 1; if Count > UnpackedSize then Count := UnpackedSize; FillChar(Dest^, Count, Source^); Inc(Source); Dec(PackedSize); Inc(Dest, Count); Dec(UnpackedSize, Count); end else begin // Copy next Count + 1 bytes from input Inc(Count); if Count > UnpackedSize then Count := UnpackedSize; if Count > PackedSize then Count := PackedSize; Move(Source^, Dest^, Count); Inc(Dest, Count); Inc(Source, Count); Dec(PackedSize, Count); Dec(UnpackedSize, Count); end; end; end; begin Result := False; SetLength(Images, 1); with GetIO, Images[0] do begin // Read PSD header Read(Handle, @Header, SizeOf(Header)); SwapHeader(Header); // Determine image data format Format := ifUnknown; case Header.Mode of cmGrayscale, cmDuoTone: begin if Header.Depth in [8, 16] then begin if Header.Channels = 1 then Format := IffFormat(Header.Depth = 8, ifGray8, ifGray16) else if Header.Channels >= 2 then Format := IffFormat(Header.Depth = 8, ifA8Gray8, ifA16Gray16); end else if (Header.Depth = 32) and (Header.Channels = 1) then Format := ifR32F; end; cmIndexed: begin if Header.Depth = 8 then Format := ifIndex8; end; cmRGB, cmMultiChannel, cmCMYK, cmLab: begin if Header.Depth in [8, 16] then begin if Header.Channels = 3 then Format := IffFormat(Header.Depth = 8, ifR8G8B8, ifR16G16B16) else if Header.Channels >= 4 then Format := IffFormat(Header.Depth = 8, ifA8R8G8B8, ifA16R16G16B16); end else if Header.Depth = 32 then begin if Header.Channels = 3 then Format := ifR32G32B32F else if Header.Channels >= 4 then Format := ifA32R32G32B32F; end; end; cmMono:; // Not supported end; // Exit if no compatible format was found if Format = ifUnknown then Exit; NewImage(Header.Columns, Header.Rows, Format, Images[0]); Info := GetFormatInfo(Format); // Read or skip Color Mode Data Block (palette) Read(Handle, @ByteCount, SizeOf(ByteCount)); ByteCount := SwapEndianLongWord(ByteCount); if Format = ifIndex8 then begin // Read palette only for indexed images Read(Handle, @RawPal, SizeOf(RawPal)); for I := 0 to 255 do begin Palette[I].A := $FF; Palette[I].R := RawPal[I + 0]; Palette[I].G := RawPal[I + 256]; Palette[I].B := RawPal[I + 512]; end; end else Seek(Handle, ByteCount, smFromCurrent); // Skip Image Resources Block Read(Handle, @ByteCount, SizeOf(ByteCount)); ByteCount := SwapEndianLongWord(ByteCount); Seek(Handle, ByteCount, smFromCurrent); // Now there is Layer and Mask Information Block Read(Handle, @ByteCount, SizeOf(ByteCount)); ByteCount := SwapEndianLongWord(ByteCount); // Skip Layer and Mask Information Block Seek(Handle, ByteCount, smFromCurrent); // Read compression flag Read(Handle, @Compression, SizeOf(Compression)); Compression := SwapEndianWord(Compression); if Compression = CompressionRLE then begin // RLE compressed PSDs (most) have first lengths of compressed scanlines // for each channel stored SetLength(RLELineSizes, Height * Header.Channels); Read(Handle, @RLELineSizes[0], Length(RLELineSizes) * SizeOf(Word)); SwapEndianWord(@RLELineSizes[0], Height * Header.Channels); MaxRLESize := RLELineSizes[0]; for I := 1 to High(RLELineSizes) do begin if MaxRLESize < RLELineSizes[I] then MaxRLESize := RLELineSizes[I]; end; end else MaxRLESize := 0; ChannelPixelSize := Info.BytesPerPixel div Info.ChannelCount; LineSize := Width * ChannelPixelSize; WidthBytes := Width * Info.BytesPerPixel; GetMem(LineBuffer, LineSize); GetMem(PackedLine, MaxRLESize); try // Image color chanels are stored separately in PSDs so we will load // one by one and copy their data to appropriate addresses of dest image. for I := 0 to Header.Channels - 1 do begin // Now determine to which color channel of destination image we are going // to write pixels. if I <= 4 then begin // If PSD has alpha channel we need to switch current channel order - // PSDs have alpha stored after blue channel but Imaging has alpha // before red. if Info.HasAlphaChannel and (Header.Mode <> cmCMYK) then begin if I = Info.ChannelCount - 1 then CurrChannel := I else CurrChannel := Info.ChannelCount - 2 - I; end else CurrChannel := Info.ChannelCount - 1 - I; end else begin // No valid channel remains CurrChannel := -1; end; if CurrChannel >= 0 then begin for Y := 0 to Height - 1 do begin if Compression = CompressionRLE then begin // Read RLE line and decompress it PackedSize := RLELineSizes[I * Height + Y]; Read(Handle, PackedLine, PackedSize); DecodeRLE(PackedLine, LineBuffer, PackedSize, LineSize); end else begin // Just read uncompressed line Read(Handle, LineBuffer, LineSize); end; // Swap endian if needed if ChannelPixelSize = 4 then SwapEndianLongWord(PLongWord(LineBuffer), Width) else if ChannelPixelSize = 2 then SwapEndianWord(PWordArray(LineBuffer), Width); if Info.ChannelCount > 1 then begin // Copy each pixel fragment to its right place in destination image for X := 0 to Width - 1 do begin Move(PByteArray(LineBuffer)[X * ChannelPixelSize], PByteArray(Bits)[Y * WidthBytes + X * Info.BytesPerPixel + CurrChannel * ChannelPixelSize], ChannelPixelSize); end; end else begin // Just copy the line Move(LineBuffer^, PByteArray(Bits)[Y * LineSize], LineSize); end; end; end else begin // Skip current color channel, not needed for image loading - just to // get stream's position to the end of PSD if Compression = CompressionRLE then begin for Y := 0 to Height - 1 do Seek(Handle, RLELineSizes[I * Height + Y], smFromCurrent); end else Seek(Handle, LineSize * Height, smFromCurrent); end; end; if Header.Mode = cmCMYK then begin // Convert CMYK images to RGB (alpha is ignored here). PSD stores CMYK // channels in the way that first requires substraction from max channel value if ChannelPixelSize = 1 then begin PCol32 := Bits; for X := 0 to Width * Height - 1 do begin Col32.A := 255 - PCol32.A; Col32.R := 255 - PCol32.R; Col32.G := 255 - PCol32.G; Col32.B := 255 - PCol32.B; CMYKToRGB(Col32.A, Col32.R, Col32.G, Col32.B, PCol32.R, PCol32.G, PCol32.B); PCol32.A := 255; Inc(PCol32); end; end else begin PCol64 := Bits; for X := 0 to Width * Height - 1 do begin Col64.A := 65535 - PCol64.A; Col64.R := 65535 - PCol64.R; Col64.G := 65535 - PCol64.G; Col64.B := 65535 - PCol64.B; CMYKToRGB16(Col64.A, Col64.R, Col64.G, Col64.B, PCol64.R, PCol64.G, PCol64.B); PCol64.A := 65535; Inc(PCol64); end; end; end; Result := True; finally FreeMem(LineBuffer); FreeMem(PackedLine); end; end; end; function TPSDFileFormat.SaveData(Handle: TImagingHandle; const Images: TDynImageDataArray; Index: LongInt): Boolean; type TURect = packed record Top, Left, Bottom, Right: LongWord; end; const BlendMode: TChar8 = '8BIMnorm'; LayerOptions: array[0..3] of Byte = (255, 0, 0, 0); LayerName: array[0..7] of AnsiChar = #7'Layer 0'; var MustBeFreed: Boolean; ImageToSave: TImageData; Info: TImageFormatInfo; Header: TPSDHeader; I, CurrChannel, ChannelPixelSize: LongInt; LayerBlockOffset, SaveOffset, ChannelInfoOffset: Integer; ChannelInfo: TPSDChannelInfo; R: TURect; LongVal: LongWord; WordVal, LayerCount: Word; RawPal: array[0..767] of Byte; ChannelDataSizes: array of Integer; function PackLine(Src, Dest: PByteArray; Length: Integer): Integer; var I, Remaining: Integer; begin Remaining := Length; Result := 0; while Remaining > 0 do begin I := 0; // Look for characters same as the first while (I < 128) and (Remaining - I > 0) and (Src[0] = Src[I]) do Inc(I); if I > 2 then begin Dest[0] := Byte(-(I - 1)); Dest[1] := Src[0]; Dest := PByteArray(@Dest[2]); Src := PByteArray(@Src[I]); Dec(Remaining, I); Inc(Result, 2); end else begin // Look for different characters I := 0; while (I < 128) and (Remaining - (I + 1) > 0) and ((Src[I] <> Src[I + 1]) or (Remaining - (I + 2) <= 0) or (Src[I] <> Src[I + 2])) do begin Inc(I); end; // If there's only 1 remaining, the previous WHILE doesn't catch it if Remaining = 1 then I := 1; if I > 0 then begin // Some distinct ones found Dest[0] := I - 1; Move(Src[0], Dest[1], I); Dest := PByteArray(@Dest[1 + I]); Src := PByteArray(@Src[I]); Dec(Remaining, I); Inc(Result, I + 1); end; end; end; end; procedure WriteChannelData(SeparateChannelStorage: Boolean); var I, X, Y, LineSize, WidthBytes, RLETableOffset, CurrentOffset, WrittenLineSize: Integer; LineBuffer, RLEBuffer: PByteArray; RLELengths: array of Word; Compression: Word; begin LineSize := ImageToSave.Width * ChannelPixelSize; WidthBytes := ImageToSave.Width * Info.BytesPerPixel; GetMem(LineBuffer, LineSize); GetMem(RLEBuffer, LineSize * 3); SetLength(RLELengths, ImageToSave.Height * Info.ChannelCount); RLETableOffset := 0; // No compression for FP32, Photoshop won't open them Compression := Iff(Info.IsFloatingPoint, CompressionNone, CompressionRLE); if not SeparateChannelStorage then begin // This is for storing background merged image. There's only one // compression flag and one RLE lenghts table for all channels WordVal := Swap(Compression); GetIO.Write(Handle, @WordVal, SizeOf(WordVal)); if Compression = CompressionRLE then begin RLETableOffset := GetIO.Tell(Handle); GetIO.Write(Handle, @RLELengths[0], SizeOf(Word) * ImageToSave.Height * Info.ChannelCount); end; end; for I := 0 to Info.ChannelCount - 1 do begin if SeparateChannelStorage then begin // Layer image data has compression flag and RLE lenghts table // independent for each channel WordVal := Swap(CompressionRLE); GetIO.Write(Handle, @WordVal, SizeOf(WordVal)); if Compression = CompressionRLE then begin RLETableOffset := GetIO.Tell(Handle); GetIO.Write(Handle, @RLELengths[0], SizeOf(Word) * ImageToSave.Height); ChannelDataSizes[I] := 0; end; end; // Now determine which color channel we are going to write to file. if Info.HasAlphaChannel then begin if I = Info.ChannelCount - 1 then CurrChannel := I else CurrChannel := Info.ChannelCount - 2 - I; end else CurrChannel := Info.ChannelCount - 1 - I; for Y := 0 to ImageToSave.Height - 1 do begin if Info.ChannelCount > 1 then begin // Copy each pixel fragment to its right place in destination image for X := 0 to ImageToSave.Width - 1 do begin Move(PByteArray(ImageToSave.Bits)[Y * WidthBytes + X * Info.BytesPerPixel + CurrChannel * ChannelPixelSize], PByteArray(LineBuffer)[X * ChannelPixelSize], ChannelPixelSize); end; end else Move(PByteArray(ImageToSave.Bits)[Y * LineSize], LineBuffer^, LineSize); // Write current channel line to file (swap endian if needed first) if ChannelPixelSize = 4 then SwapEndianLongWord(PLongWord(LineBuffer), ImageToSave.Width) else if ChannelPixelSize = 2 then SwapEndianWord(PWordArray(LineBuffer), ImageToSave.Width); if Compression = CompressionRLE then begin // Compress and write line WrittenLineSize := PackLine(LineBuffer, RLEBuffer, LineSize); RLELengths[ImageToSave.Height * I + Y] := SwapEndianWord(WrittenLineSize); GetIO.Write(Handle, RLEBuffer, WrittenLineSize); end else begin WrittenLineSize := LineSize; GetIO.Write(Handle, LineBuffer, WrittenLineSize); end; if SeparateChannelStorage then Inc(ChannelDataSizes[I], WrittenLineSize); end; if SeparateChannelStorage and (Compression = CompressionRLE) then begin // Update channel RLE lengths CurrentOffset := GetIO.Tell(Handle); GetIO.Seek(Handle, RLETableOffset, smFromBeginning); GetIO.Write(Handle, @RLELengths[ImageToSave.Height * I], SizeOf(Word) * ImageToSave.Height); GetIO.Seek(Handle, CurrentOffset, smFromBeginning); Inc(ChannelDataSizes[I], SizeOf(Word) * ImageToSave.Height); end; end; if not SeparateChannelStorage and (Compression = CompressionRLE) then begin // Update channel RLE lengths CurrentOffset := GetIO.Tell(Handle); GetIO.Seek(Handle, RLETableOffset, smFromBeginning); GetIO.Write(Handle, @RLELengths[0], SizeOf(Word) * ImageToSave.Height * Info.ChannelCount); GetIO.Seek(Handle, CurrentOffset, smFromBeginning); end; FreeMem(LineBuffer); FreeMem(RLEBuffer); end; begin Result := False; if MakeCompatible(Images[Index], ImageToSave, MustBeFreed) then with GetIO, ImageToSave do try Info := GetFormatInfo(Format); ChannelPixelSize := Info.BytesPerPixel div Info.ChannelCount; // Fill header with proper info and save it FillChar(Header, SizeOf(Header), 0); Header.Signature := SPSDMagic; Header.Version := 1; Header.Channels := Info.ChannelCount; Header.Rows := Height; Header.Columns := Width; Header.Depth := Info.BytesPerPixel div Info.ChannelCount * 8; if Info.IsIndexed then Header.Mode := cmIndexed else if Info.HasGrayChannel or (Info.ChannelCount = 1) then Header.Mode := cmGrayscale else Header.Mode := cmRGB; SwapHeader(Header); Write(Handle, @Header, SizeOf(Header)); // Write palette size and data LongVal := SwapEndianLongWord(IffUnsigned(Info.IsIndexed, SizeOf(RawPal), 0)); Write(Handle, @LongVal, SizeOf(LongVal)); if Info.IsIndexed then begin for I := 0 to Info.PaletteEntries - 1 do begin RawPal[I] := Palette[I].R; RawPal[I + 256] := Palette[I].G; RawPal[I + 512] := Palette[I].B; end; Write(Handle, @RawPal, SizeOf(RawPal)); end; // Write empty resource and layer block sizes LongVal := 0; Write(Handle, @LongVal, SizeOf(LongVal)); LayerBlockOffset := Tell(Handle); Write(Handle, @LongVal, SizeOf(LongVal)); if FSaveAsLayer and (ChannelPixelSize < 4) then // No Layers for FP32 images begin LayerCount := SwapEndianWord(Iff(Info.HasAlphaChannel, Word(-1), 1)); // Must be -1 to get transparency in Photoshop R.Top := 0; R.Left := 0; R.Bottom := SwapEndianLongWord(Height); R.Right := SwapEndianLongWord(Width); WordVal := SwapEndianWord(Info.ChannelCount); Write(Handle, @LongVal, SizeOf(LongVal)); // Layer section size, empty now Write(Handle, @LayerCount, SizeOf(LayerCount)); // Layer count Write(Handle, @R, SizeOf(R)); // Bounds rect Write(Handle, @WordVal, SizeOf(WordVal)); // Channel count ChannelInfoOffset := Tell(Handle); SetLength(ChannelDataSizes, Info.ChannelCount); // Empty channel infos FillChar(ChannelInfo, SizeOf(ChannelInfo), 0); for I := 0 to Info.ChannelCount - 1 do Write(Handle, @ChannelInfo, SizeOf(ChannelInfo)); Write(Handle, @BlendMode, SizeOf(BlendMode)); // Blend mode = normal Write(Handle, @LayerOptions, SizeOf(LayerOptions)); // Predefined options LongVal := SwapEndianLongWord(16); // Extra data size (4 (mask size) + 4 (ranges size) + 8 (name)) Write(Handle, @LongVal, SizeOf(LongVal)); LongVal := 0; Write(Handle, @LongVal, SizeOf(LongVal)); // Mask size = 0 LongVal := 0; Write(Handle, @LongVal, SizeOf(LongVal)); // Blend ranges size Write(Handle, @LayerName, SizeOf(LayerName)); // Layer name WriteChannelData(True); // Write Layer image data Write(Handle, @LongVal, SizeOf(LongVal)); // Global mask info size = 0 SaveOffset := Tell(Handle); Seek(Handle, LayerBlockOffset, smFromBeginning); // Update layer and mask section sizes LongVal := SwapEndianLongWord(SaveOffset - LayerBlockOffset - 4); Write(Handle, @LongVal, SizeOf(LongVal)); LongVal := SwapEndianLongWord(SaveOffset - LayerBlockOffset - 8); Write(Handle, @LongVal, SizeOf(LongVal)); // Update layer channel info Seek(Handle, ChannelInfoOffset, smFromBeginning); for I := 0 to Info.ChannelCount - 1 do begin ChannelInfo.ChannelID := SwapEndianWord(I); if (I = Info.ChannelCount - 1) and Info.HasAlphaChannel then ChannelInfo.ChannelID := Swap(Word(-1)); ChannelInfo.Size := SwapEndianLongWord(ChannelDataSizes[I] + 2); // datasize (incl RLE table) + comp. flag Write(Handle, @ChannelInfo, SizeOf(ChannelInfo)); end; Seek(Handle, SaveOffset, smFromBeginning); end; // Write background merged image WriteChannelData(False); Result := True; finally if MustBeFreed then FreeImage(ImageToSave); end; end; procedure TPSDFileFormat.ConvertToSupported(var Image: TImageData; const Info: TImageFormatInfo); var ConvFormat: TImageFormat; begin if Info.IsFloatingPoint then begin if Info.ChannelCount = 1 then ConvFormat := ifR32F else if Info.HasAlphaChannel then ConvFormat := ifA32R32G32B32F else ConvFormat := ifR32G32B32F; end else if Info.HasGrayChannel then ConvFormat := IffFormat(Info.HasAlphaChannel, ifA16Gray16, ifGray16) else if Info.RBSwapFormat in GetSupportedFormats then ConvFormat := Info.RBSwapFormat else ConvFormat := IffFormat(Info.HasAlphaChannel, ifA8R8G8B8, ifR8G8B8); ConvertImage(Image, ConvFormat); end; function TPSDFileFormat.TestFormat(Handle: TImagingHandle): Boolean; var Header: TPSDHeader; ReadCount: LongInt; begin Result := False; if Handle <> nil then begin ReadCount := GetIO.Read(Handle, @Header, SizeOf(Header)); SwapHeader(Header); GetIO.Seek(Handle, -ReadCount, smFromCurrent); Result := (ReadCount >= SizeOf(Header)) and (Header.Signature = SPSDMagic) and (Header.Version = 1); end; end; initialization RegisterImageFileFormat(TPSDFileFormat); { File Notes: -- 0.77.1 --------------------------------------------------- - 3 channel RGB float images are loaded and saved directly as ifR32G32B32F. -- 0.26.1 Changes/Bug Fixes --------------------------------- - PSDs are now saved with RLE compression. - Mask layer saving added to SaveData for images with alpha (shows proper transparency when opened in Photoshop). Can be enabled/disabled using option - Fixed memory leak in SaveData. -- 0.23 Changes/Bug Fixes ----------------------------------- - Saving implemented. - Loading implemented. - Unit created with initial stuff! } end.