class TypeToolObjectSetting(BaseElement): """ TypeToolObjectSetting structure. .. py:attribute:: version .. py:attribute:: transform Tuple of affine transform parameters (xx, xy, yx, yy, tx, ty). .. py:attribute:: text_version .. py:attribute:: text_data .. py:attribute:: warp_version .. py:attribute:: warp .. py:attribute:: left .. py:attribute:: top .. py:attribute:: right .. py:attribute:: bottom """ version = attr.ib(default=1, type=int) transform = attr.ib(default=(0.,) * 6, type=tuple) text_version = attr.ib(default=1, type=int, validator=in_((50,))) text_data = attr.ib(default=None, type=DescriptorBlock) warp_version = attr.ib(default=1, type=int, validator=in_((1,))) warp = attr.ib(default=None, type=DescriptorBlock) left = attr.ib(default=0, type=int) top = attr.ib(default=0, type=int) right = attr.ib(default=0, type=int) bottom = attr.ib(default=0, type=int) @classmethod def read(cls, fp, **kwargs): version = read_fmt('H', fp)[0] transform = read_fmt('6d', fp) text_version = read_fmt('H', fp)[0] text_data = DescriptorBlock.read(fp) # Engine data. if b'EngineData' in text_data: try: engine_data = text_data[b'EngineData'].value engine_data = EngineData.frombytes(engine_data) text_data[b'EngineData'].value = engine_data except: logger.warning('Failed to read engine data') warp_version = read_fmt('H', fp)[0] warp = DescriptorBlock.read(fp) left, top, right, bottom = read_fmt("4i", fp) return cls(version, transform, text_version, text_data, warp_version, warp, left, top, right, bottom) def write(self, fp, padding=4, **kwargs): written = write_fmt(fp, 'H6d', self.version, *self.transform) written += write_fmt(fp, 'H', self.text_version) written += self.text_data.write(fp, padding=1) written += write_fmt(fp, 'H', self.warp_version) written += self.warp.write(fp, padding=1) written += write_fmt( fp, '4i', self.left, self.top, self.right, self.bottom ) written += write_padding(fp, written, padding) return written
class PlacedLayerData(BaseElement): """ PlacedLayerData structure. """ kind = attr.ib(default=b'plcL', type=bytes) version = attr.ib(default=3, type=int, validator=in_((3, ))) uuid = attr.ib(default='', type=bytes) page = attr.ib(default=0, type=int) total_pages = attr.ib(default=0, type=int) anti_alias = attr.ib(default=0, type=int) layer_type = attr.ib(default=PlacedLayerType.UNKNOWN, converter=PlacedLayerType, validator=in_(PlacedLayerType)) transform = attr.ib(default=(0., ) * 8, type=tuple) warp = attr.ib(default=None) @classmethod def read(cls, fp, **kwargs): kind, version = read_fmt('4sI', fp) uuid = read_pascal_string(fp, 'macroman', padding=1) page, total_pages, anti_alias, layer_type = read_fmt('4I', fp) transform = read_fmt('8d', fp) warp = DescriptorBlock2.read(fp, padding=1) return cls(kind, version, uuid, page, total_pages, anti_alias, layer_type, transform, warp) def write(self, fp, padding=4, **kwargs): written = write_fmt(fp, '4sI', self.kind, self.version) written += write_pascal_string(fp, self.uuid, 'macroman', padding=1) written += write_fmt(fp, '4I', self.page, self.total_pages, self.anti_alias, self.layer_type.value) written += write_fmt(fp, '8d', *self.transform) written += self.warp.write(fp, padding=1) written += write_padding(fp, written, padding) return written
class Annotation(BaseElement): """ Annotation structure. .. py:attribute:: kind .. py:attribute:: is_open """ kind = attr.ib( default=b'txtA', type=bytes, validator=in_((b'txtA', b'sndM')) ) is_open = attr.ib(default=0, type=int) flags = attr.ib(default=0, type=int) optional_blocks = attr.ib(default=1, type=int) icon_location = attr.ib(factory=lambda: [0, 0, 0, 0], converter=list) popup_location = attr.ib(factory=lambda: [0, 0, 0, 0], converter=list) color = attr.ib(factory=Color) author = attr.ib(default='', type=str) name = attr.ib(default='', type=str) mod_date = attr.ib(default='', type=str) marker = attr.ib( default=b'txtC', type=bytes, validator=in_((b'txtC', b'sndM')) ) data = attr.ib(default=b'', type=bytes) @classmethod def read(cls, fp, **kwargs): kind, is_open, flags, optional_blocks = read_fmt('4s2BH', fp) icon_location = read_fmt('4i', fp) popup_location = read_fmt('4i', fp) color = Color.read(fp) author = read_pascal_string(fp, 'macroman', padding=2) name = read_pascal_string(fp, 'macroman', padding=2) mod_date = read_pascal_string(fp, 'macroman', padding=2) length, marker = read_fmt('I4s', fp) data = read_length_block(fp) return cls( kind, is_open, flags, optional_blocks, icon_location, popup_location, color, author, name, mod_date, marker, data ) def write(self, fp, **kwargs): written = write_fmt( fp, '4s2BH', self.kind, self.is_open, self.flags, self.optional_blocks ) written += write_fmt(fp, '4i', *self.icon_location) written += write_fmt(fp, '4i', *self.popup_location) written += self.color.write(fp) written += write_pascal_string(fp, self.author, 'macroman', padding=2) written += write_pascal_string(fp, self.name, 'macroman', padding=2) written += write_pascal_string( fp, self.mod_date, 'macroman', padding=2 ) written += write_fmt(fp, 'I4s', len(self.data) + 12, self.marker) written += write_length_block(fp, lambda f: write_bytes(f, self.data)) return written
class UnitFloat(NumericElement): """ Unit float structure. .. py:attribute:: unit unit of the value in :py:class:`Unit` .. py:attribute:: value `float` value """ value = attr.ib(default=0.0, type=float) unit = attr.ib(default=Unit._None, converter=Unit, validator=in_(Unit)) @classmethod def read(cls, fp): unit, value = read_fmt('4sd', fp) return cls(unit=Unit(unit), value=value) def write(self, fp): return write_fmt(fp, '4sd', self.unit.value, self.value) def _repr_pretty_(self, p, cycle): if cycle: return self.__repr__() p.pretty(self.value) p.text(' ') p.text(self.unit.name)
class ChannelInfo(BaseElement): """ Channel information. .. py:attribute:: id Channel ID: 0 = red, 1 = green, etc.; -1 = transparency mask; -2 = user supplied layer mask, -3 real user supplied layer mask (when both a user mask and a vector mask are present). See :py:class:`~psd_tools.constants.ChannelID`. .. py:attribute:: length Length of the corresponding channel data. """ id = attr.ib( default=ChannelID.CHANNEL_0, converter=ChannelID, validator=in_(ChannelID) ) length = attr.ib(default=0, type=int) @classmethod def read(cls, fp, version=1): return cls(*read_fmt(('hI', 'hQ')[version - 1], fp)) def write(self, fp, version=1): return write_fmt(fp, ('hI', 'hQ')[version - 1], *attr.astuple(self))
class UnitFloats(BaseElement): """ Unit floats structure. .. py:attribute:: unit unit of the value in :py:class:`Unit` .. py:attribute:: values List of `float` values """ unit = attr.ib(default=Unit._None, converter=Unit, validator=in_(Unit)) values = attr.ib(factory=list) @classmethod def read(cls, fp): unit, count = read_fmt('4sI', fp) values = list(read_fmt('%dd' % count, fp)) return cls(unit, values) def write(self, fp): return write_fmt(fp, '4sI%dd' % len(self.values), self.unit.value, len(self.values), *self.values) def __iter__(self): for value in self.values: yield value def __getitem__(self, index): return self.values[index] def __len__(self): return len(self.values)
class PrintScale(BaseElement): """ Print scale structure. .. py:attribute:: style .. py:attribute:: x .. py:attribute:: y .. py:attribute:: scale """ style = attr.ib( default=PrintScaleStyle.CENTERED, converter=PrintScaleStyle, validator=in_(PrintScaleStyle) ) x = attr.ib(default=0., type=float) y = attr.ib(default=0., type=float) scale = attr.ib(default=0., type=float) @classmethod def read(cls, fp, **kwargs): return cls(*read_fmt('H3f', fp)) def write(self, fp, **kwargs): return write_fmt( fp, 'H3f', self.style.value, self.x, self.y, self.scale )
class UnitFloat(NumericElement): """ Unit float structure. .. py:attribute:: unit .. py:attribute:: value """ value = attr.ib(default=0.0, type=float) unit = attr.ib(default=UnitFloatType.NONE, converter=UnitFloatType, validator=in_(UnitFloatType)) @classmethod def read(cls, fp): """Read the element from a file-like object. :param fp: file-like object """ unit, value = read_fmt('4sd', fp) return cls(unit=UnitFloatType(unit), value=value) def write(self, fp): """Write the element to a file-like object. :param fp: file-like object """ return write_fmt(fp, '4sd', self.unit.value, self.value) def _repr_pretty_(self, p, cycle): if cycle: return self.__repr__() p.pretty(self.value) p.text(' ') p.text(self.unit.name)
class ChannelMixer(BaseElement): """ ChannelMixer structure. .. py:attribute:: version .. py:attribute:: monochrome .. py:attribute:: data """ version = attr.ib(default=1, type=int, validator=in_((1,))) monochrome = attr.ib(default=0, type=int) data = attr.ib(factory=list, converter=list) unknown = attr.ib(default=b'', type=bytes, repr=False) @classmethod def read(cls, fp, **kwargs): version, monochrome = read_fmt('2H', fp) data = list(read_fmt('5h', fp)) unknown = fp.read() return cls(version, monochrome, data, unknown) def write(self, fp, **kwargs): written = write_fmt(fp, '2H', self.version, self.monochrome) written += write_fmt(fp, '5h', *self.data) written += write_bytes(fp, self.unknown) return written
class GlobalLayerMaskInfo(BaseElement): """ Global mask information. .. py:attribute:: overlay_color Overlay color space (undocumented) and color components. .. py:attribute:: opacity Opacity. 0 = transparent, 100 = opaque. .. py:attribute:: kind Kind. 0 = Color selected--i.e. inverted; 1 = Color protected; 128 = use value stored per layer. This value is preferred. The others are for backward compatibility with beta versions. """ overlay_color = attr.ib(default=None) opacity = attr.ib(default=0, type=int) kind = attr.ib(default=GlobalLayerMaskKind.PER_LAYER, converter=GlobalLayerMaskKind, validator=in_(GlobalLayerMaskKind)) @classmethod def read(cls, fp): pos = fp.tell() data = read_length_block(fp) # fmt? logger.debug('reading global layer mask info, len=%d' % (len(data))) if len(data) == 0: return cls(overlay_color=None) elif len(data) < 13: logger.warning( 'global layer mask info is broken, expected 13 bytes but found ' 'only %d' % (len(data))) fp.seek(pos) return cls(overlay_color=None) with io.BytesIO(data) as f: return cls._read_body(f) @classmethod def _read_body(cls, fp): overlay_color = list(read_fmt('5H', fp)) opacity, kind = read_fmt('HB', fp) return cls(overlay_color, opacity, kind) def write(self, fp): return write_length_block(fp, lambda f: self._write_body(f)) def _write_body(self, fp): written = 0 if self.overlay_color is not None: written = write_fmt(fp, '5H', *self.overlay_color) written += write_fmt(fp, 'HB', self.opacity, self.kind.value) written += write_padding(fp, written, 4) logger.debug('writing global layer mask info, len=%d' % (written)) return written
class SectionDividerSetting(BaseElement): """ SectionDividerSetting structure. .. py:attribute:: kind .. py:attribute:: key .. py:attribute:: sub_type """ kind = attr.ib(default=SectionDivider.OTHER, converter=SectionDivider, validator=in_(SectionDivider)) signature = attr.ib(default=None, repr=False) key = attr.ib(default=None) sub_type = attr.ib(default=None) @classmethod def read(cls, fp, **kwargs): kind = SectionDivider(read_fmt('I', fp)[0]) signature, key = None, None if is_readable(fp, 8): signature = read_fmt('4s', fp)[0] assert signature == b'8BIM', 'Invalid signature %r' % signature key = BlendMode(read_fmt('4s', fp)[0]) sub_type = None if is_readable(fp, 4): sub_type = read_fmt('I', fp)[0] return cls(kind, signature=signature, key=key, sub_type=sub_type) def write(self, fp, **kwargs): written = write_fmt(fp, 'I', self.kind.value) if self.signature and self.key: written += write_fmt(fp, '4s4s', self.signature, self.key.value) if self.sub_type is not None: written += write_fmt(fp, 'I', self.sub_type) return written
class ShadowInfo(BaseElement): """ Effects layer shadow info. .. py:attribute:: version .. py:attribute:: blur .. py:attribute:: intensity .. py:attribute:: angle .. py:attribute:: distance .. py:attribute:: color .. py:attribute:: blend_mode .. py:attribute:: enabled .. py:attribute:: use_global_angle .. py:attribute:: opacity .. py:attribute:: native_color """ version = attr.ib(default=0, type=int) blur = attr.ib(default=0, type=int) intensity = attr.ib(default=0, type=int) angle = attr.ib(default=0, type=int) distance = attr.ib(default=0, type=int) color = attr.ib(factory=Color) blend_mode = attr.ib( default=BlendMode.NORMAL, converter=BlendMode, validator=in_(BlendMode) ) enabled = attr.ib(default=0, type=int) use_global_angle = attr.ib(default=0, type=int) opacity = attr.ib(default=0, type=int) native_color = attr.ib(factory=Color) @classmethod def read(cls, fp): # TODO: Check 4-byte = 2-byte int + 2-byte fraction? version, blur, intensity, angle, distance = read_fmt('IIIiI', fp) color = Color.read(fp) signature = read_fmt('4s', fp)[0] assert signature == b'8BIM', 'Invalid signature %r' % (signature) blend_mode = BlendMode(read_fmt('4s', fp)[0]) enabled, use_global_angle, opacity = read_fmt('3B', fp) native_color = Color.read(fp) return cls( version, blur, intensity, angle, distance, color, blend_mode, enabled, use_global_angle, opacity, native_color ) def write(self, fp): written = write_fmt( fp, 'IIIiI', self.version, self.blur, self.intensity, self.angle, self.distance ) written += self.color.write(fp) written += write_fmt( fp, '4s4s3B', b'8BIM', self.blend_mode.value, self.enabled, self.use_global_angle, self.opacity ) written += self.native_color.write(fp) return written
class Pattern(BaseElement): """ Pattern structure. .. py:attribute:: version .. py:attribute:: image_mode .. py:attribute:: point .. py:attribute:: name .. py:attribute:: pattern_id .. py:attribute:: color_table .. py:attribute:: data """ version = attr.ib(default=1, type=int) image_mode = attr.ib(default=ColorMode, converter=ColorMode, validator=in_(ColorMode)) point = attr.ib(default=None) name = attr.ib(default='', type=str) pattern_id = attr.ib(default='', type=str) color_table = attr.ib(default=None) data = attr.ib(default=None) @classmethod def read(cls, fp, **kwargs): version = read_fmt('I', fp)[0] assert version == 1, 'Invalid version %d' % (version) image_mode = ColorMode(read_fmt('I', fp)[0]) point = read_fmt('2h', fp) name = read_unicode_string(fp) pattern_id = read_pascal_string(fp, encoding='ascii', padding=1) color_table = None if image_mode == ColorMode.INDEXED: color_table = [read_fmt("3B", fp) for i in range(256)] read_fmt('4x', fp) data = VirtualMemoryArrayList.read(fp) return cls(version, image_mode, point, name, pattern_id, color_table, data) def write(self, fp, **kwargs): written = write_fmt(fp, '2I', self.version, self.image_mode.value) written += write_fmt(fp, '2h', *self.point) written += write_unicode_string(fp, self.name) written += write_pascal_string(fp, self.pattern_id, encoding='ascii', padding=1) if self.color_table: for row in self.color_table: written += write_fmt(fp, '3B', *row) written += write_fmt(fp, '4x') written += self.data.write(fp) return written
class SmartObjectLayerData(BaseElement): """ VersionedDescriptorBlock structure. .. py:attribute:: kind .. py:attribute:: version .. py:attribute:: data """ kind = attr.ib(default=b'soLD', type=bytes, validator=in_((b'soLD', ))) version = attr.ib(default=5, type=int, validator=in_((4, 5))) data = attr.ib(default=None, type=DescriptorBlock) @classmethod def read(cls, fp, **kwargs): kind, version = read_fmt('4sI', fp) data = DescriptorBlock.read(fp) return cls(kind, version, data) def write(self, fp, padding=4, **kwargs): written = write_fmt(fp, '4sI', self.kind, self.version) written += self.data.write(fp, padding=1) written += write_padding(fp, written, padding) return written
class InnerGlowInfo(BaseElement, _GlowInfo): """ Effects layer inner glow info. .. py:attribute:: version .. py:attribute:: blur .. py:attribute:: intensity .. py:attribute:: color .. py:attribute:: blend_mode .. py:attribute:: enabled .. py:attribute:: opacity .. py:attribute:: invert .. py:attribute:: native_color """ version = attr.ib(default=0, type=int) blur = attr.ib(default=0, type=int) intensity = attr.ib(default=0, type=int) color = attr.ib(factory=Color) blend_mode = attr.ib( default=BlendMode.NORMAL, converter=BlendMode, validator=in_(BlendMode) ) enabled = attr.ib(default=0, type=int) opacity = attr.ib(default=0, type=int) invert = attr.ib(default=None) native_color = attr.ib(default=None) @classmethod def read(cls, fp): version, blur, intensity, color, blend_mode, enabled, opacity = ( cls._read_body(fp) ) invert, native_color = None, None if version >= 2: invert = read_fmt('B', fp)[0] native_color = Color.read(fp) return cls( version, blur, intensity, color, blend_mode, enabled, opacity, invert, native_color ) def write(self, fp): written = self._write_body(fp) if self.version >= 2: written += write_fmt(fp, 'B', self.invert) written += self.native_color.write(fp) return written
class Levels(ListElement): """ List of level records. See :py:class: `~psd_tools.psd.adjustments.LevelRecord`. .. py:attribute:: version Version. .. py:attribute:: extra_version Version of the extra field. """ version = attr.ib(default=0, type=int, validator=in_((2,))) extra_version = attr.ib(default=None) @classmethod def read(cls, fp, **kwargs): version = read_fmt('H', fp)[0] assert version == 2, 'Invalid version %d' % (version) items = [LevelRecord.read(fp) for _ in range(29)] extra_version = None if is_readable(fp, 6): signature, extra_version = read_fmt('4sH', fp) assert signature == b'Lvls', 'Invalid signature %r' % (signature) assert extra_version == 3, 'Invalid extra version %d' % ( extra_version ) count = read_fmt('H', fp)[0] items += [LevelRecord.read(fp) for _ in range(count - 29)] return cls(version=version, extra_version=extra_version, items=items) def write(self, fp, **kwargs): written = write_fmt(fp, 'H', self.version) for index in range(29): written += self[index].write(fp) if self.extra_version is not None: written += write_fmt(fp, '4sH', b'Lvls', self.extra_version) written += write_fmt(fp, 'H', len(self)) for index in range(29, len(self)): written += self[index].write(fp) written += write_padding(fp, written, 4) return written
class PhotoFilter(BaseElement): """ PhotoFilter structure. .. py:attribute:: version .. py:attribute:: xyz .. py:attribute:: color_space .. py:attribute:: color_components .. py:attribute:: density .. py:attribute:: luminosity """ version = attr.ib(default=0, type=int, validator=in_((2, 3))) xyz = attr.ib(default=(0, 0, 0), type=tuple) color_space = attr.ib(default=None) color_components = attr.ib(default=None) density = attr.ib(default=None) luminosity = attr.ib(default=None) @classmethod def read(cls, fp, **kwargs): version = read_fmt('H', fp)[0] assert version in (2, 3), 'Invalid version %d' % (version) if version == 3: xyz = read_fmt('3I', fp) color_space = None color_components = None else: xyz = None color_space = read_fmt('H', fp)[0] color_components = read_fmt('4H', fp) density, luminosity = read_fmt('IB', fp) return cls( version, xyz, color_space, color_components, density, luminosity ) def write(self, fp, **kwargs): written = write_fmt(fp, 'H', self.version) if self.version == 3: written += write_fmt(fp, '3I', *self.xyz) else: written += write_fmt( fp, 'H4H', self.color_space, *self.color_components ) written += write_fmt(fp, 'IB', self.density, self.luminosity) written += write_padding(fp, written, 4) return written
class MetadataSetting(BaseElement): """ MetadataSetting structure. """ _KNOWN_KEYS = {b'cust', b'cmls', b'extn', b'mlst', b'tmln'} signature = attr.ib(default=b'8BIM', type=bytes, repr=False, validator=in_((b'8BIM', ))) key = attr.ib(default=b'', type=bytes) copy_on_sheet = attr.ib(default=False, type=bool) data = attr.ib(default=b'', type=bytes) @classmethod def read(cls, fp, **kwargs): signature = read_fmt('4s', fp)[0] assert signature == b'8BIM', 'Invalid signature %r' % signature key, copy_on_sheet = read_fmt("4s?3x", fp) data = read_length_block(fp) if key == b'mdyn': with io.BytesIO(data) as f: data = read_fmt('I', f)[0] elif key in cls._KNOWN_KEYS: data = DescriptorBlock.frombytes(data, padding=4) else: message = 'Unknown metadata key %r' % (key) logger.warning(message) warn(message) data = data return cls(signature, key, copy_on_sheet, data) def write(self, fp, **kwargs): written = write_fmt(fp, '4s4s?3x', self.signature, self.key, self.copy_on_sheet) def writer(f): if hasattr(self.data, 'write'): return self.data.write(f, padding=4) elif isinstance(self.data, int): return write_fmt(fp, 'I', self.data) return write_bytes(f, self.data) written += write_length_block(fp, writer) return written
class DescriptorBlock(Descriptor): """ Dict-like Descriptor-based structure that has `version` field. See :py:class:`~psd_tools.psd.descriptor.Descriptor`. .. py:attribute:: version """ version = attr.ib(default=16, type=int, validator=in_((16, ))) @classmethod def read(cls, fp, **kwargs): version = read_fmt('I', fp)[0] return cls(version=version, **cls._read_body(fp)) def write(self, fp, padding=4, **kwargs): written = write_fmt(fp, 'I', self.version) written += self._write_body(fp) written += write_padding(fp, written, padding) return written
class CurvesExtraMarker(ListElement): """ Curves extra marker structure. .. py:attribute:: version """ version = attr.ib(default=4, type=int, validator=in_((3, 4))) @classmethod def read(cls, fp, **kwargs): signature, version, count = read_fmt('4sHI', fp) assert signature == b'Crv ', 'Invalid signature %r' % (signature) items = [] for i in range(count): items.append(CurvesExtraItem.read(fp, **kwargs)) return cls(version=version, items=items) def write(self, fp, **kwargs): written = write_fmt(fp, '4sHI', b'Crv ', self.version, len(self)) written += sum(item.write(fp) for item in self) return written
class SolidFillInfo(BaseElement): """ Effects layer inner glow info. .. py:attribute:: version .. py:attribute:: blend_mode .. py:attribute:: color .. py:attribute:: opacity .. py:attribute:: enabled .. py:attribute:: native_color """ version = attr.ib(default=2, type=int) blend_mode = attr.ib( default=BlendMode.NORMAL, converter=BlendMode, validator=in_(BlendMode) ) color = attr.ib(factory=Color) opacity = attr.ib(default=0, type=int) enabled = attr.ib(default=0, type=int) native_color = attr.ib(factory=Color) @classmethod def read(cls, fp): version = read_fmt('I', fp)[0] signature, blend_mode = read_fmt('4s4s', fp) assert signature == b'8BIM', 'Invalid signature %r' % (signature) color = Color.read(fp) opacity, enabled = read_fmt('2B', fp) native_color = Color.read(fp) return cls(version, blend_mode, color, opacity, enabled, native_color) def write(self, fp): written = write_fmt( fp, 'I4s4s', self.version, b'8BIM', self.blend_mode.value ) written += self.color.write(fp) written += write_fmt(fp, '2B', self.opacity, self.enabled) written += self.native_color.write(fp) return written
class UnitFloats(BaseElement): """ Unit floats structure. .. py:attribute:: unit .. py:attribute:: values """ unit = attr.ib(default=UnitFloatType.NONE, converter=UnitFloatType, validator=in_(UnitFloatType)) values = attr.ib(factory=list) @classmethod def read(cls, fp): """Read the element from a file-like object. :param fp: file-like object """ unit, count = read_fmt('4sI', fp) values = list(read_fmt('%dd' % count, fp)) return cls(unit, values) def write(self, fp): """Write the element to a file-like object. :param fp: file-like object """ return write_fmt(fp, '4sI%dd' % len(self.values), self.unit.value, len(self.values), *self.values) def __iter__(self): for value in self.values: yield value def __getitem__(self, index): return self.values[index] def __len__(self): return len(self.values)
class Slices(BaseElement): """ Slices resource. .. py:attribute:: version .. py:attribute:: data """ version = attr.ib(default=0, type=int, validator=in_((6, 7, 8))) data = attr.ib(default=None) @classmethod def read(cls, fp, **kwargs): version = read_fmt('I', fp)[0] assert version in (6, 7, 8), 'Invalid version %d' % (version) if version == 6: return cls(version=version, data=SlicesV6.read(fp)) return cls(version=version, data=DescriptorBlock.read(fp)) def write(self, fp, **kwargs): written = write_fmt(fp, 'I', self.version) written += self.data.write(fp, padding=1) return written
class SelectiveColor(BaseElement): """ SelectiveColor structure. .. py:attribute:: version .. py:attribute:: method .. py:attribute:: data """ version = attr.ib(default=1, type=int, validator=in_((1,))) method = attr.ib(default=0, type=int) data = attr.ib(factory=list, converter=list) @classmethod def read(cls, fp, **kwargs): version, method = read_fmt('2H', fp) data = [read_fmt('4h', fp) for i in range(10)] return cls(version, method, data) def write(self, fp, **kwargs): written = write_fmt(fp, '2H', self.version, self.method) for plate in self.data: written += write_fmt(fp, '4h', *plate) return written
class GradientMap(BaseElement): """ GradientMap structure. .. py:attribute:: version .. py:attribute:: is_reversed .. py:attribute:: is_dithered .. py:attribute:: name .. py:attribute:: color_stops .. py:attribute:: transparency_stops .. py:attribute:: expansion .. py:attribute:: interpolation .. py:attribute:: length .. py:attribute:: mode .. py:attribute:: random_seed .. py:attribute:: show_transparency .. py:attribute:: use_vector_color .. py:attribute:: roughness .. py:attribute:: color_model .. py:attribute:: minimum_color .. py:attribute:: maximum_color """ version = attr.ib(default=1, type=int, validator=in_((1,))) is_reversed = attr.ib(default=0, type=int) is_dithered = attr.ib(default=0, type=int) name = attr.ib(default='', type=str) color_stops = attr.ib(factory=list, converter=list) transparency_stops = attr.ib(factory=list, converter=list) expansion = attr.ib(default=2, type=int, validator=in_((2,))) interpolation = attr.ib(default=0, type=int) length = attr.ib(default=32, type=int, validator=in_((32,))) mode = attr.ib(default=0, type=int) random_seed = attr.ib(default=0, type=int) show_transparency = attr.ib(default=0, type=int) use_vector_color = attr.ib(default=0, type=int) roughness = attr.ib(default=0, type=int) color_model = attr.ib(default=0, type=int) minimum_color = attr.ib(factory=list, converter=list) maximum_color = attr.ib(factory=list, converter=list) @classmethod def read(cls, fp, **kwargs): version, is_reversed, is_dithered = read_fmt('H2B', fp) assert version == 1, 'Invalid version %s' % (version) name = read_unicode_string(fp) count = read_fmt('H', fp)[0] color_stops = [ColorStop.read(fp) for _ in range(count)] count = read_fmt('H', fp)[0] transparency_stops = [TransparencyStop.read(fp) for _ in range(count)] expansion, interpolation, length, mode = read_fmt('4H', fp) assert expansion == 2, 'Invalid expansion %d' % (expansion) random_seed, show_transparency, use_vector_color = read_fmt('I2H', fp) roughness, color_model = read_fmt('IH', fp) minimum_color = read_fmt('4H', fp) maximum_color = read_fmt('4H', fp) read_fmt('2x', fp) # Dummy? return cls(version, is_reversed, is_dithered, name, color_stops, transparency_stops, expansion, interpolation, length, mode, random_seed, show_transparency, use_vector_color, roughness, color_model, minimum_color, maximum_color) def write(self, fp, **kwargs): written = write_fmt(fp, 'H2B', self.version, self.is_reversed, self.is_dithered) written += write_unicode_string(fp, self.name) written += write_fmt(fp, 'H', len(self.color_stops)) written += sum(stop.write(fp) for stop in self.color_stops) written += write_fmt(fp, 'H', len(self.transparency_stops)) written += sum(stop.write(fp) for stop in self.transparency_stops) written += write_fmt( fp, '4HI2HIH', self.expansion, self.interpolation, self.length, self.mode, self.random_seed, self.show_transparency, self.use_vector_color, self.roughness, self.color_model ) written += write_fmt(fp, '4H', *self.minimum_color) written += write_fmt(fp, '4H', *self.maximum_color) written += write_fmt(fp, '2x') written += write_padding(fp, written, 4) return written
class VirtualMemoryArray(BaseElement): """ VirtualMemoryArrayList structure, corresponding to each channel. .. py:attribute:: is_written .. py:attribute:: depth .. py:attribute:: rectangle .. py:attribute:: pixel_depth .. py:attribute:: compression .. py:attribute:: data """ is_written = attr.ib(default=0) depth = attr.ib(default=None) rectangle = attr.ib(default=None) pixel_depth = attr.ib(default=None) compression = attr.ib(default=Compression.RAW, converter=Compression, validator=in_(Compression)) data = attr.ib(default=b'') @classmethod def read(cls, fp, **kwargs): is_written = read_fmt('I', fp)[0] if is_written == 0: return cls(is_written=is_written) length = read_fmt('I', fp)[0] if length == 0: return cls(is_written=is_written) depth = read_fmt('I', fp)[0] rectangle = read_fmt('4I', fp) pixel_depth, compression = read_fmt('HB', fp) data = fp.read(length - 23) return cls(is_written, depth, rectangle, pixel_depth, compression, data) def write(self, fp, **kwargs): written = write_fmt(fp, 'I', self.is_written) if self.is_written == 0: return written if self.depth is None: written += write_fmt(fp, 'I', 0) return written return written + write_length_block(fp, lambda f: self._write_body(f)) def _write_body(self, fp): written = write_fmt(fp, 'I', self.depth) written += write_fmt(fp, '4I', *self.rectangle) written += write_fmt(fp, 'HB', self.pixel_depth, self.compression.value) written += write_bytes(fp, self.data) return written def get_data(self): """Get decompressed bytes.""" if not self.is_written: return None width, height = self.rectangle[3], self.rectangle[2] return decompress(self.data, self.compression, width, height, self.depth, version=1) def set_data(self, size, data, depth, compression=0): """Set bytes.""" self.data = compress(data, compression, size[0], size[1], depth, version=1) self.depth = int(depth) self.pixel_depth = int(depth) self.rectangle = (0, 0, int(size[1]), int(size[0])) self.compression = Compression(compression) self.is_written = True
class TaggedBlock(BaseElement): """ Layer tagged block with extra info. .. py:attribute:: key 4-character code. See :py:class:`~psd_tools.constants.TaggedBlockID` .. py:attribute:: data Data. """ _SIGNATURES = (b'8BIM', b'8B64') _BIG_KEYS = { TaggedBlockID.USER_MASK, TaggedBlockID.LAYER_16, TaggedBlockID.LAYER_32, TaggedBlockID.LAYER, TaggedBlockID.SAVING_MERGED_TRANSPARENCY16, TaggedBlockID.SAVING_MERGED_TRANSPARENCY32, TaggedBlockID.SAVING_MERGED_TRANSPARENCY, TaggedBlockID.SAVING_MERGED_TRANSPARENCY16, TaggedBlockID.ALPHA, TaggedBlockID.FILTER_MASK, TaggedBlockID.LINKED_LAYER2, TaggedBlockID.LINKED_LAYER_EXTERNAL, TaggedBlockID.FILTER_EFFECTS1, TaggedBlockID.FILTER_EFFECTS2, TaggedBlockID.PIXEL_SOURCE_DATA2, TaggedBlockID.UNICODE_PATH_NAME, TaggedBlockID.EXPORT_SETTING1, TaggedBlockID.EXPORT_SETTING2, TaggedBlockID.COMPUTER_INFO, } signature = attr.ib(default=b'8BIM', repr=False, validator=in_(_SIGNATURES)) key = attr.ib(default=b'') data = attr.ib(default=b'', repr=True) @classmethod def read(cls, fp, version=1, padding=1): signature = read_fmt('4s', fp)[0] if signature not in cls._SIGNATURES: logger.warning('Invalid signature (%r)' % (signature)) fp.seek(-4, 1) return None key = read_fmt('4s', fp)[0] try: key = TaggedBlockID(key) except ValueError: message = 'Unknown key: %r' % (key) warn(message) logger.warning(message) fmt = cls._length_format(key, version) raw_data = read_length_block(fp, fmt=fmt, padding=padding) kls = TYPES.get(key) if kls: data = kls.frombytes(raw_data, version=version) # _raw_data = data.tobytes(version=version, # padding=1 if padding == 4 else 4) # assert raw_data == _raw_data, '%r: %s vs %s' % ( # kls, trimmed_repr(raw_data), trimmed_repr(_raw_data) # ) else: message = 'Unknown tagged block: %r, %s' % (key, trimmed_repr(raw_data)) warn(message) logger.warning(message) data = raw_data return cls(signature, key, data) def write(self, fp, version=1, padding=1): key = self.key if isinstance(self.key, bytes) else self.key.value written = write_fmt(fp, '4s4s', self.signature, key) def writer(f): if hasattr(self.data, 'write'): # It seems padding size applies at the block level here. inner_padding = 1 if padding == 4 else 4 return self.data.write(f, padding=inner_padding, version=version) return write_bytes(f, self.data) fmt = self._length_format(self.key, version) written += write_length_block(fp, writer, fmt=fmt, padding=padding) return written @classmethod def _length_format(cls, key, version): return ('I', 'Q')[int(version == 2 and key in cls._BIG_KEYS)]
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 ChannelData(BaseElement): """ Channel data. .. py:attribute:: compression Compression type. See :py:class:`~psd_tools.constants.Compression`. .. py:attribute:: data Data. """ compression = attr.ib(default=Compression.RAW, converter=Compression, validator=in_(Compression)) data = attr.ib(default=b'', type=bytes, repr=False) @classmethod def read(cls, fp): """Read the element from a file-like object. :param fp: file-like object :rtype: :py:class:`.ChannelData` """ compression = Compression(read_fmt('H', fp)[0]) data = fp.read() return cls(compression, data) def write(self, fp, **kwargs): """Write the element to a file-like object. :param fp: file-like object """ written = write_fmt(fp, 'H', self.compression.value) written += write_bytes(fp, self.data) # written += write_padding(fp, written, 2) # Seems no padding here. return written def get_data(self, width, height, depth, version=1): """Get decompressed channel data. :param width: width. :param height: height. :param depth: bit depth of the pixel. :param version: psd file version. :rtype: bytes """ return decompress(self.data, self.compression, width, height, depth, version) def set_data(self, data, width, height, depth, version=1): """Set raw channel data and compress to store. :param data: raw data bytes to write. :param compression: compression type, see :py:class:`~psd_tools.constants.Compression`. :param width: width. :param height: height. :param depth: bit depth of the pixel. :param version: psd file version. """ self.data = compress(data, self.compression, width, height, depth, version) return len(self.data) @property def _length(self): """Length of channel data block. """ return 2 + len(self.data)
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))