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 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 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 .. 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 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 Levels(ListElement): """ List of level records. See :py:class: `~psd_tools2.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 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 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 DescriptorBlock(Descriptor): """ Dict-like Descriptor-based structure. See :py:class:`~psd_tools2.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 MetadataSetting(BaseElement): """ MetadataSetting structure. """ 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 (b'cust', b'cmls', b'extn'): 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 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 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 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 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 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 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_tools2.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): """Read the element from a file-like object. :param fp: file-like object :param version: psd file version :rtype: :py:class:`.ChannelInfo` """ return cls(*read_fmt(('hI', 'hQ')[version - 1], fp)) def write(self, fp, version=1): """Write the element to a file-like object. :param fp: file-like object :param version: psd file version """ return write_fmt(fp, ('hI', 'hQ')[version - 1], *attr.astuple(self))
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 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
class ImageResource(BaseElement): """ Image resource block. .. py:attribute:: signature Binary signature, always ``b'8BIM'``. .. py:attribute:: key Unique identifier for the resource. See :py:class:`~psd_tools2.constants.ImageResourceID`. .. py:attribute:: name .. py:attribute:: data The resource data. """ signature = attr.ib(default=b'8BIM', type=bytes, repr=False, validator=in_( {b'8BIM', b'MeSa', b'AgHg', b'PHUT', b'DCSR'})) key = attr.ib(default=1000, type=int) name = attr.ib(default='', type=str) data = attr.ib(default=b'', type=bytes, repr=False) @classmethod def read(cls, fp, encoding='macroman'): """Read the element from a file-like object. :param fp: file-like object :rtype: :py:class:`.ImageResource` """ signature, key = read_fmt('4sH', fp) try: key = ImageResourceID(key) except ValueError: logger.warning('Unknown image resource %d' % (key)) name = read_pascal_string(fp, encoding, padding=2) raw_data = read_length_block(fp, padding=2) if key in TYPES: data = TYPES[key].frombytes(raw_data) # try: # _raw_data = data.tobytes(padding=1) # assert _raw_data == raw_data, '%r vs %r' % ( # _raw_data, raw_data # ) # except AssertionError as e: # logger.error(e) # raise else: data = raw_data return cls(signature, key, name, data) def write(self, fp, encoding='macroman'): """Write the element to a file-like object. :param fp: file-like object :rtype: int """ written = write_fmt(fp, '4sH', self.signature, getattr(self.key, 'value', self.key)) written += write_pascal_string(fp, self.name, encoding, 2) def writer(f): if hasattr(self.data, 'write'): return self.data.write(f, padding=1) return write_bytes(f, self.data) written += write_length_block(fp, writer, padding=2) 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): """Read the element from a file-like object. :param fp: file-like object :rtype: :py:class:`.GlobalLayerMaskInfo` """ data = read_length_block(fp) logger.debug('reading global layer mask info, len=%d' % (len(data))) if len(data) == 0: 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): """Write the element to a file-like object. :param fp: file-like object """ 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 ChannelData(BaseElement): """ Channel data. .. py:attribute:: compression Compression type. See :py:class:`~psd_tools2.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_tools2.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 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_tools2.constants.BlendMode`. .. py:attribute:: opacity Opacity, 0 = transparent, 255 = opaque. .. py:attribute:: clipping Clipping, 0 = base, 1 = non-base. See :py:class:`~psd_tools2.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_tools2.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 FileHeader(BaseElement): """ Header section of the PSD file. Example:: from psd_tools2.psd.header import FileHeader from psd_tools2.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_tools2.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 BevelInfo(BaseElement): """ Effects layer bevel info. .. py:attribute:: version .. py:attribute:: angle .. py:attribute:: depth .. py:attribute:: blur .. py:attribute:: highlight_blend_mode .. py:attribute:: shadow_blend_mode .. py:attribute:: highlight_color .. py:attribute:: shadow_color .. py:attribute:: highlight_opacity .. py:attribute:: shadow_opacity .. py:attribute:: enabled .. py:attribute:: use_global_angle .. py:attribute:: direction .. py:attribute:: real_hightlight_color .. py:attribute:: real_shadow_color """ version = attr.ib(default=0, type=int) angle = attr.ib(default=0, type=int) depth = attr.ib(default=0, type=int) blur = attr.ib(default=0, type=int) highlight_blend_mode = attr.ib(default=BlendMode.NORMAL, converter=BlendMode, validator=in_(BlendMode)) shadow_blend_mode = attr.ib(default=BlendMode.NORMAL, converter=BlendMode, validator=in_(BlendMode)) highlight_color = attr.ib(factory=Color) shadow_color = attr.ib(factory=Color) bevel_style = attr.ib(default=0, type=int) highlight_opacity = attr.ib(default=0, type=int) shadow_opacity = attr.ib(default=0, type=int) enabled = attr.ib(default=0, type=int) use_global_angle = attr.ib(default=0, type=int) direction = attr.ib(default=0, type=int) real_highlight_color = attr.ib(default=None) real_shadow_color = attr.ib(default=None) @classmethod def read(cls, fp): # TODO: Check 4-byte = 2-byte int + 2-byte fraction? version, angle, depth, blur = read_fmt('Ii2I', fp) signature, highlight_blend_mode = read_fmt('4s4s', fp) assert signature == b'8BIM', 'Invalid signature %r' % (signature) signature, shadow_blend_mode = read_fmt('4s4s', fp) assert signature == b'8BIM', 'Invalid signature %r' % (signature) highlight_color = Color.read(fp) shadow_color = Color.read(fp) bevel_style, highlight_opacity, shadow_opacity = read_fmt('3B', fp) enabled, use_global_angle, direction = read_fmt('3B', fp) real_highlight_color, real_shadow_color = None, None if version == 2: real_highlight_color = Color.read(fp) real_shadow_color = Color.read(fp) return cls( version, angle, depth, blur, highlight_blend_mode, shadow_blend_mode, highlight_color, shadow_color, bevel_style, highlight_opacity, shadow_opacity, enabled, use_global_angle, direction, real_highlight_color, real_shadow_color ) def write(self, fp): written = write_fmt( fp, 'Ii2I', self.version, self.angle, self.depth, self.blur ) written += write_fmt( fp, '4s4s4s4s', b'8BIM', self.highlight_blend_mode.value, b'8BIM', self.shadow_blend_mode.value ) written += self.highlight_color.write(fp) written += self.shadow_color.write(fp) written += write_fmt( fp, '6B', self.bevel_style, self.highlight_opacity, self.shadow_opacity, self.enabled, self.use_global_angle, self.direction ) if self.version >= 2: written += self.highlight_color.write(fp) written += self.shadow_color.write(fp) return written
class ImageData(BaseElement): """ Merged channel image data. .. py:attribute:: compression See :py:class:`~psd_tools2.constants.Compression`. .. py:attribute:: 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:`.ImageData` """ start_pos = fp.tell() compression = Compression(read_fmt('H', fp)[0]) data = fp.read() # TODO: Parse data here. Need header. logger.debug(' read image data, len=%d' % (fp.tell() - start_pos)) return cls(compression, data) def write(self, fp): """Write the element to a file-like object. :param fp: file-like object :rtype: int """ start_pos = fp.tell() written = write_fmt(fp, 'H', self.compression.value) written += write_bytes(fp, self.data) logger.debug(' wrote image data, len=%d' % (fp.tell() - start_pos)) return written def get_data(self, header): """Get decompressed data. :param header: See :py:class:`~psd_tools2.psd.header.FileHeader`. :return: list of bytes corresponding each channel. :rtype: list """ data = decompress(self.data, self.compression, header.width, header.height * header.channels, header.depth, header.version) plane_size = len(data) // header.channels with io.BytesIO(data) as f: return [f.read(plane_size) for _ in range(header.channels)] def set_data(self, data, header): """Set raw data and compress. :param data: list of raw data bytes corresponding channels. :param compression: compression type, see :py:class:`~psd_tools2.constants.Compression`. :param header: See :py:class:`~psd_tools2.psd.header.FileHeader`. :return: length of compressed data. """ self.data = compress(b''.join(data), self.compression, header.width, header.height * header.channels, header.depth, header.version) return len(self.data) @classmethod def new(cls, header, color=0, compression=Compression.RAW): """Create a new image data object. :param header: FileHeader. :param compression: compression type. :param color: default color. int or iterable for channel length. """ plane_size = header.width * header.height if isinstance(color, (bool, int, float)): color = (color, ) * header.channels if len(color) != header.channels: raise ValueError('Invalid color %s for channel size %d' % (color, header.channels)) # Bitmap is not supported here. fmt = {8: 'B', 16: 'H', 32: 'I'}.get(header.depth) data = [] for i in range(header.channels): data.append(pack(fmt, color[i]) * plane_size) self = cls(compression=compression) self.set_data(data, header) return self
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