class FileHeader(BaseElement): """ Header section of the PSD file. Example:: from psd_tools.psd.header import FileHeader from psd_tools.constants import ColorMode header = FileHeader(channels=2, height=359, width=400, depth=8, color_mode=ColorMode.GRAYSCALE) .. py:attribute:: signature Signature: always equal to ``b'8BPS'``. .. py:attribute:: version Version number. PSD is 1, and PSB is 2. .. py:attribute:: channels The number of channels in the image, including any alpha channels. .. py:attribute:: height The height of the image in pixels. .. py:attribute:: width The width of the image in pixels. .. py:attribute:: depth The number of bits per channel. .. py:attribute:: color_mode The color mode of the file. See :py:class:`~psd_tools.constants.ColorMode` """ _FORMAT = '4sH6xHIIHH' signature = attr.ib(default=b'8BPS', type=bytes, repr=False) version = attr.ib(default=1, type=int, validator=in_((1, 2))) channels = attr.ib(default=4, type=int, validator=range_(1, 57)) height = attr.ib(default=64, type=int, validator=range_(1, 300001)) width = attr.ib(default=64, type=int, validator=range_(1, 300001)) depth = attr.ib(default=8, type=int, validator=in_((1, 8, 16, 32))) color_mode = attr.ib(default=ColorMode.RGB, converter=ColorMode, validator=in_(ColorMode)) @signature.validator def _validate_signature(self, attribute, value): if value != b'8BPS': raise ValueError('This is not a PSD or PSB file') @classmethod def read(cls, fp): """Read the element from a file-like object. :param fp: file-like object :rtype: FileHeader """ return cls(*read_fmt(cls._FORMAT, fp)) def write(self, fp): """Write the element to a file-like object. :param fp: file-like object """ return write_fmt(fp, self._FORMAT, *attr.astuple(self))
class LayerRecord(BaseElement): """ Layer record. .. py:attribute:: top Top position. .. py:attribute:: left Left position. .. py:attribute:: bottom Bottom position. .. py:attribute:: right Right position. .. py:attribute:: channel_info List of :py:class:`.ChannelInfo`. .. py:attribute:: signature Blend mode signature ``b'8BIM'``. .. py:attribute:: blend_mode Blend mode key. See :py:class:`~psd_tools.constants.BlendMode`. .. py:attribute:: opacity Opacity, 0 = transparent, 255 = opaque. .. py:attribute:: clipping Clipping, 0 = base, 1 = non-base. See :py:class:`~psd_tools.constants.Clipping`. .. py:attribute:: flags See :py:class:`.LayerFlags`. .. py:attribute:: mask_data :py:class:`.MaskData` or None. .. py:attribute:: blending_ranges See :py:class:`~psd_tools.constants.LayerBlendingRanges`. .. py:attribute:: name Layer name. .. py:attribute:: tagged_blocks See :py:class:`.TaggedBlocks`. """ top = attr.ib(default=0, type=int) left = attr.ib(default=0, type=int) bottom = attr.ib(default=0, type=int) right = attr.ib(default=0, type=int) channel_info = attr.ib(factory=list) signature = attr.ib(default=b'8BIM', repr=False, type=bytes, validator=in_((b'8BIM',))) blend_mode = attr.ib(default=BlendMode.NORMAL, converter=BlendMode, validator=in_(BlendMode)) opacity = attr.ib(default=255, type=int, validator=range_(0, 255)) clipping = attr.ib(default=Clipping.BASE, converter=Clipping, validator=in_(Clipping)) flags = attr.ib(factory=LayerFlags) mask_data = attr.ib(default=None) blending_ranges = attr.ib(factory=LayerBlendingRanges) name = attr.ib(default='', type=str) tagged_blocks = attr.ib(factory=TaggedBlocks) @classmethod def read(cls, fp, encoding='macroman', version=1): """Read the element from a file-like object. :param fp: file-like object :param encoding: encoding of the string :param version: psd file version :rtype: :py:class:`.LayerRecord` """ start_pos = fp.tell() top, left, bottom, right, num_channels = read_fmt('4iH', fp) channel_info = [ ChannelInfo.read(fp, version) for i in range(num_channels) ] signature, blend_mode, opacity, clipping = read_fmt('4s4sBB', fp) flags = LayerFlags.read(fp) data = read_length_block(fp, fmt='xI') logger.debug(' read layer record, len=%d' % (fp.tell() - start_pos)) with io.BytesIO(data) as f: self = cls( top, left, bottom, right, channel_info, signature, blend_mode, opacity, clipping, flags, *cls._read_extra(f, encoding, version) ) # with io.BytesIO() as f: # self._write_extra(f, encoding, version) # assert data == f.getvalue() return self @classmethod def _read_extra(cls, fp, encoding, version): mask_data = MaskData.read(fp) blending_ranges = LayerBlendingRanges.read(fp) name = read_pascal_string(fp, encoding, padding=4) tagged_blocks = TaggedBlocks.read(fp, version=version, padding=1) return mask_data, blending_ranges, name, tagged_blocks def write(self, fp, encoding='macroman', version=1): """Write the element to a file-like object. :param fp: file-like object :param encoding: encoding of the string :param version: psd file version """ start_pos = fp.tell() written = write_fmt(fp, '4iH', self.top, self.left, self.bottom, self.right, len(self.channel_info)) written += sum(c.write(fp, version) for c in self.channel_info) written += write_fmt( fp, '4s4sBB', self.signature, self.blend_mode.value, self.opacity, self.clipping.value ) written += self.flags.write(fp) def writer(f): written = self._write_extra(f, encoding, version) logger.debug(' wrote layer record, len=%d' % ( fp.tell() - start_pos )) return written written += write_length_block(fp, writer, fmt='xI') return written def _write_extra(self, fp, encoding, version): written = 0 if self.mask_data: written += self.mask_data.write(fp) else: written += write_fmt(fp, 'I', 0) written += self.blending_ranges.write(fp) written += write_pascal_string(fp, self.name, encoding, padding=4) written += self.tagged_blocks.write(fp, version, padding=1) written += write_padding(fp, written, 2) return written @property def width(self): """Width of the layer.""" return max(self.right - self.left, 0) @property def height(self): """Height of the layer.""" return max(self.bottom - self.top, 0) @property def channel_sizes(self): """List of channel sizes: [(width, height)]. """ sizes = [] for channel in self.channel_info: if channel.id == ChannelID.USER_LAYER_MASK: sizes.append((self.mask_data.width, self.mask_data.height)) elif channel.id == ChannelID.REAL_USER_LAYER_MASK: sizes.append((self.mask_data.real_width, self.mask_data.real_height)) else: sizes.append((self.width, self.height)) return sizes
class LinkedLayer(BaseElement): """ LinkedLayer structure. .. py:attribute:: kind .. py:attribute:: version .. py:attribute:: uuid .. py:attribute:: filename .. py:attribute:: filetype .. py:attribute:: creator .. py:attribute:: filesize .. py:attribute:: open_file .. py:attribute:: linked_file .. py:attribute:: timestamp .. py:attribute:: data .. py:attribute:: child_id .. py:attribute:: mod_time .. py:attribute:: lock_state """ kind = attr.ib(default=LinkedLayerType.ALIAS, validator=in_(LinkedLayerType)) version = attr.ib(default=1, validator=range_(1, 7)) uuid = attr.ib(default='', type=str) filename = attr.ib(default='', type=str) filetype = attr.ib(default=b'\x00\x00\x00\x00', type=bytes) creator = attr.ib(default=b'\x00\x00\x00\x00', type=bytes) filesize = attr.ib(default=None) open_file = attr.ib(default=None) linked_file = attr.ib(default=None) timestamp = attr.ib(default=None) data = attr.ib(default=None) child_id = attr.ib(default=None) mod_time = attr.ib(default=None) lock_state = attr.ib(default=None) @classmethod def read(cls, fp, **kwargs): kind = LinkedLayerType(read_fmt('4s', fp)[0]) version = read_fmt('I', fp)[0] assert 1 <= version and version <= 7, 'Invalid version %d' % (version) uuid = read_pascal_string(fp, 'macroman', padding=1) filename = read_unicode_string(fp) filetype, creator, datasize, open_file = read_fmt('4s4sQB', fp) if open_file: open_file = DescriptorBlock.read(fp, padding=1) else: open_file = None linked_file = None timestamp = None data = None filesize = None child_id = None mod_time = None lock_state = None if kind == LinkedLayerType.EXTERNAL: linked_file = DescriptorBlock.read(fp, padding=1) if version > 3: timestamp = read_fmt('I4Bd', fp) filesize = read_fmt('Q', fp)[0] # External file size. if version > 2: data = fp.read(datasize) elif kind == LinkedLayerType.ALIAS: read_fmt('8x', fp) if kind == LinkedLayerType.DATA: data = fp.read(datasize) assert len(data) == datasize, '(%d vs %d)' % (len(data), datasize) # The followings are not well documented... if version >= 5: child_id = read_unicode_string(fp) if version >= 6: mod_time = read_fmt('d', fp)[0] if version >= 7: lock_state = read_fmt('B', fp)[0] if kind == LinkedLayerType.EXTERNAL and version == 2: data = fp.read(datasize) return cls(kind, version, uuid, filename, filetype, creator, filesize, open_file, linked_file, timestamp, data, child_id, mod_time, lock_state) def write(self, fp, padding=1, **kwargs): written = write_fmt(fp, '4sI', self.kind.value, self.version) written += write_pascal_string(fp, self.uuid, 'macroman', padding=1) written += write_unicode_string(fp, self.filename) written += write_fmt(fp, '4s4sQB', self.filetype, self.creator, len(self.data) if self.data is not None else 0, self.open_file is not None) if self.open_file is not None: written += self.open_file.write(fp, padding=1) if self.kind == LinkedLayerType.EXTERNAL: written += self.linked_file.write(fp, padding=1) if self.version > 3: written += write_fmt(fp, 'I4Bd', *self.timestamp) written += write_fmt(fp, 'Q', self.filesize) if self.version > 2: written += write_bytes(fp, self.data) elif self.kind == LinkedLayerType.ALIAS: written += write_fmt(fp, '8x') if self.kind == LinkedLayerType.DATA: written += write_bytes(fp, self.data) if self.child_id is not None: written += write_unicode_string(fp, self.child_id) if self.mod_time is not None: written += write_fmt(fp, 'd', self.mod_time) if self.lock_state is not None: written += write_fmt(fp, 'B', self.lock_state) if self.kind == LinkedLayerType.EXTERNAL and self.version == 2: written += write_bytes(fp, self.data) written += write_padding(fp, written, padding) return written