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
@@ -1,5 +1,4 @@
1
- require_relative 'layer_styles/color_overlay'
2
- require_relative 'layer_styles/drop_shadow'
1
+ require 'psd/renderer/layer_styles/color_overlay'
3
2
 
4
3
  class PSD
5
4
  class LayerStyles
@@ -31,12 +30,16 @@ class PSD
31
30
  'Lmns' => 'lum'
32
31
  }.freeze
33
32
 
33
+ SUPPORTED_STYLES = [
34
+ ColorOverlay
35
+ ].freeze
36
+
34
37
  attr_reader :canvas, :node, :data
35
38
 
36
39
  def initialize(canvas)
37
40
  @canvas = canvas
38
41
  @node = @canvas.node
39
- @data = @node.layer.info[:object_effects]
42
+ @data = @node.object_effects
40
43
 
41
44
  if @data.nil?
42
45
  @applied = true
@@ -48,9 +51,16 @@ class PSD
48
51
 
49
52
  def apply!
50
53
  return if @applied || data.nil?
54
+ return unless styles_enabled?
55
+
56
+ SUPPORTED_STYLES.each do |style|
57
+ next unless style.should_apply?(@canvas, data)
58
+ style.new(self).apply!
59
+ end
60
+ end
51
61
 
52
- ColorOverlay.new(self).apply! if ColorOverlay.should_apply?(data)
53
- DropShadow.new(self).apply! if DropShadow.should_apply?(data)
62
+ def styles_enabled?
63
+ data['masterFXSwitch']
54
64
  end
55
65
  end
56
66
  end
@@ -1,8 +1,27 @@
1
1
  class PSD
2
2
  class LayerStyles
3
3
  class ColorOverlay
4
- def self.should_apply?(data)
5
- data.has_key?('SoFi')
4
+ # TODO: CMYK support
5
+ def self.should_apply?(canvas, data)
6
+ data.has_key?('SoFi') &&
7
+ data['SoFi']['enab'] &&
8
+ canvas.node.header.rgb? &&
9
+ !PSD::Renderer::VectorShape.can_render?(canvas)
10
+ end
11
+
12
+ def self.can_apply?(canvas, data)
13
+ data.has_key?('SoFi') &&
14
+ data['SoFi']['enab'] &&
15
+ canvas.node.header.rgb?
16
+ end
17
+
18
+ def self.for_canvas(canvas)
19
+ data = canvas.node.object_effects
20
+ return nil if data.nil?
21
+ return nil unless can_apply?(canvas, data.data)
22
+
23
+ styles = LayerStyles.new(canvas)
24
+ self.new(styles)
6
25
  end
7
26
 
8
27
  def initialize(styles)
@@ -12,41 +31,22 @@ class PSD
12
31
  end
13
32
 
14
33
  def apply!
15
- # TODO - implement CMYK color overlay
16
- return if @node.header.cmyk?
17
-
18
- width = @canvas.width
19
- height = @canvas.height
20
-
21
- # puts width, height
22
- # puts @canvas.canvas.width, @canvas.canvas.height
23
-
24
34
  PSD.logger.debug "Layer style: layer = #{@node.name}, type = color overlay, blend mode = #{blending_mode}"
25
35
 
26
- height.times do |y|
27
- width.times do |x|
36
+ @canvas.height.times do |y|
37
+ @canvas.width.times do |x|
28
38
  pixel = @canvas[x, y]
29
39
  alpha = ChunkyPNG::Color.a(pixel)
30
40
  next if alpha == 0
31
41
 
32
- overlay_color = ChunkyPNG::Color.rgba(r, g, b, alpha)
33
- @canvas[x, y] = Compose.send(blending_mode, overlay_color, pixel)
42
+ new_pixel = Compose.send(blending_mode, overlay_color, pixel, overlay_opacity)
43
+ @canvas[x, y] = (new_pixel & 0xFFFFFF00) | alpha
34
44
  end
35
45
  end
36
46
  end
37
47
 
38
- private
39
-
40
- def blending_mode
41
- @blending_mode ||= BlendMode::BLEND_MODES[BLEND_TRANSLATION[overlay_data['Md ']].to_sym]
42
- end
43
-
44
- def overlay_data
45
- @data['SoFi']
46
- end
47
-
48
- def color_data
49
- overlay_data['Clr ']
48
+ def overlay_color
49
+ @overlay_color ||= ChunkyPNG::Color.rgb(r, g, b)
50
50
  end
51
51
 
52
52
  def r
@@ -60,6 +60,25 @@ class PSD
60
60
  def b
61
61
  @b ||= color_data['Bl '].round
62
62
  end
63
+
64
+ def a
65
+ @a ||= (overlay_data['Opct'][:value] * 2.55).ceil
66
+ end
67
+ alias_method :overlay_opacity, :a
68
+
69
+ private
70
+
71
+ def blending_mode
72
+ @blending_mode ||= BlendMode::BLEND_MODES[BLEND_TRANSLATION[overlay_data['Md '][:value]].to_sym]
73
+ end
74
+
75
+ def overlay_data
76
+ @data['SoFi']
77
+ end
78
+
79
+ def color_data
80
+ overlay_data['Clr ']
81
+ end
63
82
  end
64
83
  end
65
84
  end
@@ -3,11 +3,18 @@ class PSD
3
3
  class Mask
4
4
  attr_accessor :mask_data
5
5
 
6
- def initialize(canvas)
6
+ def initialize(canvas, mask_layer = nil)
7
7
  @canvas = canvas
8
8
  @layer = canvas.node
9
+ @mask_layer = mask_layer || @layer
9
10
 
10
- @mask_data = @layer.image.mask_data
11
+ @mask_data = @mask_layer.image.mask_data
12
+ @mask = @mask_layer.mask
13
+
14
+ @mask_width = @mask.width.to_i
15
+ @mask_height = @mask.height.to_i
16
+ @mask_left = @mask.left.to_i + @mask_layer.left_offset
17
+ @mask_top = @mask.top.to_i + @mask_layer.top_offset
11
18
 
12
19
  @doc_width = @layer.header.width.to_i
13
20
  @doc_height = @layer.header.height.to_i
@@ -15,29 +22,26 @@ class PSD
15
22
 
16
23
  def apply!
17
24
  PSD.logger.debug "Applying mask to #{@layer.name}"
18
-
19
- # Now we apply the mask
20
- i = 0
21
- @layer.mask.height.times do |y|
22
- @layer.mask.width.times do |x|
23
- doc_x = @layer.mask.left + x
24
- doc_y = @layer.mask.top + y
25
-
26
- layer_x = doc_x - @layer.left
27
- layer_y = doc_y - @layer.top
28
-
29
- next unless @canvas.canvas.include_xy?(layer_x, layer_y)
30
- color = ChunkyPNG::Color.to_truecolor_alpha_bytes(@canvas[layer_x, layer_y])
31
-
32
- # We're off the document canvas. Crop.
33
- if doc_x < 0 || doc_x > @doc_width || doc_y < 0 || doc_y > @doc_height
25
+
26
+ @canvas.height.times do |y|
27
+ @canvas.width.times do |x|
28
+ doc_x = @canvas.left + x
29
+ doc_y = @canvas.top + y
30
+
31
+ mask_x = doc_x - @mask_left
32
+ mask_y = doc_y - @mask_top
33
+
34
+ color = ChunkyPNG::Color.to_truecolor_alpha_bytes(@canvas.get_pixel(x, y))
35
+
36
+ if doc_x < 0 || doc_x >= @doc_width || doc_y < 0 || doc_y >= @doc_height
37
+ color[3] = 0
38
+ elsif mask_x < 0 || mask_x >= @mask_width || mask_y < 0 || mask_y >= @mask_height
34
39
  color[3] = 0
35
40
  else
36
- color[3] = color[3] * @mask_data[i] / 255
37
- end
41
+ color[3] = color[3] * @mask_data[@mask_width * mask_y + mask_x] / 255
42
+ end
38
43
 
39
- @canvas[layer_x, layer_y] = ChunkyPNG::Color.rgba(*color)
40
- i += 1
44
+ @canvas.set_pixel x, y, ChunkyPNG::Color.rgba(*color)
41
45
  end
42
46
  end
43
47
  end
@@ -0,0 +1,12 @@
1
+ require 'psd/renderer/canvas'
2
+
3
+ class PSD
4
+ class Renderer
5
+ class MaskCanvas < Canvas
6
+ def initialize(node, width = nil, height = nil, opts = {})
7
+ super
8
+ apply_masks
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,239 @@
1
+ require 'psd/renderer/cairo_helpers'
2
+
3
+ class PSD
4
+ class Renderer
5
+ class VectorShape
6
+ include CairoHelpers
7
+
8
+ def self.can_render?(canvas)
9
+ canvas.opts[:render_vectors] && !canvas.node.vector_mask.nil?
10
+ end
11
+
12
+ DPI = 72.0.freeze
13
+
14
+ def initialize(canvas)
15
+ @canvas = canvas
16
+ @node = @canvas.node
17
+ @path = @node.vector_mask.paths.map(&:to_hash)
18
+
19
+ @stroke_data = @node.vector_stroke ? @node.vector_stroke.data : {}
20
+ @fill_data = @node.vector_stroke_content ? @node.vector_stroke_content.data : {}
21
+
22
+ @paths = []
23
+ end
24
+
25
+ def render!
26
+ PSD.logger.debug "Beginning vector render for #{@node.name}"
27
+
28
+ find_points
29
+ render_shapes
30
+ end
31
+
32
+ private
33
+
34
+ def find_points
35
+ PSD.logger.debug "Formatting vector points..."
36
+
37
+ cur_path = nil
38
+ @path.each do |data|
39
+ next if [6, 7, 8].include? data[:record_type]
40
+
41
+ if [0, 3].include? data[:record_type]
42
+ @paths << cur_path
43
+ cur_path = []
44
+ next
45
+ end
46
+
47
+ cur_path << data.tap do |d|
48
+ if [1, 2, 4, 5].include? data[:record_type]
49
+ [:preceding, :anchor, :leaving].each do |type|
50
+ d[type][:horiz] = (d[type][:horiz] * horiz_factor) - @node.left
51
+ d[type][:vert] = (d[type][:vert] * vert_factor) - @node.top
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ @paths << cur_path
58
+ @paths.compact!
59
+
60
+ PSD.logger.debug "Vector shape has #{@paths.size} path(s)"
61
+ end
62
+
63
+ # TODO: stroke alignment
64
+ # Right now we assume the stroke style is always a overlap stroke.
65
+ def render_shapes
66
+ PSD.logger.debug "Rendering #{@paths.size} vector paths with cairo"
67
+
68
+ cairo_image_surface(@canvas.width + stroke_size, @canvas.height + stroke_size) do |cr|
69
+ cr.set_fill_rule Cairo::FILL_RULE_EVEN_ODD
70
+ cr.set_line_join stroke_join
71
+ cr.set_line_cap stroke_cap
72
+
73
+ cr.translate stroke_size / 2.0, stroke_size / 2.0
74
+
75
+ @paths.each do |path|
76
+ cr.move_to path[0][:anchor][:horiz], path[0][:anchor][:vert]
77
+
78
+ path.size.times do |i|
79
+ point_a = path[i]
80
+ point_b = path[i+1] || path[0]
81
+
82
+ cr.curve_to(
83
+ point_a[:leaving][:horiz],
84
+ point_a[:leaving][:vert],
85
+ point_b[:preceding][:horiz],
86
+ point_b[:preceding][:vert],
87
+ point_b[:anchor][:horiz],
88
+ point_b[:anchor][:vert]
89
+ )
90
+ end
91
+
92
+ cr.close_path if path.last[:closed]
93
+ end
94
+
95
+ cr.set_source_rgba fill_color
96
+ cr.fill_preserve
97
+
98
+ if has_stroke?
99
+ cr.set_source_rgba stroke_color
100
+ cr.set_line_width stroke_size
101
+ cr.stroke
102
+ end
103
+ end
104
+ end
105
+
106
+ # For debugging purposes only
107
+ def draw_debug(canvas)
108
+ @paths.each do |path|
109
+ path.each do |point|
110
+ canvas.circle(point[:anchor][:horiz].to_i, point[:anchor][:vert].to_i, 3, ChunkyPNG::Color::BLACK, ChunkyPNG::Color::BLACK)
111
+ [:leaving, :preceding].each do |type|
112
+ canvas.circle(point[type][:horiz].to_i, point[type][:vert].to_i, 3, ChunkyPNG::Color.rgb(255, 0, 0), ChunkyPNG::Color.rgb(255, 0, 0))
113
+ canvas.line(
114
+ point[:anchor][:horiz].to_i, point[:anchor][:vert].to_i,
115
+ point[type][:horiz].to_i, point[type][:vert].to_i,
116
+ ChunkyPNG::Color::BLACK
117
+ )
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ def apply_to_canvas(output)
124
+ # draw_debug(output)
125
+ output.resample_nearest_neighbor!(@canvas.width, @canvas.height)
126
+ @canvas.canvas.compose!(output, 0, 0)
127
+ end
128
+
129
+ def formatted_points
130
+ @formatted_points ||= @curve_points.map(&:to_a)
131
+ end
132
+
133
+ def horiz_factor
134
+ @horiz_factor ||= @node.root.width.to_f
135
+ end
136
+
137
+ def vert_factor
138
+ @vert_factor ||= @node.root.height.to_f
139
+ end
140
+
141
+ def stroke_color
142
+ @stroke_color ||= (
143
+ if @stroke_data['strokeEnabled']
144
+ colors = @stroke_data['strokeStyleContent']['Clr ']
145
+ [
146
+ colors['Rd '] / 255.0,
147
+ colors['Grn '] / 255.0,
148
+ colors['Bl '] / 255.0,
149
+ @stroke_data['strokeStyleOpacity'][:value] / 100.0
150
+ ]
151
+ else
152
+ [0.0, 0.0, 0.0, 0.0]
153
+ end
154
+ )
155
+ end
156
+
157
+ def fill_color
158
+ @fill_color ||= (
159
+ overlay = PSD::LayerStyles::ColorOverlay.for_canvas(@canvas)
160
+
161
+ if overlay
162
+ [
163
+ overlay.r / 255.0,
164
+ overlay.g / 255.0,
165
+ overlay.b / 255.0,
166
+ overlay.a / 255.0
167
+ ]
168
+ elsif @stroke_data['fillEnabled']
169
+ colors = @fill_data['Clr ']
170
+ [
171
+ colors['Rd '] / 255.0,
172
+ colors['Grn '] / 255.0,
173
+ colors['Bl '] / 255.0,
174
+ @stroke_data['strokeStyleOpacity'][:value] / 100.0
175
+ ]
176
+ elsif !@node.solid_color.nil?
177
+ [
178
+ @node.solid_color.r / 255.0,
179
+ @node.solid_color.g / 255.0,
180
+ @node.solid_color.b / 255.0,
181
+ 1.0
182
+ ]
183
+ else
184
+ [0.0, 0.0, 0.0, 0.0]
185
+ end
186
+ )
187
+ end
188
+
189
+ def stroke_size
190
+ @stroke_size ||= (
191
+ if @stroke_data['strokeStyleLineWidth']
192
+ value = @stroke_data['strokeStyleLineWidth'][:value]
193
+
194
+ # Convert to pixels
195
+ if @stroke_data['strokeStyleLineWidth'][:id] == '#Pnt'
196
+ value = @stroke_data['strokeStyleResolution'] * value / 72.27
197
+ end
198
+
199
+ value.to_i
200
+ else
201
+ 0
202
+ end
203
+ )
204
+ end
205
+
206
+ def stroke_cap
207
+ @stroke_cap ||= (
208
+ if @stroke_data['strokeStyleLineCapType']
209
+ case @stroke_data['strokeStyleLineCapType']
210
+ when 'strokeStyleButtCap' then Cairo::LINE_CAP_BUTT
211
+ when 'strokeStyleRoundCap' then Cairo::LINE_CAP_ROUND
212
+ when 'strokeStyleSquareCap' then Cairo::LINE_CAP_SQUARE
213
+ end
214
+ else
215
+ Cairo::LINE_CAP_BUTT
216
+ end
217
+ )
218
+ end
219
+
220
+ def stroke_join
221
+ @stroke_join ||= (
222
+ if @stroke_data['strokeStyleLineJoinType']
223
+ case @stroke_data['strokeStyleLineJoinType']
224
+ when 'strokeStyleMiterJoin' then Cairo::LINE_JOIN_MITER
225
+ when 'strokeStyleRoundJoin' then Cairo::LINE_JOIN_ROUND
226
+ when 'strokeStyleBevelJoin' then Cairo::LINE_JOIN_BEVEL
227
+ end
228
+ else
229
+ Cairo::LINE_JOIN_MITER
230
+ end
231
+ )
232
+ end
233
+
234
+ def has_stroke?
235
+ stroke_size > 0
236
+ end
237
+ end
238
+ end
239
+ end