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,12 +1,12 @@
1
1
  class PSD
2
- class Node
2
+ module Node
3
3
  module BuildPreview
4
- def renderer
5
- PSD::Renderer.new(self)
4
+ def renderer(opts = {})
5
+ PSD::Renderer.new(self, opts)
6
6
  end
7
7
 
8
8
  def to_png
9
- renderer.to_png
9
+ @png ||= renderer.to_png
10
10
  end
11
11
 
12
12
  def save_as_png(output)
@@ -0,0 +1,35 @@
1
+ require 'psd/node'
2
+
3
+ class PSD
4
+ module Node
5
+ # Represents a group, or folder, in the PSD document. It can have
6
+ # zero or more children nodes.
7
+ class Group < PSD::Node::Base
8
+ def passthru_blending?
9
+ blending_mode == 'passthru'
10
+ end
11
+
12
+ def empty?
13
+ children.each do |child|
14
+ return false unless child.empty?
15
+ end
16
+
17
+ return true
18
+ end
19
+
20
+ # Export this layer and it's children to a hash recursively.
21
+ def to_hash
22
+ super.merge({
23
+ type: :group,
24
+ children: children.map(&:to_hash)
25
+ })
26
+ end
27
+
28
+ # If the method is missing, we blindly send it to the layer.
29
+ # The layer handles the case in which the method doesn't exist.
30
+ def method_missing(method, *args, &block)
31
+ @layer.send(method, *args, &block)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,40 @@
1
+ require 'psd/node'
2
+
3
+ class PSD
4
+ module Node
5
+ class Layer < PSD::Node::Base
6
+ attr_reader :layer
7
+
8
+ [:text, :ref_x, :ref_y, :blending_mode].each do |prop|
9
+ delegate prop, to: :@layer
10
+ delegate "#{prop}=", to: :@layer
11
+ end
12
+
13
+ def empty?
14
+ width == 0 || height == 0
15
+ end
16
+
17
+ # Exports this layer to a Hash.
18
+ def to_hash
19
+ super.merge({
20
+ type: :layer,
21
+ text: @layer.text,
22
+ ref_x: @layer.reference_point.x,
23
+ ref_y: @layer.reference_point.y,
24
+ mask: @layer.mask.to_hash,
25
+ image: {
26
+ width: @layer.image.width,
27
+ height: @layer.image.height,
28
+ channels: @layer.channels_info
29
+ }
30
+ })
31
+ end
32
+
33
+ # If the method is missing, we blindly send it to the layer.
34
+ # The layer handles the case in which the method doesn't exist.
35
+ def method_missing(method, *args, &block)
36
+ @layer.send(method, *args, &block)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,90 @@
1
+ require 'psd/node'
2
+
3
+ class PSD
4
+ module Node
5
+ # Represents the root node of a Photoshop document
6
+ class Root < PSD::Node::Base
7
+ include Ancestry
8
+ include Search
9
+ include BuildPreview
10
+
11
+ attr_accessor :children
12
+ attr_reader :psd
13
+
14
+ alias_method :document_width, :width
15
+ alias_method :document_height, :height
16
+
17
+ RootLayer = Struct.new("RootLayer", :node, *Base::PROPERTIES)
18
+
19
+ def self.layer_for_psd(psd)
20
+ RootLayer.new.tap do |layer|
21
+ layer.top = 0
22
+ layer.left = 0
23
+ layer.right = psd.header.width.to_i
24
+ layer.bottom = psd.header.height.to_i
25
+ end
26
+ end
27
+
28
+ # Stores a reference to the parsed PSD and builds the
29
+ # tree hierarchy.
30
+ def initialize(psd)
31
+ super self.class.layer_for_psd(psd)
32
+
33
+ @psd = psd
34
+ build_hierarchy
35
+ end
36
+
37
+ # Returns the width and height of the entire PSD document.
38
+ def document_dimensions
39
+ [document_width, document_height]
40
+ end
41
+
42
+ # The depth of the root node is always 0.
43
+ def depth
44
+ 0
45
+ end
46
+
47
+ def opacity; 255; end
48
+ def fill_opacity; 255; end
49
+
50
+ # Recursively exports the hierarchy to a Hash
51
+ def to_hash
52
+ {
53
+ children: children.map(&:to_hash),
54
+ document: {
55
+ width: document_width,
56
+ height: document_height,
57
+ resources: {
58
+ layer_comps: @psd.layer_comps,
59
+ guides: @psd.guides,
60
+ slices: @psd.slices
61
+ }
62
+ }
63
+ }
64
+ end
65
+
66
+ private
67
+
68
+ def build_hierarchy
69
+ current_group = self
70
+ parse_stack = []
71
+
72
+ # First we build the hierarchy
73
+ @psd.layers.each do |layer|
74
+ if layer.folder?
75
+ parse_stack.push current_group
76
+ current_group = PSD::Node::Group.new(layer, parse_stack.last)
77
+ elsif layer.folder_end?
78
+ parent = parse_stack.pop
79
+ parent.children.push current_group
80
+ current_group = parent
81
+ else
82
+ current_group.children.push PSD::Node::Layer.new(layer, current_group)
83
+ end
84
+ end
85
+
86
+ update_dimensions!
87
+ end
88
+ end
89
+ end
90
+ end
@@ -1,5 +1,5 @@
1
1
  class PSD
2
- class Node
2
+ module Node
3
3
  module Search
4
4
  # Searches the tree structure for a node at the given path. The path is
5
5
  # defined by the layer/folder names. Because the PSD format does not
@@ -8,6 +8,7 @@ class PSD
8
8
  def children_at_path(path, opts={})
9
9
  path = path.split('/').delete_if { |p| p == "" } unless path.is_a?(Array)
10
10
 
11
+ path = path.dup
11
12
  query = path.shift
12
13
  matches = children.select do |c|
13
14
  if opts[:case_sensitive]
@@ -20,7 +21,7 @@ class PSD
20
21
  if path.length == 0
21
22
  return matches
22
23
  else
23
- return matches.map { |m| m.children_at_path(path, opts) }.flatten
24
+ return matches.map { |m| m.children_at_path(path.dup, opts) }.flatten
24
25
  end
25
26
  end
26
27
  alias :children_with_path :children_at_path
@@ -39,34 +40,32 @@ class PSD
39
40
  end
40
41
 
41
42
  root = PSD::Node::Root.new(psd)
42
- filter_for_comp!(comp, root)
43
-
44
- return root
45
- end
46
-
47
- private
48
-
49
- def filter_for_comp!(comp, node)
43
+
50
44
  # Force layers to be visible if they are enabled for the comp
51
- node.children.each do |c|
45
+ root.descendants.each do |c|
52
46
  set_visibility(comp, c) if Resource::Section::LayerComps.visibility_captured?(comp)
53
47
  set_position(comp, c) if Resource::Section::LayerComps.position_captured?(comp)
54
48
 
55
- filter_for_comp!(comp, c) if c.group?
49
+ PSD.logger.debug "#{c.path}: visible = #{c.visible?}, position = #{c.left}, #{c.top}"
56
50
  end
51
+
52
+ return root
57
53
  end
58
54
 
55
+ private
56
+
59
57
  def set_visibility(comp, c)
60
58
  visible = true
59
+ found = false
61
60
 
62
61
  c
63
62
  .metadata
64
63
  .data[:layer_comp]['layerSettings'].each do |l|
65
64
  visible = l['enab'] if l.has_key?('enab')
66
- break if l['compList'].include?(comp[:id])
65
+ found = true and break if l['compList'].include?(comp[:id])
67
66
  end
68
67
 
69
- c.force_visible = visible
68
+ c.force_visible = found && visible
70
69
  end
71
70
 
72
71
  def set_position(comp, c)
@@ -76,15 +75,16 @@ class PSD
76
75
  c
77
76
  .metadata
78
77
  .data[:layer_comp]['layerSettings'].each do |l|
79
- next unless l.has_key?('Ofst')
78
+ if l.has_key?('Ofst')
79
+ x = l['Ofst']['Hrzn']
80
+ y = l['Ofst']['Vrtc']
81
+ end
80
82
 
81
- x = l['Ofst']['Hrzn']
82
- y = l['Ofst']['Vrtc']
83
83
  break if l['compList'].include?(comp[:id])
84
84
  end
85
85
 
86
- c.left += x
87
- c.top += y
86
+ c.left_offset = x
87
+ c.top_offset = y
88
88
  end
89
89
  end
90
90
  end
@@ -25,22 +25,6 @@ class PSD
25
25
  end
26
26
  end
27
27
 
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
28
  # Exports the path record to an easier to work with hash.
45
29
  def to_hash
46
30
  case @record_type
@@ -51,6 +35,7 @@ class PSD
51
35
  when 1, 2, 4, 5
52
36
  {
53
37
  linked: @linked,
38
+ closed: [1, 2].include?(@record_type),
54
39
  preceding: {
55
40
  vert: @preceding_vert,
56
41
  horiz: @preceding_horiz
@@ -83,34 +68,6 @@ class PSD
83
68
  end.merge({ record_type: @record_type })
84
69
  end
85
70
 
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
71
  # Is this record a bezier point?
115
72
  def is_bezier_point?
116
73
  [1,2,4,5].include? @record_type
@@ -123,11 +80,6 @@ class PSD
123
80
  @file.seek(22, IO::SEEK_CUR)
124
81
  end
125
82
 
126
- def write_path_record(file)
127
- file.write_short @num_points
128
- file.seek(22, IO::SEEK_CUR)
129
- end
130
-
131
83
  def read_bezier_point
132
84
  @linked = [1,4].include? @record_type
133
85
 
@@ -141,15 +93,6 @@ class PSD
141
93
  @leaving_horiz = @file.read_path_number
142
94
  end
143
95
 
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
151
- end
152
-
153
96
  def read_clipboard_record
154
97
  @clipboard_top = @file.read_path_number
155
98
  @clipboard_left = @file.read_path_number
@@ -159,22 +102,9 @@ class PSD
159
102
  @file.seek(4, IO::SEEK_CUR)
160
103
  end
161
104
 
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
105
  def read_initial_fill
171
106
  @initial_fill = @file.read_short
172
107
  @file.seek(22, IO::SEEK_CUR)
173
108
  end
174
-
175
- def write_initial_fill(file)
176
- file.write_short @initial_fill
177
- file.seek(22, IO::SEEK_CUR)
178
- end
179
109
  end
180
110
  end
data/lib/psd/renderer.rb CHANGED
@@ -1,11 +1,12 @@
1
- require_relative 'renderer/canvas_management'
1
+ require 'psd/renderer/canvas_management'
2
2
 
3
3
  class PSD
4
4
  class Renderer
5
5
  include CanvasManagement
6
6
 
7
- def initialize(node)
7
+ def initialize(node, opts = {})
8
8
  @root_node = node
9
+ @opts = opts
9
10
 
10
11
  # Our canvas always starts as the full document size because
11
12
  # all measurements are relative to this size. We can later crop
@@ -23,7 +24,7 @@ class PSD
23
24
  PSD.logger.debug "Beginning render process"
24
25
 
25
26
  # Create our base canvas
26
- create_group_canvas(active_node, active_node.width, active_node.height)
27
+ create_group_canvas(active_node, active_node.width, active_node.height, base: true)
27
28
 
28
29
  # Begin the rendering process
29
30
  execute_pipeline
@@ -34,7 +35,7 @@ class PSD
34
35
  def execute_pipeline
35
36
  PSD.logger.debug "Executing pipeline on #{active_node.debug_name}"
36
37
  children.reverse.each do |child|
37
- # We skip over hidden nodes. Maybe something configurable in the future?
38
+ # We skip over hidden nodes.
38
39
  next unless child.visible?
39
40
 
40
41
  if child.group?
@@ -56,7 +57,7 @@ class PSD
56
57
  pop_node and next
57
58
  end
58
59
 
59
- canvas = Canvas.new(child)
60
+ canvas = Canvas.new(child, nil, nil, @opts)
60
61
  canvas.paint_to active_canvas
61
62
  end
62
63
  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