123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790 |
- unit GR32.ImageFormats.PSD.Writer;
- (* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1 or LGPL 2.1 with linking exception
- *
- * The contents of this file are 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/
- *
- * 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
- * Free Pascal modified version of the GNU Lesser General Public License
- * Version 2.1 (the "FPC modified LGPL License"), in which case the provisions
- * of this license are applicable instead of those above.
- * Please see the file LICENSE.txt for additional information concerning this
- * license.
- *
- * The Original Code is PSD Image Format support for Graphics32
- *
- * The Initial Developer of the Original Code is
- * Lamdalili
- *
- * Portions created by the Initial Developer are Copyright (C) 2023
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- * Anders Melander <[email protected]>
- *
- * ***** END LICENSE BLOCK ***** *)
- // WEAKPACKAGEUNIT so we can include the unit in the GR32 design time
- // package in order to have the design time editor support the various formats.
- {$WEAKPACKAGEUNIT ON}
- interface
- {$include GR32.inc}
- uses
- Classes,
- GR32.ImageFormats.PSD;
- //------------------------------------------------------------------------------
- //
- // TPhotoshopDocumentWriter
- //
- //------------------------------------------------------------------------------
- // Writes a PSD document to a stream
- //------------------------------------------------------------------------------
- type
- TPhotoshopDocumentWriter = class abstract
- public
- class procedure SaveToStream(ADocument: TPhotoshopDocument; AStream: TStream);
- end;
- //------------------------------------------------------------------------------
- //------------------------------------------------------------------------------
- //------------------------------------------------------------------------------
- implementation
- uses
- Generics.Collections,
- {$ifndef FPC}
- ZLib,
- {$else FPC}
- zstream,
- {$endif FPC}
- Math,
- SysUtils,
- GR32,
- GR32_LowLevel,
- GR32.BigEndian,
- GR32.ImageFormats.PSD.Types;
- type
- TBytesArray = array of byte;
- TPhotoshopLayerCracker = class(TCustomPhotoshopLayer);
- //------------------------------------------------------------------------------
- //
- // Scanline compression
- //
- //------------------------------------------------------------------------------
- type
- // Write all channels in one go
- // Used for background bitmap
- TPSDBitmapWriterDelegate = procedure(AStream: TStream; ALayer: TCustomPhotoshopLayer);
- // Write a single channels
- // Used for layer bitmaps
- TPSDChannelWriterDelegate = procedure(AStream: TStream; AChannel: TColor32Component; ALayer: TCustomPhotoshopLayer; var ABuffer);
- const
- // Number of channels
- PSD_CHANNELS = Ord(High(TColor32Component))-Ord(Low(TColor32Component))+1;
- {$IFNDEF RGBA_FORMAT}
- PSD_CHANNELS_IDS: array[TColor32Component] of SmallInt = (PSD_MASK_BLUE, PSD_MASK_GREEN, PSD_MASK_RED, PSD_MASK_ALPHA);
- {$ELSE}
- PSD_CHANNELS_IDS: array[TColor32Component] of SmallInt = (PSD_MASK_RED, PSD_MASK_GREEN, PSD_MASK_BLUE, PSD_MASK_ALPHA);
- {$ENDIF}
- const
- // The PSD channels in "planar" order
- PSDPlanarOrder: array[0..PSD_CHANNELS-1] of TColor32Component = (ccRed, ccGreen, ccBlue, ccAlpha);
- //------------------------------------------------------------------------------
- // RAW compression (i.e. no compression)
- //------------------------------------------------------------------------------
- type
- CompressionRAW = record
- class function WriteScanline(AStream: TStream; const ABuffer; Width: integer): Cardinal; static;
- class procedure WriteChannel(AStream: TStream; AChannel: TColor32Component; ALayer: TCustomPhotoshopLayer; var ABuffer); static;
- class procedure WriteBitmap(AStream: TStream; ALayer: TCustomPhotoshopLayer); static;
- end;
- class function CompressionRAW.WriteScanline(AStream: TStream; const ABuffer; Width: integer): Cardinal;
- begin
- Result := AStream.Write(ABuffer, Width);
- end;
- class procedure CompressionRAW.WriteChannel(AStream: TStream; AChannel: TColor32Component; ALayer: TCustomPhotoshopLayer; var ABuffer);
- var
- i: integer;
- begin
- for i := 0 to ALayer.Height - 1 do
- begin
- TPhotoshopLayerCracker(ALayer).GetChannelScanLine(AChannel, i, ABuffer);
- WriteScanline(AStream, ABuffer, ALayer.Width);
- end;
- end;
- class procedure CompressionRAW.WriteBitmap(AStream: TStream; ALayer: TCustomPhotoshopLayer);
- var
- ScanLineBuffer: TBytesArray;
- Channel: TColor32Component;
- i: integer;
- begin
- SetLength(ScanLineBuffer, ALayer.Width);
- for Channel in PSDPlanarOrder do
- for i := 0 to ALayer.Height - 1 do
- begin
- TPhotoshopLayerCracker(ALayer).GetChannelScanLine(Channel, i, ScanLineBuffer[0]);
- WriteScanline(AStream, ScanLineBuffer[0], ALayer.Width);
- end;
- end;
- //------------------------------------------------------------------------------
- // RLE compression (PackBit)
- //------------------------------------------------------------------------------
- type
- TPackBitsStream = class(TStream)
- private
- FStream: TStream;
- public
- constructor Create(AStream: TStream);
- function Read(var Buffer; Count: Longint): Longint; override;
- function Write(const Buffer; Count: Longint): Longint; override;
- end;
- constructor TPackBitsStream.Create(AStream: TStream);
- begin
- inherited Create;
- FStream := AStream;
- end;
- function TPackBitsStream.Read(var Buffer; Count: Longint): Longint;
- begin
- Assert(False, 'Not implemented');
- Result := 0;
- end;
- function TPackBitsStream.Write(const Buffer; Count: Longint): Longint;
- const
- MaxRun = 128;
- // These values are for classic PackBits encoding.
- // Other variants use other values (e.g. PDB uses PackBase=127, PackSign=1)
- PackBase = 257;
- PackSign = -1;
- var
- Index: Int64;
- RunCount: Byte;
- RunValue: Byte;
- StartIndex: integer;
- begin
- Index := 0;
- Result := 0;
- while (Index < Count) do
- begin
- (*
- ** Always encode 3-byte repeat sequences.
- ** Encode 2-byte repeat sequences only when they are at the start of the block.
- *)
- RunValue := TByteArray(Buffer)[Index];
- if (Index < Count - 1) and (TByteArray(Buffer)[Index] = TByteArray(Buffer)[Index + 1]) then
- begin
- // Do a repeat run
- RunCount := 2; // We already know that we have at least a run of two because of the test above
- Inc(Index, 2);
- while (Index < Count) and (RunValue = TByteArray(Buffer)[Index]) and (RunCount < MaxRun) do
- begin
- Inc(Index);
- Inc(RunCount);
- end;
- // Encode run count
- // RunCount := Byte(PackBase + PackSign * RunCount);
- RunCount := Byte(257 - RunCount);
- FStream.Write(RunCount, 1);
- FStream.Write(RunValue, 1);
- Inc(Result, 2);
- end else
- begin
- // Do a non-repeat run
- RunCount := 0;
- StartIndex := Index;
- while
- // We're at the end; No room for repeat runs
- ((Index + 2 >= Count) and (Index < Count)) or
- // There's at least 3 bytes left and...
- ((Index + 2 < Count) and (
- // Next 2 differ
- (RunValue <> TByteArray(Buffer)[Index + 1]) or
- // Next 2 same, but differs from the third
- (RunValue <> TByteArray(Buffer)[Index + 2]))) do
- begin
- Inc(Index);
- Inc(RunCount);
- if (RunCount = MaxRun) then
- Break;
- RunValue := TByteArray(Buffer)[Index];
- end;
- BigEndian.WriteByte(FStream, RunCount-1);
- FStream.Write(TByteArray(Buffer)[StartIndex], RunCount);
- Inc(Result, RunCount+1);
- end;
- end;
- end;
- type
- CompressionRLE = record
- class procedure WriteChannel(AStream: TStream; AChannel: TColor32Component; ALayer: TCustomPhotoshopLayer; var ABuffer); static;
- class procedure WriteBitmap(AStream: TStream; ALayer: TCustomPhotoshopLayer); static;
- end;
- class procedure CompressionRLE.WriteChannel(AStream: TStream; AChannel: TColor32Component; ALayer: TCustomPhotoshopLayer; var ABuffer);
- var
- i: integer;
- RowTablePos: Int64;
- RowSize: Word;
- RowTable: array of Word;
- SavePos: Int64;
- RLEStream: TStream;
- begin
- RowTablePos := AStream.Position;
- // Make room for row table
- AStream.Seek(ALayer.Height * SizeOf(Smallint), soFromCurrent);
- SetLength(RowTable, ALayer.Height);
- RLEStream := TPackBitsStream.Create(AStream);
- try
- for i := 0 to ALayer.Height - 1 do
- begin
- TPhotoshopLayerCracker(ALayer).GetChannelScanLine(AChannel, i, ABuffer);
- RowSize := RLEStream.Write(ABuffer, ALayer.Width);
- RowTable[i] := Swap16(RowSize);
- end;
- finally
- RLEStream.Free;
- end;
- // Rewind and update row table
- SavePos := AStream.Position;
- AStream.Position := RowTablePos;
- AStream.Write(RowTable[0], ALayer.Height * SizeOf(Word));
- AStream.Position := SavePos;
- end;
- class procedure CompressionRLE.WriteBitmap(AStream: TStream; ALayer: TCustomPhotoshopLayer);
- var
- ScanLineBuffer: TBytesArray;
- Channel: TColor32Component;
- i: integer;
- RowTablePos: Int64;
- RowSize: Word;
- RowTable: array of Word;
- SavePos: Int64;
- RLEStream: TStream;
- begin
- SetLength(ScanLineBuffer, ALayer.Width);
- SetLength(RowTable, ALayer.Height);
- RowTablePos := AStream.Position;
- // Make room for row table (for all channels)
- AStream.Seek(ALayer.Height * SizeOf(Word) * PSD_CHANNELS, soFromCurrent);
- RLEStream := TPackBitsStream.Create(AStream);
- try
- for Channel in PSDPlanarOrder do
- begin
- for i := 0 to ALayer.Height - 1 do
- begin
- TPhotoshopLayerCracker(ALayer).GetChannelScanLine(Channel, i, ScanLineBuffer[0]);
- RowSize := RLEStream.Write(ScanLineBuffer[0], ALayer.Width);
- RowTable[i] := Swap16(RowSize);
- end;
- // Rewind and update row table for the channel
- SavePos := AStream.Position;
- AStream.Position := RowTablePos;
- AStream.Write(RowTable[0], ALayer.Height * SizeOf(Word));
- // Move table pos forward to next channel
- Inc(RowTablePos, ALayer.Height * SizeOf(Word));
- AStream.Position := SavePos;
- end;
- finally
- RLEStream.Free;
- end;
- end;
- //------------------------------------------------------------------------------
- // ZIP compression
- //------------------------------------------------------------------------------
- type
- CompressionZIP = record
- class procedure WriteChannel(AStream: TStream; AChannel: TColor32Component; ALayer: TCustomPhotoshopLayer; var ABuffer); static;
- class procedure WriteBitmap(AStream: TStream; ALayer: TCustomPhotoshopLayer); static;
- end;
- class procedure CompressionZIP.WriteChannel(AStream: TStream; AChannel: TColor32Component; ALayer: TCustomPhotoshopLayer; var ABuffer);
- var
- i: integer;
- Stream: TStream;
- begin
- Stream := TCompressionStream.Create(clDefault, AStream);
- try
- for i := 0 to ALayer.Height - 1 do
- begin
- TPhotoshopLayerCracker(ALayer).GetChannelScanLine(AChannel, i, ABuffer);
- Stream.Write(ABuffer, ALayer.Width);
- end;
- finally
- Stream.Free;
- end;
- end;
- class procedure CompressionZIP.WriteBitmap(AStream: TStream; ALayer: TCustomPhotoshopLayer);
- var
- ScanLineBuffer: TBytesArray;
- Stream: TStream;
- Channel: TColor32Component;
- i: integer;
- begin
- SetLength(ScanLineBuffer, ALayer.Width);
- Stream := TCompressionStream.Create(clDefault, AStream);
- try
- for Channel in PSDPlanarOrder do
- for i := 0 to ALayer.Height - 1 do
- begin
- TPhotoshopLayerCracker(ALayer).GetChannelScanLine(Channel, i, ScanLineBuffer[0]);
- Stream.Write(ScanLineBuffer[0], ALayer.Width);
- end;
- finally
- Stream.Free;
- end;
- end;
- //------------------------------------------------------------------------------
- function GetLayerWriter(ALayer: TCustomPhotoshopLayer): TPSDChannelWriterDelegate;
- begin
- case ALayer.Compression of
- lcRLE:
- Result := CompressionRLE.WriteChannel;
- lcZIP:
- Result := CompressionZIP.WriteChannel;
- lcRAW:
- Result := CompressionRAW.WriteChannel;
- else
- raise EPhotoshopDocument.CreateFmt('Unsupported compression method: %d', [Ord(ALayer.Compression)]);
- end;
- end;
- function GetBitmapWriter(ALayer: TCustomPhotoshopLayer): TPSDBitmapWriterDelegate;
- begin
- case ALayer.Compression of
- lcRLE:
- Result := CompressionRLE.WriteBitmap;
- lcZIP:
- Result := CompressionZIP.WriteBitmap;
- lcRAW:
- Result := CompressionRAW.WriteBitmap;
- else
- raise EPhotoshopDocument.CreateFmt('Unsupported compression method: %d', [Ord(ALayer.Compression)]);
- end;
- end;
- //------------------------------------------------------------------------------
- //
- // TPhotoshopDocumentWriter
- //
- //------------------------------------------------------------------------------
- class procedure TPhotoshopDocumentWriter.SaveToStream(ADocument: TPhotoshopDocument; AStream: TStream);
- var
- SectionsCaptures: TStack<Int64>;
- function Pad(Value: Cardinal; Alignment: Cardinal = 4): integer;
- begin
- Result := (Alignment - (Value and (Alignment - 1))) and (Alignment - 1);
- end;
- function WritePadding(ASize: Cardinal): Cardinal;
- const
- Zero: byte = 0;
- begin
- Result := AStream.Position;
- while (ASize > 0) do
- begin
- AStream.Write(Zero, 1);
- Dec(ASize);
- end;
- end;
- procedure WritePadToAlignment(Value: Cardinal; Alignment: Cardinal = 4);
- begin
- WritePadding(Pad(Value, Alignment));
- end;
- function WriteRawAnsiString(const s: AnsiString): Cardinal;
- begin
- Result := Length(s);
- AStream.Write(PAnsiChar(s)^, Result);
- end;
- function WriteAnsiText(const AText: AnsiString): Cardinal;
- begin
- BigEndian.WriteByte(AStream, Length(AText));
- Result := WriteRawAnsiString(AText) + 1;
- end;
- function WriteUnicodeText(const AText: string): Cardinal;
- var
- c: Char;
- begin
- BigEndian.WriteCardinal(AStream, Length(AText));
- for c in AText do
- BigEndian.WriteWord(AStream, Ord(c));
- c := #0;
- AStream.Write(c, SizeOf(Char));
- Result := (Length(AText)+1) * SizeOf(Char) + SizeOf(Cardinal);
- end;
- procedure WriteBeginSection;
- begin
- BigEndian.WriteCardinal(AStream, 0); // field slot
- SectionsCaptures.Push(AStream.Position);
- end;
- procedure WriteEndSection(Align: Cardinal = 4);
- var
- Size: Cardinal;
- SectionStartPos: Int64;
- SavePos: Int64;
- begin
- SectionStartPos := SectionsCaptures.Pop;
- Size := AStream.Position - SectionStartPos;
- WritePadToAlignment(Size, Align);
- Size := Swap32(AStream.Position - SectionStartPos);
- SavePos := AStream.Position;
- AStream.Position := SectionStartPos - SizeOf(Cardinal); // field slot
- AStream.Write(Size, SizeOf(Size));
- AStream.Position := SavePos;
- end;
- procedure WriteEmptyImage;
- procedure WriteEmptyImageRLE;
- var
- RepeatsCount: integer;
- RemainCount: integer;
- i: integer;
- RowBuffer: array of Word;
- begin
- BigEndian.WriteWord(AStream, PSD_COMPRESSION_RLE);
- // Everything is repeats.
- // How many whole 128 byte repeats do we have?
- RepeatsCount := (ADocument.Width + 127) div 128; // round up
- // How many bytes remaining?
- RemainCount := ADocument.Width mod 128;
- SetLength(RowBuffer, RepeatsCount);
- // Write row table (all 4 channels)
- for i := 0 to ADocument.Height * PSD_CHANNELS - 1 do
- BigEndian.WriteWord(AStream, RepeatsCount * SizeOf(Word));
- (*
- ** Write RGB channels = $xxFFFFFF
- *)
- for i := 0 to RepeatsCount - 1 do
- RowBuffer[i] := Swap16($81FF); // Fill with whole 128 byte repeat runs
- if (RemainCount <> 0) then
- // Replace last entry with the remaining repeat run
- RowBuffer[RepeatsCount - 1] := Swap16(byte(-RemainCount + 1) shl 8 or $FF);
- for i := 0 to ADocument.Height * (PSD_CHANNELS-1) - 1 do
- AStream.Write(RowBuffer[0], RepeatsCount * SizeOf(Word));
- (*
- ** Write A channel = $00xxxxxx
- *)
- for i := 0 to RepeatsCount - 1 do
- RowBuffer[i] := Swap16($8100); // Fill with whole 128 byte repeat runs
- if (RemainCount <> 0) then
- // Replace last entry with the remaining repeat run
- RowBuffer[RepeatsCount - 1] := Swap16(byte(-RemainCount + 1) shl 8 or $00);
- for i := 0 to ADocument.Height - 1 do
- AStream.Write(RowBuffer[0], RepeatsCount * SizeOf(Word));
- end;
- procedure WriteEmptyImageRAW;
- var
- RowBuffer: array of byte;
- i: integer;
- begin
- BigEndian.WriteWord(AStream, PSD_COMPRESSION_NONE); // No compression
- SetLength(RowBuffer, ADocument.Width);
- // Write RGB channels = $xxFFFFFF
- FillChar(RowBuffer[0], ADocument.Width, $FF);
- for i := 0 to (ADocument.Height * (PSD_CHANNELS-1)) - 1 do
- AStream.Write(RowBuffer[0], ADocument.Width);
- // Write A channel = $00xxxxxx
- FillChar(RowBuffer[0], ADocument.Width, $00);
- for i := 0 to ADocument.Height - 1 do
- AStream.Write(RowBuffer[0], ADocument.Width);
- end;
- begin
- // Write an "empty" image containing ARGB=$00FFFFFF
- if (ADocument.Compression = lcRAW) then
- WriteEmptyImageRAW
- else
- WriteEmptyImageRLE;
- end;
- procedure WriteLayerImage(ALayer: TCustomPhotoshopLayer; AChannelsInfoPos: Int64);
- var
- LayerWriter: TPSDChannelWriterDelegate;
- Size: Cardinal;
- Channel: TColor32Component;
- ChannelsInfo: array[TColor32Component] of TPSDChannelInfo;
- ScanLineBuffer: TBytesArray;
- SavePos: Int64;
- begin
- SetLength(ScanLineBuffer, ALayer.Width);
- LayerWriter := GetLayerWriter(ALayer);
- ALayer.BeginScan;
- begin
- for Channel := Low(TColor32Component) to High(TColor32Component) do
- begin
- SavePos := AStream.Position;
- BigEndian.WriteWord(AStream, Ord(ALayer.Compression));
- LayerWriter(AStream, Channel, ALayer, ScanLineBuffer[0]);
- Size := AStream.Position - SavePos;
- ChannelsInfo[Channel].ChannelID := Swap16(Word(PSD_CHANNELS_IDS[Channel]));
- ChannelsInfo[Channel].ChannelSize := Swap32(Size);
- end;
- end;
- ALayer.EndScan;
- // Rewind and update channel table
- SavePos := AStream.Position;
- AStream.Position := AChannelsInfoPos;
- AStream.Write(ChannelsInfo, SizeOf(ChannelsInfo));
- AStream.Position := SavePos;
- end;
- procedure WriteLayerName(const AName: AnsiString; Align: Cardinal = 4);
- var
- Size: integer;
- begin
- Size := WriteAnsiText(AName);
- WritePadToAlignment(Size, Align);
- end;
- procedure WriteLayerBeginExtraInfo(const AKey: AnsiString);
- begin
- if Length(AKey) <> 4 then
- raise EPhotoshopDocument.CreateFmt('Invalid layer info key: "%s"',[string(AKey)]);
- WriteRawAnsiString('8BIM'); // Signature
- WriteRawAnsiString(AKey); // Key
- WriteBeginSection; // Size field
- end;
- procedure WriteLayerEndExtraInfo();
- begin
- // Specs state section size should be aligned to 2 bytes for most sub section types:
- //
- // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_71546
- //
- // In reality the one section we write ('luni') is aligned to 4 bytes and some readers
- // complain if it isn't.
- WriteEndSection(4);
- end;
- procedure WriteLayerRecord(ALayer: TCustomPhotoshopLayer; var AChannelsInfoPos: Int64);
- begin
- BigEndian.WriteCardinal(AStream, ALayer.Top); // Top
- BigEndian.WriteCardinal(AStream, ALayer.Left); // Left
- BigEndian.WriteCardinal(AStream, ALayer.Top + ALayer.Height); // Bottom
- BigEndian.WriteCardinal(AStream, ALayer.Left + ALayer.Width); // Right
- BigEndian.WriteWord(AStream, PSD_CHANNELS);
- // Make room for channel info list. Later updated in WriteLayerImage
- AChannelsInfoPos := AStream.Position;
- AStream.Seek(PSD_CHANNELS * SizeOf(TPSDChannelInfo), soFromCurrent);
- WriteRawAnsiString('8BIM'); // Signature
- WriteRawAnsiString(PSDBlendModeMapping[ALayer.BlendMode]); // Blend mode
- BigEndian.WriteByte(AStream, ALayer.Opacity); // Opacity
- BigEndian.WriteByte(AStream, Ord(ALayer.Clipping)); // Clipping
- BigEndian.WriteByte(AStream, byte(ALayer.Options)); // Options
- BigEndian.WriteByte(AStream, 0); // Filler, always 0
- // Variable section
- WriteBeginSection; // Extra data field
- begin
- BigEndian.WriteCardinal(AStream, 0); // Layer mask
- BigEndian.WriteCardinal(AStream, 0); // Blending ranges
- // Name of layer - ANSI
- WriteLayerName(AnsiString(ALayer.Name), 4);
- // *Layer extra info '8BIM' sequences
- WriteLayerBeginExtraInfo('luni');
- begin
- WriteUnicodeText(ALayer.Name); // unicode layer name sequence
- end;
- WriteLayerEndExtraInfo;
- end;
- WriteEndSection(4);
- end;
- procedure WriteLayerInfo;
- var
- i: integer;
- ChannelsInfoPos: array of Int64;
- begin
- WriteBeginSection(); // Layer info size field
- begin
- BigEndian.WriteWord(AStream, ADocument.Layers.Count); // Layers count
- SetLength(ChannelsInfoPos, ADocument.Layers.Count);
- for i := 0 to ADocument.Layers.Count - 1 do
- WriteLayerRecord(TCustomPhotoshopLayer(ADocument.Layers[i]), ChannelsInfoPos[i]);
- for i := 0 to ADocument.Layers.Count - 1 do
- WriteLayerImage(TCustomPhotoshopLayer(ADocument.Layers[i]), ChannelsInfoPos[i]);
- end;
- // Specs state section size should be aligned to 2 bytes:
- //
- // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_16000
- //
- // In reality it is aligned to 4 bytes and some readers complain if it isn't.
- WriteEndSection(4);
- end;
- procedure WriteLayer;
- begin
- if ADocument.Layers.Count = 0 then
- begin
- BigEndian.WriteCardinal(AStream, 0);
- exit;
- end;
- WriteBeginSection; // layer's total size field
- begin
- WriteLayerInfo;
- BigEndian.WriteCardinal(AStream, 0); // Global Mask .. optional
- // * global extra layer info '8BIM'
- end;
- WriteEndSection(4);
- end;
- procedure WriteImage;
- var
- BitmapWriter: TPSDBitmapWriterDelegate;
- begin
- BitmapWriter := GetBitmapWriter(ADocument.Background);
- ADocument.Background.BeginScan;
- begin
- BigEndian.WriteWord(AStream, Ord(ADocument.Background.Compression));
- BitmapWriter(AStream, ADocument.Background);
- end;
- ADocument.Background.EndScan;
- end;
- begin
- if (ADocument.Width = 0) or (ADocument.Height = 0) then
- raise EPhotoshopDocument.Create('Invalid PSD document size');
- // Header
- WriteRawAnsiString('8BPS');
- BigEndian.WriteWord(AStream, PSD_VERSION_PSD);
- WritePadding(6); // unused
- BigEndian.WriteWord(AStream, PSD_CHANNELS);// PSD_CHANNELS
- BigEndian.WriteCardinal(AStream, ADocument.Height); // height
- BigEndian.WriteCardinal(AStream, ADocument.Width); // width
- BigEndian.WriteWord(AStream, 8);// bit depth
- BigEndian.WriteWord(AStream, PSD_RGB);// color mode RGB = 3
- // color mode Table
- BigEndian.WriteCardinal(AStream, 0);
- // resources
- BigEndian.WriteCardinal(AStream, 0);
- SectionsCaptures := TStack<Int64>.Create;
- try
- // layer
- WriteLayer;
- finally
- SectionsCaptures.Free;
- end;
- //Image
- if (ADocument.Background = nil) then
- WriteEmptyImage
- else
- WriteImage;
- end;
- end.
|