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.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/README.md +2 -1
- data/lib/psd.rb +2 -1
- data/lib/psd/blend_mode.rb +5 -1
- data/lib/psd/channel_image.rb +6 -4
- data/lib/psd/header.rb +8 -0
- data/lib/psd/helpers.rb +13 -1
- data/lib/psd/image.rb +1 -1
- data/lib/psd/image_exports/png.rb +2 -70
- data/lib/psd/image_formats/layer_raw.rb +1 -1
- data/lib/psd/image_formats/raw.rb +2 -4
- data/lib/psd/image_modes/cmyk.rb +16 -6
- data/lib/psd/layer/helpers.rb +1 -1
- data/lib/psd/layer_info/fill_opacity.rb +2 -2
- data/lib/psd/logger.rb +7 -1
- data/lib/psd/node.rb +6 -2
- data/lib/psd/node_group.rb +9 -1
- data/lib/psd/node_root.rb +16 -3
- data/lib/psd/nodes/build_preview.rb +7 -69
- data/lib/psd/nodes/search.rb +11 -10
- data/lib/psd/renderer.rb +91 -0
- data/lib/psd/renderer/blender.rb +53 -0
- data/lib/psd/renderer/canvas.rb +95 -0
- data/lib/psd/renderer/canvas_management.rb +26 -0
- data/lib/psd/renderer/clipping_mask.rb +41 -0
- data/lib/psd/{compose.rb → renderer/compose.rb} +23 -19
- data/lib/psd/renderer/layer_styles.rb +56 -0
- data/lib/psd/renderer/layer_styles/color_overlay.rb +65 -0
- data/lib/psd/renderer/layer_styles/drop_shadow.rb +75 -0
- data/lib/psd/renderer/mask.rb +68 -0
- data/lib/psd/resources/guides.rb +35 -0
- data/lib/psd/version.rb +1 -1
- data/psd.gemspec +3 -3
- data/spec/files/blendmodes.psd +0 -0
- data/spec/files/empty-layer-subgroups.psd +0 -0
- data/spec/files/guides.psd +0 -0
- data/spec/guides_spec.rb +34 -0
- data/spec/hierarchy_spec.rb +27 -3
- data/spec/image_spec.rb +36 -35
- data/spec/parsing_spec.rb +13 -0
- metadata +23 -7
- data/lib/psd/clipping_mask.rb +0 -49
- 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
|
data/lib/psd/version.rb
CHANGED
data/psd.gemspec
CHANGED
@@ -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
|
22
|
-
gem.add_dependency
|
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
|
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
|
Binary file
|
data/spec/guides_spec.rb
ADDED
@@ -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
|
data/spec/hierarchy_spec.rb
CHANGED
@@ -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
|
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(
|
98
|
-
expect(tree.
|
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
|
data/spec/image_spec.rb
CHANGED
@@ -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 "
|
37
|
-
|
38
|
-
psd.options[:parse_layer_images] = true
|
36
|
+
describe "Renderer" do
|
37
|
+
before(:each) do
|
39
38
|
psd.parse!
|
39
|
+
end
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
63
|
-
|
64
|
-
|
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
|
-
|
67
|
-
|
72
|
+
expect(@canvas.opacity).to eq @node.opacity
|
73
|
+
expect(@canvas.fill_opacity).to eq @node.fill_opacity
|
68
74
|
|
69
|
-
expect(
|
70
|
-
expect(
|
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
|
74
|
-
|
75
|
-
|
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
|