{
  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.

