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,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