psd 1.3.3 → 1.4.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/README.md +21 -6
- data/lib/psd.rb +0 -2
- data/lib/psd/blend_mode.rb +4 -3
- data/lib/psd/channel_image.rb +29 -2
- data/lib/psd/clipping_mask.rb +40 -0
- data/lib/psd/color.rb +15 -13
- data/lib/psd/compose.rb +357 -0
- data/lib/psd/file.rb +5 -1
- data/lib/psd/image.rb +9 -0
- data/lib/psd/image_exports/png.rb +54 -1
- data/lib/psd/image_formats/raw.rb +1 -1
- data/lib/psd/image_modes/rgb.rb +4 -1
- data/lib/psd/layer.rb +3 -2
- data/lib/psd/layer/blend_modes.rb +14 -1
- data/lib/psd/layer/blending_ranges.rb +18 -16
- data/lib/psd/layer/helpers.rb +1 -1
- data/lib/psd/layer/info.rb +12 -3
- data/lib/psd/layer_info.rb +3 -2
- data/lib/psd/layer_info/blend_clipping_elements.rb +13 -0
- data/lib/psd/layer_info/blend_interior_elements.rb +13 -0
- data/lib/psd/layer_info/layer_name_source.rb +3 -1
- data/lib/psd/layer_info/vector_mask_2.rb +10 -0
- data/lib/psd/layer_info/vector_stroke.rb +12 -0
- data/lib/psd/layer_info/vector_stroke_content.rb +15 -0
- data/lib/psd/layer_mask.rb +1 -1
- data/lib/psd/layer_styles.rb +84 -0
- data/lib/psd/node.rb +2 -1
- data/lib/psd/node_layer.rb +7 -1
- data/lib/psd/nodes/ancestry.rb +12 -0
- data/lib/psd/nodes/build_preview.rb +69 -0
- data/lib/psd/nodes/search.rb +1 -0
- data/lib/psd/resources/slices.rb +27 -0
- data/lib/psd/version.rb +1 -1
- data/spec/files/slices.psd +0 -0
- data/spec/image_spec.rb +2 -2
- data/spec/slices_spec.rb +52 -0
- metadata +16 -2
data/lib/psd/file.rb
CHANGED
@@ -75,6 +75,10 @@ class PSD
|
|
75
75
|
read(length).encode('UTF-8', 'MacRoman').delete("\000")
|
76
76
|
end
|
77
77
|
|
78
|
+
def read_byte
|
79
|
+
read(1).bytes.to_a[0]
|
80
|
+
end
|
81
|
+
|
78
82
|
# Reads a unicode string, which is double the length of a normal string and encoded as UTF-16.
|
79
83
|
def read_unicode_string(length=nil)
|
80
84
|
length ||= read_int if length.nil?
|
@@ -83,7 +87,7 @@ class PSD
|
|
83
87
|
|
84
88
|
# Reads a boolean value.
|
85
89
|
def read_boolean
|
86
|
-
|
90
|
+
read_byte != 0
|
87
91
|
end
|
88
92
|
|
89
93
|
# Reads a 32-bit color space value.
|
data/lib/psd/image.rb
CHANGED
@@ -8,6 +8,9 @@ class PSD
|
|
8
8
|
include ImageMode::RGB
|
9
9
|
include Export::PNG
|
10
10
|
|
11
|
+
attr_reader :pixel_data, :opacity, :has_mask
|
12
|
+
alias :has_mask? :has_mask
|
13
|
+
|
11
14
|
# All of the possible compression formats Photoshop uses.
|
12
15
|
COMPRESSIONS = [
|
13
16
|
'Raw',
|
@@ -28,6 +31,8 @@ class PSD
|
|
28
31
|
calculate_length
|
29
32
|
@channel_data = []
|
30
33
|
@pixel_data = []
|
34
|
+
@opacity = 1.0
|
35
|
+
@has_mask = false
|
31
36
|
|
32
37
|
@start_pos = @file.tell
|
33
38
|
@end_pos = @start_pos + @length
|
@@ -110,5 +115,9 @@ class PSD
|
|
110
115
|
def pixel_step
|
111
116
|
depth == 8 ? 1 : 2
|
112
117
|
end
|
118
|
+
|
119
|
+
def pixel(i)
|
120
|
+
@pixel_data[i]
|
121
|
+
end
|
113
122
|
end
|
114
123
|
end
|
@@ -8,7 +8,7 @@ class PSD::Image
|
|
8
8
|
# data.
|
9
9
|
def to_png
|
10
10
|
PSD.logger.debug "Beginning PNG export"
|
11
|
-
png = ChunkyPNG::
|
11
|
+
png = ChunkyPNG::Canvas.new(width.to_i, height.to_i, ChunkyPNG::Color::TRANSPARENT)
|
12
12
|
|
13
13
|
i = 0
|
14
14
|
height.times do |y|
|
@@ -22,6 +22,59 @@ class PSD::Image
|
|
22
22
|
end
|
23
23
|
alias :export :to_png
|
24
24
|
|
25
|
+
def to_png_with_mask
|
26
|
+
return to_png unless has_mask?
|
27
|
+
|
28
|
+
PSD.logger.debug "Beginning PNG export with mask"
|
29
|
+
|
30
|
+
# We generate the preview at the document size instead to make applying the mask
|
31
|
+
# significantly easier.
|
32
|
+
width = @layer.header.width.to_i
|
33
|
+
height = @layer.header.height.to_i
|
34
|
+
png = ChunkyPNG::Canvas.new(width, height, ChunkyPNG::Color::TRANSPARENT)
|
35
|
+
|
36
|
+
i = 0
|
37
|
+
@layer.height.times do |y|
|
38
|
+
@layer.width.times do |x|
|
39
|
+
png[x + @layer.left, y + @layer.top] = @pixel_data[i]
|
40
|
+
i += 1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Now we apply the mask
|
45
|
+
i = 0
|
46
|
+
@layer.mask.height.times do |y|
|
47
|
+
@layer.mask.width.times do |x|
|
48
|
+
offset_x = @layer.mask.left + x
|
49
|
+
offset_y = @layer.mask.top + y
|
50
|
+
|
51
|
+
color = ChunkyPNG::Color.to_truecolor_alpha_bytes(png.get_pixel(offset_x, offset_y))
|
52
|
+
color[3] = color[3] * @mask_data[i] / 255
|
53
|
+
|
54
|
+
png.set_pixel(offset_x, offset_y, ChunkyPNG::Color.rgba(*color))
|
55
|
+
i += 1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
png.crop!(@layer.left, @layer.top, @layer.width.to_i, @layer.height.to_i)
|
60
|
+
end
|
61
|
+
|
62
|
+
def mask_to_png
|
63
|
+
return unless has_mask?
|
64
|
+
|
65
|
+
png = ChunkyPNG::Canvas.new(@layer.mask.width.to_i, @layer.mask.height.to_i, ChunkyPNG::Color::TRANSPARENT)
|
66
|
+
|
67
|
+
i = 0
|
68
|
+
@layer.mask.height.times do |y|
|
69
|
+
@layer.mask.width.times do |x|
|
70
|
+
png[x, y] = ChunkyPNG::Color.grayscale(@mask_data[i])
|
71
|
+
i += 1
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
png
|
76
|
+
end
|
77
|
+
|
25
78
|
# Saves the PNG data to disk.
|
26
79
|
def save_as_png(file)
|
27
80
|
to_png.save(file, :fast_rgba)
|
data/lib/psd/image_modes/rgb.rb
CHANGED
@@ -10,12 +10,15 @@ class PSD
|
|
10
10
|
(0...@num_pixels).step(pixel_step) do |i|
|
11
11
|
r = g = b = 0
|
12
12
|
a = 255
|
13
|
+
mask = nil
|
13
14
|
|
14
15
|
@channels_info.each_with_index do |chan, index|
|
16
|
+
next if chan[:id] == -2
|
17
|
+
|
15
18
|
val = @channel_data[i + (@channel_length * index)]
|
16
19
|
|
17
20
|
case chan[:id]
|
18
|
-
when -1 then a = val
|
21
|
+
when -1 then a = (val * opacity).to_i
|
19
22
|
when 0 then r = val
|
20
23
|
when 1 then g = val
|
21
24
|
when 2 then b = val
|
data/lib/psd/layer.rb
CHANGED
@@ -14,12 +14,13 @@ class PSD
|
|
14
14
|
include PathComponents
|
15
15
|
include PositionAndChannels
|
16
16
|
|
17
|
-
attr_reader :id, :info_keys
|
17
|
+
attr_reader :id, :info_keys, :header
|
18
18
|
attr_accessor :group_layer, :node, :file
|
19
19
|
|
20
20
|
# Initializes all of the defaults for the layer.
|
21
|
-
def initialize(file)
|
21
|
+
def initialize(file, header)
|
22
22
|
@file = file
|
23
|
+
@header = header
|
23
24
|
|
24
25
|
@mask = {}
|
25
26
|
@blending_ranges = {}
|
@@ -1,7 +1,20 @@
|
|
1
1
|
class PSD
|
2
2
|
class Layer
|
3
3
|
module BlendModes
|
4
|
-
attr_reader :blend_mode, :
|
4
|
+
attr_reader :blend_mode, :opacity
|
5
|
+
|
6
|
+
def blending_mode
|
7
|
+
if !info[:section_divider].nil? && info[:section_divider].blend_mode
|
8
|
+
BlendMode::BLEND_MODES[info[:section_divider].blend_mode.strip.to_sym]
|
9
|
+
else
|
10
|
+
@blending_mode
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Is the layer below this one a clipping mask?
|
15
|
+
def clipped?
|
16
|
+
@blend_mode.clipping == 1
|
17
|
+
end
|
5
18
|
|
6
19
|
private
|
7
20
|
|
@@ -8,14 +8,16 @@ class PSD
|
|
8
8
|
def parse_blending_ranges
|
9
9
|
length = @file.read_int
|
10
10
|
|
11
|
+
# Composite gray blend. Contains 2 black values followed by 2 white values.
|
12
|
+
# Present but irrelevant for Lab & Grayscale.
|
11
13
|
@blending_ranges[:grey] = {
|
12
14
|
source: {
|
13
|
-
black: @file.
|
14
|
-
white: @file.
|
15
|
+
black: [@file.read_byte, @file.read_byte],
|
16
|
+
white: [@file.read_byte, @file.read_byte]
|
15
17
|
},
|
16
18
|
dest: {
|
17
|
-
black: @file.
|
18
|
-
white: @file.
|
19
|
+
black: [@file.read_byte, @file.read_byte],
|
20
|
+
white: [@file.read_byte, @file.read_byte]
|
19
21
|
}
|
20
22
|
}
|
21
23
|
|
@@ -25,12 +27,12 @@ class PSD
|
|
25
27
|
@blending_ranges[:num_channels].times do
|
26
28
|
@blending_ranges[:channels] << {
|
27
29
|
source: {
|
28
|
-
black: @file.
|
29
|
-
white: @file.
|
30
|
+
black: [@file.read_byte, @file.read_byte],
|
31
|
+
white: [@file.read_byte, @file.read_byte]
|
30
32
|
},
|
31
33
|
dest: {
|
32
|
-
black: @file.
|
33
|
-
white: @file.
|
34
|
+
black: [@file.read_byte, @file.read_byte],
|
35
|
+
white: [@file.read_byte, @file.read_byte]
|
34
36
|
}
|
35
37
|
}
|
36
38
|
end
|
@@ -41,16 +43,16 @@ class PSD
|
|
41
43
|
length += @blending_ranges[:num_channels] * 8
|
42
44
|
outfile.write_int length
|
43
45
|
|
44
|
-
outfile.
|
45
|
-
outfile.
|
46
|
-
outfile.
|
47
|
-
outfile.
|
46
|
+
outfile.write @blending_ranges[:grey][:source][:black].pack('CC')
|
47
|
+
outfile.write @blending_ranges[:grey][:source][:white].pack('CC')
|
48
|
+
outfile.write @blending_ranges[:grey][:dest][:black].pack('CC')
|
49
|
+
outfile.write @blending_ranges[:grey][:dest][:white].pack('CC')
|
48
50
|
|
49
51
|
@blending_ranges[:num_channels].times do |i|
|
50
|
-
outfile.
|
51
|
-
outfile.
|
52
|
-
outfile.
|
53
|
-
outfile.
|
52
|
+
outfile.write @blending_ranges[:channels][i][:source][:black].pack('CC')
|
53
|
+
outfile.write @blending_ranges[:channels][i][:source][:white].pack('CC')
|
54
|
+
outfile.write @blending_ranges[:channels][i][:dest][:black].pack('CC')
|
55
|
+
outfile.write @blending_ranges[:channels][i][:dest][:white].pack('CC')
|
54
56
|
end
|
55
57
|
|
56
58
|
@file.seek length + 4, IO::SEEK_CUR
|
data/lib/psd/layer/helpers.rb
CHANGED
data/lib/psd/layer/info.rb
CHANGED
@@ -3,6 +3,8 @@ class PSD
|
|
3
3
|
module Info
|
4
4
|
# All of the extra layer info sections that we know how to parse.
|
5
5
|
LAYER_INFO = {
|
6
|
+
blend_clipping_elements: BlendClippingElements,
|
7
|
+
blend_interior_elements: BlendInteriorElements,
|
6
8
|
type: TypeTool,
|
7
9
|
legacy_type: LegacyTypeTool,
|
8
10
|
metadata: MetadataSetting,
|
@@ -15,7 +17,10 @@ class PSD
|
|
15
17
|
layer_id: LayerID,
|
16
18
|
fill_opacity: FillOpacity,
|
17
19
|
placed_layer: PlacedLayer,
|
18
|
-
vector_mask: VectorMask
|
20
|
+
vector_mask: VectorMask,
|
21
|
+
vector_mask_2: VectorMask2,
|
22
|
+
vector_stroke: VectorStroke,
|
23
|
+
vector_stroke_content: VectorStrokeContent
|
19
24
|
}
|
20
25
|
|
21
26
|
attr_reader :adjustments
|
@@ -46,7 +51,7 @@ class PSD
|
|
46
51
|
PSD.logger.debug "Layer Info: key = #{key}, start = #{pos}, length = #{length}"
|
47
52
|
|
48
53
|
begin
|
49
|
-
i = info.new(
|
54
|
+
i = info.new(self, length)
|
50
55
|
i.parse
|
51
56
|
|
52
57
|
@adjustments[name] = i
|
@@ -60,7 +65,7 @@ class PSD
|
|
60
65
|
end
|
61
66
|
|
62
67
|
if !info_parsed
|
63
|
-
PSD.logger.debug "Skipping: key = #{key}, pos = #{@file.tell}, length = #{length}"
|
68
|
+
PSD.logger.debug "Skipping: layer = #{name}, key = #{key}, pos = #{@file.tell}, length = #{length}"
|
64
69
|
@file.seek pos + length
|
65
70
|
end
|
66
71
|
|
@@ -73,6 +78,10 @@ class PSD
|
|
73
78
|
@extra_data_end = @file.tell
|
74
79
|
end
|
75
80
|
|
81
|
+
def vector_mask
|
82
|
+
info[:vector_mask_2] || info[:vector_mask]
|
83
|
+
end
|
84
|
+
|
76
85
|
def export_extra_data(outfile)
|
77
86
|
outfile.write @file.read(@extra_data_end - @extra_data_begin)
|
78
87
|
if @path_components && !@path_components.empty?
|
data/lib/psd/layer_info.rb
CHANGED
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative '../layer_info'
|
2
|
+
|
3
|
+
class PSD
|
4
|
+
class VectorStrokeContent < LayerInfo
|
5
|
+
@key = 'vscg'
|
6
|
+
|
7
|
+
attr_reader :key
|
8
|
+
|
9
|
+
def parse
|
10
|
+
key = @file.read_string(4)
|
11
|
+
version = @file.read_int
|
12
|
+
@data = Descriptor.new(@file).parse
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/psd/layer_mask.rb
CHANGED
@@ -0,0 +1,84 @@
|
|
1
|
+
class PSD
|
2
|
+
class LayerStyles
|
3
|
+
# Blend modes in layer effects use different keys
|
4
|
+
# than normal layer blend modes. Thanks Adobe.
|
5
|
+
BLEND_TRANSLATION = {
|
6
|
+
'Nrml' => 'norm',
|
7
|
+
'Dslv' => 'diss',
|
8
|
+
'Drkn' => 'dark',
|
9
|
+
'Mltp' => 'mul',
|
10
|
+
'CBrn' => 'idiv',
|
11
|
+
'linearBurn' => 'lbrn',
|
12
|
+
'Lghn' => 'lite',
|
13
|
+
'Scrn' => 'scrn',
|
14
|
+
'CDdg' => 'div',
|
15
|
+
'linearDodge' => 'lddg',
|
16
|
+
'Ovrl' => 'over',
|
17
|
+
'SftL' => 'sLit',
|
18
|
+
'HrdL' => 'hLit',
|
19
|
+
'vividLight' => 'vLit',
|
20
|
+
'linearLight' => 'lLit',
|
21
|
+
'pinLight' => 'pLit',
|
22
|
+
'hardMix' => 'hMix',
|
23
|
+
'Dfrn' => 'diff',
|
24
|
+
'Xclu' => 'smud',
|
25
|
+
'H ' => 'hue',
|
26
|
+
'Strt' => 'sat',
|
27
|
+
'Clr ' => 'colr',
|
28
|
+
'Lmns' => 'lum'
|
29
|
+
}
|
30
|
+
|
31
|
+
attr_reader :layer, :data, :png
|
32
|
+
|
33
|
+
def initialize(layer, png=nil)
|
34
|
+
@layer = layer
|
35
|
+
@data = layer.info[:object_effects]
|
36
|
+
@png = png || layer.image.to_png
|
37
|
+
|
38
|
+
if @data.nil?
|
39
|
+
@applied = true
|
40
|
+
else
|
41
|
+
@data = @data.data
|
42
|
+
@applied = false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def apply!
|
47
|
+
return png if @applied || data.nil?
|
48
|
+
|
49
|
+
apply_color_overlay if data.has_key?('SoFi')
|
50
|
+
png
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def apply_color_overlay
|
56
|
+
overlay_data = data['SoFi']
|
57
|
+
color_data = overlay_data['Clr ']
|
58
|
+
blending_mode = BlendMode::BLEND_MODES[BLEND_TRANSLATION[overlay_data['Md ']].to_sym]
|
59
|
+
|
60
|
+
width = layer.width.to_i
|
61
|
+
height = layer.height.to_i
|
62
|
+
|
63
|
+
PSD.logger.debug("Layer style: layer = #{layer.name}, type = color overlay, blend mode = #{blending_mode}")
|
64
|
+
|
65
|
+
for y in 0...height do
|
66
|
+
for x in 0...width do
|
67
|
+
pixel = png.get_pixel(x, y)
|
68
|
+
alpha = ChunkyPNG::Color.a(pixel)
|
69
|
+
next if alpha == 0
|
70
|
+
|
71
|
+
overlay_color = ChunkyPNG::Color.rgba(
|
72
|
+
color_data['Rd '].round,
|
73
|
+
color_data['Grn '].round,
|
74
|
+
color_data['Bl '].round,
|
75
|
+
alpha
|
76
|
+
)
|
77
|
+
|
78
|
+
color = Compose.send(blending_mode, overlay_color, pixel)
|
79
|
+
png.set_pixel(x, y, color)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|