psd 1.5.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ef242e267a313a38f9ef7cebab141f870c85555e
|
4
|
+
data.tar.gz: ee3814e3f35306f61b9b26d6124ef021ec22cc00
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d7260fadf4dc3a2e99126427a85ce355832a897a058d681e7afe708116c427cf76210f766fcc7641b5c43e889c9c295f8a38c269af16d6ffc96ce861ac21565b
|
7
|
+
data.tar.gz: e97f791180a8452ed959fd72c831929d95d0666f7178d8f4d7dcf92bf229c6004e761782b251a8d3958e324d3a0de139be746db609d21921dd81947f27c37ae2
|
data/.travis.yml
CHANGED
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=
|
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/*',
|
data/lib/psd/blend_mode.rb
CHANGED
@@ -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.
|
data/lib/psd/channel_image.rb
CHANGED
@@ -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
|
-
@
|
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
|
data/lib/psd/header.rb
CHANGED
data/lib/psd/helpers.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/psd/image.rb
CHANGED
@@ -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!
|
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|
|
data/lib/psd/image_modes/cmyk.rb
CHANGED
@@ -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 =
|
9
|
-
|
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
|
-
|
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
|
data/lib/psd/layer/helpers.rb
CHANGED
data/lib/psd/logger.rb
CHANGED
data/lib/psd/node.rb
CHANGED
@@ -21,7 +21,7 @@ class PSD
|
|
21
21
|
@children << layer
|
22
22
|
end
|
23
23
|
|
24
|
-
@force_visible =
|
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
|
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,
|
data/lib/psd/node_group.rb
CHANGED
@@ -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.
|
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.
|
data/lib/psd/node_root.rb
CHANGED
@@ -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
|
-
[
|
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
|
-
|
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
|
-
|
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
|
-
|
46
|
-
|
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
|
58
|
-
|
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
|