Source code for pytoshop.layers

# -*- coding: utf-8 -*-


"""
Sections related to image layers.
"""


from __future__ import unicode_literals, absolute_import


from collections import OrderedDict
import os


import numpy as np


import six


from .blending_range import BlendingRanges
from . import codecs
from . import docs
from . import enums
from . import tagged_block
from . import util


from typing import Any, BinaryIO, Dict, List, Optional, Tuple, TYPE_CHECKING, Union  # NOQA
if TYPE_CHECKING:
    from . import core  # NOQA


[docs]class LayerMask(object): """ Layer mask / adjustment layer data. """ def __init__(self, top=0, # type: int left=0, # type: int bottom=0, # type: int right=0, # type: int default_color=False, # type: bool position_relative_to_layer=False, # type: bool layer_mask_disabled=False, # type: bool invert_layer_mask_when_blending=False, # type: bool user_mask_from_rendering_other_data=False, # type: bool user_mask_density=None, # type: Optional[int] user_mask_feather=None, # type: Optional[int] vector_mask_density=None, # type: Optional[int] vector_mask_feather=None, # type: Optional[int] real_flags=0, # type: int real_user_mask_background=False, # type: bool real_top=0, # type: int real_left=0, # type: int real_bottom=0, # type: int real_right=0 # type: int ): # type: (...) -> None self.top = top self.left = left self.bottom = bottom self.right = right self.default_color = default_color self.position_relative_to_layer = position_relative_to_layer self.layer_mask_disabled = layer_mask_disabled self.invert_layer_mask_when_blending = invert_layer_mask_when_blending self.user_mask_from_rendering_other_data = \ user_mask_from_rendering_other_data self.user_mask_density = user_mask_density self.user_mask_feather = user_mask_feather self.vector_mask_density = vector_mask_density self.vector_mask_feather = vector_mask_feather self.real_flags = real_flags self.real_user_mask_background = real_user_mask_background self.real_top = real_top self.real_left = real_left self.real_bottom = real_bottom self.real_right = real_right @property def top(self): # type: (...) -> int "Top of rectangle enclosing layer mask" return self._top @top.setter def top(self, value): # type: (int) -> None if (not isinstance(value, int) or value < -(1 << 31) or value > (1 << 31)): raise ValueError("top must be a 32-bit integer") self._top = value @property def left(self): # type: (...) -> int "Left of rectangle enclosing layer mask" return self._left @left.setter def left(self, value): # type: (int) -> None if (not isinstance(value, int) or value < -(1 << 31) or value > (1 << 31)): raise ValueError("left must be a 32-bit integer") self._left = value @property def bottom(self): # type: (...) -> int "Bottom of rectangle enclosing layer mask" return self._bottom @bottom.setter def bottom(self, value): # type: (int) -> None if (not isinstance(value, int) or value < -(1 << 31) or value > (1 << 31)): raise ValueError("bottom must be a 32-bit integer") self._bottom = value @property def right(self): # type: (...) -> int "Right of rectangle enclosing layer mask" return self._right @right.setter def right(self, value): # type: (int) -> None if (not isinstance(value, int) or value < -(1 << 31) or value > (1 << 31)): raise ValueError("right must be a 32-bit integer") self._right = value @property def default_color(self): # type: (...) -> bool "Default color for mask" return self._default_color @default_color.setter def default_color(self, value): # type: (Any) -> None self._default_color = bool(value) @property def position_relative_to_layer(self): # type: (...) -> bool "position relative to layer" return self._position_relative_to_layer @position_relative_to_layer.setter def position_relative_to_layer(self, value): # type: (Any) -> None self._position_relative_to_layer = bool(value) @property def layer_mask_disabled(self): # type: (...) -> bool "Layer mask disabled" return self._layer_mask_disabled @layer_mask_disabled.setter def layer_mask_disabled(self, value): # type: (Any) -> None self._layer_mask_disabled = bool(value) @property def invert_layer_mask_when_blending(self): # type: (...) -> bool "Invert layer mask when blending (obsolete)" return self._invert_layer_mask_when_blending @invert_layer_mask_when_blending.setter def invert_layer_mask_when_blending(self, value): # type: (Any) -> None self._invert_layer_mask_when_blending = bool(value) @property def user_mask_from_rendering_other_data(self): # type: (...) -> bool """ Indicates that the user mask actually came from rendering other data. """ return self._user_mask_from_rendering_other_data @user_mask_from_rendering_other_data.setter def user_mask_from_rendering_other_data(self, value): # type: (Any) -> None self._user_mask_from_rendering_other_data = bool(value) @property def user_mask_density(self): # type: (...) -> Optional[int] "User mask density" return self._user_mask_density @user_mask_density.setter def user_mask_density(self, value): # type: (Optional[int]) -> None if (value is not None and (not isinstance(value, int) or value < 0 or value > 255)): raise ValueError( "user_mask_density must be an int in range 0 to 255 or None" ) self._user_mask_density = value @property def user_mask_feather(self): # type: (...) -> Optional[int] "User mask feather" return self._user_mask_feather @user_mask_feather.setter def user_mask_feather(self, value): # type: (Optional[int]) -> None if (value is not None and (not isinstance(value, int) or value < 0 or value > 255)): raise ValueError( "user_mask_feather must be an int in range 0 to 255 or None" ) self._user_mask_feather = value @property def vector_mask_density(self): # type: (...) -> Optional[int] "Vector mask density" return self._vector_mask_density @vector_mask_density.setter def vector_mask_density(self, value): # type: (Optional[int]) -> None if (value is not None and (not isinstance(value, int) or value < 0 or value > 255)): raise ValueError( "vector_mask_density must be an int in range 0 to 255 or None" ) self._vector_mask_density = value @property def vector_mask_feather(self): # type: (...) -> Optional[int] "Vector mask feather" return self._vector_mask_feather @vector_mask_feather.setter def vector_mask_feather(self, value): # type: (Optional[int]) -> None if (value is not None and (not isinstance(value, int) or value < 0 or value > 255)): raise ValueError( "vector_mask_feather must be an int in range 0 to 255 or None" ) self._vector_mask_feather = value @property def real_flags(self): # type: (...) -> int return self._real_flags @real_flags.setter def real_flags(self, value): # type: (int) -> None if not isinstance(value, int): raise TypeError("real_flags must be an int") self._real_flags = value @property def real_user_mask_background(self): # type: (...) -> bool return self._real_user_mask_background @real_user_mask_background.setter def real_user_mask_background(self, value): # type: (Any) -> None self._real_user_mask_background = bool(value) @property def real_top(self): # type: (...) -> int return self._real_top @real_top.setter def real_top(self, value): # type: (int) -> None if (not isinstance(value, int) or value < -(1 << 31) or value > (1 << 31)): raise ValueError("real_top must be a 32-bit integer") self._real_top = value @property def real_left(self): # type: (...) -> int return self._real_left @real_left.setter def real_left(self, value): # type: (int) -> None if (not isinstance(value, int) or value < -(1 << 31) or value > (1 << 31)): raise ValueError("real_left must be a 32-bit integer") self._real_left = value @property def real_bottom(self): # type: (...) -> int return self._real_bottom @real_bottom.setter def real_bottom(self, value): # type: (int) -> None if (not isinstance(value, int) or value < -(1 << 31) or value > (1 << 31)): raise ValueError("real_bottom must be a 32-bit integer") self._real_bottom = value @property def real_right(self): # type: (...) -> int return self._real_right @real_right.setter def real_right(self, value): # type: (int) -> None if (not isinstance(value, int) or value < -(1 << 31) or value > (1 << 31)): raise ValueError("real_right must be a 32-bit integer") self._real_right = value @property def width(self): # type: (...) -> int """ Width of the mask layer. """ return self.right - self.left @property def height(self): # type: (...) -> int """ Height of the mask layer. """ return self.bottom - self.top @property def shape(self): # type: (...) -> Tuple[int, int] """ Shape of the mask layer ``(height, width)``. """ return (self.height, self.width) @property def real_width(self): # type: (...) -> int """ Real width of the mask layer. """ return self.real_right - self.real_left @property def real_height(self): # type: (...) -> int """ Real height of the mask layer. """ return self.real_bottom - self.real_top @property def real_shape(self): # type: (...) -> Tuple[int, int] """ Real shape of the mask layer ``(height, width)``. """ return (self.real_height, self.real_width)
[docs] def length(self, header): # type: (core.Header) -> int length = 16 + 1 + 1 mask_flags = self._get_mask_flags() if mask_flags: length += 1 if self.user_mask_density is not None: length += 1 if self.user_mask_feather is not None: length += 8 if self.vector_mask_density is not None: length += 1 if self.vector_mask_feather is not None: length += 8 length += 1 + 1 + 16 return length
length.__doc__ = docs.length # type: ignore
[docs] def total_length(self, header): # type: (core.Header) -> int return 4 + self.length(header)
total_length.__doc__ = docs.total_length # type: ignore def _get_mask_flags(self): # type: (...) -> int return util.pack_bitflags( self.user_mask_density is not None, self.user_mask_feather is not None, self.vector_mask_density is not None, self.vector_mask_feather is not None)
[docs] @classmethod @util.trace_read def read(cls, fd): # type: (BinaryIO) -> LayerMask length = util.read_value(fd, 'I') d = {} # type: Dict[unicode, Any] end = fd.tell() + length util.log("length: {}, end: {}", length, end) if length == 0: return cls(**d) top, left, bottom, right = util.read_value(fd, 'iiii') d['top'] = top d['left'] = left d['bottom'] = bottom d['right'] = right util.log("position: ({}, {}, {}, {})", top, left, bottom, right) d['default_color'] = bool(util.read_value(fd, 'B')) flags = util.read_value(fd, 'B') (d['position_relative_to_layer'], d['layer_mask_disabled'], d['invert_layer_mask_when_blending'], d['user_mask_from_rendering_other_data']) = util.unpack_bitflags( flags, 4) util.log("default_color: {}, flags: {}", d['default_color'], flags) if length == 20: util.log("done early") fd.seek(end) return cls(**d) if flags & 16: mask_parameters = util.read_value(fd, 'B') (has_user_mask_density, has_user_mask_feather, has_vector_mask_density, has_vector_mask_feather) = util.unpack_bitflags( mask_parameters, 4) if has_user_mask_density: d['user_mask_density'] = util.read_value(fd, 'B') if has_user_mask_feather: d['user_mask_feather'] = util.read_value(fd, 'd') if has_vector_mask_density: d['vector_mask_density'] = util.read_value(fd, 'B') if has_vector_mask_feather: d['vector_mask_feather'] = util.read_value(fd, 'd') d['real_flags'] = util.read_value(fd, 'B') d['real_user_mask_background'] = bool(util.read_value(fd, 'B')) util.log( "real_flags: {}, real_user_mask_background: {}", d['real_flags'], d['real_user_mask_background'] ) top, left, bottom, right = util.read_value(fd, 'iiii') d['real_top'] = top d['real_left'] = left d['real_bottom'] = bottom d['real_right'] = right util.log( "real position: ({}, {}, {}, {})", top, left, bottom, right ) fd.seek(end) return cls(**d)
read.__func__.__doc__ = docs.read
[docs] @util.trace_write def write(self, fd, header): # type: (BinaryIO, core.Header) -> None def write_rectangle(top, left, bottom, right): util.write_value(fd, 'iiii', top, left, bottom, right) def write_default_color(color): if color: util.write_value(fd, 'B', 255) else: util.write_value(fd, 'B', 0) util.write_value(fd, 'I', self.length(header)) write_rectangle(self.top, self.left, self.bottom, self.right) write_default_color(self.default_color) mask_flags = self._get_mask_flags() flags = util.pack_bitflags( self.position_relative_to_layer, self.layer_mask_disabled, self.invert_layer_mask_when_blending, self.user_mask_from_rendering_other_data, mask_flags != 0) util.write_value(fd, 'B', flags) if mask_flags: util.write_value(fd, 'B', mask_flags) if self.user_mask_density is not None: util.write_value(fd, 'B', self.user_mask_density) if self.user_mask_feather is not None: util.write_value(fd, 'd', self.user_mask_feather) if self.vector_mask_density is not None: util.write_value(fd, 'B', self.vector_mask_density) if self.vector_mask_feather is not None: util.write_value(fd, 'd', self.vector_mask_feather) util.write_value(fd, 'B', self.real_flags) write_default_color(self.real_user_mask_background) write_rectangle(self.real_top, self.real_left, self.real_bottom, self.real_right)
write.__doc__ = docs.write
[docs]class ChannelImageData(object): """ A single plane of channel image data. """ def __init__(self, image=None, # type: Optional[np.ndarray] fd=None, # type: Optional[BinaryIO] offset=None, # type: Optional[int] size=None, # type: Optional[int] shape=None, # type: Optional[Tuple[int, int]] depth=None, # type: Optional[int] version=None, # type: Optional[int] compression=enums.Compression.raw # type: int ): # type: (...) -> None self.compression = compression case_a = image is not None case_b = (fd is not None or offset is not None or size is not None or shape is not None or depth is not None or version is not None) if case_a and case_b: raise ValueError( "May not provide both image and other parameters") self._image = image self._fd = fd self._offset = offset self._size = size self._shape = shape self._depth = depth self._version = version @property def compression(self): # type: (...) -> int "Compression method. See `enums.Compression`." return self._compression @compression.setter def compression(self, value): # type: (int) -> None if value not in list(enums.Compression): # type: ignore raise ValueError("Invalid compression type.") self._compression = value @property def image(self): # type: (...) -> np.ndarray if self._image is not None: return self._image if (self._fd is None or self._offset is None or self._size is None or self._shape is None or self._depth is None or self._version is None): raise RuntimeError( "Inconsistent file descriptor state") tell = self._fd.tell() try: self._fd.seek(self._offset) data = self._fd.read(self._size) return codecs.decompress_image( data, self.compression, self._shape, self._depth, self._version) finally: self._fd.seek(tell) @image.setter def image(self, image): # type: (np.ndarray) -> None self._image = image @property def shape(self): # type: (...) -> Tuple[int, int] if self._image is not None: return self._image.shape if self._shape is None: raise RuntimeError("Inconsistent state") return self._shape @property def dtype(self): # type: (...) -> np.dtype if self._image is not None: return self._image.dtype if self._depth is None: raise RuntimeError("Inconsistent state") return np.dtype(codecs.color_depth_dtype_map[self._depth])
[docs] @classmethod @util.trace_read def read(cls, fd, # type: BinaryIO header, # type: core.Header shape, # type: Tuple[int, int] size # type: int ): # type: (...) -> ChannelImageData compression = util.read_value(fd, 'H') util.log("compression: {}", enums.Compression(compression)) offset = fd.tell() fd.seek(size, 1) return cls(fd=fd, offset=offset, size=size, shape=shape, depth=header.depth, version=header.version, compression=compression)
read.__func__.__doc__ = docs.read
[docs] @util.trace_write def write(self, fd, # type: BinaryIO header, # type: core.Header shape # type: Tuple[int, int] ): # type: (...) -> int start = fd.tell() util.write_value(fd, 'H', self.compression) if self._image is not None: codecs.compress_image( fd, self.image, self.compression, shape, 1, header.depth, header.version) else: if (self._fd is None or self._offset is None or self._size is None): raise RuntimeError("Inconsistent state") if header.version == self._version: tell = self._fd.tell() try: self._fd.seek(self._offset) data = self._fd.read(self._size) finally: self._fd.seek(tell) fd.write(data) else: codecs.compress_image( fd, self.image, self.compression, shape, 1, header.depth, header.version) return fd.tell() - start
write.__doc__ = docs.write
[docs]class LayerRecord(object): """ Layer record. There is one of these per logical layer in the file. """ def __init__(self, top=0, # type: int left=0, # type: int bottom=0, # type: int right=0, # type: int blend_mode=enums.BlendMode.normal, # type: bytes opacity=255, # type: int clipping=False, # type: bool transparency_protected=False, # type: bool visible=True, # type: bool pixel_data_irrelevant=False, # type: bool name='', # type: unicode channels=None, # type: Dict[int, ChannelImageData] blocks=None, # type: List[tagged_block.TaggedBlock] color_mode=None # type: Optional[int] ): # type: (...) -> None if blocks is None: blocks = [] if channels is None: channels = {} self.top = top self.left = left self.bottom = bottom self.right = right self.blend_mode = blend_mode self.opacity = opacity self.clipping = clipping self.transparency_protected = transparency_protected self.visible = visible self.pixel_data_irrelevant = pixel_data_irrelevant self.name = name self.channels = channels self.blocks = blocks self._color_mode = color_mode self._fd = None # type: Optional[BinaryIO] self._mask = None # type: Optional[LayerMask] self._mask_offset = None # type: Optional[int] self._blending_ranges = None # type: Optional[BlendingRanges] self._blending_ranges_offset = None # type: Optional[int] self._channel_data_lengths = [] # type: List[int] self._channel_ids = [] # type: List[int] @property def top(self): # type: (...) -> int "Top of rectangle enclosing layer" return self._top @top.setter def top(self, value): # type: (int) -> None if (not isinstance(value, int) or value < -(1 << 31) or value > (1 << 31)): raise ValueError("top must be a 32-bit integer") self._top = value @property def left(self): # type: (...) -> int "Left of rectangle enclosing layer" return self._left @left.setter def left(self, value): # type: (int) -> None if (not isinstance(value, int) or value < -(1 << 31) or value > (1 << 31)): raise ValueError("left must be a 32-bit integer") self._left = value @property def bottom(self): # type: (...) -> int "Bottom of rectangle enclosing layer" return self._bottom @bottom.setter def bottom(self, value): # type: (int) -> None if (not isinstance(value, int) or value < -(1 << 31) or value > (1 << 31)): raise ValueError("bottom must be a 32-bit integer") self._bottom = value @property def right(self): # type: (...) -> int "Right of rectangle enclosing layer" return self._right @right.setter def right(self, value): # type: (int) -> None if (not isinstance(value, int) or value < -(1 << 31) or value > (1 << 31)): raise ValueError("right must be a 32-bit integer") self._right = value @property def blend_mode(self): # type: (...) -> bytes "Blend mode. See `enums.BlendMode`" return self._blend_mode @blend_mode.setter def blend_mode(self, value): # type: (bytes) -> None if value not in list(enums.BlendMode): # type: ignore raise ValueError("Invalid blend mode.") self._blend_mode = value @property def opacity(self): # type: (...) -> int "Opacity. 0=transparent, 255=opaque" return self._opacity @opacity.setter def opacity(self, value): # type: (int) -> None if not isinstance(value, int) or value < 0 or value > 255: raise ValueError("opacity must be an int in range 0 to 255") self._opacity = value @property def clipping(self): # type: (...) -> bool "Clipping. False=base, True=non-base" return self._clipping @clipping.setter def clipping(self, value): # type: (Any) -> None self._clipping = bool(value) @property def transparency_protected(self): # type: (...) -> bool "Transparency protected" return self._transparency_protected @transparency_protected.setter def transparency_protected(self, value): # type: (Any) -> None self._transparency_protected = bool(value) @property def visible(self): # type: (...) -> bool "Visible" return self._visible @visible.setter def visible(self, value): # type: (Any) -> None self._visible = bool(value) @property def pixel_data_irrelevant(self): # type: (...) -> bool "Pixel data is irrelevant to appearance of document" return self._pixel_data_irrelevant @pixel_data_irrelevant.setter def pixel_data_irrelevant(self, value): # type: (Any) -> None self._pixel_data_irrelevant = bool(value) @property def name(self): # type: (...) -> unicode "Name of layer" return self._name @name.setter def name(self, value): # type: (Union[bytes, unicode]) -> None if isinstance(value, bytes): value = value.decode('ascii') if (not isinstance(value, six.text_type) or len(value) > 255): raise ValueError("name must be unicode string of length < 255") self._name = value @property def channels(self): # type: (...) -> Dict[int, ChannelImageData] """ Dictionary from `enums.ChannelId` to `ChannelImageData`. For safety against different color modes, it is better to use `get_channel` and `set_channel`. """ return self._channels @channels.setter def channels(self, value): # type: (Dict[int, ChannelImageData]) -> None if not isinstance(value, dict): raise TypeError("channels must be a dict") for key, val in value.items(): enums.ChannelId(key) if not isinstance(val, ChannelImageData): raise ValueError( "Each channel must be ChannelImageData instance") value = OrderedDict( sorted([(k, v) for (k, v) in value.items()])) self._channels = value
[docs] def get_channel(self, color): # type: (int) -> ChannelImageData """ Get a channel for a given color. Raises an error if the color space doesn't have the given color. Parameters ---------- color : enums.ColorChannel Returns ------- channel : ChannelImageData """ return util.get_channel(color, self._color_mode, self._channels)
[docs] def set_channel(self, color, channel): # type: (int, ChannelImageData) -> None """ Set a channel for a given color. Raises an error if the color space doesn't have the given color. Parameters ---------- color : enums.ColorChannel channel : ChannelImageData """ return util.set_channel( color, channel, self._color_mode, self._channels )
@property def blocks(self): # type: (...) -> List[tagged_block.TaggedBlock] """ List of `tagged_block.TaggedBlock` items with additional information about this layer. """ return self._blocks @blocks.setter def blocks(self, value): # type: (List[tagged_block.TaggedBlock]) -> None util.assert_is_list_of(value, tagged_block.TaggedBlock) self._blocks = value @property def mask(self): # type: (...) -> LayerMask if self._mask is not None: return self._mask else: if getattr(self, '_mask_offset', None): if (self._fd is None or self._mask_offset is None): raise RuntimeError("Inconsistent state") start = self._fd.tell() try: self._fd.seek(self._mask_offset) self._mask = LayerMask.read(self._fd) finally: self._fd.seek(start) del self._mask_offset return self._mask # type: ignore else: self._mask = LayerMask() return self._mask @mask.setter def mask(self, mask): # type: (LayerMask) -> None if not isinstance(mask, LayerMask): raise TypeError("Must be a LayerMask instance") self._mask = mask @property def blending_ranges(self): # type: (...) -> BlendingRanges if self._blending_ranges is not None: return self._blending_ranges else: if getattr(self, '_blending_ranges_offset', None): if (self._fd is None or self._blending_ranges_offset is None): raise RuntimeError("Internal inconsistency") start = self._fd.tell() try: self._fd.seek(self._blending_ranges_offset) self._blending_ranges = BlendingRanges.read( self._fd, len(self.channels)) finally: self._fd.seek(start) return self._blending_ranges # type: ignore else: self._blending_ranges = BlendingRanges() return self._blending_ranges @blending_ranges.setter def blending_ranges(self, blending_ranges): # type: (BlendingRanges) -> None if not isinstance(blending_ranges, BlendingRanges): raise TypeError("Must be a BlendingRanges instance") self._blending_ranges = blending_ranges @property def width(self): # type: (...) -> int """ Width of the layer. """ return self.right - self.left @property def height(self): # type: (...) -> int """ Height of the layer. """ return self.bottom - self.top @property def shape(self): # type: (...) -> Tuple[int, int] """ Shape of the layer ``(height, width)``. """ return (self.height, self.width) @property def blocks_map(self): # type: (...) -> Dict[bytes, tagged_block.TaggedBlock] """ A mapping from tagged block codes to `tagged_block.TaggedBlock` instances. This is a convenience to more easily get associated tagged blocks. """ return dict((x.code, x) for x in self.blocks)
[docs] @classmethod @util.trace_read def read(cls, fd, header): # type: (BinaryIO, core.Header) -> LayerRecord top, left, bottom, right = util.read_value(fd, 'iiii') util.log("position: ({}, {}, {}, {})", top, left, bottom, right) num_channels = util.read_value(fd, 'H') channel_ids = [] channel_data_lengths = [] if header.version == 1: fmt = 'hI' else: fmt = 'hQ' for i in range(num_channels): channel_id, data_length = util.read_value(fd, fmt) channel_ids.append(channel_id) channel_data_lengths.append(data_length) util.log( "num_channels: {}, channel_ids: {}, channel_data_lengths: {}", num_channels, channel_ids, channel_data_lengths ) (blend_mode_signature, blend_mode, opacity, clipping, flags, _, extra_length) = util.read_value(fd, '4s4sBBBBI') if blend_mode_signature != b'8BIM': raise ValueError( "Invalid blend mode signature '{}'".format( blend_mode_signature)) clipping = bool(clipping) (transparency_protected, visible, _, _, pixel_data_irrelevant) = util.unpack_bitflags(flags, 5) visible = not visible util.log( "blend_mode: {}, opacity: {}, clipping: {}, flags: {}", blend_mode, opacity, clipping, flags ) end = fd.tell() + extra_length util.log("extra_length: {}, end: {}", extra_length, end) mask_offset = fd.tell() mask_length = util.read_value(fd, 'I') fd.seek(mask_length, os.SEEK_CUR) blending_ranges_offset = fd.tell() blending_ranges_length = util.read_value(fd, 'I') fd.seek(blending_ranges_length, os.SEEK_CUR) name = util.read_pascal_string(fd, 4) util.log("name: {}", name) blocks = [] while fd.tell() < end: blocks.append( tagged_block.TaggedBlock.read(fd, header)) fd.seek(end) result = cls( top=top, left=left, bottom=bottom, right=right, blend_mode=blend_mode, opacity=opacity, clipping=clipping, transparency_protected=transparency_protected, visible=visible, pixel_data_irrelevant=pixel_data_irrelevant, name=name, blocks=blocks, color_mode=header.color_mode ) result._channel_data_lengths = channel_data_lengths result._channel_ids = channel_ids result._mask_offset = mask_offset result._blending_ranges_offset = blending_ranges_offset result._fd = fd return result
read.__func__.__doc__ = docs.read
[docs] def read_channel_data(self, fd, header): # type: (BinaryIO, core.Header) -> None """ Read the `ChannelImageData` for this layer. """ channels = \ OrderedDict() # type: OrderedDict[int, ChannelImageData] for channel_id, channel_length in zip( self._channel_ids, self._channel_data_lengths): if channel_id == enums.ChannelId.user_layer_mask: shape = self.mask.shape elif channel_id == enums.ChannelId.real_user_layer_mask: shape = self.mask.real_shape else: shape = self.shape channels[channel_id] = ChannelImageData.read( fd, header, shape, channel_length - 2) self._channels = channels
[docs] @util.trace_write def write(self, fd, header): # type: (BinaryIO, core.Header) -> None util.write_value( fd, 'iiii', self.top, self.left, self.bottom, self.right ) util.write_value(fd, 'H', len(self.channels)) self.channel_lengths_offset = fd.tell() if header.version == 1: fd.seek(6 * len(self.channels), 1) else: fd.seek(10 * len(self.channels), 1) flags = util.pack_bitflags( self.transparency_protected, not self.visible, False, True, self.pixel_data_irrelevant) extra_length = ( self.mask.total_length(header) + self.blending_ranges.total_length(header) + util.pascal_string_length(self.name, 4) + sum(x.total_length(header) for x in self.blocks) ) util.write_value( fd, '4s4sBBBBI', b'8BIM', self.blend_mode, self.opacity, int(self.clipping), flags, 0, extra_length) self.mask.write(fd, header) self.blending_ranges.write(fd, header) util.write_pascal_string(fd, self.name, 4) for block in self.blocks: block.write(fd, header)
write.__doc__ = docs.write
[docs] def write_channel_data(self, fd, header): # type: (BinaryIO, core.Header) -> None """ Write the `ChannelImageData` for this layer. """ lengths = [] for channel_id, data in self.channels.items(): if channel_id == enums.ChannelId.user_layer_mask: shape = self.mask.shape elif channel_id == enums.ChannelId.real_user_layer_mask: shape = self.mask.real_shape else: shape = self.shape lengths.append(data.write(fd, header, shape)) offset = fd.tell() fd.seek(self.channel_lengths_offset) if header.version == 1: fmt = 'hI' else: fmt = 'hQ' for channel_id, length in zip(self.channels.keys(), lengths): util.write_value(fd, fmt, channel_id, length) fd.seek(offset)
[docs]class LayerInfo(object): """ A set of `LayerRecord` instances. """ def __init__(self, layer_records=None, # type: List[LayerRecord] use_alpha_channel=False # type: bool ): # type: (...) -> None if layer_records is None: layer_records = [] self.layer_records = layer_records self.use_alpha_channel = use_alpha_channel @property def layer_records(self): # type: (...) -> List[LayerRecord] "List of `LayerRecord` instances" return self._layer_records @layer_records.setter def layer_records(self, value): # type: (List[LayerRecord]) -> None util.assert_is_list_of(value, LayerRecord) self._layer_records = value @property def use_alpha_channel(self): # type: (...) -> bool """ Indicates that the first channel contains transparency data for the merged result. """ return self._use_alpha_channel @use_alpha_channel.setter def use_alpha_channel(self, value): # type: (Any) -> None self._use_alpha_channel = bool(value)
[docs] @classmethod @util.trace_read def read(cls, fd, header): # type: (BinaryIO, core.Header) -> LayerInfo if header.version == 1: length = util.read_value(fd, 'I') else: length = util.read_value(fd, 'Q') end = fd.tell() + length util.log("length: {}, end: {}", length, end) if length > 0: layer_count = util.read_value(fd, 'h') if layer_count < 0: layer_count = abs(layer_count) use_alpha_channel = True else: use_alpha_channel = False util.log("layer_count: {}, use_alpha_channel: {}", layer_count, use_alpha_channel) layer_records = [ LayerRecord.read(fd, header) for i in range(layer_count) ] for layer in layer_records: layer.read_channel_data(fd, header) fd.seek(end) return cls( layer_records=layer_records, use_alpha_channel=use_alpha_channel) else: return cls()
read.__func__.__doc__ = docs.read
[docs] @util.trace_write def write(self, fd, header): # type: (BinaryIO, core.Header) -> None start = fd.tell() if header.version == 1: fd.seek(4, 1) else: fd.seek(8, 1) layer_count = len(self.layer_records) if layer_count == 0: return if self.use_alpha_channel: layer_count *= -1 util.write_value(fd, 'h', layer_count) for layer in self.layer_records: layer.write(fd, header) for layer in self.layer_records: layer.write_channel_data(fd, header) end = fd.tell() fd.seek(start) if header.version == 1: util.write_value(fd, 'I', end - start - 4) else: util.write_value(fd, 'Q', end - start - 8) fd.seek(end)
write.__doc__ = docs.write
[docs]class GlobalLayerMaskInfo(object): """ Global layer mask info. """ def __init__( self, overlay_color_space=b'\0' * 10, # type: bytes opacity=100, # type: int kind=enums.LayerMaskKind.use_value_stored_per_layer # type: int ): # type: (...) -> None self.overlay_color_space = overlay_color_space self.opacity = opacity self.kind = kind @property def overlay_color_space(self): # type: (...) -> bytes "Undocumented" return self._overlay_color_space @overlay_color_space.setter def overlay_color_space(self, value): # type: (bytes) -> None if not isinstance(value, bytes) or len(value) != 10: raise ValueError( "overlay_color_space must be a length 10 bytes string" ) self._overlay_color_space = value @property def opacity(self): # type: (...) -> int "Opacity. 0=transparent, 100=opaque" return self._opacity @opacity.setter def opacity(self, value): # type: (int) -> None if (not isinstance(value, int) or value < 0 or value > 100): raise ValueError("opacity must be an int in the range 0 to 100") self._opacity = value @property def kind(self): # type: (...) -> int "Layer mask kind. See `enums.LayerMaskKind`" return self._kind @kind.setter def kind(self, value): # type: (int) -> None if value not in list(enums.LayerMaskKind): # type: ignore raise ValueError("Invalid layer mask kind") self._kind = value
[docs] @classmethod @util.trace_read def read(cls, fd, header): # type: (BinaryIO, core.Header) -> GlobalLayerMaskInfo length = util.read_value(fd, 'I') end = fd.tell() + length util.log("length: {}, end: {}", length, end) if length == 0: return cls() overlay_color_space, opacity, kind = util.read_value(fd, '10sHB') util.log( "overlay_color_space: {}, opacity: {}, kind: {}", overlay_color_space, opacity, kind ) fd.seek(end) return cls( overlay_color_space=overlay_color_space, opacity=opacity, kind=kind)
read.__func__.__doc__ = docs.read
[docs] @util.trace_write def write(self, fd, header): # type: (BinaryIO, core.Header) -> None util.write_value( fd, 'I10sHB3s', 16, self.overlay_color_space, self.opacity, self.kind, b'\0\0\0' )
write.__doc__ = docs.write
[docs]class LayerAndMaskInfo(object): """ Layer and mask information section. """ def __init__( self, layer_info=None, # type: Optional[LayerInfo] global_layer_mask_info=None, # type: Optional[GlobalLayerMaskInfo] additional_layer_info=None # type: List[tagged_block.TaggedBlock] ): # type: (...) -> None if layer_info is None: layer_info = LayerInfo() if additional_layer_info is None: additional_layer_info = [] self.layer_info = layer_info self.global_layer_mask_info = global_layer_mask_info self.additional_layer_info = additional_layer_info @property def layer_info(self): # type: (...) -> LayerInfo "Layer info. See `LayerInfo`." return self._layer_info @layer_info.setter def layer_info(self, value): # type: (LayerInfo) -> None if not isinstance(value, LayerInfo): raise TypeError("layer_info must be LayerInfo instance.") self._layer_info = value @property def global_layer_mask_info(self): # type: (...) -> GlobalLayerMaskInfo "Global layer mask info. See `GlobalLayerMaskInfo`." return self._global_layer_mask_info @global_layer_mask_info.setter def global_layer_mask_info(self, value): # type: (GlobalLayerMaskInfo) -> None if value is not None and not isinstance(value, GlobalLayerMaskInfo): raise TypeError( "global_layer_mask_info must be GlobalLayerMaskInfo " "instance or None" ) self._global_layer_mask_info = value @property def additional_layer_info(self): # type: (...) -> List[tagged_block.TaggedBlock] "List of additional layer info. See `TaggedBlock`." return self._additional_layer_info @additional_layer_info.setter def additional_layer_info(self, value): # type: (List[tagged_block.TaggedBlock]) -> None util.assert_is_list_of(value, tagged_block.TaggedBlock) self._additional_layer_info = value @property def additional_layer_info_map(self): # type: (...) -> Dict[bytes, tagged_block.TaggedBlock] """ A mapping from tagged block codes to `tagged_block.TaggedBlock` instances. This is a convenience to more easily get associated tagged blocks. """ return dict((x.code, x) for x in self.additional_layer_info)
[docs] @classmethod @util.trace_read def read(cls, fd, header): # type: (BinaryIO, core.Header) -> LayerAndMaskInfo if header.version == 1: length = util.read_value(fd, 'I') else: length = util.read_value(fd, 'Q') end = fd.tell() + length util.log("length: {}, end: {}", length, end) layer_info = LayerInfo.read(fd, header) global_layer_mask_info = None additional_layer_info = [] if fd.tell() < end: global_layer_mask_info = GlobalLayerMaskInfo.read(fd, header) while fd.tell() < end: additional_layer_info.append( tagged_block.TaggedBlock.read(fd, header, 4)) return cls(layer_info=layer_info, global_layer_mask_info=global_layer_mask_info, additional_layer_info=additional_layer_info)
read.__func__.__doc__ = docs.read
[docs] @util.trace_write def write(self, fd, header): # type: (BinaryIO, core.Header) -> None start = fd.tell() if header.version == 1: fd.seek(4, 1) else: fd.seek(8, 1) self.layer_info.write(fd, header) if (self.global_layer_mask_info is not None or len(self.additional_layer_info)): if self.global_layer_mask_info is None: global_layer_mask_info = GlobalLayerMaskInfo() else: global_layer_mask_info = self.global_layer_mask_info global_layer_mask_info.write(fd, header) for layer_info in self.additional_layer_info: layer_info.write(fd, header, 4) end = fd.tell() fd.seek(start) if header.version == 1: util.write_value(fd, 'I', end - start - 4) else: util.write_value(fd, 'Q', end - start - 8) fd.seek(end)
write.__doc__ = docs.write