psd 1.5.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/README.md +2 -1
  4. data/lib/psd.rb +2 -1
  5. data/lib/psd/blend_mode.rb +5 -1
  6. data/lib/psd/channel_image.rb +6 -4
  7. data/lib/psd/header.rb +8 -0
  8. data/lib/psd/helpers.rb +13 -1
  9. data/lib/psd/image.rb +1 -1
  10. data/lib/psd/image_exports/png.rb +2 -70
  11. data/lib/psd/image_formats/layer_raw.rb +1 -1
  12. data/lib/psd/image_formats/raw.rb +2 -4
  13. data/lib/psd/image_modes/cmyk.rb +16 -6
  14. data/lib/psd/layer/helpers.rb +1 -1
  15. data/lib/psd/layer_info/fill_opacity.rb +2 -2
  16. data/lib/psd/logger.rb +7 -1
  17. data/lib/psd/node.rb +6 -2
  18. data/lib/psd/node_group.rb +9 -1
  19. data/lib/psd/node_root.rb +16 -3
  20. data/lib/psd/nodes/build_preview.rb +7 -69
  21. data/lib/psd/nodes/search.rb +11 -10
  22. data/lib/psd/renderer.rb +91 -0
  23. data/lib/psd/renderer/blender.rb +53 -0
  24. data/lib/psd/renderer/canvas.rb +95 -0
  25. data/lib/psd/renderer/canvas_management.rb +26 -0
  26. data/lib/psd/renderer/clipping_mask.rb +41 -0
  27. data/lib/psd/{compose.rb → renderer/compose.rb} +23 -19
  28. data/lib/psd/renderer/layer_styles.rb +56 -0
  29. data/lib/psd/renderer/layer_styles/color_overlay.rb +65 -0
  30. data/lib/psd/renderer/layer_styles/drop_shadow.rb +75 -0
  31. data/lib/psd/renderer/mask.rb +68 -0
  32. data/lib/psd/resources/guides.rb +35 -0
  33. data/lib/psd/version.rb +1 -1
  34. data/psd.gemspec +3 -3
  35. data/spec/files/blendmodes.psd +0 -0
  36. data/spec/files/empty-layer-subgroups.psd +0 -0
  37. data/spec/files/guides.psd +0 -0
  38. data/spec/guides_spec.rb +34 -0
  39. data/spec/hierarchy_spec.rb +27 -3
  40. data/spec/image_spec.rb +36 -35
  41. data/spec/parsing_spec.rb +13 -0
  42. metadata +23 -7
  43. data/lib/psd/clipping_mask.rb +0 -49
  44. data/lib/psd/layer_styles.rb +0 -84
@@ -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 based on the layers/groups that belong to the comp only.
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
- node.children.select! do |c|
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'].map { |l| !l.has_key?('enab') || l['enab'] == true ? l['compList'] : nil }
53
- .flatten
54
- .compact
55
- .include?(id)
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
- node.children.each do |c|
59
- c.force_visible = true
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
@@ -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