LazPaint LZP Format

From Free Pascal wiki

LZP File format specification

In this document, hexadecimal numbers follow Pascal convention, i.e. they are prefixed with a dollar sign. For example $10 means 16. As an exception, sequences of bytes are not prefixed.

The default encoding for numbers is little endian. So the hexadecimal number $12345678 would be stored as the sequence of bytes 78 56 34 12.

Numbers are 32-bit signed integers, unless stated otherwise. So -1 is stored like $FFFFFFFF.

Global structure

The file is composed of:

Offset  Content
------  --------------------
0       File header
?       Thumbnail (optional)
?       Flat image
?       Layer structure (optional)

Note: some old formats do not have a header and start directly with the flat image. Image readers may discard those files. However for your information, in this case, images are always stored with ZStream compression and the size of the image is deduced from the size of the flat image.

File header

Offset  Size  Name                Content
------  ----  ------------------  ------------------------
0       8     magic               Always contains the string 'LazPaint'
8       4     zero1               Contains the number 0
12      4     headerSize          Size of this header

16      4     width               Width of the image canvas
20      4     height              Height of the image canvas
24      4     nbLayers            Number of layers
28      4     previewOffset       Offset (from the beginning of the file) of the flattened image

32      4     zero2               Contains the number 0
36      4     compressionMode     Contains flags indicating the compression and the presence of a thumbnail
40      4     reserved1           Reserved for future use. Set to 0.
44      4     layersOffset        Offset (from the beginning of the file) of the layer structure

headerSize contains the size of this header. An image reader must read this to know which fields are available. It must be greater or equal to 48.

width and height are the dimension of the canvas. There is no real limitation for these numbers however it is recommended to keep it under or equal to 8192.

nbLayers is the number of layers. The minimum is 1. There is no real limitation however it is recommended to keep it under or equal to 99.

layersOffset can be 0, in which case the image is flat and that there is no layer description.

Compression

compressionMode contains flags indicating the compression and the presence of a thumbnail.

The low order byte indicates the compression of the flattened image. It is the value (compressionMode and $ff):

- $01 indicates ZStream compression

- $02 indicates RLE compression

- any other value should be considered as an indication that the image reader cannot interpret the content of the file

The following flags can be defined:

- $100 indicates that there is thumbnail in PNG format; check with (compressionMode and $100) <> 0

If there is a thumbnail, it is directly after the header. The reader should read headerSize to have the correct location.

The compression of the layers is defined in the layer structure.

Thumbnail

Its presence is recommended however optional. Its presence is specified by the flag $100 of compressionMode.

Currently, it is a PNG file embedded directly after the header. It is provided as a way to facilitate quick preview.

PNG format is widespread so it is easy to implement a browser that would display the thumbnail. To do so, check that the header contains magic and that the flag for the thumbnail is set in compressionMode. Then jump to the end of the header using headerSize. Then read the PNG file.

There is no real limitation but the recommended size for the thumbnail is 128 along the largest dimension. The other dimension may be smaller if the image is not a square. Of course if the image is smaller than 128 then the thumbnail will be smaller than 128.

Flat image

The file always include a flattened image to make it easier for image viewers. It is in the LZP single image format.

If there is no layer structure, the caption of this image is the name of the unique layer. If there is a layer structure, then the caption of this image is discarded.

Layer structure

Offset  Size  Content
------  ----  -------------------------------------------------------
0       20    Contains the string 'TBGRALayeredBitmap' followed by the bytes 1A 00
20      4     Size S of the additional header (must be at least 12)
24      4     Number of layers N (should be identical to the number in the file header)
28      4     Selected layer index. Equals to -1 if no layer is selected, otherwise positive from 0 to N-1
32      4     Stack flags

The following flags are defined: - $01: the layers are blended without gamma correction - $02: the layers are compressed with RLE compression (otherwise they are compressed with ZStream compression)

The total header size of the layer structure is equal to 20 + S.

Then for each layer there is a layer entry.

The first layer entry is the bottom most layer. Then each layer is blended over the previous ones. The last layer is on top and is not hidden by any other layer.

Layer entry

Header of the layer entry:

Offset  Size  Content
------  ----  -------------------------------------------------------
0       4     Size S1 of the optional fields that follows
4       4     Layer flags (lowest bit means that it is visible)
8       4     Layer blend operation
12      4     Layer horizontal offset in pixels
16      4     Layer vertical offset in pixels
20      4     Layer ID (if used, each layer must have a different ID in a given file)
24      4     Layer opacity from 0 (transparent) to 65535 (opaque)
28      4     Size S2 of image data
4+S1    S2    Image data
4+S1+S2       Next entry

All fields are optional so in theory, S1 can be set to zero and the image data can directly follow. However it is recommended to write the whole header.

The default values are: visible, blend operation boTransparent, offset (0,0), opaque.

Blend operations

Note:

- values (Mix, Under, Over) are considered to be between 0 and 255

- GE means gamma expansion. GE(x) = Power(x/255, 1.7)*65535

- GC means gamma compression. GC(x) = Power(x/65535, 1/1.7)*255

- Mix is an intermediate value that is then blended over the previous layers, and that is computed for each RGB channel. The alpha channel is computed differently (see below table).

Value  Name               Description
-----  -----------------  ----------------------------------------------------
0      LinearBlend        Blend over the previous layers without gamma correction
1      Transparent        Blend over the previous layers 
                          (with gamma correction unless it is specified otherwise in the stack flags)

2      Lighten            Mix := Max(Over, Under)
3      Screen             Mix := 255 - ((255 - Under) * (255 - Over) shr 8)
4      Additive           Mix := GC(GE(Under)+GE(Over))
5      LinearAdd          Mix := Under + Over
6      ColorDodge         Mix := if Over = 255 then 255
                                 else Min(255, (Under shl 8) div (255 - Over))
7      Divide             Mix := if Over = 0 then 255
                                 else Min(255, (Under shl 8) div Over)
8      NiceGlow           Mix := if Under = 255 then 255
                                 else Min(255, Over*Over div (255 - Under))
9      SoftLight          Mix := ((255 - Under)*Over shr 7 + Under)*Under div 255
10     HardLight          Mix := if Over <= 128 then Under*Over shr 7
                                 else 255 - ((255 - Under)*(255 - Over) shr 7)

11     Glow               Mix := if Under = 255 then 255
                                 else Min(255, Over*Over div (255 - Under))
12     Reflect            Mix := if Over = 255 then 255
                                 else Min(255, Under*Under div (255 - Over))
13     Overlay            Mix := if Under <= 128 then Under*Over shr 7
                                 else 255 - ((255 - Under)*(255 - Over) shr 7)
14     DarkOverlay        Mix := if GE(Under) <= 32768 then Under*Over shr 7
                                 else GC(65535 - ((65535 - GE(Under))*(65535 - GE(Over)) shr 15)
15     Darken             Mix := Min(Under, Over)
16     Multiply           Mix := Under*Over shr 8
17     ColorBurn          Mix := if Over = 0 then 0
                                 else Max(0, 255 - (Under shl 8) div Over)

18     Difference         Mix := GC(Abs(GE(Under) - GE(Over)))
19     LinearDifference   Mix := Abs(Under - Over)
20     Exclusion          Mix := GC(GE(Under) + GE(Over) - (GE(Under)*GE(Over) shr 15)
21     LinearExclusion    Mix := Under + Over - (Under*Over shr 7)
22     Subtract           Mix := GC(Max(0, GE(Under) - GE(Over)))
23     LinearSubtract     Mix := Max(0, Under - Over)
24     SubtractInverse    Mix := GC(Max(0, GE(Under) - (65535 - GE(Over))))
25     LinearSubtractInv  Mix := Max(0, Under - (255 - Over))

26     Negation           Mix := if GE(Under) + GE(Over) <= 65535 then GC(GE(Under) + GE(Over))
                                 else GC(131071 - (GE(Under) + GE(Over)))
27     LinearNegation     Mix := if Under + Over <= 255 then Under + Over
                                 else 511 - (Under + Over)
28     Xor                Mix := Under xor Over

The value that is actually merged over is computed like that for RGB channels:

ToBeMergedOver := (Mix * UnderAlpha + Over * (255 - UnderAlpha)) div 255

In other words, if the layer underneath is transparent, the merged color is simply the color of the layer above.

Computation of blend-over

Linear blend-over:

var 
  a1f, a2f, a12, a12m: DWord; //unsigned
begin
  a12  := 65025 - (255 - UnderAlpha) * (255 - OverAlpha);
  a1f := UnderAlpha * (255 - OverAlpha);
  a2f := OverAlpha * 255;
  Under := (Under * a1f + Over * a2f + (a12 shr 1)) div a12; //for each channel
  UnderAlpha := (a12 + a12 shr 7) shr 8;   
end;

Blend-over with gamma correction:

var 
  a1f, a2f, a12, a12m: DWord; //unsigned
begin
  a12  := 65025 - (255 - UnderAlpha) * (255 - OverAlpha);
  a1f := UnderAlpha * (255 - OverAlpha);
  a2f := OverAlpha * 255;
  Under := GC( (GE(Under) * a1f + GE(Over) * a2f + (a12 shr 1)) div a12 ); //for each channel
  UnderAlpha := (a12 + a12 shr 7) shr 8;   
end;