Skip to content

Commit

Permalink
Merge pull request #119 from psd-tools/1.8.20
Browse files Browse the repository at this point in the history
1.8.20
  • Loading branch information
kyamagu committed Jun 13, 2019
2 parents 247fc81 + 1ffb2e4 commit 0dab74d
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 23 deletions.
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
1.8.20 (2019-06-13)
-------------------

- support gradient styles.

1.8.19 (2019-06-11)
-------------------

Expand Down
10 changes: 8 additions & 2 deletions src/psd_tools/api/layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -556,11 +556,15 @@ def __init__(self, *args):

@property
def _setting(self):
# Can be None.
return self.tagged_blocks.get_data(Tag.SECTION_DIVIDER_SETTING)

@property
def blend_mode(self):
return self._setting.blend_mode
setting = self._setting
if setting:
return self._setting.blend_mode
return super(Group, self).blend_mode

@blend_mode.setter
def blend_mode(self, value):
Expand All @@ -569,7 +573,9 @@ def blend_mode(self, value):
self._record.blend_mode = BlendMode.NORMAL
else:
self._record.blend_mode = _value
self._setting.blend_mode = _value
setting = self._setting
if setting:
setting.blend_mode = _value


class Artboard(Group):
Expand Down
81 changes: 61 additions & 20 deletions src/psd_tools/composer/vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,20 +145,38 @@ def draw_pattern_fill(image, psd, setting, mode=None):
def draw_gradient_fill(image, setting, mode=None):
try:
import numpy as np
from scipy import interpolate
except ImportError:
logger.error('Gradient fill requires numpy and scipy.')
return None

angle = float(setting.get(b'Angl'))
gradient_kind = setting.get(b'Type').get_name()
if gradient_kind == 'Linear':
Z = _make_linear_gradient(image.width, image.height, -angle)
angle = float(setting.get(Key.Angle, 0))
scale = float(setting.get(Key.Scale, 100.)) / 100.
scale *= min(image.height, image.width)
X, Y = np.meshgrid(
np.linspace(-image.width / scale, image.width / scale, image.width),
np.linspace(-image.height / scale, image.height / scale, image.height),
)

gradient_kind = setting.get(Key.Type).enum
if gradient_kind == Enum.Linear:
Z = _make_linear_gradient(X, Y, angle)
elif gradient_kind == Enum.Radial:
Z = _make_radial_gradient(X, Y)
elif gradient_kind == Enum.Angle:
Z = _make_angle_gradient(X, Y, angle)
elif gradient_kind == Enum.Reflected:
Z = _make_reflected_gradient(X, Y, angle)
elif gradient_kind == Enum.Diamond:
Z = _make_diamond_gradient(X, Y, angle)
else:
logger.warning('Only linear gradient is supported.')
logger.warning('Unknown gradient style: %s.' % (gradient_kind))
Z = np.ones((image.height, image.width)) * 0.5

gradient_image = _apply_color_map(image.mode, setting.get(b'Grad'), Z)
Z = np.maximum(0, np.minimum(1, Z))
if bool(setting.get(Key.Reverse, False)):
Z = 1 - Z

gradient_image = _apply_color_map(image.mode, setting.get(Key.Gradient), Z)
if gradient_image:
if mode:
if image.mode.endswith('A'):
Expand All @@ -168,21 +186,43 @@ def draw_gradient_fill(image, setting, mode=None):
image.paste(gradient_image)


def _make_linear_gradient(width, height, angle=90.):
def _make_linear_gradient(X, Y, angle):
"""Generates index map for linear gradients."""
import numpy as np
X, Y = np.meshgrid(np.linspace(0, 1, width), np.linspace(0, 1, height))
theta = np.radians(angle % 360)
c, s = np.cos(theta), np.sin(theta)
if 0 <= theta and theta < 0.5 * np.pi:
Z = np.abs(c * X + s * Y)
elif 0.5 * np.pi <= theta and theta < np.pi:
Z = np.abs(c * (X - width) + s * Y)
elif np.pi <= theta and theta < 1.5 * np.pi:
Z = np.abs(c * (X - width) + s * (Y - height))
elif 1.5 * np.pi <= theta and theta < 2.0 * np.pi:
Z = np.abs(c * X + s * (Y - height))
return (Z - Z.min()) / (Z.max() - Z.min())
Z = .5 * (np.cos(theta) * X - np.sin(theta) * Y + 1)
return Z


def _make_radial_gradient(X, Y):
"""Generates index map for radial gradients."""
import numpy as np
Z = np.sqrt(np.power(X, 2) + np.power(Y, 2))
return Z


def _make_angle_gradient(X, Y, angle):
"""Generates index map for angle gradients."""
import numpy as np
Z = (((180 * np.arctan2(Y, X) / np.pi) + angle) % 360) / 360
return Z


def _make_reflected_gradient(X, Y, angle):
"""Generates index map for reflected gradients."""
import numpy as np
theta = np.radians(angle % 360)
Z = np.abs((np.cos(theta) * X - np.sin(theta) * Y))
return Z


def _make_diamond_gradient(X, Y, angle):
"""Generates index map for diamond gradients."""
import numpy as np
theta = np.radians(angle % 360)
Z = np.abs(np.cos(theta) * X - np.sin(theta) *
Y) + np.abs(np.sin(theta) * X + np.cos(theta) * Y)
return Z


def _apply_color_map(mode, grad, Z):
Expand Down Expand Up @@ -220,7 +260,7 @@ def _apply_color_map(mode, grad, Z):

rng = np.random.RandomState(seed)
G = rng.binomial(1, .5, (256, len(maximum))).astype(np.float)
size = int(roughness * 4)
size = max(1, 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)
Expand All @@ -244,6 +284,7 @@ def _apply_color_map(mode, grad, Z):
if len(stops) == 1:
X = [0., 1.]
Y = [Y[0], Y[0]]
# TODO: Workaround zero-division.
G = interpolate.interp1d(X, Y, axis=0, fill_value='extrapolate')
pixels = G(Z).astype(np.uint8)
if pixels.shape[-1] == 1:
Expand Down
1 change: 1 addition & 0 deletions src/psd_tools/psd/tagged_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,7 @@ def read(cls, fp, **kwargs):
elif key in (b'cust', b'cmls', b'extn', b'mlst'):
data = DescriptorBlock.frombytes(data, padding=4)
else:
# TODO: Unknown b'tmln' key.
message = 'Unknown metadata key %r' % (key)
logger.warning(message)
warn(message)
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.8.19'
__version__ = '1.8.20'
Binary file added tests/psd_files/gradient-styles.psd
Binary file not shown.
10 changes: 10 additions & 0 deletions tests/psd_tools/composer/test_vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,13 @@ def test_draw_gradient_fill():

setting.get(b'Type').enum = Enum.Radial.value
draw_gradient_fill(image, setting)


def test_gradient_styles():
psd = PSDImage.open(full_name('gradient-styles.psd'))
for artboard in psd[0:3]:
for layer in artboard:
setting = layer.tagged_blocks.get_data(Tag.GRADIENT_FILL_SETTING)
reference = layer.compose().convert('RGB')
rendered = layer.compose(force=True).convert('RGB')
assert _calculate_hash_error(reference, rendered) <= 0.1

0 comments on commit 0dab74d

Please sign in to comment.