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
@@ -0,0 +1,56 @@
1
+ require_relative 'layer_styles/color_overlay'
2
+ require_relative 'layer_styles/drop_shadow'
3
+
4
+ class PSD
5
+ class LayerStyles
6
+ # Blend modes in layer effects use different keys
7
+ # than normal layer blend modes. Thanks Adobe.
8
+ BLEND_TRANSLATION = {
9
+ 'Nrml' => 'norm',
10
+ 'Dslv' => 'diss',
11
+ 'Drkn' => 'dark',
12
+ 'Mltp' => 'mul',
13
+ 'CBrn' => 'idiv',
14
+ 'linearBurn' => 'lbrn',
15
+ 'Lghn' => 'lite',
16
+ 'Scrn' => 'scrn',
17
+ 'CDdg' => 'div',
18
+ 'linearDodge' => 'lddg',
19
+ 'Ovrl' => 'over',
20
+ 'SftL' => 'sLit',
21
+ 'HrdL' => 'hLit',
22
+ 'vividLight' => 'vLit',
23
+ 'linearLight' => 'lLit',
24
+ 'pinLight' => 'pLit',
25
+ 'hardMix' => 'hMix',
26
+ 'Dfrn' => 'diff',
27
+ 'Xclu' => 'smud',
28
+ 'H ' => 'hue',
29
+ 'Strt' => 'sat',
30
+ 'Clr ' => 'colr',
31
+ 'Lmns' => 'lum'
32
+ }.freeze
33
+
34
+ attr_reader :canvas, :node, :data
35
+
36
+ def initialize(canvas)
37
+ @canvas = canvas
38
+ @node = @canvas.node
39
+ @data = @node.layer.info[:object_effects]
40
+
41
+ if @data.nil?
42
+ @applied = true
43
+ else
44
+ @data = @data.data
45
+ @applied = false
46
+ end
47
+ end
48
+
49
+ def apply!
50
+ return if @applied || data.nil?
51
+
52
+ ColorOverlay.new(self).apply! if ColorOverlay.should_apply?(data)
53
+ DropShadow.new(self).apply! if DropShadow.should_apply?(data)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,65 @@
1
+ class PSD
2
+ class LayerStyles
3
+ class ColorOverlay
4
+ def self.should_apply?(data)
5
+ data.has_key?('SoFi')
6
+ end
7
+
8
+ def initialize(styles)
9
+ @canvas = styles.canvas
10
+ @node = styles.node
11
+ @data = styles.data
12
+ end
13
+
14
+ 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
+ PSD.logger.debug "Layer style: layer = #{@node.name}, type = color overlay, blend mode = #{blending_mode}"
25
+
26
+ height.times do |y|
27
+ width.times do |x|
28
+ pixel = @canvas[x, y]
29
+ alpha = ChunkyPNG::Color.a(pixel)
30
+ next if alpha == 0
31
+
32
+ overlay_color = ChunkyPNG::Color.rgba(r, g, b, alpha)
33
+ @canvas[x, y] = Compose.send(blending_mode, overlay_color, pixel)
34
+ end
35
+ end
36
+ end
37
+
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 ']
50
+ end
51
+
52
+ def r
53
+ @r ||= color_data['Rd '].round
54
+ end
55
+
56
+ def g
57
+ @g ||= color_data['Grn '].round
58
+ end
59
+
60
+ def b
61
+ @b ||= color_data['Bl '].round
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,75 @@
1
+ class PSD
2
+ class LayerStyles
3
+ # Not ready yet.
4
+ class DropShadow
5
+ def self.should_apply?(data)
6
+ #data.has_key?('DrSh')
7
+ false
8
+ end
9
+
10
+ def initialize(styles)
11
+ end
12
+
13
+ def apply!
14
+ end
15
+
16
+ private
17
+
18
+ def apply_drop_shadow
19
+
20
+ end
21
+
22
+ def drop_shadow
23
+ data['DrSh']
24
+ end
25
+
26
+ def drop_shadow_blend_mode
27
+ drop_shadow['Md ']
28
+ end
29
+
30
+ def drop_shadow_opacity
31
+ drop_shadow['Opct'][:value]
32
+ end
33
+
34
+ def drop_shadow_light_angle
35
+ drop_shadow['lagl'][:value]
36
+ end
37
+
38
+ def drop_shadow_use_global_light?
39
+ drop_shadow['uglg']
40
+ end
41
+
42
+ def drop_shadow_distance
43
+ drop_shadow['Dstn'][:value]
44
+ end
45
+
46
+ def drop_shadow_spread
47
+ drop_shadow['Ckmt'][:value]
48
+ end
49
+
50
+ def drop_shadow_size
51
+ drop_shadow['blur'][:value]
52
+ end
53
+
54
+ def drop_shadow_noise
55
+ drop_shadow['Nose'][:value]
56
+ end
57
+
58
+ def drop_shadow_antialiased?
59
+ drop_shadow['AntA']
60
+ end
61
+
62
+ def drop_shadow_contour
63
+ drop_shadow['TrnS']['Nm ']
64
+ end
65
+
66
+ def drop_shadow_contour_curve
67
+ drop_shadow['TrnS']['Crv ']
68
+ end
69
+
70
+ def drop_shadow_knock_out?
71
+ drop_shadow['layerConceals']
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,68 @@
1
+ class PSD
2
+ class Renderer
3
+ class Mask
4
+ attr_accessor :pixel_data, :mask_data, :layer_width, :layer_height
5
+
6
+ def initialize(canvas, options = {})
7
+ @canvas = canvas
8
+ @layer = canvas.node
9
+ @options = options
10
+
11
+ @pixel_data = @canvas.pixels
12
+ @mask_data = @layer.image.mask_data
13
+
14
+ @layer_width = (@layer.folder? ? @layer.mask.width : @layer.width).to_i
15
+ @layer_height = (@layer.folder? ? @layer.mask.height : @layer.height).to_i
16
+ end
17
+
18
+ def apply!
19
+ PSD.logger.debug "Applying mask to #{@layer.name}"
20
+
21
+ # We generate the preview at the document size instead to make applying the mask
22
+ # significantly easier.
23
+ width = @layer.header.width.to_i
24
+ height = @layer.header.height.to_i
25
+ png = ChunkyPNG::Canvas.new(width, height, ChunkyPNG::Color::TRANSPARENT)
26
+
27
+ i = 0
28
+ @layer_height.times do |y|
29
+ @layer_width.times do |x|
30
+ offset_x = x + @layer.left
31
+ offset_y = y + @layer.top
32
+
33
+ i +=1 and next if offset_x < 0 || offset_y < 0 || offset_x >= png.width || offset_y >= png.height
34
+
35
+ png[offset_x, offset_y] = @pixel_data[i]
36
+ i += 1
37
+ end
38
+ end
39
+
40
+ # Now we apply the mask
41
+ i = 0
42
+ @layer.mask.height.times do |y|
43
+ @layer.mask.width.times do |x|
44
+ offset_x = @layer.mask.left + x
45
+ offset_y = @layer.mask.top + y
46
+
47
+ i += 1 and next if offset_x < 0 || offset_y < 0 || offset_x >= png.width || offset_y >= png.height
48
+
49
+ color = ChunkyPNG::Color.to_truecolor_alpha_bytes(png[offset_x, offset_y])
50
+ color[3] = color[3] * @mask_data[i] / 255
51
+
52
+ png[offset_x, offset_y] = ChunkyPNG::Color.rgba(*color)
53
+ i += 1
54
+ end
55
+ end
56
+
57
+ crop_left = PSD::Util.clamp(@layer.left, 0, png.width)
58
+ crop_top = PSD::Util.clamp(@layer.top, 0, png.height)
59
+ crop_width = PSD::Util.clamp(@layer_width, 0, png.width - crop_left)
60
+ crop_height = PSD::Util.clamp(@layer_height, 0, png.height - crop_top)
61
+
62
+ png.crop!(crop_left, crop_top, crop_width, crop_height)
63
+
64
+ @canvas.canvas = png
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,35 @@
1
+ class PSD
2
+ class Resource
3
+ class Section
4
+ class Guides < Section
5
+ def self.id; 1032; end
6
+ def self.name; :guides; end
7
+
8
+ def parse
9
+ # Descriptor version
10
+ @file.seek 4, IO::SEEK_CUR
11
+
12
+ # Future implementation of document-specific grids
13
+ @file.seek 8, IO::SEEK_CUR
14
+
15
+ num_guides = @file.read_int
16
+
17
+ @data = []
18
+
19
+ num_guides.times do
20
+ location = @file.read_int / 32
21
+ direction = @file.read_byte == 0 ? "vertical" : "horizontal"
22
+
23
+ @data.push({ :location => location, :direction => direction })
24
+ end
25
+
26
+ @resource.data = self
27
+ end
28
+
29
+ def to_a
30
+ @data
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,3 +1,3 @@
1
1
  class PSD
2
- VERSION = "1.5.0"
2
+ VERSION = "2.0.0"
3
3
  end
@@ -18,11 +18,11 @@ Gem::Specification.new do |gem|
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
20
 
21
- gem.add_dependency "rake"
22
- gem.add_dependency "bindata"
21
+ gem.add_dependency 'rake'
22
+ gem.add_dependency 'bindata'
23
23
  gem.add_dependency 'psd-enginedata', '~> 1.0'
24
24
 
25
- gem.add_dependency(RUBY_ENGINE =~ /jruby/ ? 'chunky_png' : 'oily_png')
25
+ gem.add_dependency 'chunky_png'
26
26
 
27
27
  gem.test_files = Dir.glob("spec/**/*")
28
28
  gem.add_development_dependency 'rspec'
Binary file
Binary file
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Guides' do
4
+ describe "Handle file with slice properly" do
5
+ it "should successfully parse the PSD file that has guides" do
6
+ psd = PSD.new('spec/files/guides.psd')
7
+ psd.parse!
8
+
9
+ # File should parse
10
+ expect(psd).to be_parsed
11
+
12
+ # Guides should not be nil
13
+ expect(psd.resources[:guides]).to_not be_nil
14
+
15
+ # Each guide should have a position and direction
16
+ psd.resources[:guides].data.to_a.each do |guide|
17
+ expect(guide).to_not be_nil
18
+ expect(guide[:location]).to_not be_nil
19
+ expect(guide[:direction]).to_not be_nil
20
+ end
21
+ end
22
+ end
23
+
24
+ describe "Handle file without guides properly" do
25
+ it "should successfully parse a PSD file which does not have guides" do
26
+ psd = PSD.new('spec/files/simplest.psd')
27
+ psd.parse!
28
+
29
+ expect(psd).to be_parsed
30
+
31
+ expect(psd.resources[:guides].data.to_a.size).to be 0
32
+ end
33
+ end
34
+ end
@@ -91,11 +91,12 @@ describe "Hierarchy" do
91
91
  expect { @tree.filter_by_comp('WAT') }.to raise_error("Layer comp not found")
92
92
  end
93
93
 
94
- it "should correctly filter and produce a new tree when filtering by layer comp" do
94
+ it "should correctly set visibility when filtering by layer comp" do
95
95
  tree = @tree.filter_by_comp('Version A')
96
96
  expect(tree).to be_an_instance_of(PSD::Node::Root)
97
- expect(tree.children.size).to eq(1)
98
- expect(tree.children[0].name).to eq("Version A")
97
+ expect(tree.children.size).to eq(3)
98
+ expect(tree.children_at_path('Version A').first).to be_visible
99
+ expect(tree.children_at_path('Version B').first).to_not be_visible
99
100
  end
100
101
 
101
102
  it "should return a new tree when filtering by layer comps" do
@@ -125,4 +126,27 @@ describe "Hierarchy" do
125
126
  expect(@psd.tree.children[0].top).to eq 450
126
127
  end
127
128
  end
129
+
130
+ describe "Size Calculation With Empty Layer in Subgroups" do
131
+ before(:each) do
132
+ @psd = PSD.new('spec/files/empty-layer-subgroups.psd')
133
+ @psd.parse!
134
+ end
135
+
136
+ it 'should correctly identify empty nodes' do
137
+ expect(@psd.tree.children_at_path('group/not empty').first).to_not be_empty
138
+ expect(@psd.tree.children_at_path('group/Group 2').first).to be_empty
139
+ expect(@psd.tree.children_at_path('group/Group 2/Group 1').first).to be_empty
140
+ expect(@psd.tree.children_at_path('group/Group 2/subgroup 1/subgroup 2').first).to be_empty
141
+ expect(@psd.tree.children_at_path('group/Group 2/subgroup 1/subgroup 2/empty layer').first).to be_empty
142
+ expect(@psd.tree.children[0]).to_not be_empty
143
+ end
144
+
145
+ it "should correctly calculate the size of a group" do
146
+ expect(@psd.tree.children[0].width).to eq 264
147
+ expect(@psd.tree.children[0].height).to eq 198
148
+ expect(@psd.tree.children[0].left).to eq 448
149
+ expect(@psd.tree.children[0].top).to eq 356
150
+ end
151
+ end
128
152
  end
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe 'Image Exporting' do
4
- let(:psd) { PSD.new('spec/files/pixel.psd') }
4
+ let!(:psd) { PSD.new('spec/files/pixel.psd') }
5
5
 
6
6
  describe "the full preview image" do
7
7
  it "should successfully parse the image data" do
@@ -33,52 +33,53 @@ describe 'Image Exporting' do
33
33
  end
34
34
  end
35
35
 
36
- describe "layer images" do
37
- it "should successfully parse the image data" do
38
- psd.options[:parse_layer_images] = true
36
+ describe "Renderer" do
37
+ before(:each) do
39
38
  psd.parse!
39
+ end
40
40
 
41
- image = psd.tree.children.first.image
42
- expect(image.width).to eq(1)
43
- expect(image.height).to eq(1)
41
+ it "should be available via any tree node" do
42
+ [psd.tree, psd.tree.children.first].each do |node|
43
+ expect(node).to respond_to(:renderer)
44
+ expect(node).to respond_to(:to_png)
45
+ expect(node).to respond_to(:save_as_png)
46
+ end
47
+ end
44
48
 
45
- expect(image.pixel_data).to eq([ChunkyPNG::Color.rgba(0, 100, 200, 255)])
49
+ it "returns a Renderer object" do
50
+ [psd.tree, psd.tree.children.first].each do |node|
51
+ expect(node.renderer).to be_an_instance_of(PSD::Renderer)
52
+ end
46
53
  end
47
54
 
48
- describe "as PNG" do
49
- it "should produce a valid PNG object" do
50
- psd.options[:parse_layer_images] = true
51
- psd.parse!
55
+ it "produces a correct PNG" do
56
+ expect(psd.tree.to_png).to be_an_instance_of(ChunkyPNG::Canvas)
57
+ expect(psd.tree.to_png.pixels).to eq([ChunkyPNG::Color.rgba(0, 100, 200, 255)])
58
+ expect(psd.tree.to_png[0, 0]).to eq(ChunkyPNG::Color.rgba(0, 100, 200, 255))
59
+ end
52
60
 
53
- png = psd.tree.children.first.image.to_png
54
- expect(png).to be_an_instance_of(ChunkyPNG::Canvas)
55
- expect(png.width).to eq(1)
56
- expect(png.height).to eq(1)
57
- expect(
58
- ChunkyPNG::Color.to_truecolor_alpha_bytes(png[0,0])
59
- ).to eq([0, 100, 200, 255])
61
+ describe "Canvas" do
62
+ before do
63
+ @node = psd.tree.children.first
64
+ @canvas = PSD::Renderer::Canvas.new(@node)
60
65
  end
61
66
 
62
- it "memorizes the png instance" do
63
- psd.options[:parse_layer_images] = true
64
- psd.parse!
67
+ it 'is initialized properly' do
68
+ expect(@canvas.node).to be @node
69
+ expect(@canvas.width).to eq @node.width
70
+ expect(@canvas.height).to eq @node.height
65
71
 
66
- node = psd.tree.children.first
67
- png = node.image.to_png
72
+ expect(@canvas.opacity).to eq @node.opacity
73
+ expect(@canvas.fill_opacity).to eq @node.fill_opacity
68
74
 
69
- expect(png).to be_an_instance_of(ChunkyPNG::Canvas)
70
- expect(node.image.to_png.__id__).to eq(png.__id__)
75
+ expect(@canvas.canvas).to be_an_instance_of(ChunkyPNG::Canvas)
76
+ expect(@canvas.instance_variable_get(:@pixel_data)).to be_nil
77
+ expect(@canvas.canvas.pixels.length).to be 1
71
78
  end
72
79
 
73
- it "memorizes the png_with_mask instance" do
74
- psd = PSD.new('spec/files/path.psd', parse_layer_images: true)
75
- psd.parse!
76
-
77
- node = psd.tree.children.first
78
- png = node.image.to_png_with_mask
79
-
80
- expect(png).to be_an_instance_of(ChunkyPNG::Canvas)
81
- expect(node.image.to_png_with_mask.__id__).to eq(png.__id__)
80
+ it 'delegates array methods to internal canvas' do
81
+ expect(@canvas[0, 0]).to eq ChunkyPNG::Color.rgba(0, 100, 200, 255)
82
+ expect(@canvas).to respond_to(:[]=)
82
83
  end
83
84
  end
84
85
  end