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
@@ -1,8 +1,26 @@
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
+ end
10
+
11
+ def self.can_apply?(canvas, data)
12
+ data.has_key?('SoFi') &&
13
+ data['SoFi']['enab'] &&
14
+ canvas.node.header.rgb?
15
+ end
16
+
17
+ def self.for_canvas(canvas)
18
+ data = canvas.node.object_effects
19
+ return nil if data.nil?
20
+ return nil unless can_apply?(canvas, data.data)
21
+
22
+ styles = LayerStyles.new(canvas)
23
+ self.new(styles)
6
24
  end
7
25
 
8
26
  def initialize(styles)
@@ -12,41 +30,22 @@ class PSD
12
30
  end
13
31
 
14
32
  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
33
  PSD.logger.debug "Layer style: layer = #{@node.name}, type = color overlay, blend mode = #{blending_mode}"
25
34
 
26
- height.times do |y|
27
- width.times do |x|
35
+ @canvas.height.times do |y|
36
+ @canvas.width.times do |x|
28
37
  pixel = @canvas[x, y]
29
38
  alpha = ChunkyPNG::Color.a(pixel)
30
39
  next if alpha == 0
31
40
 
32
- overlay_color = ChunkyPNG::Color.rgba(r, g, b, alpha)
33
- @canvas[x, y] = Compose.send(blending_mode, overlay_color, pixel)
41
+ new_pixel = Compose.send(blending_mode, overlay_color, pixel, overlay_opacity)
42
+ @canvas[x, y] = (new_pixel & 0xFFFFFF00) | alpha
34
43
  end
35
44
  end
36
45
  end
37
46
 
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 ']
47
+ def overlay_color
48
+ @overlay_color ||= ChunkyPNG::Color.rgb(r, g, b)
50
49
  end
51
50
 
52
51
  def r
@@ -60,6 +59,25 @@ class PSD
60
59
  def b
61
60
  @b ||= color_data['Bl '].round
62
61
  end
62
+
63
+ def a
64
+ @a ||= (overlay_data['Opct'][:value] * 2.55).ceil
65
+ end
66
+ alias_method :overlay_opacity, :a
67
+
68
+ private
69
+
70
+ def blending_mode
71
+ @blending_mode ||= BlendMode::BLEND_MODES[BLEND_TRANSLATION[overlay_data['Md '][:value]].to_sym]
72
+ end
73
+
74
+ def overlay_data
75
+ @data['SoFi']
76
+ end
77
+
78
+ def color_data
79
+ overlay_data['Clr ']
80
+ end
63
81
  end
64
82
  end
65
83
  end
@@ -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
+ ]
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
@@ -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
data/lib/psd/renderer.rb CHANGED
@@ -1,11 +1,20 @@
1
- require_relative 'renderer/canvas_management'
1
+ require 'psd/renderer/blender'
2
+ require 'psd/renderer/canvas'
3
+ require 'psd/renderer/canvas_management'
4
+ require 'psd/renderer/clipping_mask'
5
+ require 'psd/renderer/compose'
6
+ require 'psd/renderer/layer_styles'
7
+ require 'psd/renderer/mask'
8
+ require 'psd/renderer/mask_canvas'
2
9
 
3
10
  class PSD
4
11
  class Renderer
5
12
  include CanvasManagement
6
13
 
7
- def initialize(node)
14
+ def initialize(node, opts = {})
8
15
  @root_node = node
16
+ @opts = opts
17
+ @render_hidden = opts.delete(:render_hidden)
9
18
 
10
19
  # Our canvas always starts as the full document size because
11
20
  # all measurements are relative to this size. We can later crop
@@ -23,7 +32,7 @@ class PSD
23
32
  PSD.logger.debug "Beginning render process"
24
33
 
25
34
  # Create our base canvas
26
- create_group_canvas(active_node, active_node.width, active_node.height)
35
+ create_group_canvas(active_node, active_node.width, active_node.height, base: true)
27
36
 
28
37
  # Begin the rendering process
29
38
  execute_pipeline
@@ -34,8 +43,10 @@ class PSD
34
43
  def execute_pipeline
35
44
  PSD.logger.debug "Executing pipeline on #{active_node.debug_name}"
36
45
  children.reverse.each do |child|
37
- # We skip over hidden nodes. Maybe something configurable in the future?
38
- next unless child.visible?
46
+ # We skip over hidden nodes unless explicitly set otherwise.
47
+ # If rendering a single PSD::Node::Layer, this is automatically
48
+ # set so that hidden layers will render.
49
+ next if !@render_hidden && !child.visible?
39
50
 
40
51
  if child.group?
41
52
  push_node(child)
@@ -56,7 +67,7 @@ class PSD
56
67
  pop_node and next
57
68
  end
58
69
 
59
- canvas = Canvas.new(child)
70
+ canvas = Canvas.new(child, nil, nil, @opts)
60
71
  canvas.paint_to active_canvas
61
72
  end
62
73
  end
@@ -88,4 +99,4 @@ class PSD
88
99
  @node_stack.last
89
100
  end
90
101
  end
91
- end
102
+ end
@@ -1,10 +1,18 @@
1
1
  class PSD
2
2
  class Resource
3
- class Section
3
+ module Section
4
4
  def self.factory(file, resource)
5
+ if path_resource?(resource)
6
+ section = Section::SavedPath.new(file, resource)
7
+ section.parse
8
+ return section.name
9
+ end
10
+
5
11
  Section.constants.each do |c|
12
+ next if c == :Base
13
+
6
14
  section = Section.const_get(c)
7
- next unless section.id == resource.id
15
+ next unless section.resource_id == resource.id
8
16
 
9
17
  section.new(file, resource).parse
10
18
  return section.name
@@ -13,10 +21,9 @@ class PSD
13
21
  return nil
14
22
  end
15
23
 
16
- def initialize(file, resource)
17
- @file = file
18
- @resource = resource
24
+ def self.path_resource?(resource)
25
+ resource.id >= 2000 && resource.id <= 2997
19
26
  end
20
27
  end
21
28
  end
22
- end
29
+ end
@@ -0,0 +1,33 @@
1
+ class PSD
2
+ class Resource
3
+ module Section
4
+ class Base
5
+ def self.resource_id(id = nil)
6
+ @resource_id = id unless id.nil?
7
+ @resource_id
8
+ end
9
+
10
+ def self.name(name = nil)
11
+ @name = name unless name.nil?
12
+ @name
13
+ end
14
+
15
+ def initialize(file, resource)
16
+ @file = file
17
+ @resource = resource
18
+ end
19
+
20
+ def resource_id; self.class.resource_id; end
21
+ def name; self.class.name; end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ require 'psd/resources/guides'
28
+ require 'psd/resources/layer_comps'
29
+ require 'psd/resources/resolution_info'
30
+ require 'psd/resources/saved_path'
31
+ require 'psd/resources/slices'
32
+ require 'psd/resources/work_path'
33
+ require 'psd/resources/xmp_metadata'
@@ -1,9 +1,11 @@
1
+ require 'psd/resources/base'
2
+
1
3
  class PSD
2
4
  class Resource
3
- class Section
4
- class Guides < Section
5
- def self.id; 1032; end
6
- def self.name; :guides; end
5
+ module Section
6
+ class Guides < Base
7
+ resource_id 1032
8
+ name :guides
7
9
 
8
10
  def parse
9
11
  # Descriptor version
@@ -1,9 +1,11 @@
1
+ require 'psd/resources/base'
2
+
1
3
  class PSD
2
4
  class Resource
3
- class Section
4
- class LayerComps < Section
5
- def self.id; 1065; end
6
- def self.name; :layer_comps; end
5
+ module Section
6
+ class LayerComps < Base
7
+ resource_id 1065
8
+ name :layer_comps
7
9
 
8
10
  def self.visibility_captured?(comp)
9
11
  comp[:captured_info] & 0b001 > 0
@@ -0,0 +1,48 @@
1
+ require 'psd/resources/base'
2
+
3
+ class PSD
4
+ class Resource
5
+ module Section
6
+ class ResolutionInfo < Base
7
+ RES_UNIT_NAMES = %w(pixel/inch pixel/cm).freeze
8
+ UNIT_NAMES = %w(in cm pt picas columns).freeze
9
+
10
+ resource_id 1005
11
+ name :resolution_info
12
+
13
+ attr_reader :h_res, :h_res_unit, :width_unit
14
+ attr_reader :v_res, :v_res_unit, :height_unit
15
+
16
+ def parse
17
+ # 32-bit fixed-point number (16.16)
18
+ @h_res = @file.read_uint.to_f / (1 << 16)
19
+ @h_res_unit = @file.read_ushort
20
+ @width_unit = @file.read_ushort
21
+
22
+ # 32-bit fixed-point number (16.16)
23
+ @v_res = @file.read_uint.to_f / (1 << 16)
24
+ @v_res_unit = @file.read_ushort
25
+ @height_unit = @file.read_ushort
26
+
27
+ @resource.data = self
28
+ end
29
+
30
+ def h_res_unit_name
31
+ RES_UNIT_NAMES.fetch(h_res_unit - 1, 'unknown')
32
+ end
33
+
34
+ def v_res_unit_name
35
+ RES_UNIT_NAMES.fetch(v_res_unit - 1, 'unknown')
36
+ end
37
+
38
+ def width_unit_name
39
+ UNIT_NAMES.fetch(width_unit - 1, 'unknown')
40
+ end
41
+
42
+ def height_unit_name
43
+ UNIT_NAMES.fetch(height_unit - 1, 'unknown')
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,19 @@
1
+ require 'psd/resources/base'
2
+
3
+ class PSD
4
+ class Resource
5
+ module Section
6
+ class SavedPath < Base
7
+ def parse
8
+ paths = []
9
+ record_count = @resource.size / 26
10
+ record_count.times do
11
+ paths << PathRecord.new(@file)
12
+ end
13
+
14
+ @resource.data = paths
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end