psd 2.1.2 → 3.9.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.
Files changed (130) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +2 -0
  3. data/README.md +60 -20
  4. data/lib/psd/blend_mode.rb +46 -38
  5. data/lib/psd/channel_image.rb +11 -5
  6. data/lib/psd/color.rb +14 -5
  7. data/lib/psd/descriptor.rb +39 -16
  8. data/lib/psd/file.rb +9 -6
  9. data/lib/psd/header.rb +38 -33
  10. data/lib/psd/helpers.rb +2 -2
  11. data/lib/psd/image.rb +21 -16
  12. data/lib/psd/image_formats/layer_rle.rb +2 -2
  13. data/lib/psd/image_formats/rle.rb +4 -10
  14. data/lib/psd/image_modes/cmyk.rb +18 -5
  15. data/lib/psd/image_modes/greyscale.rb +6 -1
  16. data/lib/psd/image_modes/rgb.rb +17 -5
  17. data/lib/psd/layer/blend_modes.rb +15 -13
  18. data/lib/psd/layer/helpers.rb +8 -10
  19. data/lib/psd/layer/info/black_white.rb +33 -0
  20. data/lib/psd/{layer_info → layer/info}/blend_clipping_elements.rb +4 -2
  21. data/lib/psd/{layer_info → layer/info}/blend_interior_elements.rb +4 -2
  22. data/lib/psd/layer/info/brightness_contrast.rb +21 -0
  23. data/lib/psd/layer/info/channel_blending_restrictions.rb +27 -0
  24. data/lib/psd/layer/info/channel_mixer.rb +27 -0
  25. data/lib/psd/layer/info/color_balance.rb +23 -0
  26. data/lib/psd/layer/info/color_lookup.rb +14 -0
  27. data/lib/psd/layer/info/content_generator.rb +42 -0
  28. data/lib/psd/layer/info/curves.rb +78 -0
  29. data/lib/psd/layer/info/effects_layer.rb +174 -0
  30. data/lib/psd/layer/info/exposure.rb +20 -0
  31. data/lib/psd/{layer_info → layer/info}/fill_opacity.rb +4 -2
  32. data/lib/psd/layer/info/gradient_fill.rb +14 -0
  33. data/lib/psd/layer/info/gradient_map.rb +69 -0
  34. data/lib/psd/layer/info/hue_saturation.rb +41 -0
  35. data/lib/psd/layer/info/invert.rb +17 -0
  36. data/lib/psd/layer/info/knockout.rb +22 -0
  37. data/lib/psd/layer/info/layer_effects.rb +16 -0
  38. data/lib/psd/{layer_info → layer/info}/layer_group.rb +4 -2
  39. data/lib/psd/{layer_info → layer/info}/layer_id.rb +4 -2
  40. data/lib/psd/layer/info/layer_mask_as_global_mask.rb +16 -0
  41. data/lib/psd/{layer_info → layer/info}/layer_name_source.rb +4 -2
  42. data/lib/psd/{layer_info → layer/info}/layer_section_divider.rb +4 -2
  43. data/lib/psd/{layer_info → layer/info}/legacy_typetool.rb +6 -4
  44. data/lib/psd/layer/info/levels.rb +48 -0
  45. data/lib/psd/{layer_info → layer/info}/locked.rb +4 -2
  46. data/lib/psd/{layer_info → layer/info}/metadata_setting.rb +5 -3
  47. data/lib/psd/{layer_info → layer/info}/object_effects.rb +5 -5
  48. data/lib/psd/layer/info/pattern.rb +14 -0
  49. data/lib/psd/layer/info/pattern_fill.rb +15 -0
  50. data/lib/psd/layer/info/photo_filter.rb +44 -0
  51. data/lib/psd/{layer_info → layer/info}/placed_layer.rb +4 -2
  52. data/lib/psd/layer/info/posterize.rb +16 -0
  53. data/lib/psd/{layer_info → layer/info}/reference_point.rb +4 -2
  54. data/lib/psd/layer/info/selective_color.rb +32 -0
  55. data/lib/psd/layer/info/sheet_color.rb +36 -0
  56. data/lib/psd/layer/info/solid_color.rb +36 -0
  57. data/lib/psd/layer/info/threshold.rb +16 -0
  58. data/lib/psd/layer/info/transparency_shapes_layer.rb +16 -0
  59. data/lib/psd/{layer_info → layer/info}/typetool.rb +27 -13
  60. data/lib/psd/{layer_info → layer/info}/unicode_name.rb +4 -2
  61. data/lib/psd/{layer_info → layer/info}/vector_mask.rb +7 -5
  62. data/lib/psd/layer/info/vector_mask_as_global_mask.rb +16 -0
  63. data/lib/psd/layer/info/vector_origination.rb +14 -0
  64. data/lib/psd/{layer_info → layer/info}/vector_stroke.rb +4 -2
  65. data/lib/psd/{layer_info → layer/info}/vector_stroke_content.rb +4 -2
  66. data/lib/psd/layer/info/vibrance.rb +22 -0
  67. data/lib/psd/layer/info.rb +106 -19
  68. data/lib/psd/layer/position_and_channels.rb +2 -6
  69. data/lib/psd/layer.rb +13 -16
  70. data/lib/psd/layer_info.rb +0 -4
  71. data/lib/psd/layer_mask.rb +61 -50
  72. data/lib/psd/lazy_execute.rb +5 -1
  73. data/lib/psd/mask.rb +7 -1
  74. data/lib/psd/node.rb +142 -49
  75. data/lib/psd/nodes/ancestry.rb +7 -6
  76. data/lib/psd/nodes/build_preview.rb +4 -4
  77. data/lib/psd/nodes/group.rb +36 -0
  78. data/lib/psd/nodes/layer.rb +45 -0
  79. data/lib/psd/nodes/layer_comps.rb +93 -0
  80. data/lib/psd/nodes/locking.rb +36 -0
  81. data/lib/psd/nodes/root.rb +87 -0
  82. data/lib/psd/nodes/search.rb +8 -65
  83. data/lib/psd/path_record.rb +5 -70
  84. data/lib/psd/renderer/blender.rb +10 -5
  85. data/lib/psd/renderer/cairo_helpers.rb +46 -0
  86. data/lib/psd/renderer/canvas.rb +41 -20
  87. data/lib/psd/renderer/canvas_management.rb +2 -2
  88. data/lib/psd/renderer/clipping_mask.rb +5 -4
  89. data/lib/psd/renderer/compose.rb +59 -69
  90. data/lib/psd/renderer/layer_styles/color_overlay.rb +45 -27
  91. data/lib/psd/renderer/layer_styles.rb +15 -5
  92. data/lib/psd/renderer/mask.rb +26 -22
  93. data/lib/psd/renderer/mask_canvas.rb +12 -0
  94. data/lib/psd/renderer/vector_shape.rb +239 -0
  95. data/lib/psd/renderer.rb +18 -7
  96. data/lib/psd/resource_section.rb +13 -6
  97. data/lib/psd/resources/base.rb +33 -0
  98. data/lib/psd/resources/guides.rb +6 -4
  99. data/lib/psd/resources/layer_comps.rb +6 -4
  100. data/lib/psd/resources/resolution_info.rb +48 -0
  101. data/lib/psd/resources/saved_path.rb +19 -0
  102. data/lib/psd/resources/slices.rb +7 -5
  103. data/lib/psd/resources/work_path.rb +22 -0
  104. data/lib/psd/resources/xmp_metadata.rb +46 -0
  105. data/lib/psd/resources.rb +17 -21
  106. data/lib/psd/slice.rb +44 -0
  107. data/lib/psd/slices.rb +13 -0
  108. data/lib/psd/util.rb +4 -2
  109. data/lib/psd/version.rb +1 -1
  110. data/lib/psd.rb +31 -45
  111. data/psd.gemspec +2 -3
  112. data/spec/files/alignment_modes.psd +0 -0
  113. data/spec/files/blendmodes.psd +0 -0
  114. data/spec/files/example.psb +0 -0
  115. data/spec/hierarchy_spec.rb +5 -0
  116. data/spec/locked_spec.rb +8 -8
  117. data/spec/psb_parsing_spec.rb +57 -0
  118. data/spec/text_spec.rb +13 -1
  119. metadata +115 -75
  120. data/circle.yml +0 -6
  121. data/lib/psd/layer_info/vector_mask_2.rb +0 -10
  122. data/lib/psd/node_exporting.rb +0 -20
  123. data/lib/psd/node_group.rb +0 -86
  124. data/lib/psd/node_layer.rb +0 -81
  125. data/lib/psd/node_root.rb +0 -93
  126. data/lib/psd/nodes/has_children.rb +0 -13
  127. data/lib/psd/nodes/lock_to_origin.rb +0 -7
  128. data/lib/psd/nodes/parse_layers.rb +0 -18
  129. data/lib/psd/renderer/layer_styles/drop_shadow.rb +0 -75
  130. data/lib/psd/section.rb +0 -26
@@ -19,28 +19,13 @@ class PSD
19
19
  case @record_type
20
20
  when 0, 3 then read_path_record
21
21
  when 1, 2, 4, 5 then read_bezier_point
22
+ when 6 then read_path_fill_rule_record
22
23
  when 7 then read_clipboard_record
23
24
  when 8 then read_initial_fill
24
25
  else @file.seek(24, IO::SEEK_CUR)
25
26
  end
26
27
  end
27
28
 
28
- # Writes out the path to file.
29
- def write(outfile)
30
- outfile.write_short @record_type
31
- case @record_type
32
- when 0 then write_path_record(outfile)
33
- when 3 then write_path_record(outfile)
34
- when 1 then write_bezier_point(outfile)
35
- when 2 then write_bezier_point(outfile)
36
- when 4 then write_bezier_point(outfile)
37
- when 5 then write_bezier_point(outfile)
38
- when 7 then write_clipboard_record(outfile)
39
- when 8 then write_initial_fill(outfile)
40
- else outfile.seek(24, IO::SEEK_CUR)
41
- end
42
- end
43
-
44
29
  # Exports the path record to an easier to work with hash.
45
30
  def to_hash
46
31
  case @record_type
@@ -51,6 +36,7 @@ class PSD
51
36
  when 1, 2, 4, 5
52
37
  {
53
38
  linked: @linked,
39
+ closed: [1, 2].include?(@record_type),
54
40
  preceding: {
55
41
  vert: @preceding_vert,
56
42
  horiz: @preceding_horiz
@@ -83,34 +69,6 @@ class PSD
83
69
  end.merge({ record_type: @record_type })
84
70
  end
85
71
 
86
- # Attempts to translate the path
87
- def translate(x=0, y=0)
88
- return unless is_bezier_point?
89
-
90
- document_width, document_height = @layer.document_dimensions
91
- translate_x_ratio = x.to_f / document_width.to_f
92
- translate_y_ratio = y.to_f / document_height.to_f
93
-
94
- @preceding_vert += translate_y_ratio
95
- @preceding_horiz += translate_x_ratio
96
- @anchor_vert += translate_y_ratio
97
- @anchor_horiz += translate_x_ratio
98
- @leaving_vert += translate_y_ratio
99
- @leaving_horiz += translate_x_ratio
100
- end
101
-
102
- # Attempts to scale the path
103
- def scale(xr, yr)
104
- return unless is_bezier_point?
105
-
106
- @preceding_vert *= yr
107
- @preceding_horiz *= xr
108
- @anchor_vert *= yr
109
- @anchor_horiz *= xr
110
- @leaving_vert *= yr
111
- @leaving_horiz *= xr
112
- end
113
-
114
72
  # Is this record a bezier point?
115
73
  def is_bezier_point?
116
74
  [1,2,4,5].include? @record_type
@@ -123,11 +81,6 @@ class PSD
123
81
  @file.seek(22, IO::SEEK_CUR)
124
82
  end
125
83
 
126
- def write_path_record(file)
127
- file.write_short @num_points
128
- file.seek(22, IO::SEEK_CUR)
129
- end
130
-
131
84
  def read_bezier_point
132
85
  @linked = [1,4].include? @record_type
133
86
 
@@ -141,13 +94,8 @@ class PSD
141
94
  @leaving_horiz = @file.read_path_number
142
95
  end
143
96
 
144
- def write_bezier_point(outfile)
145
- outfile.write_path_number @preceding_vert
146
- outfile.write_path_number @preceding_horiz
147
- outfile.write_path_number @anchor_vert
148
- outfile.write_path_number @anchor_horiz
149
- outfile.write_path_number @leaving_vert
150
- outfile.write_path_number @leaving_horiz
97
+ def read_path_fill_rule_record
98
+ @file.seek(24, IO::SEEK_CUR)
151
99
  end
152
100
 
153
101
  def read_clipboard_record
@@ -159,22 +107,9 @@ class PSD
159
107
  @file.seek(4, IO::SEEK_CUR)
160
108
  end
161
109
 
162
- def write_clipboard_record(file)
163
- [@clipboard_top, @clipboard_left, @clipboard_bottom,
164
- @clipboard_right, @clipboard_resolution].each do |point|
165
- file.write_path_number point
166
- end
167
- file.seek(4, IO::SEEK_CUR)
168
- end
169
-
170
110
  def read_initial_fill
171
111
  @initial_fill = @file.read_short
172
112
  @file.seek(22, IO::SEEK_CUR)
173
113
  end
174
-
175
- def write_initial_fill(file)
176
- file.write_short @initial_fill
177
- file.seek(22, IO::SEEK_CUR)
178
- end
179
114
  end
180
- end
115
+ end
@@ -16,7 +16,8 @@ class PSD
16
16
  # Composes the foreground Canvas onto the background Canvas using the
17
17
  # blending mode specified by the foreground.
18
18
  def compose!
19
- PSD.logger.debug "Composing #{fg.node.debug_name} onto #{bg.node.debug_name} with #{fg.node.blending_mode} blending"
19
+ PSD.logger.debug "#{fg.node.debug_name} -> #{bg.node.debug_name}: #{fg.node.blending_mode} blending"
20
+ PSD.logger.debug "fg: (#{fg.left}, #{fg.top}) #{fg.width}x#{fg.height}; bg: (#{bg.left}, #{bg.top}) #{bg.width}x#{bg.height}"
20
21
 
21
22
  offset_x = fg.left - bg.left
22
23
  offset_y = fg.top - bg.top
@@ -30,12 +31,12 @@ class PSD
30
31
 
31
32
  color = Compose.send(
32
33
  fg.node.blending_mode,
33
- fg.canvas[x, y],
34
- bg.canvas[base_x, base_y],
35
- compose_options
34
+ fg.get_pixel(x, y),
35
+ bg.get_pixel(base_x, base_y),
36
+ calculated_opacity
36
37
  )
37
38
 
38
- bg.canvas[base_x, base_y] = color
39
+ bg.set_pixel base_x, base_y, color
39
40
  end
40
41
  end
41
42
  end
@@ -48,6 +49,10 @@ class PSD
48
49
  fill_opacity: @fill_opacity
49
50
  }
50
51
  end
52
+
53
+ def calculated_opacity
54
+ @calculated_opacity ||= compose_options[:opacity] * compose_options[:fill_opacity] / 255
55
+ end
51
56
  end
52
57
  end
53
58
  end
@@ -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,35 @@
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
+ extend Forwardable
5
5
 
6
- def initialize(node, width = nil, height = nil, color = ChunkyPNG::Color::TRANSPARENT)
6
+ attr_reader :canvas, :node, :opts, :width, :height, :opacity, :fill_opacity
7
+
8
+ def_delegators :node, :top, :right, :bottom, :left
9
+ def_delegators :canvas, :[], :[]=, :get_pixel, :set_pixel
10
+
11
+ def initialize(node, width = nil, height = nil, opts = {})
7
12
  @node = node
13
+ @opts = opts
8
14
  @pixel_data = @node.group? ? [] : @node.image.pixel_data
9
15
 
10
16
  @width = (width || @node.width).to_i
11
17
  @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
18
 
17
19
  @opacity = @node.opacity.to_f
18
20
  @fill_opacity = @node.fill_opacity.to_f
19
21
 
20
- @canvas = ChunkyPNG::Canvas.new(@width, @height, color)
21
-
22
- initialize_canvas unless @node.group?
22
+ initialize_canvas
23
23
  end
24
24
 
25
25
  def paint_to(base)
26
26
  PSD.logger.debug "Painting #{node.name} to #{base.node.debug_name}"
27
27
 
28
- apply_mask
28
+ apply_masks
29
29
  apply_clipping_mask
30
30
  apply_layer_styles
31
31
  apply_layer_opacity
32
+
32
33
  compose_pixels(base)
33
34
  end
34
35
 
@@ -38,9 +39,6 @@ class PSD
38
39
  @height = @canvas.height
39
40
  end
40
41
 
41
- def [](x, y); @canvas[x, y]; end
42
- def []=(x, y, value); @canvas[x, y] = value; end
43
-
44
42
  def method_missing(method, *args, &block)
45
43
  @canvas.send(method, *args, &block)
46
44
  end
@@ -48,20 +46,43 @@ class PSD
48
46
  private
49
47
 
50
48
  def initialize_canvas
51
- PSD.logger.debug "Initializing canvas for #{node.debug_name}"
49
+ PSD.logger.debug "Initializing canvas for #{node.debug_name}; color = #{ChunkyPNG::Color.to_truecolor_alpha_bytes(fill_color)}"
50
+
51
+ @canvas = ChunkyPNG::Canvas.new(@width, @height, fill_color)
52
+ return if @node.group? || has_fill? || @opts[:base]
52
53
 
53
54
  # Sorry, ChunkyPNG.
54
55
  @canvas.send(:replace_canvas!, width, height, @pixel_data)
55
-
56
+ ensure
56
57
  # This can now be referenced by @canvas.pixels
57
58
  @pixel_data = nil
58
59
  end
59
60
 
60
- def apply_mask
61
- return unless @node.image.has_mask?
61
+ def has_fill?
62
+ !@opts[:base] && @node.layer? && @node.solid_color
63
+ end
64
+
65
+ def fill_color
66
+ if has_fill?
67
+ @node.solid_color.color
68
+ else
69
+ ChunkyPNG::Color::TRANSPARENT
70
+ end
71
+ end
62
72
 
63
- PSD.logger.debug "Applying layer mask to #{node.name}"
64
- Mask.new(self).apply!
73
+ def apply_masks
74
+ ([@node] + @node.ancestors).each do |n|
75
+ next unless n.raster_mask?
76
+ break if n.group? && !n.passthru_blending?
77
+
78
+ if n.layer?
79
+ PSD.logger.debug "Applying raster mask to #{@node.name}"
80
+ Mask.new(self).apply!
81
+ else
82
+ PSD.logger.debug "Applying raster mask to #{@node.name} from #{n.name}"
83
+ Mask.new(self, n).apply!
84
+ end
85
+ end
65
86
  end
66
87
 
67
88
  def apply_clipping_mask
@@ -92,4 +113,4 @@ class PSD
92
113
  end
93
114
  end
94
115
  end
95
- end
116
+ end
@@ -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
@@ -5,21 +5,17 @@ class PSD
5
5
  # Mostly based on similar code from libpsd.
6
6
  module Compose
7
7
  extend self
8
-
9
- DEFAULT_OPTS = {
10
- opacity: 255,
11
- fill_opacity: 255
12
- }
8
+ extend ChunkyPNG::Color
13
9
 
14
10
  #
15
11
  # Normal blend modes
16
12
  #
17
13
 
18
- def normal(fg, bg, opts={})
19
- return apply_opacity(fg, opts) if fully_transparent?(bg)
14
+ def normal(fg, bg, opacity)
15
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
20
16
  return bg if fully_transparent?(fg)
21
17
 
22
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
18
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
23
19
  new_r = blend_channel(r(bg), r(fg), mix_alpha)
24
20
  new_g = blend_channel(g(bg), g(fg), mix_alpha)
25
21
  new_b = blend_channel(b(bg), b(fg), mix_alpha)
@@ -32,11 +28,11 @@ class PSD
32
28
  # Subtractive blend modes
33
29
  #
34
30
 
35
- def darken(fg, bg, opts={})
36
- return apply_opacity(fg, opts) if fully_transparent?(bg)
31
+ def darken(fg, bg, opacity)
32
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
37
33
  return bg if fully_transparent?(fg)
38
34
 
39
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
35
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
40
36
  new_r = r(fg) <= r(bg) ? blend_channel(r(bg), r(fg), mix_alpha) : r(bg)
41
37
  new_g = g(fg) <= g(bg) ? blend_channel(g(bg), g(fg), mix_alpha) : g(bg)
42
38
  new_b = b(fg) <= b(bg) ? blend_channel(b(bg), b(fg), mix_alpha) : b(bg)
@@ -44,11 +40,11 @@ class PSD
44
40
  rgba(new_r, new_g, new_b, dst_alpha)
45
41
  end
46
42
 
47
- def multiply(fg, bg, opts={})
48
- return apply_opacity(fg, opts) if fully_transparent?(bg)
43
+ def multiply(fg, bg, opacity)
44
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
49
45
  return bg if fully_transparent?(fg)
50
46
 
51
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
47
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
52
48
  new_r = blend_channel(r(bg), r(fg) * r(bg) >> 8, mix_alpha)
53
49
  new_g = blend_channel(g(bg), g(fg) * g(bg) >> 8, mix_alpha)
54
50
  new_b = blend_channel(b(bg), b(fg) * b(bg) >> 8, mix_alpha)
@@ -56,11 +52,11 @@ class PSD
56
52
  rgba(new_r, new_g, new_b, dst_alpha)
57
53
  end
58
54
 
59
- def color_burn(fg, bg, opts={})
60
- return apply_opacity(fg, opts) if fully_transparent?(bg)
55
+ def color_burn(fg, bg, opacity)
56
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
61
57
  return bg if fully_transparent?(fg)
62
58
 
63
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
59
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
64
60
 
65
61
  calculate_foreground = Proc.new do |b, f|
66
62
  if f > 0
@@ -78,11 +74,11 @@ class PSD
78
74
  rgba(new_r, new_g, new_b, dst_alpha)
79
75
  end
80
76
 
81
- def linear_burn(fg, bg, opts={})
82
- return apply_opacity(fg, opts) if fully_transparent?(bg)
77
+ def linear_burn(fg, bg, opacity)
78
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
83
79
  return bg if fully_transparent?(fg)
84
80
 
85
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
81
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
86
82
 
87
83
  new_r = blend_channel(r(bg), (r(fg) < (255 - r(bg))) ? 0 : r(fg) - (255 - r(bg)), mix_alpha)
88
84
  new_g = blend_channel(g(bg), (g(fg) < (255 - g(bg))) ? 0 : g(fg) - (255 - g(bg)), mix_alpha)
@@ -95,11 +91,11 @@ class PSD
95
91
  # Additive blend modes
96
92
  #
97
93
 
98
- def lighten(fg, bg, opts={})
99
- return apply_opacity(fg, opts) if fully_transparent?(bg)
94
+ def lighten(fg, bg, opacity)
95
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
100
96
  return bg if fully_transparent?(fg)
101
97
 
102
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
98
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
103
99
 
104
100
  new_r = r(fg) >= r(bg) ? blend_channel(r(bg), r(fg), mix_alpha) : r(bg)
105
101
  new_g = g(fg) >= g(bg) ? blend_channel(g(bg), g(fg), mix_alpha) : g(bg)
@@ -108,11 +104,11 @@ class PSD
108
104
  rgba(new_r, new_g, new_b, dst_alpha)
109
105
  end
110
106
 
111
- def screen(fg, bg, opts={})
112
- return apply_opacity(fg, opts) if fully_transparent?(bg)
107
+ def screen(fg, bg, opacity)
108
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
113
109
  return bg if fully_transparent?(fg)
114
110
 
115
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
111
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
116
112
 
117
113
  new_r = blend_channel(r(bg), 255 - ((255 - r(bg)) * (255 - r(fg)) >> 8), mix_alpha)
118
114
  new_g = blend_channel(g(bg), 255 - ((255 - g(bg)) * (255 - g(fg)) >> 8), mix_alpha)
@@ -121,11 +117,11 @@ class PSD
121
117
  rgba(new_r, new_g, new_b, dst_alpha)
122
118
  end
123
119
 
124
- def color_dodge(fg, bg, opts={})
125
- return apply_opacity(fg, opts) if fully_transparent?(bg)
120
+ def color_dodge(fg, bg, opacity)
121
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
126
122
  return bg if fully_transparent?(fg)
127
123
 
128
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
124
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
129
125
 
130
126
  calculate_foreground = Proc.new do |b, f|
131
127
  f < 255 ? [(b << 8) / (255 - f), 255].min : b
@@ -138,11 +134,11 @@ class PSD
138
134
  rgba(new_r, new_g, new_b, dst_alpha)
139
135
  end
140
136
 
141
- def linear_dodge(fg, bg, opts={})
142
- return apply_opacity(fg, opts) if fully_transparent?(bg)
137
+ def linear_dodge(fg, bg, opacity)
138
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
143
139
  return bg if fully_transparent?(fg)
144
140
 
145
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
141
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
146
142
 
147
143
  new_r = blend_channel(r(bg), (r(bg) + r(fg)) > 255 ? 255 : r(bg) + r(fg), mix_alpha)
148
144
  new_g = blend_channel(g(bg), (g(bg) + g(fg)) > 255 ? 255 : g(bg) + g(fg), mix_alpha)
@@ -156,11 +152,11 @@ class PSD
156
152
  # Contrasting blend modes
157
153
  #
158
154
 
159
- def overlay(fg, bg, opts={})
160
- return apply_opacity(fg, opts) if fully_transparent?(bg)
155
+ def overlay(fg, bg, opacity)
156
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
161
157
  return bg if fully_transparent?(fg)
162
158
 
163
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
159
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
164
160
 
165
161
  calculate_foreground = Proc.new do |b, f|
166
162
  if b < 128
@@ -177,11 +173,11 @@ class PSD
177
173
  rgba(new_r, new_g, new_b, dst_alpha)
178
174
  end
179
175
 
180
- def soft_light(fg, bg, opts={})
181
- return apply_opacity(fg, opts) if fully_transparent?(bg)
176
+ def soft_light(fg, bg, opacity)
177
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
182
178
  return bg if fully_transparent?(fg)
183
179
 
184
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
180
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
185
181
 
186
182
  calculate_foreground = Proc.new do |b, f|
187
183
  c1 = b * f >> 8
@@ -196,11 +192,11 @@ class PSD
196
192
  rgba(new_r, new_g, new_b, dst_alpha)
197
193
  end
198
194
 
199
- def hard_light(fg, bg, opts={})
200
- return apply_opacity(fg, opts) if fully_transparent?(bg)
195
+ def hard_light(fg, bg, opacity)
196
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
201
197
  return bg if fully_transparent?(fg)
202
198
 
203
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
199
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
204
200
 
205
201
  calculate_foreground = Proc.new do |b, f|
206
202
  if f < 128
@@ -217,11 +213,11 @@ class PSD
217
213
  rgba(new_r, new_g, new_b, dst_alpha)
218
214
  end
219
215
 
220
- def vivid_light(fg, bg, opts={})
221
- return apply_opacity(fg, opts) if fully_transparent?(bg)
216
+ def vivid_light(fg, bg, opacity)
217
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
222
218
  return bg if fully_transparent?(fg)
223
219
 
224
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
220
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
225
221
 
226
222
  calculate_foreground = Proc.new do |b, f|
227
223
  if f < 255
@@ -238,11 +234,11 @@ class PSD
238
234
  rgba(new_r, new_g, new_b, dst_alpha)
239
235
  end
240
236
 
241
- def linear_light(fg, bg, opts={})
242
- return apply_opacity(fg, opts) if fully_transparent?(bg)
237
+ def linear_light(fg, bg, opacity)
238
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
243
239
  return bg if fully_transparent?(fg)
244
240
 
245
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
241
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
246
242
 
247
243
  calculate_foreground = Proc.new do |b, f|
248
244
  if b < 255
@@ -259,11 +255,11 @@ class PSD
259
255
  rgba(new_r, new_g, new_b, dst_alpha)
260
256
  end
261
257
 
262
- def pin_light(fg, bg, opts={})
263
- return apply_opacity(fg, opts) if fully_transparent?(bg)
258
+ def pin_light(fg, bg, opacity)
259
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
264
260
  return bg if fully_transparent?(fg)
265
261
 
266
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
262
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
267
263
 
268
264
  calculate_foreground = Proc.new do |b, f|
269
265
  if f >= 128
@@ -280,11 +276,11 @@ class PSD
280
276
  rgba(new_r, new_g, new_b, dst_alpha)
281
277
  end
282
278
 
283
- def hard_mix(fg, bg, opts={})
284
- return apply_opacity(fg, opts) if fully_transparent?(bg)
279
+ def hard_mix(fg, bg, opacity)
280
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
285
281
  return bg if fully_transparent?(fg)
286
282
 
287
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
283
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
288
284
 
289
285
  new_r = blend_channel(r(bg), (r(bg) + r(fg) <= 255) ? 0 : 255, mix_alpha)
290
286
  new_g = blend_channel(g(bg), (g(bg) + g(fg) <= 255) ? 0 : 255, mix_alpha)
@@ -297,11 +293,11 @@ class PSD
297
293
  # Inversion blend modes
298
294
  #
299
295
 
300
- def difference(fg, bg, opts={})
301
- return apply_opacity(fg, opts) if fully_transparent?(bg)
296
+ def difference(fg, bg, opacity)
297
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
302
298
  return bg if fully_transparent?(fg)
303
299
 
304
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
300
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
305
301
 
306
302
  new_r = blend_channel(r(bg), (r(bg) - r(fg)).abs, mix_alpha)
307
303
  new_g = blend_channel(g(bg), (g(bg) - g(fg)).abs, mix_alpha)
@@ -310,11 +306,11 @@ class PSD
310
306
  rgba(new_r, new_g, new_b, dst_alpha)
311
307
  end
312
308
 
313
- def exclusion(fg, bg, opts={})
314
- return apply_opacity(fg, opts) if fully_transparent?(bg)
309
+ def exclusion(fg, bg, opacity)
310
+ return apply_opacity(fg, opacity) if fully_transparent?(bg)
315
311
  return bg if fully_transparent?(fg)
316
312
 
317
- mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
313
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
318
314
 
319
315
  new_r = blend_channel(r(bg), r(bg) + r(fg) - (r(bg) * r(fg) >> 7), mix_alpha)
320
316
  new_g = blend_channel(g(bg), g(bg) + g(fg) - (g(bg) * g(fg) >> 7), mix_alpha)
@@ -325,14 +321,12 @@ class PSD
325
321
 
326
322
  # If the blend mode is missing, we fall back to normal composition.
327
323
  def method_missing(method, *args, &block)
328
- return ChunkyPNG::Color.send(method, *args) if ChunkyPNG::Color.respond_to?(method)
329
324
  normal(*args)
330
325
  end
331
326
 
332
327
  private
333
328
 
334
- def calculate_alphas(fg, bg, opts)
335
- opacity = calculate_opacity(opts)
329
+ def calculate_alphas(fg, bg, opacity)
336
330
  src_alpha = a(fg) * opacity >> 8
337
331
  dst_alpha = a(bg)
338
332
 
@@ -342,12 +336,8 @@ class PSD
342
336
  return mix_alpha, dst_alpha
343
337
  end
344
338
 
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)
339
+ def apply_opacity(color, opacity)
340
+ (color & 0xffffff00) | ((color & 0x000000ff) * opacity / 255)
351
341
  end
352
342
 
353
343
  def blend_channel(bg, fg, alpha)
@@ -358,4 +348,4 @@ class PSD
358
348
  bg + ((255 - bg) * fg >> 8)
359
349
  end
360
350
  end
361
- end
351
+ end