def _apply_opacity(layer_image, layer): layer_opacity = layer.opacity if layer.has_tag(TaggedBlock.BLEND_FILL_OPACITY): layer_opacity *= layer.get_tag(TaggedBlock.BLEND_FILL_OPACITY) if layer_opacity == 255: return layer_image return pil_support.apply_opacity(layer_image, layer_opacity)
def final_image_as_PIL(self): """ blank_pic = new_blank_pic_as_PIL(self.bbox.width, self.bbox.height) if not self.visible: return blank_pic try: final_pic = Image.blend(blank_pic, self.image, self.opacity/255) return final_pic except ValueError: print(self.bbox) return blank_pic """ if not self.visible: blank_pic = new_blank_pic_as_PIL(self.bbox.width, self.bbox.height) return blank_pic return pil_support.apply_opacity(self._image, self.opacity)
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 compose(layers, respect_visibility=True, ignore_blend_mode=True, skip_layer=lambda layer: False, bbox=None): """ Compose layers to a single ``PIL.Image`` (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. Adjustment and layer effects are ignored. This is experimental. :param layers: a layer, or an iterable of layers :param respect_visibility: Take visibility flag into account :param ignore_blend_mode: Ignore blending mode :param skip_layer: skip composing the given layer if returns True :rtype: `PIL.Image` """ # FIXME: this currently assumes PIL if isinstance(layers, psd_tools.user_api.layers._RawLayer): layers = [layers] if bbox is None: bbox = combined_bbox(layers) if bbox.is_empty(): return None result = Image.new( "RGBA", (bbox.width, bbox.height), color=(255, 255, 255, 0) # fixme: transparency is incorrect ) for layer in reversed(layers): if skip_layer(layer) or not layer.has_box() or ( not layer.visible and respect_visibility): continue if layer.is_group(): layer_image = layer.as_PIL( respect_visibility=respect_visibility, ignore_blend_mode=ignore_blend_mode, skip_layer=skip_layer) else: layer_image = layer.as_PIL() if not layer_image: continue if not ignore_blend_mode and layer.blend_mode != "normal": logger.warning("Blend mode is not implemented: %s", layer.blend_mode) continue clip_image = None if len(layer.clip_layers): clip_box = combined_bbox(layer.clip_layers) if not clip_box.is_empty(): intersect = clip_box.intersect(layer.bbox) if not intersect.is_empty(): clip_image = compose( layer.clip_layers, respect_visibility, ignore_blend_mode, skip_layer) clip_image = clip_image.crop( intersect.offset((clip_box.x1, clip_box.y1))) clip_mask = layer_image.crop( intersect.offset((layer.bbox.x1, layer.bbox.y1))) layer_opacity = layer.opacity if layer.has_tag(TaggedBlock.BLEND_FILL_OPACITY): layer_opacity *= layer.get_tag(TaggedBlock.BLEND_FILL_OPACITY) layer_image = pil_support.apply_opacity(layer_image, layer_opacity) layer_image = _apply_coloroverlay(layer, layer_image) layer_offset = layer.bbox.offset((bbox.x1, bbox.y1)) mask = None if layer.has_mask(): mask_box = layer.mask.bbox if not layer.mask.disabled and not mask_box.is_empty(): mask_color = layer.mask.background_color mask = Image.new("L", layer_image.size, color=(mask_color,)) mask.paste( layer.mask.as_PIL(), mask_box.offset((layer.bbox.x1, layer.bbox.y1)) ) if layer_image.mode == 'RGBA': tmp = Image.new("RGBA", result.size, color=(255, 255, 255, 0)) tmp.paste(layer_image, layer_offset, mask=mask) result = Image.alpha_composite(result, tmp) elif layer_image.mode == 'RGB': result.paste(layer_image, layer_offset, mask=mask) else: logger.warning( "layer image mode is unsupported for merging: %s", layer_image.mode) continue if clip_image is not None: offset = (intersect.x1 - bbox.x1, intersect.y1 - bbox.y1) if clip_image.mode == 'RGBA': tmp = Image.new("RGBA", result.size, color=(255, 255, 255, 0)) tmp.paste(clip_image, offset, mask=clip_mask) result = Image.alpha_composite(result, tmp) elif clip_image.mode == 'RGB': result.paste(clip_image, offset, mask=clip_mask) return result
def merge_layers(layers, respect_visibility=True, ignore_blend_mode=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 experimental. """ # FIXME: this currently assumes PIL from PIL import Image if bbox is None: bbox = combined_bbox(layers) if bbox.is_empty(): return None result = Image.new( "RGBA", (bbox.width, bbox.height), color=(255, 255, 255, 0) # fixme: transparency is incorrect ) for layer in reversed(layers): if skip_layer(layer) or not layer.has_box() or (not layer.visible and respect_visibility): continue if layer.is_group(): layer_image = layer.as_PIL(respect_visibility=respect_visibility, ignore_blend_mode=ignore_blend_mode, skip_layer=skip_layer) else: layer_image = layer.as_PIL() if not layer_image: continue if not ignore_blend_mode and layer.blend_mode != "normal": logger.warning("Blend mode is not implemented: %s", layer.blend_mode) continue clip_image = None if len(layer.clip_layers): clip_box = combined_bbox(layer.clip_layers) if not clip_box.is_empty(): intersect = clip_box.intersect(layer.bbox) if not intersect.is_empty(): clip_image = merge_layers(layer.clip_layers, respect_visibility, ignore_blend_mode, skip_layer) clip_image = clip_image.crop( intersect.offset((clip_box.x1, clip_box.y1))) clip_mask = layer_image.crop( intersect.offset((layer.bbox.x1, layer.bbox.y1))) layer_image = pil_support.apply_opacity(layer_image, layer.opacity) layer_offset = layer.bbox.offset((bbox.x1, bbox.y1)) mask = None if layer.has_mask(): mask_box = layer.mask.bbox if not layer.mask.disabled and not mask_box.is_empty(): mask = Image.new("L", layer_image.size, color=(0, )) mask.paste(layer.mask.as_PIL(), mask_box.offset((layer.bbox.x1, layer.bbox.y1))) if layer_image.mode == 'RGBA': tmp = Image.new("RGBA", result.size, color=(255, 255, 255, 0)) tmp.paste(layer_image, layer_offset, mask=mask) result = Image.alpha_composite(result, tmp) elif layer_image.mode == 'RGB': result.paste(layer_image, layer_offset, mask=mask) else: logger.warning("layer image mode is unsupported for merging: %s", layer_image.mode) continue if clip_image is not None: offset = (intersect.x1 - bbox.x1, intersect.y1 - bbox.y1) if clip_image.mode == 'RGBA': tmp = Image.new("RGBA", result.size, color=(255, 255, 255, 0)) tmp.paste(clip_image, offset, mask=clip_mask) result = Image.alpha_composite(result, tmp) elif clip_image.mode == 'RGB': result.paste(clip_image, offset, mask=clip_mask) return result
def merge_layers(layers, respect_visibility=True, ignore_blend_mode=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 experimental. """ # FIXME: this currently assumes PIL from PIL import Image if bbox is None: bbox = combined_bbox(layers) if bbox.is_empty(): return None result = Image.new( "RGBA", (bbox.width, bbox.height), color=(255, 255, 255, 0) # fixme: transparency is incorrect ) for layer in reversed(layers): if skip_layer(layer) or not layer.has_box() or (not layer.visible and respect_visibility): continue if layer.is_group(): layer_image = layer.as_PIL(respect_visibility=respect_visibility, ignore_blend_mode=ignore_blend_mode, skip_layer=skip_layer) else: layer_image = layer.as_PIL() if not layer_image: continue if not ignore_blend_mode and layer.blend_mode != "normal": logger.warning("Blend mode is not implemented: %s", layer.blend_mode) continue clip_mask_exists = False if len(layer.clip_layers): clip_box = combined_bbox(layer.clip_layers) if not clip_box.is_empty(): intersect = clip_box.intersect(layer.bbox) if not intersect.is_empty(): clip_image = merge_layers(layer.clip_layers, respect_visibility, ignore_blend_mode, skip_layer) clip_image = clip_image.crop( intersect.offset((clip_box.x1, clip_box.y1))) clip_mask = layer_image.crop( intersect.offset((layer.bbox.x1, layer.bbox.y1))) clip_mask_exists = True 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_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 if clip_mask_exists: location = (intersect.x1 - bbox.x1, intersect.y1 - bbox.y1) if clip_image.mode == 'RGBA': tmp = Image.new("RGBA", result.size, color=(255, 255, 255, 0)) tmp.paste(clip_image, location, mask=clip_mask) result = Image.alpha_composite(result, tmp) elif clip_image.mode == 'RGB': result.paste(clip_image, location, mask=clip_mask) 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, 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 final_image_as_PIL(self): if not self.visible: blank_pic = new_blank_pic_as_PIL(self.bbox.width, self.bbox.height) return blank_pic return pil_support.apply_opacity(self.image, self.opacity)