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.
__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
__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
__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 channelvalue (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.