def _decode_section_divider(data): data_length = len(data) blend_mode = None sub_type = None fp = io.BytesIO(data) tp = read_fmt("I", fp)[0] if not SectionDivider.is_known(tp): warnings.warn("Unknown section divider type (%s)" % tp) if data_length >= 12: sig = fp.read(4) if sig != b"8BIM": raise Error("Invalid signature in section divider block (%r)" % sig) blend_mode = fp.read(4) if not BlendMode.is_known(blend_mode): warnings.warn("Unknown section divider blend mode (%s)" % blend_mode) if data_length >= 16: sub_type = read_fmt("I", fp)[0] if not SectionDividerSub.is_known(sub_type): warnings.warn("Unknown section divider sub-type (%s)" % sub_type) return Divider(tp, blend_mode, sub_type)
def blend_mode(self, value): if isinstance(value, BlendMode): self._record.blend_mode = value elif hasattr(BlendMode, value.upper()): self._record.blend_mode = getattr(BlendMode, value.upper()) else: self._record.blend_mode = BlendMode(value)
def _repr_pretty_(self, p, cycle): if cycle: p.text(repr(self)) else: blend_mode = self.blend_mode if blend_mode is not None: blend_mode = BlendMode.name_of(blend_mode) sub_type = self.sub_type if sub_type is not None: sub_type = SectionDividerSub.name_of(sub_type) p.begin_group(2, "Divider(") p.begin_group(0) p.break_() p.text("type = %s," % SectionDivider.name_of(self.type)) p.break_() p.text("blend_mode = %s," % blend_mode) p.break_() p.text("sub_type = %s" % sub_type) p.end_group(2) p.break_() p.end_group(0, ")")
def blend_mode(self): """ Blend mode of this layer. See :py:class:`~psd_tools.constants.BlendMode` :rtype: str """ return BlendMode.human_name_of(self._record.blend_mode)
def _read_body(cls, fp): # TODO: Check 4-byte = 2-byte int + 2-byte fraction? version, blur, intensity = read_fmt('III', 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, opacity = read_fmt('2B', fp) return version, blur, intensity, color, blend_mode, enabled, opacity
def _read_layer_record(fp, encoding): """ Reads single layer record. """ top, left, bottom, right, num_channels = read_fmt("4i H", fp) logger.debug(' top=%d, left=%d, bottom=%d, right=%d, num_channels=%d', top, left, bottom, right, num_channels) channel_info = [] for channel_num in range(num_channels): info = ChannelInfo(*read_fmt("hI", fp)) channel_info.append(info) sig = fp.read(4) if sig != b'8BIM': raise Error("Error parsing layer: invalid signature (%r)" % sig) blend_mode = fp.read(4) if not BlendMode.is_known(blend_mode): warnings.warn("Unknown blend mode (%s)" % blend_mode) opacity, clipping, flags, extra_length = read_fmt("BBBxI", fp) if not Clipping.is_known(clipping): warnings.warn("Unknown clipping (%s)" % clipping) logger.debug(' extra_length=%s', extra_length) flags = LayerFlags( bool(flags & 1), not bool(flags & 2), # why "not"? bool(flags & 16) if bool(flags & 8) else None ) start_pos = fp.tell() mask_data = _read_layer_mask_data(fp) blending_ranges = _read_layer_blending_ranges(fp) name = read_pascal_string(fp, encoding, 4) remaining_length = extra_length - (fp.tell() - start_pos) logger.debug(' reading layer tagged blocks...') logger.debug(' length=%d, start_pos=%d', remaining_length, fp.tell()) tagged_blocks = _read_layer_tagged_blocks(fp, remaining_length) remaining_length = extra_length - (fp.tell() - start_pos) if remaining_length > 0: fp.seek(remaining_length, 1) # skip the remainder logger.debug(' skipping %s bytes', remaining_length) return LayerRecord( top, left, bottom, right, num_channels, channel_info, blend_mode, opacity, clipping, flags, mask_data, blending_ranges, name, tagged_blocks )
def blend_mode(self, value): _value = BlendMode(value) if _value == BlendMode.PASS_THROUGH: self._record.blend_mode = BlendMode.NORMAL else: self._record.blend_mode = _value setting = self._setting if setting: setting.blend_mode = _value
def _read_layer_record(fp, encoding, version): """ Reads single layer record. """ top, left, bottom, right, num_channels = read_fmt("4i H", fp) logger.debug(' top=%d, left=%d, bottom=%d, right=%d, num_channels=%d', top, left, bottom, right, num_channels) channel_info = [] for channel_num in range(num_channels): if version == 1: info = ChannelInfo(*read_fmt("hI", fp)) elif version == 2: info = ChannelInfo(*read_fmt("hQ", fp)) channel_info.append(info) sig = fp.read(4) if sig != b'8BIM': raise Error("Error parsing layer: invalid signature (%r)" % sig) blend_mode = fp.read(4) if not BlendMode.is_known(blend_mode): warnings.warn("Unknown blend mode (%s)" % blend_mode) opacity, clipping, flags, extra_length = read_fmt("BBBxI", fp) if not Clipping.is_known(clipping): warnings.warn("Unknown clipping (%s)" % clipping) logger.debug(' extra_length=%s', extra_length) flags = LayerFlags( bool(flags & 1), not bool(flags & 2), # why "not"? bool(flags & 16) if bool(flags & 8) else None) start_pos = fp.tell() mask_data = _read_layer_mask_data(fp) blending_ranges = _read_layer_blending_ranges(fp) name = read_pascal_string(fp, encoding, 4) remaining_length = extra_length - (fp.tell() - start_pos) logger.debug(' reading layer tagged blocks...') logger.debug(' length=%d, start_pos=%d', remaining_length, fp.tell()) tagged_blocks = _read_layer_tagged_blocks(fp, remaining_length, version) remaining_length = extra_length - (fp.tell() - start_pos) if remaining_length > 0: fp.seek(remaining_length, 1) # skip the remainder logger.debug(' skipping %s bytes', remaining_length) return LayerRecord(top, left, bottom, right, num_channels, channel_info, blend_mode, opacity, clipping, flags, mask_data, blending_ranges, name, tagged_blocks)
def _read_blend_mode(fp): sig = fp.read(4) if sig != b'8BIM': raise Error("Error parsing layer effect: invalid signature (%r)" % sig) blend_mode = fp.read(4) if not BlendMode.is_known(blend_mode): warnings.warn("Unknown blend mode (%s)" % blend_mode) return blend_mode
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 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 __repr__(self): blend_mode = self.blend_mode if blend_mode is not None: blend_mode = BlendMode.name_of(blend_mode) sub_type = self.sub_type if sub_type is not None: sub_type = SectionDividerSub.name_of(sub_type) return "Divider(type=%s, blend_mode=%s, sub_type=%s)" % ( SectionDivider.name_of(self.type), blend_mode, sub_type, )
def _read_layer_record(fp, encoding): """ Reads single layer record. """ top, left, bottom, right, num_channels = read_fmt("4i H", fp) channel_info = [] for channel_num in range(num_channels): info = ChannelInfo(*read_fmt("hI", fp)) channel_info.append(info) sig = fp.read(4) if sig != b'8BIM': raise Error("Error parsing layer: invalid signature (%r)" % sig) blend_mode = fp.read(4).decode('ascii') if not BlendMode.is_known(blend_mode): warnings.warn("Unknown blend mode (%s)" % blend_mode) opacity, clipping, flags, extra_length = read_fmt("BBBxI", fp) flags = LayerFlags(bool(flags & 1), not bool(flags & 2)) # why not? if not Clipping.is_known(clipping): warnings.warn("Unknown clipping: %s" % clipping) start = fp.tell() mask_data = _read_layer_mask_data(fp) blending_ranges = _read_layer_blending_ranges(fp) name = read_pascal_string(fp, encoding, 4) remaining_length = extra_length - (fp.tell()-start) tagged_blocks = _read_layer_tagged_blocks(fp, remaining_length) remaining_length = extra_length - (fp.tell()-start) fp.seek(remaining_length, 1) # skip the reminder return LayerRecord( top, left, bottom, right, num_channels, channel_info, blend_mode, opacity, clipping, flags, mask_data, blending_ranges, name, tagged_blocks )
def merge_layers(layers, respect_visibility=True, skip_layer=lambda layer: False, background=None, bbox=None): """ Merges layers together (the first layer is on top). By default hidden layers are not rendered; pass ``respect_visibility=False`` to render them. In order to skip some layers pass ``skip_layer`` function which should take ``layer` as an argument and return True or False. If ``bbox`` is not None, it should be an instance of ``BBox`` class with coordinates; returned image will be restricted to this rectangle. If ``background`` is not None, ``bbox`` should be passed as well. It should be an image such as background.size == (bbox.width, bbox.height) This is highly experimental. """ if _blend_modes is not None: blend_functions = { BlendMode.NORMAL: None, BlendMode.DISSOLVE: _blend_modes.dissolve, BlendMode.DARKEN: _blend_modes.darken, BlendMode.MULTIPLY: _blend_modes.multiply, BlendMode.COLOR_BURN: _blend_modes.color_burn, BlendMode.LINEAR_BURN: _blend_modes.linear_burn, BlendMode.DARKER_COLOR: _blend_modes.darker_color, # Photoshop bug BlendMode.LIGHTEN: _blend_modes.lighten, BlendMode.SCREEN: _blend_modes.screen, BlendMode.COLOR_DODGE: _blend_modes.color_dodge, BlendMode.LINEAR_DODGE: _blend_modes.linear_dodge, BlendMode.LIGHTER_COLOR: _blend_modes.lighter_color, # Photoshop bug BlendMode.OVERLAY: _blend_modes.overlay, BlendMode.SOFT_LIGHT: _blend_modes.soft_light, # max deviation - +/-1 tone BlendMode.HARD_LIGHT: _blend_modes.hard_light, # Photoshop bug BlendMode.VIVID_LIGHT: _blend_modes.vivid_light, # max deviation - +2 tone Photoshop bug BlendMode.LINEAR_LIGHT: _blend_modes.linear_light, # Photoshop bug BlendMode.PIN_LIGHT: _blend_modes.pin_light, # max deviation - +1 tone BlendMode.HARD_MIX: _blend_modes.hard_mix, BlendMode.DIFFERENCE: _blend_modes.difference, BlendMode.EXCLUSION: _blend_modes.exclusion, # max deviation - +/-1 tone BlendMode.SUBTRACT: _blend_modes.subtract, BlendMode.DIVIDE: _blend_modes.divide, BlendMode.HUE: _blend_modes.hue, # max deviation - +/-2 luminance level Photoshop bug BlendMode.SATURATION: _blend_modes.saturation, # max deviation - +/-2 luminance level Photoshop bug BlendMode.COLOR: _blend_modes.color, # max deviation - +/-2 luminance level Photoshop bug BlendMode.LUMINOSITY: _blend_modes.luminosity # max deviation - +/-2 luminance level Photoshop bug } else: logger.warning( '"_blend_modes" C extension is not found. ' \ 'Blend modes are unavailable for merging!' ) if background is None: if bbox is None: bbox = combined_bbox(layers) if bbox is None: return None # creating a base image... result = Image.new( "RGBA", (bbox.width, bbox.height), color = (255, 255, 255, 0) ) else: if bbox is None or background.size != (bbox.width, bbox.height): return None # using existing image as a base... result = background for layer in reversed(layers): if layer is None: continue if respect_visibility and not layer.visible: continue layer_bbox = layer.bbox if layer_bbox.width == 0 or layer_bbox.height == 0: continue if layer_bbox.x2 < bbox.x1 or layer_bbox.y2 < bbox.y1 \ or layer_bbox.x1 > bbox.x2 or layer_bbox.y1 > bbox.y2: logger.debug("Layer outside of bbox. Skipping...") continue if skip_layer(layer): continue use_dissolve = (layer.blend_mode == BlendMode.DISSOLVE) if isinstance(layer, psd_tools.Group): # if group's blend mode is PASS_THROUGH, # then its layers should be merged as if they aren't in group if layer.blend_mode == BlendMode.PASS_THROUGH: layer_image = merge_layers( layer.layers, respect_visibility, skip_layer, result.copy(), bbox ) else: layer_image = merge_layers( layer.layers, respect_visibility, skip_layer, None, bbox ) x = y = 0 background = result else: layer_image = layer.as_PIL() x, y = layer_bbox.x1 - bbox.x1, layer_bbox.y1 - bbox.y1 w, h = layer_image.size # checking if layer is inside the area of rendering... if x < 0 or y < 0 or x + w > bbox.width or y + h > bbox.height: # layer doesn't fit the bbox crop_bbox = ( max(-x, 0), max(-y, 0), min(w, bbox.width - x), min(h, bbox.height - y) ) logger.debug("Cropping layer to (%s, %s, %s, %s)...", *crop_bbox) layer_image = layer_image.crop(crop_bbox) x += crop_bbox[0] y += crop_bbox[1] w, h = layer_image.size if result.size != layer_image.size: background = result.crop((x, y, x + w, y + h)) else: background = result # layer_image = pil_support.apply_opacity(layer_image, layer.fill) # layer_image = _blend(background, layer_image, None, use_dissolve) if _blend_modes is not None: # getting a blending function based on layers' blend mode... # if the given mode is not implemented, Normal mode will be used instead func = blend_functions.get(layer.blend_mode) if func is None and layer.blend_mode not in (BlendMode.NORMAL, BlendMode.PASS_THROUGH): logger.warning( "Blend mode is not implemented: %s. Using NORMAL mode...", BlendMode.name_of(layer.blend_mode) ) else: logger.debug( "Blending using %s mode...", BlendMode.name_of(layer.blend_mode) ) else: func = None use_dissolve = False layer_image = pil_support.apply_opacity(layer_image, layer.opacity) layer_image = _blend(background, layer_image, func, use_dissolve) if layer_image.size == result.size: result = layer_image else: result.paste(layer_image, (x, y)) return result
def merge_layers(layers, respect_visibility=True, skip_layer=lambda layer: False, bbox=None): """ Merges layers together (the first layer is on top). By default hidden layers are not rendered; pass ``respect_visibility=False`` to render them. In order to skip some layers pass ``skip_layer`` function which should take ``layer` as an argument and return True or False. If ``bbox`` is not None, it should be a 4-tuple with coordinates; returned image will be restricted to this rectangle. This is highly experimental. """ # FIXME: this currently assumes PIL from PIL import Image if bbox is None: bbox = combined_bbox(layers) if bbox is None: return None result = Image.new( "RGBA", (bbox.width, bbox.height), color=(255, 255, 255, 0 ) # fixme: transparency calculation is incorrect ) for layer in reversed(layers): if layer is None: continue if layer.bbox.width == 0 and layer.bbox.height == 0: continue if skip_layer(layer): continue if not layer.visible and respect_visibility: continue if isinstance(layer, psd_tools.Group): layer_image = merge_layers(layer.layers, respect_visibility, skip_layer) else: layer_image = layer.as_PIL() layer_image = pil_support.apply_opacity(layer_image, layer.opacity) x, y = layer.bbox.x1 - bbox.x1, layer.bbox.y1 - bbox.y1 w, h = layer_image.size if x < 0 or y < 0: # image doesn't fit the bbox x_overflow = -min(x, 0) y_overflow = -min(y, 0) logger.debug("cropping.. (%s, %s)", x_overflow, y_overflow) layer_image = layer_image.crop((x_overflow, y_overflow, w, h)) x += x_overflow y += y_overflow if w + x > bbox.width or h + y > bbox.height: # FIXME logger.debug("cropping..") if layer.blend_mode == BlendMode.NORMAL: if layer_image.mode == 'RGBA': tmp = Image.new("RGBA", result.size, color=(255, 255, 255, 0)) tmp.paste(layer_image, (x, y)) result = Image.alpha_composite(result, tmp) elif layer_image.mode == 'RGB': result.paste(layer_image, (x, y)) else: logger.warning( "layer image mode is unsupported for merging: %s", layer_image.mode) continue else: logger.warning("Blend mode is not implemented: %s", BlendMode.name_of(layer.blend_mode)) continue return result
def merge_layers(layers, respect_visibility=True, skip_layer=lambda layer: False, bbox=None): """ Merges layers together (the first layer is on top). By default hidden layers are not rendered; pass ``respect_visibility=False`` to render them. In order to skip some layers pass ``skip_layer`` function which should take ``layer` as an argument and return True or False. If ``bbox`` is not None, it should be a 4-tuple with coordinates; returned image will be restricted to this rectangle. This is highly experimental. """ # FIXME: this currently assumes PIL from PIL import Image if bbox is None: bbox = combined_bbox(layers) if bbox is None: return None result = Image.new( "RGBA", (bbox.width, bbox.height), color=(255, 255, 255, 0) # fixme: transparency calculation is incorrect ) for layer in reversed(layers): if layer is None: continue if layer.bbox.width == 0 and layer.bbox.height == 0: continue if skip_layer(layer): continue if not layer.visible and respect_visibility: continue if isinstance(layer, psd_tools.Group): layer_image = merge_layers(layer.layers, respect_visibility, skip_layer) else: layer_image = layer.as_PIL() layer_image = pil_support.apply_opacity(layer_image, layer.opacity) x, y = layer.bbox.x1 - bbox.x1, layer.bbox.y1 - bbox.y1 w, h = layer_image.size if x < 0 or y < 0: # image doesn't fit the bbox x_overflow = - min(x, 0) y_overflow = - min(y, 0) logger.debug("cropping.. (%s, %s)", x_overflow, y_overflow) layer_image = layer_image.crop((x_overflow, y_overflow, w, h)) x += x_overflow y += y_overflow if w+x > bbox.width or h+y > bbox.height: # FIXME logger.debug("cropping..") if layer.blend_mode == BlendMode.NORMAL: if layer_image.mode == 'RGBA': tmp = Image.new("RGBA", result.size, color=(255, 255, 255, 0)) tmp.paste(layer_image, (x, y)) result = Image.alpha_composite(result, tmp) elif layer_image.mode == 'RGB': result.paste(layer_image, (x,y)) else: logger.warning("layer image mode is unsupported for merging: %s", layer_image.mode) continue else: logger.warning("Blend mode is not implemented: %s", BlendMode.name_of(layer.blend_mode)) continue return result
def blend_mode(self, value): self._record.blend_mode = BlendMode(value)