def compose_layer(layer, force=False, **kwargs): """Compose a single layer with pixels.""" from PIL import Image, ImageChops assert layer.bbox != (0, 0, 0, 0), 'Layer bbox is (0, 0, 0, 0)' image = layer.topil(**kwargs) if image is None or force: texture = create_fill(layer) if texture is not None: image = texture if image is None: return image # TODO: Group should have the following too. # Apply vector mask. if layer.has_vector_mask() and (force or not layer.has_pixels()): vector_mask = draw_vector_mask(layer) if image.mode.endswith('A'): offset = vector_mask.info['offset'] vector_mask = ImageChops.darker(image.getchannel('A'), vector_mask) vector_mask.info['offset'] = offset image.putalpha(vector_mask) # Apply stroke. if layer.has_stroke() and layer.stroke.enabled: image = draw_stroke(image, layer, vector_mask) # Apply mask. image = apply_mask(layer, image) # Apply layer fill effects. effect_base = image.copy() apply_opacity(image, layer.tagged_blocks.get_data(Tag.BLEND_FILL_OPACITY, 255)) image = apply_effect(layer, image, effect_base) # Clip layers. if layer.has_clip_layers(): clip_box = Group.extract_bbox(layer.clip_layers) offset = image.info.get('offset', layer.offset) bbox = offset + (offset[0] + image.width, offset[1] + image.height) if intersect(bbox, clip_box) != (0, 0, 0, 0): clip_image = compose(layer.clip_layers, bbox=bbox, context=image.copy()) if image.mode.endswith('A'): mask = image.getchannel('A') else: mask = Image.new('L', image.size, 255) if clip_image.mode.endswith('A'): mask = ImageChops.darker(clip_image.getchannel('A'), mask) clip_image.putalpha(mask) image = blend(image, clip_image, (0, 0)) # Apply opacity. apply_opacity(image, layer.opacity) return image
def test_compose_artboard(): psd = PSDImage.open(full_name('artboard.psd')) document_image = psd.compose() assert document_image.size == psd.size artboard = psd[0] artboard_image = artboard.compose() assert artboard_image.size == artboard.size assert artboard.size != Group.extract_bbox(artboard)
def _init(self): """Initialize layer structure.""" group_stack = [self] clip_stack = [] last_layer = None for record, channels in self._record._iter_layers(): current_group = group_stack[-1] blocks = record.tagged_blocks end_of_group = False divider = blocks.get_data('SECTION_DIVIDER_SETTING', None) divider = blocks.get_data('NESTED_SECTION_DIVIDER_SETTING', divider) if divider is not None: if divider.kind == SectionDivider.BOUNDING_SECTION_DIVIDER: layer = Group(self, None, None, current_group) group_stack.append(layer) elif divider.kind in (SectionDivider.OPEN_FOLDER, SectionDivider.CLOSED_FOLDER): layer = group_stack.pop() assert layer is not self layer._record = record layer._channels = channels end_of_group = True elif ('TYPE_TOOL_OBJECT_SETTING' in blocks or 'TYPE_TOOL_INFO' in blocks): layer = TypeLayer(self, record, channels, current_group) elif (record.flags.pixel_data_irrelevant and ('VECTOR_ORIGINATION_DATA' in blocks or 'VECTOR_MASK_SETTING1' in blocks or 'VECTOR_MASK_SETTING2' in blocks or 'VECTOR_STROKE_DATA' in blocks or 'VECTOR_STROKE_CONTENT_DATA' in blocks)): layer = ShapeLayer(self, record, channels, current_group) elif ('SMART_OBJECT_LAYER_DATA1' in blocks or 'SMART_OBJECT_LAYER_DATA2' in blocks or 'PLACED_LAYER1' in blocks or 'PLACED_LAYER2' in blocks): layer = SmartObjectLayer(self, record, channels, current_group) else: layer = None for key in adjustments.TYPES.keys(): if key in blocks: layer = adjustments.TYPES[key](self, record, channels, current_group) break # If nothing applies, this is a pixel layer. if layer is None: layer = PixelLayer(self, record, channels, current_group) if record.clipping == Clipping.NON_BASE: clip_stack.append(layer) else: if clip_stack: last_layer._clip_layers = clip_stack clip_stack = [] if not end_of_group: current_group._layers.append(layer) last_layer = layer if clip_stack and last_layer: last_layer._clip_layers = clip_stack
def _apply_layer_ops(layer, image, force=False, bbox=None): """Apply layer masks, effects, and clipping.""" from PIL import Image, ImageChops # Apply vector mask. if layer.has_vector_mask() and (force or not layer.has_pixels()): offset = image.info.get('offset', layer.offset) mask_box = offset + (offset[0] + image.width, offset[1] + image.height) vector_mask = draw_vector_mask(layer, mask_box) if image.mode.endswith('A'): offset = vector_mask.info['offset'] vector_mask = ImageChops.darker(image.getchannel('A'), vector_mask) vector_mask.info['offset'] = offset image.putalpha(vector_mask) # Apply stroke. if layer.has_stroke() and layer.stroke.enabled: image = draw_stroke(image, layer, vector_mask) # Apply mask. image = apply_mask(layer, image, bbox=bbox) # Apply layer fill effects. apply_opacity( image, layer.tagged_blocks.get_data(Tag.BLEND_FILL_OPACITY, 255) ) if layer.effects.enabled: image = apply_effect(layer, image, image.copy()) # Clip layers. if layer.has_clip_layers(): clip_box = Group.extract_bbox(layer.clip_layers) offset = image.info.get('offset', layer.offset) bbox = offset + (offset[0] + image.width, offset[1] + image.height) if intersect(bbox, clip_box) != (0, 0, 0, 0): clip_image = compose( layer.clip_layers, force=force, bbox=bbox, context=image.copy() ) if image.mode.endswith('A'): mask = image.getchannel('A') else: mask = Image.new('L', image.size, 255) if clip_image.mode.endswith('A'): mask = ImageChops.darker(clip_image.getchannel('A'), mask) clip_image.putalpha(mask) image = blend(image, clip_image, (0, 0)) # Apply opacity. apply_opacity(image, layer.opacity) return image
def test_group_extract_bbox(): psd = PSDImage.open(full_name('hidden-groups.psd')) assert Group.extract_bbox(psd[1:], False) == (40, 72, 83, 134) assert Group.extract_bbox(psd[1:], True) == (25, 34, 83, 134)
def _init(self): """Initialize layer structure.""" group_stack = [self] clip_stack = [] last_layer = None for record, channels in self._record._iter_layers(): current_group = group_stack[-1] blocks = record.tagged_blocks end_of_group = False layer = None divider = blocks.get_data(Tag.SECTION_DIVIDER_SETTING, None) divider = blocks.get_data(Tag.NESTED_SECTION_DIVIDER_SETTING, divider) if divider is not None: if divider.kind == SectionDivider.BOUNDING_SECTION_DIVIDER: layer = Group(self, None, None, current_group) group_stack.append(layer) elif divider.kind in (SectionDivider.OPEN_FOLDER, SectionDivider.CLOSED_FOLDER): layer = group_stack.pop() assert layer is not self layer._record = record layer._channels = channels for key in (Tag.ARTBOARD_DATA1, Tag.ARTBOARD_DATA2, Tag.ARTBOARD_DATA3): if key in blocks: layer = Artboard._move(layer) end_of_group = True else: logger.warning('Divider %s found.' % divider.kind) elif (Tag.TYPE_TOOL_OBJECT_SETTING in blocks or Tag.TYPE_TOOL_INFO in blocks): layer = TypeLayer(self, record, channels, current_group) elif (Tag.SMART_OBJECT_LAYER_DATA1 in blocks or Tag.SMART_OBJECT_LAYER_DATA2 in blocks or Tag.PLACED_LAYER1 in blocks or Tag.PLACED_LAYER2 in blocks): layer = SmartObjectLayer(self, record, channels, current_group) else: for key in adjustments.TYPES.keys(): if key in blocks: layer = adjustments.TYPES[key](self, record, channels, current_group) break # If nothing applies, this is either a shape or pixel layer. if layer is None: if (record.flags.pixel_data_irrelevant and (Tag.VECTOR_ORIGINATION_DATA in blocks or Tag.VECTOR_MASK_SETTING1 in blocks or Tag.VECTOR_MASK_SETTING2 in blocks or Tag.VECTOR_STROKE_DATA in blocks or Tag.VECTOR_STROKE_CONTENT_DATA in blocks)): layer = ShapeLayer(self, record, channels, current_group) else: layer = PixelLayer(self, record, channels, current_group) assert layer is not None if record.clipping == Clipping.NON_BASE: clip_stack.append(layer) else: if clip_stack: last_layer._clip_layers = clip_stack clip_stack = [] if not end_of_group: current_group._layers.append(layer) last_layer = layer if clip_stack and last_layer: last_layer._clip_layers = clip_stack
def compose(layers, bbox=None, context=None, layer_filter=None, color=None, **kwargs): """ Compose layers to a single :py:class:`PIL.Image`. If the layers do not have visible pixels, the function returns `None`. Example:: image = compose([layer1, layer2]) In order to skip some layers, pass `layer_filter` function which should take `layer` as an argument and return `True` to keep the layer or return `False` to skip:: image = compose( layers, layer_filter=lambda x: x.is_visible() and x.kind == 'type' ) By default, visible layers are composed. .. note:: This function is experimental and does not guarantee Photoshop-quality rendering. Currently the following are ignored: - Adjustments layers - Layer effects - Blending mode (all blending modes become normal) Shape drawing is inaccurate if the PSD file is not saved with maximum compatibility. :param layers: a layer, or an iterable of layers. :param bbox: (left, top, bottom, right) tuple that specifies a region to compose. By default, all the visible area is composed. The origin is at the top-left corner of the PSD document. :param context: `PIL.Image` object for the backdrop rendering context. Must be used with the correct `bbox` size. :param layer_filter: a callable that takes a layer and returns `bool`. :param color: background color in `int` or `tuple`. :return: :py:class:`PIL.Image` or `None`. """ from PIL import Image if not hasattr(layers, '__iter__'): layers = [layers] def _default_filter(layer): return layer.is_visible() layer_filter = layer_filter or _default_filter valid_layers = [x for x in layers if layer_filter(x)] if len(valid_layers) == 0: return context if bbox is None: bbox = Group.extract_bbox(valid_layers) if bbox == (0, 0, 0, 0): return context if context is None: mode = get_pil_mode(valid_layers[0]._psd.color_mode, True) context = Image.new( mode, (bbox[2] - bbox[0], bbox[3] - bbox[1]), color=color if color is not None else 'white', ) context.putalpha(0) # Alpha must be forced to correctly blend. context.info['offset'] = (bbox[0], bbox[1]) for layer in valid_layers: if intersect(layer.bbox, bbox) == (0, 0, 0, 0): continue if layer.is_group(): if layer.blend_mode == BlendMode.PASS_THROUGH: context = layer.compose(context=context, bbox=bbox, **kwargs) continue else: image = layer.compose(**kwargs) else: image = compose_layer(layer, **kwargs) if image is None: continue logger.debug('Composing %s' % layer) offset = image.info.get('offset', layer.offset) offset = (offset[0] - bbox[0], offset[1] - bbox[1]) context = blend(context, image, offset, layer.blend_mode) return context