ImageLayer#

About#

As with LayeredFile, we only provide documentation for the 8bit type here but 16- and 32-bit are both equally supported, all the classes can be seen below.

ImageLayer_8bit

ImageLayer_16bit

ImageLayer_32bit

One major change in the python bindings compared to the C++ code is that we allow for dict-like indexing of channels through the __getitem__ method. This is very convenient as it significantly reduces the amount of code that has to be written.

We currently support two methods of accessing channel data this way, by logical index or by channel id. Both of these methods are documented below

Examples#

Important

When accessing image data this way the image data is always copied out of the underlying structure meaning we can keep accessing the same channel.

While this does mirror more pythonic behaviour, when wanting to explicitly remove a channels information from the layer (to save on memory) we have to revert to the individual psapi.LayeredFile_8bit.get_channel_by_id() and psapi.LayeredFile_8bit.get_channel_by_index() methods with the do_copy parameter set to false.

import psapi
import numpy as np

layered_file = psapi.LayeredFile.read("File.psb")
layer_red = layered_file["Layer_Red"]
channel_r = layer_red[0] # 0-based indexing, raises KeyError if index is out of range
# We can also extract alpha and masks this way with index -1 and -2 respectively!
import psapi
import numpy as np

layered_file = psapi.LayeredFile.read("File.psb")
layer_red = layered_file["Layer_Red"]
channel_r = layer_red[psapi.enum.ChannelID.red] # raises KeyError if index doesnt exist

Construction of an ImageLayer class is also intended to be as easy as possible which is why we support 3 different ways of passing data into the ImageLayer,

  • construction by dict with integer indexing

  • construction by dict with ChannelID enum indexing

  • construction by np.ndarray

Check out the examples below to find out how to leverage each of these to your advantage.

This method is likely the simplest (but least explicit) way of constructing an image layer. It does make some assumptions to achieve this simplicity. One of these assumptions is that a layer (for RGB) must have at least 3, but not more than 4 channels. This is due to us taking the required channels (for RGB this would be R, G, B. For CMYK this would be C, M, Y, K etc.) and allowing for one last optional alpha channel.

So if we continue our RGB example we could have a np.ndarray with shape (3, 32, 32) which would tell the PhotoshopAPI that we are dealing with no alpha channel (white alpha). If we however specify the following shape (4, 32, 32), the 4th channel is assumed to be the alpha channel and is interpreted as such.

We support both 2D arrays (flattened image data) or 3D arrays (explicit rows and columns) which work interchangeably as long as the data is in row-major order.

import psapi
import numpy as np

document_color_mode = psapi.enum.ColorMode.rgb
width = 32
height = 32
file = psapi.LayeredFile_8bit(document_color_mode, width, height)

# This method has the image data flattened (each channel is a 1D array)
img_data_np = np.zeros((4, height * width), np.uint8)
img_data_np[0] = 255    # Set the red component
img_data_np[3] = 128    # Set the alpha component

# Construct our layer instance, width and height must be specified for this to work!
img_lr_np = psapi.ImageLayer_8bit(
        img_data_np,
        layer_name="img_lr_np",
        width=width,
        height=height,
        color_mode=document_color_mode
)

# Add to the file and write out
file.add_layer(img_lr_np)
file.write("Out.psd")
import psapi
import numpy as np

document_color_mode = psapi.enum.ColorMode.rgb
width = 32
height = 32
file = psapi.LayeredFile_8bit(document_color_mode, width, height)

# This method has the channel data as 2D array in row-major fashion
img_data_np = np.zeros((4, height, width), np.uint8)
img_data_np[0] = 255    # Set the red component
img_data_np[3] = 128    # Set the alpha component

# Construct our layer instance, width and height must be specified for this to work!
img_lr_np = psapi.ImageLayer_8bit(
        img_data_np,
        layer_name="img_lr_np",
        width=width,
        height=height,
        color_mode=document_color_mode
)

# Add to the file and write out
file.add_layer(img_lr_np)
file.write("Out.psd")

Similarly to the numpy example these can also be constructed and passed either as flattened array or 2D array by simply interchanging this np.full((height, width), 0, np.uint8) to this np.full((height * width), 0, np.uint8).

import psapi
import numpy as np

document_color_mode = psapi.enum.ColorMode.rgb
width = 32
height = 32
file = psapi.LayeredFile_8bit(document_color_mode, width, height)

# We can use logical indices for these as should be familiar from other software
image_dict = {}
image_dict[0] = np.full((height, width), 0, np.uint8)           # Red channel
image_dict[1] = np.full((height, width), 255, np.uint8)         # Green channel
image_dict[2] = np.full((height, width), 0, np.uint8)           # Blue channel
image_dict[-1] = np.full((height, width), 128, np.uint8)        # Alpha channel

# Construct our layer instance, width and height must be specified for this to work!
img_lr = psapi.ImageLayer_8bit(
        image_dict,
        layer_name="img_lr_dict",
        width=width,
        height=height,
        color_mode=document_color_mode)

# Add to the file and write out
file.add_layer(img_lr)
file.write("Out.psd")

Similarly to the numpy example these can also be constructed and passed either as flattened array or 2D array by simply interchanging this np.full((height, width), 0, np.uint8) to this np.full((height * width), 0, np.uint8).

Attention

This method of construction is unfortunately currently unsupported and will result in photoshop files that cannot be opened due to limitations in pybind11 as seen in this issue. Support for this is planned however once that is resolved so keep tuned. This message will disappear once it is fixed :)

import psapi
import numpy as np

document_color_mode = psapi.enum.ColorMode.rgb
width = 32
height = 32
file = psapi.LayeredFile_8bit(document_color_mode, width, height)

# This method is a bit more explicit about the naming of channels
image_dict = {}
image_dict[psapi.enum.ChannelID.red] = np.full((height, width), 0, np.uint8)
image_dict[psapi.enum.ChannelID.green] = np.full((height, width), 255, np.uint8)
image_dict[psapi.enum.ChannelID.blue] = np.full((height, width), 0, np.uint8)
image_dict[psapi.enum.ChannelID.alpha] = np.full((height, width), 128, np.uint8)

# Construct our layer instance, width and height must be specified for this to work!
img_lr = psapi.ImageLayer_8bit(
        image_dict,
        layer_name="img_lr_dict",
        width=width,
        height=height,
        color_mode=document_color_mode
)

# Add to the file and write out
file.add_layer(img_lr)
file.write("Out.psd")

Class Reference ImageLayer#


class psapi.ImageLayer_8bit#

This class defines a single image layer in a LayeredFile. There must be at least one of these in any given file for it to be valid

Attributes:
image_datadict[int, numpy.ndarray]

Property: A dictionary of the image data mapped by an int where the channel mapping is e.g. [R: 0, G: 1, B: 2]. Accessing this property will decompress and load the image data into memory therefore incurring a performance and memory penalty. If you only wish to get a list of all the channels use the num_channels or channels properties instead.

All channels are the same size except for the mask channel (-2) which may have any size.

namestr

The name of the layer, cannot be longer than 255

blend_modeenum.BlendMode

The blend mode of the layer, ‘Passthrough’ is reserved for group layers

opacityfloat

The layers opacity from 0.0 - 1.0

widthint

The width of the layer ranging up to 30,000 for PSD and 300,000 for PSB, this does not have to match the files width

heightint

The height of the layer ranging up to 30,000 for PSD and 300,000 for PSB, this does not have to match the files height

center_xfloat

The center of the layer in regards to the canvas, a layer at center_x = 0 is perfectly centered around the document

center_yfloat

The center of the layer in regards to the canvas, a layer at center_y = 0 is perfectly centered around the document

is_locked: bool

The locked state of the layer, this locks all pixel channels

is_visible: bool

Whether the layer is visible

mask: np.ndarray

The layers’ mask channel, may be empty

mask_disabled: bool

Whether the mask is disabled. Ignored if no mask is present

mask_relative_to_layer: bool

Whether the masks position is relative to the layer. Ignored if no mask is present

mask_default_color: int

The masks’ default color outside of the masks bounding box from 0-255. Ignored if no mask is present

mask_density: int

Optional mask density from 0-255, this is equivalent to layers’ opacity. Ignored if no mask is present

mask_feather: float

Optional mask feather. Ignored if no mask is present

mask_position: psapi.geometry.Point2D

The masks’ canvas coordinates, these represent the center of the mask in terms of the canvas (file). Ignored if no mask is present

mask_width: int

The masks’ width, this does not have to correspond with the layers’ width

mask_height: int

The masks’ height, this does not have to correspond with the layers’ height

__init__(*args, **kwargs)#

Overloaded function.

  1. __init__(self: psapi.ImageLayer_8bit, image_data: numpy.ndarray[numpy.uint8], layer_name: str, layer_mask: Optional[numpy.ndarray[numpy.uint8]] = None, width: int = 0, height: int = 0, blend_mode: psapi.enum.BlendMode = <BlendMode.normal: 1>, pos_x: float = 0, pos_y: float = 0, opacity: float = 1.0, compression: psapi.enum.Compression = <Compression.zipprediction: 3>, color_mode: psapi.enum.ColorMode = <ColorMode.rgb: 3>, is_visible: bool = True, is_locked: bool = False) -> None

    Construct an image layer from image data passed as numpy.ndarray

    param image_data:

    The image data as 2- or 3-Dimensional numpy array where the first dimension is the number of channels.

    If its a 2D ndarray the second dimension must hold the image data in row-major order with the size being height*width. An example could be the following shape: (3, 1024) for an RGB layer that is 32*32px.

    If its a 3D ndarray the second and third dimension hold height and width respectively. An example could be the following shape: (3, 32, 32) for the same RGB layer

    We also support adding alpha channels this way, those are always stored as the last channel and are optional. E.g. for RGB there could be a ndarray like this (4, 32, 32) and it would automatically identify the last channel as alpha. For the individual color modes there is always a set of required channels such as R, G and B for RGB or C, M, Y, K for CMYK with the optional alpha that can be appended to the end.

    The size must be the same as the width and height parameter

    type image_data:

    numpy.ndarray

    param layer_name:

    The name of the group, its length must not exceed 255

    type layer_name:

    str

    param layer_mask:

    Optional layer mask, must have the same dimensions as height * width but can be a 1- or 2-dimensional array with row-major ordering (for a numpy 2D array this would mean with a shape of (height, width)

    type layer_mask:

    numpy.ndarray

    param width:

    Optional, width of the layer, does not have to be the same size as the document, limited to 30,000 for PSD files and 300,000 for PSB files. For group layers this is only relevant for the layer mask and can be left out otherwise

    type width:

    int

    param height:

    Optional, height of the layer, does not have to be the same size as the document, limited to 30,000 for PSD files and 300,000 for PSB files. For group layers this is only relevant for the layer mask and can be left out otherwise

    type height:

    int

    param blend_mode:

    Optional, the blend mode of the layer, ‘Passthrough’ is the default for groups.

    type blend_mode:

    psapi.enum.BlendMode

    param pos_x:

    Optional, the relative offset of the layer to the center of the document, 0 indicates the layer is centered. For group layers this is only relevant for the layer mask and can be left out otherwise

    type pos_x:

    float

    param pos_y:

    Optional, the relative offset of the layer to the center of the document, 0 indicates the layer is centered. For group layers this is only relevant for the layer mask and can be left out otherwise

    type pos_y:

    float

    param opacity:

    The opacity of the layer from 0-1. Defaults to 1.0

    type opacity:

    float

    param compression:

    The compression to apply to all the channels of the layer, including mask channels

    type compression:

    psapi.enum.Compression

    param color_mode:

    The color mode of the Layer, this must be identical to the color mode of the document. Defaults to RGB

    type color_mode:

    psapi.enum.ColorMode

    param is_visible:

    Whether the group is visible

    type is_visible:

    bool

    param is_locked:

    Whether the group is locked

    type is_locked:

    bool

    raises:

    ValueError: if length of layer name is greater than 255

    ValueError: if size of layer mask is not width*height

    ValueError: if width of layer is negative

    ValueError: if height of layer is negative

    ValueError: if opacity is not between 0-255

    ValueError: if the channel size is not the same as width * height

  2. __init__(self: psapi.ImageLayer_8bit, image_data: Dict[int, numpy.ndarray[numpy.uint8]], layer_name: str, layer_mask: Optional[numpy.ndarray[numpy.uint8]] = None, width: int = 0, height: int = 0, blend_mode: psapi.enum.BlendMode = <BlendMode.normal: 1>, pos_x: float = 0, pos_y: float = 0, opacity: float = 1.0, compression: psapi.enum.Compression = <Compression.zipprediction: 3>, color_mode: psapi.enum.ColorMode = <ColorMode.rgb: 3>, is_visible: bool = True, is_locked: bool = False) -> None

    Construct an image layer from image data passed as dict with integers as key

    param image_data:

    The image data as a dictionary with channel indices as integers. E.g. for a RGB image layer

    data = {
        0 : numpy.ndarray,
        1 : numpy.ndarray,
        2 : numpy.ndarray
    }
    
    type image_data:

    dict[numpy.ndarray]

    param layer_name:

    The name of the group, its length must not exceed 255

    type layer_name:

    str

    param layer_mask:

    Optional layer mask, must have the same dimensions as height * width but can be a 1- or 2-dimensional array with row-major ordering (for a numpy 2D array this would mean with a shape of (height, width)

    type layer_mask:

    numpy.ndarray

    param width:

    Optional, width of the layer, does not have to be the same size as the document, limited to 30,000 for PSD files and 300,000 for PSB files. For group layers this is only relevant for the layer mask and can be left out otherwise

    type width:

    int

    param height:

    Optional, height of the layer, does not have to be the same size as the document, limited to 30,000 for PSD files and 300,000 for PSB files. For group layers this is only relevant for the layer mask and can be left out otherwise

    type height:

    int

    param blend_mode:

    Optional, the blend mode of the layer, ‘Passthrough’ is the default for groups.

    type blend_mode:

    psapi.enum.BlendMode

    param pos_x:

    Optional, the relative offset of the layer to the center of the document, 0 indicates the layer is centered. For group layers this is only relevant for the layer mask and can be left out otherwise

    type pos_x:

    float

    param pos_y:

    Optional, the relative offset of the layer to the center of the document, 0 indicates the layer is centered. For group layers this is only relevant for the layer mask and can be left out otherwise

    type pos_y:

    float

    param opacity:

    The opacity of the layer from 0-1. Defaults to 1.0

    type opacity:

    float

    param compression:

    The compression to apply to all the channels of the layer, including mask channels

    type compression:

    psapi.enum.Compression

    param color_mode:

    The color mode of the Layer, this must be identical to the color mode of the document. Defaults to RGB

    type color_mode:

    psapi.enum.ColorMode

    param is_visible:

    Whether the group is visible

    type is_visible:

    bool

    param is_locked:

    Whether the group is locked

    type is_locked:

    bool

    raises:

    ValueError: if length of layer name is greater than 255

    ValueError: if size of layer mask is not width*height

    ValueError: if width of layer is negative

    ValueError: if height of layer is negative

    ValueError: if opacity is not between 0-255

    ValueError: if the channel size is not the same as width * height

  3. __init__(self: psapi.ImageLayer_8bit, image_data: Dict[psapi.enum.ChannelID, numpy.ndarray[numpy.uint8]], layer_name: str, layer_mask: Optional[numpy.ndarray[numpy.uint8]] = None, width: int = 0, height: int = 0, blend_mode: psapi.enum.BlendMode = <BlendMode.normal: 1>, pos_x: float = 0, pos_y: float = 0, opacity: float = 1.0, compression: psapi.enum.Compression = <Compression.zipprediction: 3>, color_mode: psapi.enum.ColorMode = <ColorMode.rgb: 3>, is_visible: bool = True, is_locked: bool = False) -> None

    Construct an image layer from image data passed as dict with psapi.enum.ChannelID as key

    param image_data:

    The image data as a dictionary with channel IDs as enums. E.g. for a RGB image layer

    data = {
        psapi.enum.ChannelID.red : numpy.ndarray,
        psapi.enum.ChannelID.green : numpy.ndarray,
        psapi.enum.ChannelID.blue : numpy.ndarray
    }
    
    type image_data:

    dict[numpy.ndarray]

    param layer_name:

    The name of the group, its length must not exceed 255

    type layer_name:

    str

    param layer_mask:

    Optional layer mask, must have the same dimensions as height * width but can be a 1- or 2-dimensional array with row-major ordering (for a numpy 2D array this would mean with a shape of (height, width)

    type layer_mask:

    numpy.ndarray

    param width:

    Optional, width of the layer, does not have to be the same size as the document, limited to 30,000 for PSD files and 300,000 for PSB files. For group layers this is only relevant for the layer mask and can be left out otherwise

    type width:

    int

    param height:

    Optional, height of the layer, does not have to be the same size as the document, limited to 30,000 for PSD files and 300,000 for PSB files. For group layers this is only relevant for the layer mask and can be left out otherwise

    type height:

    int

    param blend_mode:

    Optional, the blend mode of the layer, ‘Passthrough’ is the default for groups.

    type blend_mode:

    psapi.enum.BlendMode

    param pos_x:

    Optional, the relative offset of the layer to the center of the document, 0 indicates the layer is centered. For group layers this is only relevant for the layer mask and can be left out otherwise

    type pos_x:

    float

    param pos_y:

    Optional, the relative offset of the layer to the center of the document, 0 indicates the layer is centered. For group layers this is only relevant for the layer mask and can be left out otherwise

    type pos_y:

    float

    param opacity:

    The opacity of the layer from 0-1. Defaults to 1.0

    type opacity:

    float

    param compression:

    The compression to apply to all the channels of the layer, including mask channels

    type compression:

    psapi.enum.Compression

    param color_mode:

    The color mode of the Layer, this must be identical to the color mode of the document. Defaults to RGB

    type color_mode:

    psapi.enum.ColorMode

    param is_visible:

    Whether the group is visible

    type is_visible:

    bool

    param is_locked:

    Whether the group is locked

    type is_locked:

    bool

    raises:

    ValueError: if length of layer name is greater than 255

    ValueError: if size of layer mask is not width*height

    ValueError: if width of layer is negative

    ValueError: if height of layer is negative

    ValueError: if opacity is not between 0-255

    ValueError: if the channel size is not the same as width * height

__getitem__(self: psapi.ImageLayer_8bit, key: int) numpy.ndarray[numpy.uint8]#

Get the specified channel from the image data, this may also be the mask channel at index -2. If -2 is passed this function is identical to get_mask(). The mask channel will have the shape { mask_height(), mask_width() } while any other channel will have the shape { height(), width() }.

Generally accessing each channel individually is slower than accessing all of them with get_image_data() as that function is better parallelized. So if you wish to extract more than a couple channels it is recommended to get all of them.

raises ValueError:

if the specified index does not exist on the layer

Returns:

The extracted channel

Return type:

numpy.ndarray

channel_indices(self: psapi.ImageLayer_8bit, include_mask: bool = True) List[int]#

Retrieve a list of all the channel indices.

param include_mask:

Whether to include the mask channel

get_channel_by_id(self: psapi.ImageLayer_8bit, key: psapi.enum.ChannelID) numpy.ndarray[numpy.uint8]#
Get the specified channel from the image data, this may also be the mask channel at index -2.

If -2 is passed this function is identical to get_mask(). The mask channel will have the shape { mask_height(), mask_width() } while any other channel will have the shape { height(), width() }.

Generally accessing each channel individually is slower than accessing all of them with get_image_data() as that function is better parallelized. So if you wish to extract more than a couple channels it is recommended to get all of them.

param psapi.enum.ColorMode key:

The key to access.

raises ValueError:

if the specified index does not exist on the layer

Returns:

The extracted channel

Return type:

numpy.ndarray

get_channel_by_index(self: psapi.ImageLayer_8bit, key: int) numpy.ndarray[numpy.uint8]#
Get the specified channel from the image data, this may also be the mask channel at index -2.

If -2 is passed this function is identical to get_mask(). The mask channel will have the shape { mask_height(), mask_width() } while any other channel will have the shape { height(), width() }.

Generally accessing each channel individually is slower than accessing all of them with get_image_data() as that function is better parallelized. So if you wish to extract more than a couple channels it is recommended to get all of them.

param int:

The key to access.

raises ValueError:

if the specified index does not exist on the layer

Returns:

The extracted channel

Return type:

numpy.ndarray

get_image_data(self: psapi.ImageLayer_8bit) Dict[int, numpy.ndarray[numpy.uint8]]#
Get all the channels of the layer (including masks) as a dict mapped by intnp.ndarray. This includes

any mask channel which would be found at index -2. While all non-mask channels are guaranteed to be the same size as width() * height() this does not hold true for the mask channel which would be the size of mask_width() and mask_height()

Returns:

The extracted image data

Return type:

dict[int, numpy.ndarray]

has_mask(self: psapi.Layer_8bit) bool#

Check whether the layer has an associated mask component (pixel mask)

property mask#

The layers’ pixel mask, this is a 2-dimensional array stored in format { height, width }. A pixel mask may have any dimensions and does not have to match a layers’ width or height. To get the pixel value outside of the masks’ bbox use the mask_default_color property.

property mask_default_color#

The masks’ default color outside of the masks bounding box. Ignored if no mask is present. From 0-255 regardless of bit depth

property mask_density#

Optional mask density from 0-255, this is equivalent to layers’ opacity. Ignored if no mask is present

property mask_disabled#

Whether the mask is disabled. Ignored if no mask is present

property mask_feather#

Optional mask feather. Ignored if no mask is present

mask_height(self: psapi.Layer_8bit) int#

The masks’ height in pixels. This does not always have to correspond with the layers’ height.

property mask_position#

The masks’ canvas coordinates, these represent the center of the mask in terms of the canvas (file). Ignored if no mask is present

property mask_relative_to_layer#

Whether the masks position is relative to the layer. Ignored if no mask is present

mask_width(self: psapi.Layer_8bit) int#

The masks’ width in pixels. This does not always have to correspond with the layers’ width.

num_channels(self: psapi.ImageLayer_8bit, include_mask: bool = True) int#

Retrieve the total number of channels held by the layer

param include_mask:

Whether to include the mask channel

set_channel_by_id(self: psapi.ImageLayer_8bit, key: psapi.enum.ChannelID, data: numpy.ndarray[numpy.uint8]) None#
Set/replace the channel for a layer at the provided index. This may also be the mask channel (-2).

If the provided image data does not have the shape { height, width } or { mask_height, mask_width } this function raises a ValueError.

Parameters:
  • key (psapi.enum.ChannelID) – The ID of the channel

  • value (np.ndarray) – The channel data with dimensions (height, width)

set_channel_by_index(self: psapi.ImageLayer_8bit, key: int, data: numpy.ndarray[numpy.uint8]) None#
Set/replace the channel for a layer at the provided index. This may also be the mask channel (-2).

If the provided image data does not have the shape { height, width } or { mask_height, mask_width } this function raises a ValueError.

Parameters:
  • key (int) – The index of the channel

  • value (np.ndarray) – The channel data with dimensions (height, width)

set_image_data(self: psapi.ImageLayer_8bit, data: numpy.ndarray[numpy.uint8] | Dict[int, numpy.ndarray[numpy.uint8]], width: int | None = None, height: int | None = None) None#
Set the image data of all the channels (may include mask channels), optionally passing in new dimensions that the

layer should assume (when replacing with different image data). While all channels must be identical in size, the mask channel (index -2) may be any other size and we will extract the dimensions from the 2d numpy array instead.

param data:

The image data to set onto the layer, this may be a ndarray with e.g. 3 or 4 dimensions for RGB or a dict mapping the indices directly to individual channels. For RGB there must always be the indices 0, 1, 2 to represent the R, G and B channels and the same applies to the other color modes.

type data:

np.ndarray | dict[int, numpy.ndarray]

param width:

An optional width in case the new image data does not have the same width as the layer before. If this is specified the height parameter must also be provided

type width:

Optional[int]

param height:

An optional height in case the new image data does not have the same height as the layer before. If this is specified the width parameter must also be provided

type height:

Optional[int]

set_mask_compression(self: psapi.Layer_8bit, arg0: psapi.enum.Compression) None#

Set the masks’ write compression in terms of one of the Photoshop compression codecs. The mask channel may have any compression codec applied to it and this does not need to match the layers’ compression in any way. All compression codecs are valid in the PhotoshopAPI.