psd 2.1.2 → 3.1.2

Sign up to get free protection for your applications and to get access to all the features.
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