Source

psd-tools / src / psd_tools / user_api / psd_image.py

Diff from to

src/psd_tools/user_api/psd_image.py

 import weakref              # FIXME: there should be weakrefs in this module
 import psd_tools.reader
 import psd_tools.decoder
-from psd_tools.constants import TaggedBlock, SectionDivider
+from psd_tools.constants import TaggedBlock, SectionDivider, BlendMode
 from psd_tools.user_api.layers import group_layers
 from psd_tools.user_api import pymaging_support
 from psd_tools.user_api import pil_support
         """
         return combined_bbox(self.layers)
 
+
+    def as_PIL(self):
+        """
+        Returns a PIL image for this group.
+        This is highly experimental.
+        """
+        return merge_layers(self.layers, respect_visibility=True)
+
     def _add_layer(self, child):
         self.layers.append(child)
 
         """
         return pil_support.extract_composite_image(self.decoded_data)
 
+    def as_PIL_merged(self):
+        """
+        Returns a PIL image for this PSD file.
+        Image is obtained by merging all layers.
+        This is highly experimental.
+        """
+        bbox = BBox(0, 0, self.header.width, self.header.height)
+        return merge_layers(self.layers, bbox=bbox)
+
     def as_pymaging(self):
         """
         Returns a pymaging.Image for this PSD file.
         return pymaging_support.extract_layer_image(self.decoded_data, index)
 
 
+class _RootGroup(Group):
+    """ A fake group for holding all layers """
+
+    @property
+    def visible(self):
+        return True
+
+    @property
+    def visible_global(self):
+        return True
+
+
 def combined_bbox(layers):
     """
     Returns a bounding box for ``layers`` or None if this is not possible.
     return BBox(min(lefts), min(tops), max(rights), max(bottoms))
 
 
-class _RootGroup(Group):
-    """ A fake group for holding all layers """
+def merge_layers(layers, respect_visibility=True, skip_layer=lambda layer: False, bbox=None):
+    """
+    Merges layers together (the first layer is on top).
 
-    @property
-    def visible(self):
-        return True
+    By default hidden layers are not rendered;
+    pass ``respect_visibility=False`` to render them.
 
-    @property
-    def visible_global(self):
-        return True
+    In order to skip some layers pass ``skip_layer`` function which
+    should take ``layer` as an argument and return True or False.
+
+    If ``crop_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 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':
+                result.paste(layer_image, (x,y), layer_image)
+            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