psd 1.5.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/README.md +2 -1
- data/lib/psd.rb +2 -1
- data/lib/psd/blend_mode.rb +5 -1
- data/lib/psd/channel_image.rb +6 -4
- data/lib/psd/header.rb +8 -0
- data/lib/psd/helpers.rb +13 -1
- data/lib/psd/image.rb +1 -1
- data/lib/psd/image_exports/png.rb +2 -70
- data/lib/psd/image_formats/layer_raw.rb +1 -1
- data/lib/psd/image_formats/raw.rb +2 -4
- data/lib/psd/image_modes/cmyk.rb +16 -6
- data/lib/psd/layer/helpers.rb +1 -1
- data/lib/psd/layer_info/fill_opacity.rb +2 -2
- data/lib/psd/logger.rb +7 -1
- data/lib/psd/node.rb +6 -2
- data/lib/psd/node_group.rb +9 -1
- data/lib/psd/node_root.rb +16 -3
- data/lib/psd/nodes/build_preview.rb +7 -69
- data/lib/psd/nodes/search.rb +11 -10
- data/lib/psd/renderer.rb +91 -0
- data/lib/psd/renderer/blender.rb +53 -0
- data/lib/psd/renderer/canvas.rb +95 -0
- data/lib/psd/renderer/canvas_management.rb +26 -0
- data/lib/psd/renderer/clipping_mask.rb +41 -0
- data/lib/psd/{compose.rb → renderer/compose.rb} +23 -19
- data/lib/psd/renderer/layer_styles.rb +56 -0
- data/lib/psd/renderer/layer_styles/color_overlay.rb +65 -0
- data/lib/psd/renderer/layer_styles/drop_shadow.rb +75 -0
- data/lib/psd/renderer/mask.rb +68 -0
- data/lib/psd/resources/guides.rb +35 -0
- data/lib/psd/version.rb +1 -1
- data/psd.gemspec +3 -3
- data/spec/files/blendmodes.psd +0 -0
- data/spec/files/empty-layer-subgroups.psd +0 -0
- data/spec/files/guides.psd +0 -0
- data/spec/guides_spec.rb +34 -0
- data/spec/hierarchy_spec.rb +27 -3
- data/spec/image_spec.rb +36 -35
- data/spec/parsing_spec.rb +13 -0
- metadata +23 -7
- data/lib/psd/clipping_mask.rb +0 -49
- data/lib/psd/layer_styles.rb +0 -84
data/lib/psd/nodes/search.rb
CHANGED
@@ -26,7 +26,7 @@ class PSD
|
|
26
26
|
alias :children_with_path :children_at_path
|
27
27
|
|
28
28
|
# Given a layer comp ID, name, or :last for last document state, create a new
|
29
|
-
# tree
|
29
|
+
# tree with layer/group visibility altered based on the layer comp.
|
30
30
|
def filter_by_comp(id)
|
31
31
|
if id.is_a?(String)
|
32
32
|
comp = psd.layer_comps.select { |c| c[:name] == id }.first
|
@@ -46,18 +46,19 @@ class PSD
|
|
46
46
|
private
|
47
47
|
|
48
48
|
def filter_for_comp!(id, node)
|
49
|
-
|
49
|
+
# Force layers to be visible if they are enabled for the comp
|
50
|
+
node.children.each do |c|
|
51
|
+
enabled = c.visible?
|
52
|
+
|
50
53
|
c
|
51
54
|
.metadata
|
52
|
-
.data[:layer_comp]['layerSettings'].
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
end
|
55
|
+
.data[:layer_comp]['layerSettings'].each do |l|
|
56
|
+
enabled = l['enab'] if l.has_key?('enab')
|
57
|
+
break if l['compList'].include?(id)
|
58
|
+
end
|
57
59
|
|
58
|
-
|
59
|
-
c
|
60
|
-
filter_for_comp!(id, c) if c.is_a?(PSD::Node::Group)
|
60
|
+
c.force_visible = enabled
|
61
|
+
filter_for_comp!(id, c) if c.group?
|
61
62
|
end
|
62
63
|
end
|
63
64
|
end
|
data/lib/psd/renderer.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
require_relative 'renderer/canvas_management'
|
2
|
+
|
3
|
+
class PSD
|
4
|
+
class Renderer
|
5
|
+
include CanvasManagement
|
6
|
+
|
7
|
+
def initialize(node)
|
8
|
+
@root_node = node
|
9
|
+
|
10
|
+
# Our canvas always starts as the full document size because
|
11
|
+
# all measurements are relative to this size. We can later crop
|
12
|
+
# the image if needed.
|
13
|
+
@width = @root_node.document_dimensions[0].to_i
|
14
|
+
@height = @root_node.document_dimensions[1].to_i
|
15
|
+
|
16
|
+
@canvas_stack = []
|
17
|
+
@node_stack = [@root_node]
|
18
|
+
|
19
|
+
@rendered = false
|
20
|
+
end
|
21
|
+
|
22
|
+
def render!
|
23
|
+
PSD.logger.debug "Beginning render process"
|
24
|
+
|
25
|
+
# Create our base canvas
|
26
|
+
create_group_canvas(active_node, active_node.width, active_node.height)
|
27
|
+
|
28
|
+
# Begin the rendering process
|
29
|
+
execute_pipeline
|
30
|
+
|
31
|
+
@rendered = true
|
32
|
+
end
|
33
|
+
|
34
|
+
def execute_pipeline
|
35
|
+
PSD.logger.debug "Executing pipeline on #{active_node.debug_name}"
|
36
|
+
children.reverse.each do |child|
|
37
|
+
# We skip over hidden nodes. Maybe something configurable in the future?
|
38
|
+
next unless child.visible?
|
39
|
+
|
40
|
+
if child.group?
|
41
|
+
push_node(child)
|
42
|
+
|
43
|
+
if child.passthru_blending?
|
44
|
+
PSD.logger.debug "#{child.name} is a group with passthru blending"
|
45
|
+
execute_pipeline
|
46
|
+
else
|
47
|
+
PSD.logger.debug "#{child.name} is a group with #{child.blending_mode} blending"
|
48
|
+
|
49
|
+
create_group_canvas(child)
|
50
|
+
execute_pipeline
|
51
|
+
|
52
|
+
child_canvas = pop_canvas
|
53
|
+
child_canvas.paint_to active_canvas
|
54
|
+
end
|
55
|
+
|
56
|
+
pop_node and next
|
57
|
+
end
|
58
|
+
|
59
|
+
canvas = Canvas.new(child)
|
60
|
+
canvas.paint_to active_canvas
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_png
|
65
|
+
render! unless @rendered
|
66
|
+
active_canvas.canvas
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def children
|
72
|
+
if active_node.layer?
|
73
|
+
[active_node]
|
74
|
+
else
|
75
|
+
active_node.children
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def push_node(node)
|
80
|
+
@node_stack << node
|
81
|
+
end
|
82
|
+
|
83
|
+
def pop_node
|
84
|
+
@node_stack.pop
|
85
|
+
end
|
86
|
+
|
87
|
+
def active_node
|
88
|
+
@node_stack.last
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
class PSD
|
2
|
+
class Renderer
|
3
|
+
class Blender
|
4
|
+
attr_reader :fg, :bg
|
5
|
+
|
6
|
+
# Takes a foreground Canvas and a background Canvas
|
7
|
+
def initialize(fg, bg)
|
8
|
+
@fg = fg
|
9
|
+
@bg = bg
|
10
|
+
|
11
|
+
@opacity = @fg.opacity.to_i
|
12
|
+
@fill_opacity = @fg.fill_opacity.to_i
|
13
|
+
PSD.logger.debug "Blender: name = #{fg.node.name}, opacity = #{@opacity}, fill opacity = #{@fill_opacity}"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Composes the foreground Canvas onto the background Canvas using the
|
17
|
+
# blending mode specified by the foreground.
|
18
|
+
def compose!
|
19
|
+
PSD.logger.debug "Composing #{fg.node.debug_name} onto #{bg.node.debug_name} with #{fg.node.blending_mode} blending"
|
20
|
+
|
21
|
+
offset_x = fg.left - bg.left
|
22
|
+
offset_y = fg.top - bg.top
|
23
|
+
|
24
|
+
fg.height.times do |y|
|
25
|
+
fg.width.times do |x|
|
26
|
+
base_x = x + offset_x
|
27
|
+
base_y = y + offset_y
|
28
|
+
|
29
|
+
next if base_x < 0 || base_y < 0 || base_x >= bg.width || base_y >= bg.height
|
30
|
+
|
31
|
+
color = Compose.send(
|
32
|
+
fg.node.blending_mode,
|
33
|
+
fg.canvas[x, y],
|
34
|
+
bg.canvas[base_x, base_y],
|
35
|
+
compose_options
|
36
|
+
)
|
37
|
+
|
38
|
+
bg.canvas[base_x, base_y] = color
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def compose_options
|
46
|
+
{
|
47
|
+
opacity: @opacity,
|
48
|
+
fill_opacity: @fill_opacity
|
49
|
+
}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
class PSD
|
2
|
+
class Renderer
|
3
|
+
class Canvas
|
4
|
+
attr_reader :canvas, :node, :width, :height, :left, :right, :top, :bottom, :opacity, :fill_opacity
|
5
|
+
|
6
|
+
def initialize(node, width = nil, height = nil, color = ChunkyPNG::Color::TRANSPARENT)
|
7
|
+
@node = node
|
8
|
+
@pixel_data = @node.group? ? [] : @node.image.pixel_data
|
9
|
+
|
10
|
+
@width = (width || @node.width).to_i
|
11
|
+
@height = (height || @node.height).to_i
|
12
|
+
@left = @node.left.to_i
|
13
|
+
@right = @node.right.to_i
|
14
|
+
@top = @node.top.to_i
|
15
|
+
@bottom = @node.bottom.to_i
|
16
|
+
|
17
|
+
@opacity = @node.opacity.to_f
|
18
|
+
@fill_opacity = @node.fill_opacity.to_f
|
19
|
+
|
20
|
+
@canvas = ChunkyPNG::Canvas.new(@width, @height, color)
|
21
|
+
|
22
|
+
initialize_canvas unless @node.group?
|
23
|
+
end
|
24
|
+
|
25
|
+
def paint_to(base)
|
26
|
+
PSD.logger.debug "Painting #{node.name} to #{base.node.debug_name}"
|
27
|
+
|
28
|
+
apply_mask
|
29
|
+
apply_clipping_mask
|
30
|
+
apply_layer_styles
|
31
|
+
apply_layer_opacity
|
32
|
+
compose_pixels(base)
|
33
|
+
end
|
34
|
+
|
35
|
+
def canvas=(canvas)
|
36
|
+
@canvas = canvas
|
37
|
+
@width = @canvas.width
|
38
|
+
@height = @canvas.height
|
39
|
+
end
|
40
|
+
|
41
|
+
def [](x, y); @canvas[x, y]; end
|
42
|
+
def []=(x, y, value); @canvas[x, y] = value; end
|
43
|
+
|
44
|
+
def method_missing(method, *args, &block)
|
45
|
+
@canvas.send(method, *args, &block)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def initialize_canvas
|
51
|
+
PSD.logger.debug "Initializing canvas for #{node.debug_name}"
|
52
|
+
|
53
|
+
# Sorry, ChunkyPNG.
|
54
|
+
@canvas.send(:replace_canvas!, width, height, @pixel_data)
|
55
|
+
|
56
|
+
# This can now be referenced by @canvas.pixels
|
57
|
+
@pixel_data = nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def apply_mask
|
61
|
+
return unless @node.image.has_mask?
|
62
|
+
|
63
|
+
PSD.logger.debug "Applying layer mask to #{node.name}"
|
64
|
+
Mask.new(self).apply!
|
65
|
+
end
|
66
|
+
|
67
|
+
def apply_clipping_mask
|
68
|
+
return unless @node.clipped?
|
69
|
+
ClippingMask.new(self).apply!
|
70
|
+
end
|
71
|
+
|
72
|
+
def apply_layer_styles
|
73
|
+
PSD.logger.debug "Applying layer styles to #{node.name}"
|
74
|
+
LayerStyles.new(self).apply!
|
75
|
+
end
|
76
|
+
|
77
|
+
def apply_layer_opacity
|
78
|
+
return if @node.root?
|
79
|
+
PSD.logger.debug "Adjusting opacity for #{node.name}"
|
80
|
+
|
81
|
+
@node.ancestors.each do |parent|
|
82
|
+
break unless parent.passthru_blending?
|
83
|
+
@opacity = (@opacity * parent.opacity.to_f) / 255.0
|
84
|
+
end
|
85
|
+
|
86
|
+
PSD.logger.debug "Inherited opacity for #{@node.debug_name} is #{@opacity}"
|
87
|
+
@opacity = @opacity.to_i
|
88
|
+
end
|
89
|
+
|
90
|
+
def compose_pixels(base)
|
91
|
+
Blender.new(self, base).compose!
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class PSD
|
2
|
+
class Renderer
|
3
|
+
module CanvasManagement
|
4
|
+
def active_canvas
|
5
|
+
@canvas_stack.last
|
6
|
+
end
|
7
|
+
|
8
|
+
def create_group_canvas(node, width=@width, height=@height)
|
9
|
+
PSD.logger.debug "Group canvas created. Node = #{node.name || ":root:"}, width = #{width}, height = #{height}"
|
10
|
+
push_canvas Canvas.new(node, width, height)
|
11
|
+
end
|
12
|
+
|
13
|
+
def push_canvas(canvas)
|
14
|
+
@canvas_stack << canvas
|
15
|
+
end
|
16
|
+
|
17
|
+
def pop_canvas
|
18
|
+
@canvas_stack.pop
|
19
|
+
end
|
20
|
+
|
21
|
+
def stack_inspect
|
22
|
+
@canvas_stack.map { |c| c.node.name || ":root:" }.join("\n")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class PSD
|
2
|
+
class Renderer
|
3
|
+
class ClippingMask
|
4
|
+
attr_reader :canvas, :mask
|
5
|
+
|
6
|
+
def initialize(canvas)
|
7
|
+
@canvas = canvas
|
8
|
+
@node = @canvas.node
|
9
|
+
|
10
|
+
mask_node = @canvas.node.next_sibling
|
11
|
+
@mask = Canvas.new(mask_node)
|
12
|
+
end
|
13
|
+
|
14
|
+
def apply!
|
15
|
+
return unless @node.clipped?
|
16
|
+
|
17
|
+
PSD.logger.debug "Applying clipping mask #{mask.node.name} to #{@node.name}"
|
18
|
+
|
19
|
+
@canvas.height.times do |y|
|
20
|
+
@canvas.width.times do |x|
|
21
|
+
doc_x = @canvas.left + x
|
22
|
+
doc_y = @canvas.top + y
|
23
|
+
|
24
|
+
mask_x = doc_x - @mask.left
|
25
|
+
mask_y = doc_y - @mask.top
|
26
|
+
|
27
|
+
if mask_x < 0 || mask_x > mask.width || mask_y < 0 || mask_y > mask.height
|
28
|
+
alpha = 0
|
29
|
+
else
|
30
|
+
pixel = mask.canvas.pixels[mask_y * mask.width + mask_x]
|
31
|
+
alpha = pixel.nil? ? 0 : ChunkyPNG::Color.a(pixel)
|
32
|
+
end
|
33
|
+
|
34
|
+
color = @canvas[x, y]
|
35
|
+
@canvas[x, y] = (color & 0xffffff00) | (ChunkyPNG::Color.a(color) * alpha / 255)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -15,9 +15,8 @@ class PSD
|
|
15
15
|
# Normal blend modes
|
16
16
|
#
|
17
17
|
|
18
|
-
# Normal composition, delegate to ChunkyPNG
|
19
18
|
def normal(fg, bg, opts={})
|
20
|
-
return fg if fully_transparent?(bg)
|
19
|
+
return apply_opacity(fg, opts) if fully_transparent?(bg)
|
21
20
|
return bg if fully_transparent?(fg)
|
22
21
|
|
23
22
|
mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
|
@@ -27,13 +26,14 @@ class PSD
|
|
27
26
|
|
28
27
|
rgba(new_r, new_g, new_b, dst_alpha)
|
29
28
|
end
|
29
|
+
alias_method :passthru, :normal
|
30
30
|
|
31
31
|
#
|
32
32
|
# Subtractive blend modes
|
33
33
|
#
|
34
34
|
|
35
35
|
def darken(fg, bg, opts={})
|
36
|
-
return fg if fully_transparent?(bg)
|
36
|
+
return apply_opacity(fg, opts) if fully_transparent?(bg)
|
37
37
|
return bg if fully_transparent?(fg)
|
38
38
|
|
39
39
|
mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
|
@@ -45,7 +45,7 @@ class PSD
|
|
45
45
|
end
|
46
46
|
|
47
47
|
def multiply(fg, bg, opts={})
|
48
|
-
return fg if fully_transparent?(bg)
|
48
|
+
return apply_opacity(fg, opts) if fully_transparent?(bg)
|
49
49
|
return bg if fully_transparent?(fg)
|
50
50
|
|
51
51
|
mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
|
@@ -57,7 +57,7 @@ class PSD
|
|
57
57
|
end
|
58
58
|
|
59
59
|
def color_burn(fg, bg, opts={})
|
60
|
-
return fg if fully_transparent?(bg)
|
60
|
+
return apply_opacity(fg, opts) if fully_transparent?(bg)
|
61
61
|
return bg if fully_transparent?(fg)
|
62
62
|
|
63
63
|
mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
|
@@ -79,7 +79,7 @@ class PSD
|
|
79
79
|
end
|
80
80
|
|
81
81
|
def linear_burn(fg, bg, opts={})
|
82
|
-
return fg if fully_transparent?(bg)
|
82
|
+
return apply_opacity(fg, opts) if fully_transparent?(bg)
|
83
83
|
return bg if fully_transparent?(fg)
|
84
84
|
|
85
85
|
mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
|
@@ -96,7 +96,7 @@ class PSD
|
|
96
96
|
#
|
97
97
|
|
98
98
|
def lighten(fg, bg, opts={})
|
99
|
-
return fg if fully_transparent?(bg)
|
99
|
+
return apply_opacity(fg, opts) if fully_transparent?(bg)
|
100
100
|
return bg if fully_transparent?(fg)
|
101
101
|
|
102
102
|
mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
|
@@ -109,7 +109,7 @@ class PSD
|
|
109
109
|
end
|
110
110
|
|
111
111
|
def screen(fg, bg, opts={})
|
112
|
-
return fg if fully_transparent?(bg)
|
112
|
+
return apply_opacity(fg, opts) if fully_transparent?(bg)
|
113
113
|
return bg if fully_transparent?(fg)
|
114
114
|
|
115
115
|
mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
|
@@ -122,7 +122,7 @@ class PSD
|
|
122
122
|
end
|
123
123
|
|
124
124
|
def color_dodge(fg, bg, opts={})
|
125
|
-
return fg if fully_transparent?(bg)
|
125
|
+
return apply_opacity(fg, opts) if fully_transparent?(bg)
|
126
126
|
return bg if fully_transparent?(fg)
|
127
127
|
|
128
128
|
mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
|
@@ -139,7 +139,7 @@ class PSD
|
|
139
139
|
end
|
140
140
|
|
141
141
|
def linear_dodge(fg, bg, opts={})
|
142
|
-
return fg if fully_transparent?(bg)
|
142
|
+
return apply_opacity(fg, opts) if fully_transparent?(bg)
|
143
143
|
return bg if fully_transparent?(fg)
|
144
144
|
|
145
145
|
mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
|
@@ -157,7 +157,7 @@ class PSD
|
|
157
157
|
#
|
158
158
|
|
159
159
|
def overlay(fg, bg, opts={})
|
160
|
-
return fg if fully_transparent?(bg)
|
160
|
+
return apply_opacity(fg, opts) if fully_transparent?(bg)
|
161
161
|
return bg if fully_transparent?(fg)
|
162
162
|
|
163
163
|
mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
|
@@ -178,7 +178,7 @@ class PSD
|
|
178
178
|
end
|
179
179
|
|
180
180
|
def soft_light(fg, bg, opts={})
|
181
|
-
return fg if fully_transparent?(bg)
|
181
|
+
return apply_opacity(fg, opts) if fully_transparent?(bg)
|
182
182
|
return bg if fully_transparent?(fg)
|
183
183
|
|
184
184
|
mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
|
@@ -197,7 +197,7 @@ class PSD
|
|
197
197
|
end
|
198
198
|
|
199
199
|
def hard_light(fg, bg, opts={})
|
200
|
-
return fg if fully_transparent?(bg)
|
200
|
+
return apply_opacity(fg, opts) if fully_transparent?(bg)
|
201
201
|
return bg if fully_transparent?(fg)
|
202
202
|
|
203
203
|
mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
|
@@ -218,7 +218,7 @@ class PSD
|
|
218
218
|
end
|
219
219
|
|
220
220
|
def vivid_light(fg, bg, opts={})
|
221
|
-
return fg if fully_transparent?(bg)
|
221
|
+
return apply_opacity(fg, opts) if fully_transparent?(bg)
|
222
222
|
return bg if fully_transparent?(fg)
|
223
223
|
|
224
224
|
mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
|
@@ -239,7 +239,7 @@ class PSD
|
|
239
239
|
end
|
240
240
|
|
241
241
|
def linear_light(fg, bg, opts={})
|
242
|
-
return fg if fully_transparent?(bg)
|
242
|
+
return apply_opacity(fg, opts) if fully_transparent?(bg)
|
243
243
|
return bg if fully_transparent?(fg)
|
244
244
|
|
245
245
|
mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
|
@@ -260,7 +260,7 @@ class PSD
|
|
260
260
|
end
|
261
261
|
|
262
262
|
def pin_light(fg, bg, opts={})
|
263
|
-
return fg if fully_transparent?(bg)
|
263
|
+
return apply_opacity(fg, opts) if fully_transparent?(bg)
|
264
264
|
return bg if fully_transparent?(fg)
|
265
265
|
|
266
266
|
mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
|
@@ -281,7 +281,7 @@ class PSD
|
|
281
281
|
end
|
282
282
|
|
283
283
|
def hard_mix(fg, bg, opts={})
|
284
|
-
return fg if fully_transparent?(bg)
|
284
|
+
return apply_opacity(fg, opts) if fully_transparent?(bg)
|
285
285
|
return bg if fully_transparent?(fg)
|
286
286
|
|
287
287
|
mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
|
@@ -298,7 +298,7 @@ class PSD
|
|
298
298
|
#
|
299
299
|
|
300
300
|
def difference(fg, bg, opts={})
|
301
|
-
return fg if fully_transparent?(bg)
|
301
|
+
return apply_opacity(fg, opts) if fully_transparent?(bg)
|
302
302
|
return bg if fully_transparent?(fg)
|
303
303
|
|
304
304
|
mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
|
@@ -311,7 +311,7 @@ class PSD
|
|
311
311
|
end
|
312
312
|
|
313
313
|
def exclusion(fg, bg, opts={})
|
314
|
-
return fg if fully_transparent?(bg)
|
314
|
+
return apply_opacity(fg, opts) if fully_transparent?(bg)
|
315
315
|
return bg if fully_transparent?(fg)
|
316
316
|
|
317
317
|
mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
|
@@ -346,6 +346,10 @@ class PSD
|
|
346
346
|
opts[:opacity] * opts[:fill_opacity] / 255
|
347
347
|
end
|
348
348
|
|
349
|
+
def apply_opacity(color, opts)
|
350
|
+
(color & 0xffffff00) | ((color & 0x000000ff) * calculate_opacity(opts) / 255)
|
351
|
+
end
|
352
|
+
|
349
353
|
def blend_channel(bg, fg, alpha)
|
350
354
|
((bg << 8) + (fg - bg) * alpha) >> 8
|
351
355
|
end
|