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
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
|