psd 2.1.2 → 3.1.2

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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/psd.rb +8 -6
  4. data/lib/psd/blend_mode.rb +46 -38
  5. data/lib/psd/channel_image.rb +9 -5
  6. data/lib/psd/descriptor.rb +39 -16
  7. data/lib/psd/header.rb +33 -32
  8. data/lib/psd/image_formats/rle.rb +4 -10
  9. data/lib/psd/image_modes/rgb.rb +4 -4
  10. data/lib/psd/layer.rb +1 -15
  11. data/lib/psd/layer/blend_modes.rb +12 -12
  12. data/lib/psd/layer/helpers.rb +8 -10
  13. data/lib/psd/layer/info.rb +9 -7
  14. data/lib/psd/layer/position_and_channels.rb +0 -4
  15. data/lib/psd/layer_info.rb +0 -4
  16. data/lib/psd/layer_info/blend_clipping_elements.rb +4 -2
  17. data/lib/psd/layer_info/blend_interior_elements.rb +4 -2
  18. data/lib/psd/layer_info/fill_opacity.rb +4 -2
  19. data/lib/psd/layer_info/layer_group.rb +4 -2
  20. data/lib/psd/layer_info/layer_id.rb +4 -2
  21. data/lib/psd/layer_info/layer_name_source.rb +4 -2
  22. data/lib/psd/layer_info/layer_section_divider.rb +4 -2
  23. data/lib/psd/layer_info/legacy_typetool.rb +5 -3
  24. data/lib/psd/layer_info/locked.rb +4 -2
  25. data/lib/psd/layer_info/metadata_setting.rb +4 -2
  26. data/lib/psd/layer_info/object_effects.rb +4 -2
  27. data/lib/psd/layer_info/pattern.rb +14 -0
  28. data/lib/psd/layer_info/placed_layer.rb +4 -2
  29. data/lib/psd/layer_info/reference_point.rb +4 -2
  30. data/lib/psd/layer_info/sheet_color.rb +18 -0
  31. data/lib/psd/layer_info/solid_color.rb +36 -0
  32. data/lib/psd/layer_info/typetool.rb +4 -2
  33. data/lib/psd/layer_info/unicode_name.rb +4 -2
  34. data/lib/psd/layer_info/vector_mask.rb +4 -2
  35. data/lib/psd/layer_info/vector_origination.rb +14 -0
  36. data/lib/psd/layer_info/vector_stroke.rb +4 -2
  37. data/lib/psd/layer_info/vector_stroke_content.rb +4 -2
  38. data/lib/psd/layer_mask.rb +2 -8
  39. data/lib/psd/lazy_execute.rb +5 -1
  40. data/lib/psd/node.rb +112 -48
  41. data/lib/psd/nodes/ancestry.rb +80 -75
  42. data/lib/psd/nodes/build_preview.rb +4 -4
  43. data/lib/psd/nodes/group.rb +35 -0
  44. data/lib/psd/nodes/layer.rb +40 -0
  45. data/lib/psd/nodes/root.rb +90 -0
  46. data/lib/psd/nodes/search.rb +19 -19
  47. data/lib/psd/path_record.rb +1 -71
  48. data/lib/psd/renderer.rb +6 -5
  49. data/lib/psd/renderer/blender.rb +10 -5
  50. data/lib/psd/renderer/cairo_helpers.rb +46 -0
  51. data/lib/psd/renderer/canvas.rb +39 -19
  52. data/lib/psd/renderer/canvas_management.rb +2 -2
  53. data/lib/psd/renderer/clipping_mask.rb +5 -4
  54. data/lib/psd/renderer/compose.rb +61 -68
  55. data/lib/psd/renderer/layer_styles.rb +15 -5
  56. data/lib/psd/renderer/layer_styles/color_overlay.rb +46 -27
  57. data/lib/psd/renderer/mask.rb +26 -22
  58. data/lib/psd/renderer/mask_canvas.rb +12 -0
  59. data/lib/psd/renderer/vector_shape.rb +239 -0
  60. data/lib/psd/resource_section.rb +4 -7
  61. data/lib/psd/resources.rb +4 -19
  62. data/lib/psd/resources/base.rb +27 -0
  63. data/lib/psd/resources/guides.rb +6 -4
  64. data/lib/psd/resources/layer_comps.rb +6 -4
  65. data/lib/psd/resources/slices.rb +7 -5
  66. data/lib/psd/version.rb +1 -1
  67. data/psd.gemspec +1 -2
  68. data/spec/files/blendmodes.psd +0 -0
  69. data/spec/hierarchy_spec.rb +5 -0
  70. metadata +27 -26
  71. data/lib/psd/layer_info/vector_mask_2.rb +0 -10
  72. data/lib/psd/node_exporting.rb +0 -20
  73. data/lib/psd/node_group.rb +0 -86
  74. data/lib/psd/node_layer.rb +0 -81
  75. data/lib/psd/node_root.rb +0 -93
  76. data/lib/psd/nodes/has_children.rb +0 -13
  77. data/lib/psd/nodes/lock_to_origin.rb +0 -7
  78. data/lib/psd/nodes/parse_layers.rb +0 -18
  79. data/lib/psd/renderer/layer_styles/drop_shadow.rb +0 -75
  80. data/lib/psd/section.rb +0 -26
@@ -0,0 +1,46 @@
1
+ class PSD
2
+ class Renderer
3
+ # Adapted from
4
+ # http://www.hokstad.com/simple-drawing-in-ruby-with-cairo
5
+ module CairoHelpers
6
+ def cairo_image_surface(w, h, bg=nil)
7
+ surface = Cairo::ImageSurface.new(w, h)
8
+ cr = Cairo::Context.new(surface)
9
+
10
+ if bg
11
+ cr.set_source_rgba(*bg)
12
+ cr.paint
13
+ end
14
+
15
+ yield cr
16
+
17
+ surface.finish
18
+
19
+ data = cr.target.data.to_s[0, 4 * w * h]
20
+
21
+ # Cairo data is stored as BGRA, ugh.
22
+ data = data.unpack("N*").map do |color|
23
+ color = ChunkyPNG::Color.to_truecolor_alpha_bytes(color)
24
+ ChunkyPNG::Color.rgba(color[2], color[1], color[0], color[3])
25
+ end
26
+
27
+ ChunkyPNG::Canvas.new(w, h, data)
28
+ end
29
+
30
+ def cairo_path(cr, *pairs)
31
+ first = true
32
+ pairs.each do |cmd|
33
+ if cmd == :c
34
+ cr.close_path
35
+ first = true
36
+ elsif first
37
+ cr.move_to(*cmd)
38
+ first = false
39
+ else
40
+ cr.curve_to(*cmd)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,34 +1,34 @@
1
1
  class PSD
2
2
  class Renderer
3
3
  class Canvas
4
- attr_reader :canvas, :node, :width, :height, :left, :right, :top, :bottom, :opacity, :fill_opacity
4
+ attr_reader :canvas, :node, :opts, :width, :height, :opacity, :fill_opacity
5
+
6
+ [:top, :right, :bottom, :left].each { |dir| delegate dir, to: :node }
5
7
 
6
- def initialize(node, width = nil, height = nil, color = ChunkyPNG::Color::TRANSPARENT)
8
+ delegate :[], :[]=, :get_pixel, :set_pixel, to: :canvas
9
+
10
+ def initialize(node, width = nil, height = nil, opts = {})
7
11
  @node = node
12
+ @opts = opts
8
13
  @pixel_data = @node.group? ? [] : @node.image.pixel_data
9
14
 
10
15
  @width = (width || @node.width).to_i
11
16
  @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
 
17
18
  @opacity = @node.opacity.to_f
18
19
  @fill_opacity = @node.fill_opacity.to_f
19
20
 
20
- @canvas = ChunkyPNG::Canvas.new(@width, @height, color)
21
-
22
- initialize_canvas unless @node.group?
21
+ initialize_canvas
23
22
  end
24
23
 
25
24
  def paint_to(base)
26
25
  PSD.logger.debug "Painting #{node.name} to #{base.node.debug_name}"
27
26
 
28
- apply_mask
27
+ apply_masks
29
28
  apply_clipping_mask
30
29
  apply_layer_styles
31
30
  apply_layer_opacity
31
+
32
32
  compose_pixels(base)
33
33
  end
34
34
 
@@ -38,9 +38,6 @@ class PSD
38
38
  @height = @canvas.height
39
39
  end
40
40
 
41
- def [](x, y); @canvas[x, y]; end
42
- def []=(x, y, value); @canvas[x, y] = value; end
43
-
44
41
  def method_missing(method, *args, &block)
45
42
  @canvas.send(method, *args, &block)
46
43
  end
@@ -48,20 +45,43 @@ class PSD
48
45
  private
49
46
 
50
47
  def initialize_canvas
51
- PSD.logger.debug "Initializing canvas for #{node.debug_name}"
48
+ PSD.logger.debug "Initializing canvas for #{node.debug_name}; color = #{ChunkyPNG::Color.to_truecolor_alpha_bytes(fill_color)}"
49
+
50
+ @canvas = ChunkyPNG::Canvas.new(@width, @height, fill_color)
51
+ return if @node.group? || has_fill? || @opts[:base]
52
52
 
53
53
  # Sorry, ChunkyPNG.
54
54
  @canvas.send(:replace_canvas!, width, height, @pixel_data)
55
-
55
+ ensure
56
56
  # This can now be referenced by @canvas.pixels
57
57
  @pixel_data = nil
58
58
  end
59
59
 
60
- def apply_mask
61
- return unless @node.image.has_mask?
60
+ def has_fill?
61
+ !@opts[:base] && @node.layer? && @node.solid_color
62
+ end
62
63
 
63
- PSD.logger.debug "Applying layer mask to #{node.name}"
64
- Mask.new(self).apply!
64
+ def fill_color
65
+ if has_fill?
66
+ @node.solid_color.color
67
+ else
68
+ ChunkyPNG::Color::TRANSPARENT
69
+ end
70
+ end
71
+
72
+ def apply_masks
73
+ ([@node] + @node.ancestors).each do |n|
74
+ next unless n.raster_mask?
75
+ break if n.group? && !n.passthru_blending?
76
+
77
+ if n.layer?
78
+ PSD.logger.debug "Applying raster mask to #{@node.name}"
79
+ Mask.new(self).apply!
80
+ else
81
+ PSD.logger.debug "Applying raster mask to #{@node.name} from #{n.name}"
82
+ Mask.new(self, n).apply!
83
+ end
84
+ end
65
85
  end
66
86
 
67
87
  def apply_clipping_mask
@@ -5,9 +5,9 @@ class PSD
5
5
  @canvas_stack.last
6
6
  end
7
7
 
8
- def create_group_canvas(node, width=@width, height=@height)
8
+ def create_group_canvas(node, width = @width, height = @height, opts = {})
9
9
  PSD.logger.debug "Group canvas created. Node = #{node.name || ":root:"}, width = #{width}, height = #{height}"
10
- push_canvas Canvas.new(node, width, height)
10
+ push_canvas Canvas.new(node, width, height, @opts.merge(opts))
11
11
  end
12
12
 
13
13
  def push_canvas(canvas)
@@ -7,8 +7,9 @@ class PSD
7
7
  @canvas = canvas
8
8
  @node = @canvas.node
9
9
 
10
- mask_node = @canvas.node.next_sibling
11
- @mask = Canvas.new(mask_node)
10
+ mask_node = @canvas.node.clipping_mask
11
+
12
+ @mask = MaskCanvas.new(mask_node)
12
13
  end
13
14
 
14
15
  def apply!
@@ -31,8 +32,8 @@ class PSD
31
32
  alpha = pixel.nil? ? 0 : ChunkyPNG::Color.a(pixel)
32
33
  end
33
34
 
34
- color = @canvas[x, y]
35
- @canvas[x, y] = (color & 0xffffff00) | (ChunkyPNG::Color.a(color) * alpha / 255)
35
+ color = @canvas.get_pixel(x, y)
36
+ @canvas.set_pixel x, y, (color & 0xffffff00) | (ChunkyPNG::Color.a(color) * alpha / 255)
36
37
  end
37
38
  end
38
39
  end
@@ -6,20 +6,15 @@ class PSD
6
6
  module Compose
7
7
  extend self
8
8
 
9
- DEFAULT_OPTS = {
10
- opacity: 255,
11
- fill_opacity: 255
12
- }
13
-
14
9
  #
15
10
  # Normal blend modes
16
11
  #
17
12
 
18
- def normal(fg, bg, opts={})
19
- return apply_opacity(fg, opts) if fully_transparent?(bg)
13
+ def normal(fg, bg, opacity)
14
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
20
15
  return bg if fully_transparent?(fg)
21
16
 
22
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
17
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
23
18
  new_r = blend_channel(r(bg), r(fg), mix_alpha)
24
19
  new_g = blend_channel(g(bg), g(fg), mix_alpha)
25
20
  new_b = blend_channel(b(bg), b(fg), mix_alpha)
@@ -32,11 +27,11 @@ class PSD
32
27
  # Subtractive blend modes
33
28
  #
34
29
 
35
- def darken(fg, bg, opts={})
36
- return apply_opacity(fg, opts) if fully_transparent?(bg)
30
+ def darken(fg, bg, opacity)
31
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
37
32
  return bg if fully_transparent?(fg)
38
33
 
39
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
34
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
40
35
  new_r = r(fg) <= r(bg) ? blend_channel(r(bg), r(fg), mix_alpha) : r(bg)
41
36
  new_g = g(fg) <= g(bg) ? blend_channel(g(bg), g(fg), mix_alpha) : g(bg)
42
37
  new_b = b(fg) <= b(bg) ? blend_channel(b(bg), b(fg), mix_alpha) : b(bg)
@@ -44,11 +39,11 @@ class PSD
44
39
  rgba(new_r, new_g, new_b, dst_alpha)
45
40
  end
46
41
 
47
- def multiply(fg, bg, opts={})
48
- return apply_opacity(fg, opts) if fully_transparent?(bg)
42
+ def multiply(fg, bg, opacity)
43
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
49
44
  return bg if fully_transparent?(fg)
50
45
 
51
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
46
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
52
47
  new_r = blend_channel(r(bg), r(fg) * r(bg) >> 8, mix_alpha)
53
48
  new_g = blend_channel(g(bg), g(fg) * g(bg) >> 8, mix_alpha)
54
49
  new_b = blend_channel(b(bg), b(fg) * b(bg) >> 8, mix_alpha)
@@ -56,11 +51,11 @@ class PSD
56
51
  rgba(new_r, new_g, new_b, dst_alpha)
57
52
  end
58
53
 
59
- def color_burn(fg, bg, opts={})
60
- return apply_opacity(fg, opts) if fully_transparent?(bg)
54
+ def color_burn(fg, bg, opacity)
55
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
61
56
  return bg if fully_transparent?(fg)
62
57
 
63
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
58
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
64
59
 
65
60
  calculate_foreground = Proc.new do |b, f|
66
61
  if f > 0
@@ -78,11 +73,11 @@ class PSD
78
73
  rgba(new_r, new_g, new_b, dst_alpha)
79
74
  end
80
75
 
81
- def linear_burn(fg, bg, opts={})
82
- return apply_opacity(fg, opts) if fully_transparent?(bg)
76
+ def linear_burn(fg, bg, opacity)
77
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
83
78
  return bg if fully_transparent?(fg)
84
79
 
85
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
80
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
86
81
 
87
82
  new_r = blend_channel(r(bg), (r(fg) < (255 - r(bg))) ? 0 : r(fg) - (255 - r(bg)), mix_alpha)
88
83
  new_g = blend_channel(g(bg), (g(fg) < (255 - g(bg))) ? 0 : g(fg) - (255 - g(bg)), mix_alpha)
@@ -95,11 +90,11 @@ class PSD
95
90
  # Additive blend modes
96
91
  #
97
92
 
98
- def lighten(fg, bg, opts={})
99
- return apply_opacity(fg, opts) if fully_transparent?(bg)
93
+ def lighten(fg, bg, opacity)
94
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
100
95
  return bg if fully_transparent?(fg)
101
96
 
102
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
97
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
103
98
 
104
99
  new_r = r(fg) >= r(bg) ? blend_channel(r(bg), r(fg), mix_alpha) : r(bg)
105
100
  new_g = g(fg) >= g(bg) ? blend_channel(g(bg), g(fg), mix_alpha) : g(bg)
@@ -108,11 +103,11 @@ class PSD
108
103
  rgba(new_r, new_g, new_b, dst_alpha)
109
104
  end
110
105
 
111
- def screen(fg, bg, opts={})
112
- return apply_opacity(fg, opts) if fully_transparent?(bg)
106
+ def screen(fg, bg, opacity)
107
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
113
108
  return bg if fully_transparent?(fg)
114
109
 
115
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
110
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
116
111
 
117
112
  new_r = blend_channel(r(bg), 255 - ((255 - r(bg)) * (255 - r(fg)) >> 8), mix_alpha)
118
113
  new_g = blend_channel(g(bg), 255 - ((255 - g(bg)) * (255 - g(fg)) >> 8), mix_alpha)
@@ -121,11 +116,11 @@ class PSD
121
116
  rgba(new_r, new_g, new_b, dst_alpha)
122
117
  end
123
118
 
124
- def color_dodge(fg, bg, opts={})
125
- return apply_opacity(fg, opts) if fully_transparent?(bg)
119
+ def color_dodge(fg, bg, opacity)
120
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
126
121
  return bg if fully_transparent?(fg)
127
122
 
128
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
123
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
129
124
 
130
125
  calculate_foreground = Proc.new do |b, f|
131
126
  f < 255 ? [(b << 8) / (255 - f), 255].min : b
@@ -138,11 +133,11 @@ class PSD
138
133
  rgba(new_r, new_g, new_b, dst_alpha)
139
134
  end
140
135
 
141
- def linear_dodge(fg, bg, opts={})
142
- return apply_opacity(fg, opts) if fully_transparent?(bg)
136
+ def linear_dodge(fg, bg, opacity)
137
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
143
138
  return bg if fully_transparent?(fg)
144
139
 
145
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
140
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
146
141
 
147
142
  new_r = blend_channel(r(bg), (r(bg) + r(fg)) > 255 ? 255 : r(bg) + r(fg), mix_alpha)
148
143
  new_g = blend_channel(g(bg), (g(bg) + g(fg)) > 255 ? 255 : g(bg) + g(fg), mix_alpha)
@@ -156,11 +151,11 @@ class PSD
156
151
  # Contrasting blend modes
157
152
  #
158
153
 
159
- def overlay(fg, bg, opts={})
160
- return apply_opacity(fg, opts) if fully_transparent?(bg)
154
+ def overlay(fg, bg, opacity)
155
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
161
156
  return bg if fully_transparent?(fg)
162
157
 
163
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
158
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
164
159
 
165
160
  calculate_foreground = Proc.new do |b, f|
166
161
  if b < 128
@@ -177,11 +172,11 @@ class PSD
177
172
  rgba(new_r, new_g, new_b, dst_alpha)
178
173
  end
179
174
 
180
- def soft_light(fg, bg, opts={})
181
- return apply_opacity(fg, opts) if fully_transparent?(bg)
175
+ def soft_light(fg, bg, opacity)
176
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
182
177
  return bg if fully_transparent?(fg)
183
178
 
184
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
179
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
185
180
 
186
181
  calculate_foreground = Proc.new do |b, f|
187
182
  c1 = b * f >> 8
@@ -196,11 +191,11 @@ class PSD
196
191
  rgba(new_r, new_g, new_b, dst_alpha)
197
192
  end
198
193
 
199
- def hard_light(fg, bg, opts={})
200
- return apply_opacity(fg, opts) if fully_transparent?(bg)
194
+ def hard_light(fg, bg, opacity)
195
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
201
196
  return bg if fully_transparent?(fg)
202
197
 
203
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
198
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
204
199
 
205
200
  calculate_foreground = Proc.new do |b, f|
206
201
  if f < 128
@@ -217,11 +212,11 @@ class PSD
217
212
  rgba(new_r, new_g, new_b, dst_alpha)
218
213
  end
219
214
 
220
- def vivid_light(fg, bg, opts={})
221
- return apply_opacity(fg, opts) if fully_transparent?(bg)
215
+ def vivid_light(fg, bg, opacity)
216
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
222
217
  return bg if fully_transparent?(fg)
223
218
 
224
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
219
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
225
220
 
226
221
  calculate_foreground = Proc.new do |b, f|
227
222
  if f < 255
@@ -238,11 +233,11 @@ class PSD
238
233
  rgba(new_r, new_g, new_b, dst_alpha)
239
234
  end
240
235
 
241
- def linear_light(fg, bg, opts={})
242
- return apply_opacity(fg, opts) if fully_transparent?(bg)
236
+ def linear_light(fg, bg, opacity)
237
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
243
238
  return bg if fully_transparent?(fg)
244
239
 
245
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
240
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
246
241
 
247
242
  calculate_foreground = Proc.new do |b, f|
248
243
  if b < 255
@@ -259,11 +254,11 @@ class PSD
259
254
  rgba(new_r, new_g, new_b, dst_alpha)
260
255
  end
261
256
 
262
- def pin_light(fg, bg, opts={})
263
- return apply_opacity(fg, opts) if fully_transparent?(bg)
257
+ def pin_light(fg, bg, opacity)
258
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
264
259
  return bg if fully_transparent?(fg)
265
260
 
266
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
261
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
267
262
 
268
263
  calculate_foreground = Proc.new do |b, f|
269
264
  if f >= 128
@@ -280,11 +275,11 @@ class PSD
280
275
  rgba(new_r, new_g, new_b, dst_alpha)
281
276
  end
282
277
 
283
- def hard_mix(fg, bg, opts={})
284
- return apply_opacity(fg, opts) if fully_transparent?(bg)
278
+ def hard_mix(fg, bg, opacity)
279
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
285
280
  return bg if fully_transparent?(fg)
286
281
 
287
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
282
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
288
283
 
289
284
  new_r = blend_channel(r(bg), (r(bg) + r(fg) <= 255) ? 0 : 255, mix_alpha)
290
285
  new_g = blend_channel(g(bg), (g(bg) + g(fg) <= 255) ? 0 : 255, mix_alpha)
@@ -297,11 +292,11 @@ class PSD
297
292
  # Inversion blend modes
298
293
  #
299
294
 
300
- def difference(fg, bg, opts={})
301
- return apply_opacity(fg, opts) if fully_transparent?(bg)
295
+ def difference(fg, bg, opacity)
296
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
302
297
  return bg if fully_transparent?(fg)
303
298
 
304
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
299
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
305
300
 
306
301
  new_r = blend_channel(r(bg), (r(bg) - r(fg)).abs, mix_alpha)
307
302
  new_g = blend_channel(g(bg), (g(bg) - g(fg)).abs, mix_alpha)
@@ -310,11 +305,11 @@ class PSD
310
305
  rgba(new_r, new_g, new_b, dst_alpha)
311
306
  end
312
307
 
313
- def exclusion(fg, bg, opts={})
314
- return apply_opacity(fg, opts) if fully_transparent?(bg)
308
+ def exclusion(fg, bg, opacity)
309
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
315
310
  return bg if fully_transparent?(fg)
316
311
 
317
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
312
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
318
313
 
319
314
  new_r = blend_channel(r(bg), r(bg) + r(fg) - (r(bg) * r(fg) >> 7), mix_alpha)
320
315
  new_g = blend_channel(g(bg), g(bg) + g(fg) - (g(bg) * g(fg) >> 7), mix_alpha)
@@ -323,16 +318,18 @@ class PSD
323
318
  rgba(new_r, new_g, new_b, dst_alpha)
324
319
  end
325
320
 
321
+ [:r, :g, :b, :a, :rgba, :fully_transparent?].each do |meth|
322
+ define_method(meth) { |*args| ChunkyPNG::Color.send(meth, *args) }
323
+ end
324
+
326
325
  # If the blend mode is missing, we fall back to normal composition.
327
326
  def method_missing(method, *args, &block)
328
- return ChunkyPNG::Color.send(method, *args) if ChunkyPNG::Color.respond_to?(method)
329
327
  normal(*args)
330
328
  end
331
329
 
332
330
  private
333
331
 
334
- def calculate_alphas(fg, bg, opts)
335
- opacity = calculate_opacity(opts)
332
+ def calculate_alphas(fg, bg, opacity)
336
333
  src_alpha = a(fg) * opacity >> 8
337
334
  dst_alpha = a(bg)
338
335
 
@@ -342,12 +339,8 @@ class PSD
342
339
  return mix_alpha, dst_alpha
343
340
  end
344
341
 
345
- def calculate_opacity(opts)
346
- opts[:opacity] * opts[:fill_opacity] / 255
347
- end
348
-
349
- def apply_opacity(color, opts)
350
- (color & 0xffffff00) | ((color & 0x000000ff) * calculate_opacity(opts) / 255)
342
+ def apply_opacity(color, opacity)
343
+ (color & 0xffffff00) | ((color & 0x000000ff) * opacity / 255)
351
344
  end
352
345
 
353
346
  def blend_channel(bg, fg, alpha)