Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
1.9.14 (2020-07-10)
-------------------

- [api] Bugfix for PSDImage composite layer_filter option.
- [api] Bugfix for transparency and alpha distinction.
- [psd] Rename COMPOSITOR_INFO.
- [composite] Fix stroke effect target shape.

1.9.13 (2020-05-25)
-------------------

Expand Down
39 changes: 28 additions & 11 deletions src/psd_tools/api/numpy_io.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import numpy as np
import logging

from psd_tools.constants import ChannelID, Tag, ColorMode
from psd_tools.constants import ChannelID, Tag, ColorMode, Resource

logger = logging.getLogger(__name__)

Expand All @@ -26,7 +26,8 @@ def get_array(layer, channel):


def get_image_data(psd, channel):
if (channel == 'mask') or (channel == 'shape' and not has_alpha(psd)):
if (channel == 'mask'
) or (channel == 'shape' and not has_transparency(psd)):
return np.ones((psd.height, psd.width, 1), dtype=np.float32)

lut = None
Expand All @@ -42,7 +43,7 @@ def get_image_data(psd, channel):
data = _remove_background(data, psd)

if channel == 'shape':
return data[:, :, -1:]
return np.expand_dims(data[:, :, get_transparency_index(psd)], 2)
elif channel == 'color':
if psd.color_mode == ColorMode.MULTICHANNEL:
return data
Expand Down Expand Up @@ -76,25 +77,25 @@ def _find_channel(layer, width, height, condition):
)
elif channel == 'shape':
return _find_channel(
layer, layer.width,
layer.height, lambda x: x.id == ChannelID.TRANSPARENCY_MASK
layer, layer.width, layer.height,
lambda x: x.id == ChannelID.TRANSPARENCY_MASK
)
elif channel == 'mask':
if layer.mask._has_real():
channel_id = ChannelID.REAL_USER_LAYER_MASK
else:
channel_id = ChannelID.USER_LAYER_MASK
return _find_channel(
layer, layer.mask.width,
layer.mask.height, lambda x: x.id == channel_id
layer, layer.mask.width, layer.mask.height,
lambda x: x.id == channel_id
)

color = _find_channel(
layer, layer.width, layer.height, lambda x: x.id >= 0
)
shape = _find_channel(
layer, layer.width,
layer.height, lambda x: x.id == ChannelID.TRANSPARENCY_MASK
layer, layer.width, layer.height,
lambda x: x.id == ChannelID.TRANSPARENCY_MASK
)
if shape is None:
return color
Expand All @@ -111,15 +112,31 @@ def get_pattern(pattern):
axis=1).reshape((height, width, -1))


def has_alpha(psd):
def has_transparency(psd):
keys = (
Tag.SAVING_MERGED_TRANSPARENCY,
Tag.SAVING_MERGED_TRANSPARENCY16,
Tag.SAVING_MERGED_TRANSPARENCY32,
)
if psd.tagged_blocks and any(key in psd.tagged_blocks for key in keys):
return True
return psd.channels > EXPECTED_CHANNELS.get(psd.color_mode)
if psd.channels > EXPECTED_CHANNELS.get(psd.color_mode):
alpha_ids = psd.image_resources.get_data(Resource.ALPHA_IDENTIFIERS)
if alpha_ids and all(x > 0 for x in alpha_ids):
return False
return True
return False


def get_transparency_index(psd):
alpha_ids = psd.image_resources.get_data(Resource.ALPHA_IDENTIFIERS)
if alpha_ids:
try:
offset = alpha_ids.index(0)
return psd.channels - len(alpha_ids) + offset
except ValueError:
pass
return -1 # Assume the last channel is the transparency


def _parse_array(data, depth, lut=None):
Expand Down
9 changes: 3 additions & 6 deletions src/psd_tools/api/pil_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import io

from psd_tools.constants import ColorMode, ChannelID, Resource
from .numpy_io import has_alpha
from .numpy_io import has_transparency, get_transparency_index

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -51,9 +51,6 @@ def get_pil_channels(pil_mode):

def convert_image_data_to_pil(psd, channel, apply_icc):
"""Convert ImageData to PIL Image.

.. note:: Image resources contain extra alpha channels in these keys:
`ALPHA_NAMES_UNICODE`, `ALPHA_NAMES_PASCAL`, `ALPHA_IDENTIFIERS`.
"""
from PIL import Image

Expand All @@ -74,8 +71,8 @@ def convert_image_data_to_pil(psd, channel, apply_icc):
if channel is None:
channels = [_create_image(size, c, psd.depth) for c in channel_data]

if has_alpha(psd):
alpha = channels[-1]
if has_transparency(psd):
alpha = channels[get_transparency_index(psd)]

if psd.color_mode == ColorMode.INDEXED:
image = channels[0]
Expand Down
2 changes: 1 addition & 1 deletion src/psd_tools/api/psd_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def composite(
:return: :py:class:`PIL.Image`.
"""
from psd_tools.composite import composite_pil
if not (ignore_preview or force) and self.has_preview():
if not (ignore_preview or force or layer_filter) and self.has_preview():
return self.topil()
return composite_pil(self, color, alpha, viewport, layer_filter, force)

Expand Down
7 changes: 4 additions & 3 deletions src/psd_tools/composite/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def composite_pil(
):
from PIL import Image
from psd_tools.api.pil_io import get_pil_mode
from psd_tools.api.numpy_io import has_alpha
from psd_tools.api.numpy_io import has_transparency

UNSUPPORTED_MODES = {
ColorMode.DUOTONE,
Expand All @@ -46,7 +46,7 @@ def composite_pil(
skip_alpha = (
color_mode not in (ColorMode.GRAYSCALE, ColorMode.RGB) or (
layer.kind == 'psdimage' and layer.has_preview() and
not has_alpha(layer)
not has_transparency(layer)
)
)
if not skip_alpha:
Expand Down Expand Up @@ -211,7 +211,8 @@ def apply(self, layer):
self._apply_color_overlay(layer, color, shape, alpha)
self._apply_pattern_overlay(layer, color, shape, alpha)
self._apply_gradient_overlay(layer, color, shape, alpha)
if layer.has_vector_mask():
if ((self._force and layer.has_vector_mask()) or (
not layer.has_pixels()) and has_fill(layer)):
self._apply_stroke_effect(layer, color, shape_mask, alpha)
else:
self._apply_stroke_effect(layer, color, shape, alpha)
Expand Down
2 changes: 1 addition & 1 deletion src/psd_tools/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ class Tag(bytes, Enum):
CHANNEL_MIXER = b'mixr'
COLOR_BALANCE = b'blnc'
COLOR_LOOKUP = b'clrL'
COMPUTER_INFO = b'cinf' # Undocumented.
COMPOSITOR_INFO = b'cinf' # Undocumented.
CONTENT_GENERATOR_EXTRA_DATA = b'CgEd'
CURVES = b'curv'
EFFECTS_LAYER = b'lrFX'
Expand Down
4 changes: 2 additions & 2 deletions src/psd_tools/psd/tagged_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
Tag.BLEND_CLIPPING_ELEMENTS: ByteElement,
Tag.BLEND_FILL_OPACITY: ByteElement,
Tag.BLEND_INTERIOR_ELEMENTS: ByteElement,
Tag.COMPUTER_INFO: DescriptorBlock,
Tag.COMPOSITOR_INFO: DescriptorBlock,
Tag.CONTENT_GENERATOR_EXTRA_DATA: DescriptorBlock,
Tag.EFFECTS_LAYER: EffectsLayer,
Tag.EXPORT_SETTING1: DescriptorBlock,
Expand Down Expand Up @@ -223,7 +223,7 @@ class TaggedBlock(BaseElement):
Tag.UNICODE_PATH_NAME,
Tag.EXPORT_SETTING1,
Tag.EXPORT_SETTING2,
Tag.COMPUTER_INFO,
Tag.COMPOSITOR_INFO,
Tag.ARTBOARD_DATA2,
}

Expand Down
2 changes: 1 addition & 1 deletion src/psd_tools/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.9.13'
__version__ = '1.9.14'
13 changes: 11 additions & 2 deletions tests/psd_tools/composite/test_composite.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def test_composite_viewport():
('grayscale', 8, 'L', False),
('index_color', 8, 'P', False),
('rgb', 8, 'RGB', False),
('rgba', 8, 'RGBA', False),
('rgba', 8, 'RGB', False), # Extra alpha is not transparency
('lab', 8, 'LAB', False),
('multichannel', 16, 'L', False),
('bitmap', 1, '1', True),
Expand All @@ -153,7 +153,7 @@ def test_composite_viewport():
('grayscale', 8, 'L', True),
('index_color', 8, 'RGB', True),
('rgb', 8, 'RGB', True),
('rgba', 8, 'RGBA', True),
('rgba', 8, 'RGB', True), # Extra alpha is not transparency
('lab', 8, 'LAB', True),
('multichannel', 16, 'L', True),
]
Expand All @@ -169,6 +169,15 @@ def test_composite_pil(colormode, depth, mode, ignore_preview):
assert isinstance(layer.composite(), Image.Image)


def test_composite_layer_filter():
psd = PSDImage.open(full_name('colormodes/4x4_8bit_rgba.psd'))
# Check layer_filter.
rendered = psd.composite(layer_filter=lambda x: False)
reference = psd.topil()
assert all(a != b for a, b in zip(
rendered.getextrema(), reference.getextrema()))


def test_apply_mask():
from PIL import Image
psd = PSDImage.open(full_name('masks/2.psd'))
Expand Down
2 changes: 1 addition & 1 deletion tests/psd_tools/composite/test_effects.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
('effects/stroke-effects.psd', ),
('effects/shape-fx2.psd', ),
])
@pytest.mark.xfail(strict=True) # TODO: Fix me!
@pytest.mark.xfail
def test_stroke_effects(filename):
err = check_composite_quality(filename, threshold=0.01)

Expand Down