psd 1.5.0 → 2.0.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/README.md +2 -1
  4. data/lib/psd.rb +2 -1
  5. data/lib/psd/blend_mode.rb +5 -1
  6. data/lib/psd/channel_image.rb +6 -4
  7. data/lib/psd/header.rb +8 -0
  8. data/lib/psd/helpers.rb +13 -1
  9. data/lib/psd/image.rb +1 -1
  10. data/lib/psd/image_exports/png.rb +2 -70
  11. data/lib/psd/image_formats/layer_raw.rb +1 -1
  12. data/lib/psd/image_formats/raw.rb +2 -4
  13. data/lib/psd/image_modes/cmyk.rb +16 -6
  14. data/lib/psd/layer/helpers.rb +1 -1
  15. data/lib/psd/layer_info/fill_opacity.rb +2 -2
  16. data/lib/psd/logger.rb +7 -1
  17. data/lib/psd/node.rb +6 -2
  18. data/lib/psd/node_group.rb +9 -1
  19. data/lib/psd/node_root.rb +16 -3
  20. data/lib/psd/nodes/build_preview.rb +7 -69
  21. data/lib/psd/nodes/search.rb +11 -10
  22. data/lib/psd/renderer.rb +91 -0
  23. data/lib/psd/renderer/blender.rb +53 -0
  24. data/lib/psd/renderer/canvas.rb +95 -0
  25. data/lib/psd/renderer/canvas_management.rb +26 -0
  26. data/lib/psd/renderer/clipping_mask.rb +41 -0
  27. data/lib/psd/{compose.rb → renderer/compose.rb} +23 -19
  28. data/lib/psd/renderer/layer_styles.rb +56 -0
  29. data/lib/psd/renderer/layer_styles/color_overlay.rb +65 -0
  30. data/lib/psd/renderer/layer_styles/drop_shadow.rb +75 -0
  31. data/lib/psd/renderer/mask.rb +68 -0
  32. data/lib/psd/resources/guides.rb +35 -0
  33. data/lib/psd/version.rb +1 -1
  34. data/psd.gemspec +3 -3
  35. data/spec/files/blendmodes.psd +0 -0
  36. data/spec/files/empty-layer-subgroups.psd +0 -0
  37. data/spec/files/guides.psd +0 -0
  38. data/spec/guides_spec.rb +34 -0
  39. data/spec/hierarchy_spec.rb +27 -3
  40. data/spec/image_spec.rb +36 -35
  41. data/spec/parsing_spec.rb +13 -0
  42. metadata +23 -7
  43. data/lib/psd/clipping_mask.rb +0 -49
  44. data/lib/psd/layer_styles.rb +0 -84
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 87dd8d12bd49feb7ad2502047fa2ec73d4aff7b1
4
- data.tar.gz: 5647eca77e19f0a4ebb3ac59448d8b26d0f56da8
3
+ metadata.gz: ef242e267a313a38f9ef7cebab141f870c85555e
4
+ data.tar.gz: ee3814e3f35306f61b9b26d6124ef021ec22cc00
5
5
  SHA512:
6
- metadata.gz: d82f2fde33a13a60863548bc75788ffd279651e1c66a553749d794080733463e4f78cf13cfbcbf58a4d6296579f678cc3e32a0e6419c83ef7273e1b41d1406c5
7
- data.tar.gz: 23a3d1ba099ed12c363958af463393e56702e2ac2a02ba67a9af247016fbe2f384f893f5ea5dad4bdb651cb76196f9a77d42efe69cf7ced445ea6177e4ffbffe
6
+ metadata.gz: d7260fadf4dc3a2e99126427a85ce355832a897a058d681e7afe708116c427cf76210f766fcc7641b5c43e889c9c295f8a38c269af16d6ffc96ce861ac21565b
7
+ data.tar.gz: e97f791180a8452ed959fd72c831929d95d0666f7178d8f4d7dcf92bf229c6004e761782b251a8d3958e324d3a0de139be746db609d21921dd81947f27c37ae2
@@ -2,5 +2,5 @@ language: ruby
2
2
  rvm:
3
3
  - 1.9.3
4
4
  - 2.0.0
5
+ - 2.1.0
5
6
  - jruby-19mode
6
- # - rbx-19mode
data/README.md CHANGED
@@ -202,7 +202,7 @@ psd.image.save_as_png 'path/to/output.png' # writes PNG to disk
202
202
  If you run into any problems parsing a PSD, you can enable debug logging via the `PSD_DEBUG` environment variable. For example:
203
203
 
204
204
  ``` bash
205
- PSD_DEBUG=STDOUT bundle exec examples/parse.rb
205
+ PSD_DEBUG=true bundle exec examples/parse.rb
206
206
  ```
207
207
 
208
208
  You can also give a path to a file instead. If you need to enable debugging programatically:
@@ -232,3 +232,4 @@ There are a few features that are currently missing from PSD.rb.
232
232
  * More image modes + depths for image exporting
233
233
  * A few layer info blocks
234
234
  * Support for rendering all layer styles
235
+ * Render engine fixes for groups with lowered opacity
data/lib/psd.rb CHANGED
@@ -1,13 +1,14 @@
1
1
  require "bindata"
2
2
  require "psd/enginedata"
3
+ require "chunky_png"
3
4
 
4
5
  require_relative 'psd/section'
5
6
 
6
7
  dir_root = File.dirname(File.absolute_path(__FILE__)) + '/psd'
7
8
  [
9
+ '/image_exports/*',
8
10
  '/image_formats/*',
9
11
  '/image_modes/*',
10
- '/image_exports/*',
11
12
  '/nodes/*',
12
13
  '/layer_info/*',
13
14
  '/layer/*',
@@ -36,7 +36,11 @@ class PSD
36
36
  lLit: 'linear light',
37
37
  pLit: 'pin light',
38
38
  hMix: 'hard mix',
39
- pass: 'passthru'
39
+ pass: 'passthru',
40
+ dkCl: 'darker color',
41
+ lgCl: 'lighter color',
42
+ fsub: 'subtract',
43
+ fdiv: 'divide'
40
44
  }
41
45
 
42
46
  # Get the readable name for this blend mode.
@@ -55,6 +55,8 @@ class PSD
55
55
  @height = @layer.height
56
56
  end
57
57
 
58
+ @length = @width * @height
59
+
58
60
  start = @file.tell
59
61
 
60
62
  PSD.logger.debug "Channel ##{ch_info[:id]}, length = #{ch_info[:length]}"
@@ -68,13 +70,13 @@ class PSD
68
70
  end
69
71
  end
70
72
 
71
- @width = @layer.width
72
- @height = @layer.height
73
-
74
- if @channel_data.length != @length
73
+ if @channel_data.length != (@length * @channels_info.length)
75
74
  PSD.logger.error "#{@channel_data.length} read; expected #{@length}"
76
75
  end
77
76
 
77
+ @width = @layer.width
78
+ @height = @layer.height
79
+
78
80
  parse_user_mask
79
81
  process_image_data
80
82
  end
@@ -57,5 +57,13 @@ class PSD
57
57
  def height
58
58
  rows
59
59
  end
60
+
61
+ def rgb?
62
+ mode == 3
63
+ end
64
+
65
+ def cmyk?
66
+ mode == 4
67
+ end
60
68
  end
61
69
  end
@@ -32,8 +32,20 @@ class PSD
32
32
  @root ||= PSD::Node::Root.new(self)
33
33
  end
34
34
 
35
+ def resource(id)
36
+ @resources[id].nil? ? nil : @resources[id].data
37
+ end
38
+
35
39
  def layer_comps
36
- @resources[:layer_comps].data.to_a
40
+ resource(:layer_comps).to_a
41
+ end
42
+
43
+ def guides
44
+ resource(:guides).to_a
45
+ end
46
+
47
+ def slices
48
+ resource(:slices).to_a
37
49
  end
38
50
  end
39
51
  end
@@ -17,7 +17,7 @@ class PSD
17
17
  'RLE',
18
18
  'ZIP',
19
19
  'ZIPPrediction'
20
- ]
20
+ ].freeze
21
21
 
22
22
  # Store a reference to the file and the header. We also do a few simple calculations
23
23
  # to figure out the number of pixels in the image and the length of each channel.
@@ -1,5 +1,3 @@
1
- require RUBY_ENGINE =~ /jruby/ ? 'chunky_png' : 'oily_png'
2
-
3
1
  class PSD::Image
4
2
  module Export
5
3
  # PNG image export. This is the default export format.
@@ -15,7 +13,7 @@ class PSD::Image
15
13
  i = 0
16
14
  height.times do |y|
17
15
  width.times do |x|
18
- @png[x,y] = @pixel_data[i]
16
+ @png[x, y] = @pixel_data[i]
19
17
  i += 1
20
18
  end
21
19
  end
@@ -24,76 +22,10 @@ class PSD::Image
24
22
  end
25
23
  alias :export :to_png
26
24
 
27
- def to_png_with_mask
28
- return to_png unless has_mask?
29
- return @png_with_mask if @png_with_mask
30
-
31
- PSD.logger.debug "Beginning PNG export with mask"
32
-
33
- # We generate the preview at the document size instead to make applying the mask
34
- # significantly easier.
35
- width = @layer.header.width.to_i
36
- height = @layer.header.height.to_i
37
- @png_with_mask = ChunkyPNG::Canvas.new(width, height, ChunkyPNG::Color::TRANSPARENT)
38
-
39
- i = 0
40
- @layer.height.times do |y|
41
- @layer.width.times do |x|
42
- offset_x = x + @layer.left
43
- offset_y = y + @layer.top
44
-
45
- i +=1 and next if offset_x < 0 || offset_y < 0 || offset_x >= @png_with_mask.width || offset_y >= @png_with_mask.height
46
-
47
- @png_with_mask[offset_x, offset_y] = @pixel_data[i]
48
- i += 1
49
- end
50
- end
51
-
52
- # Now we apply the mask
53
- i = 0
54
- @layer.mask.height.times do |y|
55
- @layer.mask.width.times do |x|
56
- offset_x = @layer.mask.left + x
57
- offset_y = @layer.mask.top + y
58
-
59
- i += 1 and next if offset_x < 0 || offset_y < 0 || offset_x >= @png_with_mask.width || offset_y >= @png_with_mask.height
60
-
61
- color = ChunkyPNG::Color.to_truecolor_alpha_bytes(@png_with_mask[offset_x, offset_y])
62
- color[3] = color[3] * @mask_data[i] / 255
63
-
64
- @png_with_mask[offset_x, offset_y] = ChunkyPNG::Color.rgba(*color)
65
- i += 1
66
- end
67
- end
68
-
69
- crop_left = PSD::Util.clamp(@layer.left, 0, @png_with_mask.width)
70
- crop_top = PSD::Util.clamp(@layer.top, 0, @png_with_mask.height)
71
- crop_width = PSD::Util.clamp(@layer.width.to_i, 0, @png_with_mask.width - crop_left)
72
- crop_height = PSD::Util.clamp(@layer.height.to_i, 0, @png_with_mask.height - crop_top)
73
-
74
- @png_with_mask.crop!(crop_left, crop_top, crop_width, crop_height)
75
- end
76
-
77
- def mask_to_png
78
- return unless has_mask?
79
-
80
- png = ChunkyPNG::Canvas.new(@layer.mask.width.to_i, @layer.mask.height.to_i, ChunkyPNG::Color::TRANSPARENT)
81
-
82
- i = 0
83
- @layer.mask.height.times do |y|
84
- @layer.mask.width.times do |x|
85
- png[x, y] = ChunkyPNG::Color.grayscale(@mask_data[i])
86
- i += 1
87
- end
88
- end
89
-
90
- png
91
- end
92
-
93
25
  # Saves the PNG data to disk.
94
26
  def save_as_png(file)
95
27
  to_png.save(file, :fast_rgba)
96
28
  end
97
29
  end
98
30
  end
99
- end
31
+ end
@@ -7,7 +7,7 @@ class PSD
7
7
  # of the RAW encoding parser a bit. This version is aware of the current
8
8
  # channel data position, since layers that have RAW encoding often use RLE
9
9
  # encoded alpha channels.
10
- def parse_raw!(length = @length)
10
+ def parse_raw!
11
11
  PSD.logger.debug "Attempting to parse RAW encoded channel..."
12
12
 
13
13
  (@chan_pos...(@chan_pos + @ch_info[:length] - 2)).each do |i|
@@ -4,10 +4,8 @@ class PSD
4
4
  module RAW
5
5
  private
6
6
 
7
- def parse_raw!(length = @length)
8
- length.times do |i|
9
- @channel_data[i] = @file.read(1).bytes.to_a[0]
10
- end
7
+ def parse_raw!
8
+ @channel_data = @file.read(@length).bytes.to_a
11
9
  end
12
10
  end
13
11
  end
@@ -5,14 +5,24 @@ class PSD
5
5
 
6
6
  def combine_cmyk_channel
7
7
  (0...@num_pixels).step(pixel_step) do |i|
8
- c = @channel_data[i]
9
- m = @channel_data[i + @channel_length]
10
- y = @channel_data[i + @channel_length * 2]
11
- k = @channel_data[i + @channel_length * 3]
12
- a = (channels == 5 ? @channel_data[i + @channel_length * 4] : 255)
8
+ c = m = y = k = 0
9
+ a = 255
13
10
 
14
- rgb = PSD::Color.cmyk_to_rgb(255 - c, 255 - m, 255 - y, 255 - k)
11
+ @channels_info.each_with_index do |chan, index|
12
+ next if chan[:id] == -2
13
+
14
+ val = @channel_data[i + (@channel_length * index)]
15
15
 
16
+ case chan[:id]
17
+ when -1 then a = val
18
+ when 0 then c = val
19
+ when 1 then m = val
20
+ when 2 then y = val
21
+ when 3 then k = val
22
+ end
23
+ end
24
+
25
+ rgb = PSD::Color.cmyk_to_rgb(255 - c, 255 - m, 255 - y, 255 - k)
16
26
  @pixel_data.push ChunkyPNG::Color.rgba(*rgb.values, a)
17
27
  end
18
28
  end
@@ -70,7 +70,7 @@ class PSD
70
70
 
71
71
  def fill_opacity
72
72
  return 255 unless info.has_key?(:fill_opacity)
73
- info[:fill_opacity].enabled
73
+ info[:fill_opacity].value
74
74
  end
75
75
  end
76
76
  end
@@ -4,10 +4,10 @@ class PSD
4
4
  class FillOpacity < LayerInfo
5
5
  @key = 'iOpa'
6
6
 
7
- attr_reader :enabled
7
+ attr_reader :value
8
8
 
9
9
  def parse
10
- @enabled = @file.read_boolean
10
+ @value = @file.read_byte.to_i
11
11
  end
12
12
  end
13
13
  end
@@ -7,7 +7,13 @@ class PSD
7
7
  end
8
8
 
9
9
  module ClassMethods
10
- attr_accessor :debug
10
+ attr_reader :debug
11
+ attr_writer :logger
12
+
13
+ def debug=(enabled)
14
+ @debug = enabled
15
+ @logger = nil
16
+ end
11
17
 
12
18
  def logger
13
19
  return @logger if @logger
@@ -21,7 +21,7 @@ class PSD
21
21
  @children << layer
22
22
  end
23
23
 
24
- @force_visible = false
24
+ @force_visible = nil
25
25
  end
26
26
 
27
27
  def hidden?
@@ -29,7 +29,7 @@ class PSD
29
29
  end
30
30
 
31
31
  def visible?
32
- force_visible || @layer.visible?
32
+ force_visible.nil? ? @layer.visible? : force_visible
33
33
  end
34
34
 
35
35
  def psd
@@ -44,6 +44,10 @@ class PSD
44
44
  is_a?(PSD::Node::Group) || is_a?(PSD::Node::Root)
45
45
  end
46
46
 
47
+ def debug_name
48
+ root? ? ":root:" : name
49
+ end
50
+
47
51
  def to_hash
48
52
  hash = {
49
53
  type: nil,
@@ -46,8 +46,16 @@ class PSD::Node
46
46
  @children.each{ |c| c.show! }
47
47
  end
48
48
 
49
+ def passthru_blending?
50
+ blending_mode == 'passthru'
51
+ end
52
+
49
53
  def empty?
50
- @children.empty?
54
+ @children.each do |child|
55
+ return false unless child.empty?
56
+ end
57
+
58
+ return true
51
59
  end
52
60
 
53
61
  # Export this layer and it's children to a hash recursively.
@@ -7,6 +7,7 @@ class PSD::Node
7
7
  include PSD::Node::ParseLayers
8
8
 
9
9
  attr_accessor :children
10
+ attr_reader :psd
10
11
 
11
12
  # Stores a reference to the parsed PSD and builds the
12
13
  # tree hierarchy.
@@ -21,25 +22,32 @@ class PSD::Node
21
22
  children: children.map(&:to_hash),
22
23
  document: {
23
24
  width: document_width,
24
- height: document_height
25
+ height: document_height,
26
+ resources: {
27
+ layer_comps: @psd.layer_comps,
28
+ guides: @psd.guides,
29
+ slices: @psd.slices
30
+ }
25
31
  }
26
32
  }
27
33
  end
28
34
 
29
35
  # Returns the width and height of the entire PSD document.
30
36
  def document_dimensions
31
- [@psd.header.width, @psd.header.height]
37
+ [document_width, document_height]
32
38
  end
33
39
 
34
40
  # The width of the full PSD document as defined in the header.
35
41
  def document_width
36
42
  @psd.header.width.to_i
37
43
  end
44
+ alias_method :width, :document_width
38
45
 
39
46
  # The height of the full PSD document as defined in the header.
40
47
  def document_height
41
48
  @psd.header.height.to_i
42
49
  end
50
+ alias_method :height, :document_height
43
51
 
44
52
  # The root node has no name since it's not an actual layer or group.
45
53
  def name
@@ -51,7 +59,12 @@ class PSD::Node
51
59
  0
52
60
  end
53
61
 
54
- def psd; @psd; end
62
+ [:top, :right, :bottom, :left].each do |meth|
63
+ define_method(meth) { 0 }
64
+ end
65
+
66
+ def opacity; 255; end
67
+ def fill_opacity; 255; end
55
68
 
56
69
  private
57
70
 
@@ -1,78 +1,16 @@
1
- class PSD
1
+ class PSD
2
2
  class Node
3
3
  module BuildPreview
4
- include PSD::Image::Export::PNG
5
-
6
- alias :orig_to_png :to_png
7
- def to_png
8
- return build_png if group?
9
- layer.image.to_png_with_mask
10
- end
11
-
12
- def build_png(png=nil)
13
- png ||= create_canvas
14
-
15
- children.reverse.each do |c|
16
- next unless c.visible?
17
-
18
- if c.group?
19
- if c.blending_mode == 'passthru'
20
- c.build_png(png)
21
- else
22
- compose! c, png, c.build_png, 0, 0
23
- end
24
- else
25
- compose!(
26
- c,
27
- png,
28
- c.image.to_png_with_mask,
29
- PSD::Util.clamp(c.left.to_i, 0, png.width),
30
- PSD::Util.clamp(c.top.to_i, 0, png.height)
31
- )
32
- end
33
- end
34
-
35
- png
36
- end
37
-
38
- private
39
-
40
- def create_canvas
41
- width, height = document_dimensions
42
- ChunkyPNG::Canvas.new(width.to_i, height.to_i, ChunkyPNG::Color::TRANSPARENT)
4
+ def renderer
5
+ PSD::Renderer.new(self)
43
6
  end
44
7
 
45
- # Modified from ChunkyPNG::Canvas#compose! in order to support various blend modes.
46
- def compose!(layer, base, other, offset_x, offset_y)
47
- blending_mode = layer.blending_mode.gsub(/ /, '_')
48
- PSD.logger.warn("Blend mode #{blending_mode} is not implemented") unless Compose.respond_to?(blending_mode)
49
- PSD.logger.debug("Blending #{layer.name} with #{blending_mode} blend mode")
50
-
51
- other = ClippingMask.new(layer, other).apply
52
- LayerStyles.new(layer, other).apply!
53
-
54
- blend_pixels!(blending_mode, layer, base, other, offset_x, offset_y)
8
+ def to_png
9
+ renderer.to_png
55
10
  end
56
11
 
57
- def blend_pixels!(blending_mode, layer, base, other, offset_x, offset_y)
58
- other.height.times do |y|
59
- other.width.times do |x|
60
- base_x = x + offset_x
61
- base_y = y + offset_y
62
-
63
- next if base_x < 0 || base_y < 0 || base_x >= base.width || base_y >= base.height
64
-
65
- color = Compose.send(
66
- blending_mode,
67
- other[x, y],
68
- base[base_x, base_y],
69
- opacity: layer.opacity,
70
- fill_opacity: layer.fill_opacity
71
- )
72
-
73
- base[base_x, base_y] = color
74
- end
75
- end
12
+ def save_as_png(output)
13
+ to_png.save(output)
76
14
  end
77
15
  end
78
16
  end