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
7 changes: 5 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
1.8.18 (2019-06-05)
1.8.19 (2019-06-11)
-------------------

- fix broken `psd_tools.composer.vector` module in 1.8.17.
- fix broken `psd_tools.composer.vector` module in 1.8.17;
- experimental support for color noise gradient;
- bugfix for clip masks;
- bugfix for CMYK composing.

1.8.17 (2019-06-05)
-------------------
Expand Down
4 changes: 2 additions & 2 deletions src/psd_tools/api/pil_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,13 @@ def convert_pattern_to_pil(pattern, version=1):
_create_channel(size, c.get_data(version), c.pixel_depth).convert('L')
for c in pattern.data.channels if c.is_written
]
if len(channels) == len(mode) + 1:
if mode in ('RGB', 'L') and len(channels) == len(mode) + 1:
mode += 'A' # TODO: Perhaps doesn't work for some modes.
if mode == 'P':
image = channels[0]
image.putpalette([x for rgb in pattern.color_table for x in rgb])
else:
image = Image.merge(mode, channels)
image = Image.merge(mode, channels[:len(mode)])
if mode == 'CMYK':
image = image.point(lambda x: 255 - x)
return image
Expand Down
12 changes: 8 additions & 4 deletions src/psd_tools/composer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,9 @@ def compose_layer(layer, force=False, **kwargs):
clip_box = Group.extract_bbox(layer.clip_layers)
inter_box = intersect(layer.bbox, clip_box)
if inter_box != (0, 0, 0, 0):
clip_image = compose(layer.clip_layers, bbox=layer.bbox)
offset = image.info.get('offset', layer.offset)
bbox = offset + (offset[0] + image.width, offset[1] + image.height)
clip_image = compose(layer.clip_layers, bbox=bbox)
mask = image.getchannel('A')
if clip_image.mode.endswith('A'):
mask = ImageChops.multiply(clip_image.getchannel('A'), mask)
Expand Down Expand Up @@ -239,9 +241,11 @@ def apply_mask(layer, image):
image_.paste(image, (layer.left - bbox[0], layer.top - bbox[1]))
mask = Image.new('L', size, color=color)
mask_image = layer.mask.topil()
mask.paste(
mask_image, (mask_bbox[0] - bbox[0], mask_bbox[1] - bbox[1])
)
if mask_image:
mask.paste(
mask_image,
(mask_bbox[0] - bbox[0], mask_bbox[1] - bbox[1])
)
if image_.mode.endswith('A'):
mask = ImageChops.darker(image_.getchannel('A'), mask)
image_.putalpha(mask)
Expand Down
120 changes: 82 additions & 38 deletions src/psd_tools/composer/vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from psd_tools.api.pil_io import convert_pattern_to_pil
from psd_tools.composer.blend import blend
from psd_tools.terminology import Enum, Key

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -88,7 +89,8 @@ def draw_solid_color_fill(image, setting, mode=None):
draw.rectangle((0, 0, canvas.width, canvas.height), fill=color)
del draw
if mode:
canvas.putalpha(image.getchannel('A'))
if image.mode.endswith('A'):
canvas.putalpha(image.getchannel('A'))
blend(image, canvas, (0, 0), mode=mode)
else:
image.paste(canvas)
Expand Down Expand Up @@ -122,7 +124,10 @@ def draw_pattern_fill(image, psd, setting, mode=None):
panel.putalpha(opacity)

pattern_image = Image.new(image.mode, image.size)
mask = image.getchannel('A') if mode else Image.new('L', image.size, 255)
if mode and image.mode.endswith('A'):
mask = image.getchannel('A')
else:
mask = Image.new('L', image.size, 255)

for left in range(0, pattern_image.width, panel.width):
for top in range(0, pattern_image.height, panel.height):
Expand Down Expand Up @@ -154,11 +159,13 @@ def draw_gradient_fill(image, setting, mode=None):
Z = np.ones((image.height, image.width)) * 0.5

gradient_image = _apply_color_map(image.mode, setting.get(b'Grad'), Z)
if mode:
gradient_image.putalpha(image.getchannel('A'))
blend(image, gradient_image, (0, 0), mode=mode)
else:
image.paste(gradient_image)
if gradient_image:
if mode:
if image.mode.endswith('A'):
gradient_image.putalpha(image.getchannel('A'))
blend(image, gradient_image, (0, 0), mode=mode)
else:
image.paste(gradient_image)


def _make_linear_gradient(width, height, angle=90.):
Expand All @@ -184,38 +191,75 @@ def _apply_color_map(mode, grad, Z):
from scipy import interpolate
from PIL import Image

stops = grad.get(b'Clrs')
scalar = {
'RGB': 1.0,
'L': 2.55,
'CMYK': 2.55,
}.get(mode, 1.0)

X = [stop.get(b'Lctn').value / 4096. for stop in stops]
Y = [
tuple(int(scalar * x.value) for x in stop.get(b'Clr ').values())
for stop in stops
]
if len(stops) == 1:
X = [0., 1.]
Y = [Y[0], Y[0]]
G = interpolate.interp1d(X, Y, axis=0, fill_value='extrapolate')
pixels = G(Z).astype(np.uint8)
if pixels.shape[-1] == 1:
pixels = pixels[:, :, 0]

image = Image.fromarray(pixels, mode.rstrip('A'))
if b'Trns' in grad and mode.endswith('A'):
stops = grad.get(b'Trns')
X = [stop.get(b'Lctn').value / 4096 for stop in stops]
Y = [stop.get(b'Opct').value * 2.55 for stop in stops]
if grad.get(b'GrdF').enum == Enum.ColorNoise:
"""
TODO: Improve noise gradient quality.

Example:

Descriptor(b'Grdn'){
'Nm ': 'Custom\x00',
'GrdF': (b'GrdF', b'ClNs'),
'ShTr': False,
'VctC': False,
'ClrS': (b'ClrS', b'RGBC'),
'RndS': 3650322,
'Smth': 2048,
'Mnm ': [0, 0, 0, 0],
'Mxm ': [0, 100, 100, 100]
}
"""
logger.debug('Noise gradient is not accurate.')
from scipy.ndimage.filters import maximum_filter1d, uniform_filter1d
roughness = grad.get(
Key.Smoothness
).value / 4096. # Larger is sharper.
maximum = np.array([x.value for x in grad.get(Key.Maximum)])
minimum = np.array([x.value for x in grad.get(Key.Minimum)])
seed = grad.get(Key.RandomSeed).value

rng = np.random.RandomState(seed)
G = rng.binomial(1, .5, (256, len(maximum))).astype(np.float)
size = int(roughness * 4)
G = maximum_filter1d(G, size, axis=0)
G = uniform_filter1d(G, size * 64, axis=0)
G = (2.55 * ((maximum - minimum) * G + minimum)).astype(np.uint8)
Z = (255 * Z).astype(np.uint8)
pixels = G[Z]
if pixels.shape[-1] == 1:
pixels = pixels[:, :, 0]
image = Image.fromarray(pixels, mode)
elif grad.get(b'GrdF').enum == Enum.CustomStops:
scalar = {
'RGB': 1.0,
'L': 2.55,
'CMYK': 2.55,
}.get(mode, 1.0)
stops = grad.get(b'Clrs')
X = [stop.get(b'Lctn').value / 4096. for stop in stops]
Y = [
tuple(int(scalar * x.value) for x in stop.get(b'Clr ').values())
for stop in stops
]
if len(stops) == 1:
X = [0., 1.]
Y = [Y[0], Y[0]]
G_opacity = interpolate.interp1d(
X, Y, axis=0, fill_value='extrapolate'
)
alpha = G_opacity(Z).astype(np.uint8)
image.putalpha(Image.fromarray(alpha, 'L'))

G = interpolate.interp1d(X, Y, axis=0, fill_value='extrapolate')
pixels = G(Z).astype(np.uint8)
if pixels.shape[-1] == 1:
pixels = pixels[:, :, 0]

image = Image.fromarray(pixels, mode.rstrip('A'))
if b'Trns' in grad and mode.endswith('A'):
stops = grad.get(b'Trns')
X = [stop.get(b'Lctn').value / 4096 for stop in stops]
Y = [stop.get(b'Opct').value * 2.55 for stop in stops]
if len(stops) == 1:
X = [0., 1.]
Y = [Y[0], Y[0]]
G_opacity = interpolate.interp1d(
X, Y, axis=0, fill_value='extrapolate'
)
alpha = G_opacity(Z).astype(np.uint8)
image.putalpha(Image.fromarray(alpha, 'L'))
return image
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.8.18'
__version__ = '1.8.19'
1 change: 1 addition & 0 deletions tests/psd_tools/composer/test_composer.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
'background-red-opacity-80.psd',
),
('32bit.psd', ),
('clipping-mask2.psd', ),
]


Expand Down