psd 1.5.0 → 2.0.0

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