psd 1.3.2 → 1.3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/psd.rb +2 -1
- data/lib/psd/layer.rb +17 -295
- data/lib/psd/layer/blend_modes.rb +17 -0
- data/lib/psd/layer/blending_ranges.rb +60 -0
- data/lib/psd/layer/channel_image.rb +12 -0
- data/lib/psd/layer/exporting.rb +28 -0
- data/lib/psd/layer/helpers.rb +53 -0
- data/lib/psd/layer/info.rb +88 -0
- data/lib/psd/layer/mask.rb +19 -0
- data/lib/psd/layer/name.rb +33 -0
- data/lib/psd/layer/path_components.rb +22 -0
- data/lib/psd/layer/position_and_channels.rb +47 -0
- data/lib/psd/layer_info/layer_group.rb +30 -0
- data/lib/psd/layer_info/layer_section_divider.rb +14 -1
- data/lib/psd/layer_mask.rb +1 -1
- data/lib/psd/version.rb +1 -1
- metadata +13 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 448518141d3b745904f809280d0e826e08bf8dc0
|
4
|
+
data.tar.gz: 4c0725157152e2f924b95cf023603a9b86bd1f54
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e559032f4b160eb26451c38aa4bc3fa7869aa6984f60c5d062596087cf32a38fdc181e3c703a56b22ba88fc965abe727fa158eb427f76042e46c55f694593ab3
|
7
|
+
data.tar.gz: 4e31eb123c902d3381e3895d262589f8d269aca4e30c8a74f541d03ef6a9e527aefc9c895df5e358f62e5e8f5b584e4a6a5070b4cf83a10892c629ba46550d96
|
data/lib/psd.rb
CHANGED
@@ -9,7 +9,8 @@ dir_root = File.dirname(File.absolute_path(__FILE__)) + '/psd'
|
|
9
9
|
'/image_modes/*',
|
10
10
|
'/image_exports/*',
|
11
11
|
'/nodes/*',
|
12
|
-
'/layer_info
|
12
|
+
'/layer_info/*',
|
13
|
+
'/layer/*',
|
13
14
|
'/**/*'
|
14
15
|
].each do |path|
|
15
16
|
Dir.glob(dir_root + path) { |file| require file if File.file?(file) }
|
data/lib/psd/layer.rb
CHANGED
@@ -3,38 +3,24 @@ class PSD
|
|
3
3
|
# that layer.
|
4
4
|
class Layer
|
5
5
|
include Section
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
LAYER_INFO = {
|
20
|
-
type: TypeTool,
|
21
|
-
legacy_type: LegacyTypeTool,
|
22
|
-
metadata: MetadataSetting,
|
23
|
-
layer_name_source: LayerNameSource,
|
24
|
-
object_effects: ObjectEffects,
|
25
|
-
name: UnicodeName,
|
26
|
-
section_divider: LayerSectionDivider,
|
27
|
-
reference_point: ReferencePoint,
|
28
|
-
layer_id: LayerID,
|
29
|
-
fill_opacity: FillOpacity,
|
30
|
-
placed_layer: PlacedLayer,
|
31
|
-
vector_mask: VectorMask
|
32
|
-
}
|
6
|
+
include BlendModes
|
7
|
+
include BlendingRanges
|
8
|
+
include ChannelImage
|
9
|
+
include Exporting
|
10
|
+
include Helpers
|
11
|
+
include Info
|
12
|
+
include Mask
|
13
|
+
include Name
|
14
|
+
include PathComponents
|
15
|
+
include PositionAndChannels
|
16
|
+
|
17
|
+
attr_reader :id, :info_keys
|
18
|
+
attr_accessor :group_layer, :node, :file
|
33
19
|
|
34
20
|
# Initializes all of the defaults for the layer.
|
35
21
|
def initialize(file)
|
36
22
|
@file = file
|
37
|
-
|
23
|
+
|
38
24
|
@mask = {}
|
39
25
|
@blending_ranges = {}
|
40
26
|
@adjustments = {}
|
@@ -42,10 +28,8 @@ class PSD
|
|
42
28
|
@blend_mode = {}
|
43
29
|
@group_layer = nil
|
44
30
|
|
45
|
-
@layer_type = 'normal'
|
46
31
|
@blending_mode = 'normal'
|
47
32
|
@opacity = 255
|
48
|
-
@fill_opacity = 255
|
49
33
|
|
50
34
|
# Just used for tracking which layer adjustments we're parsing.
|
51
35
|
# Not essential.
|
@@ -56,9 +40,9 @@ class PSD
|
|
56
40
|
def parse(index=nil)
|
57
41
|
start_section
|
58
42
|
|
59
|
-
@
|
43
|
+
@id = index
|
60
44
|
|
61
|
-
|
45
|
+
parse_position_and_channels
|
62
46
|
parse_blend_modes
|
63
47
|
|
64
48
|
extra_len = @file.read_int
|
@@ -67,7 +51,7 @@ class PSD
|
|
67
51
|
parse_mask_data
|
68
52
|
parse_blending_ranges
|
69
53
|
parse_legacy_layer_name
|
70
|
-
|
54
|
+
parse_layer_info
|
71
55
|
|
72
56
|
PSD.logger.debug "Layer name = #{name}"
|
73
57
|
|
@@ -77,278 +61,16 @@ class PSD
|
|
77
61
|
return self
|
78
62
|
end
|
79
63
|
|
80
|
-
# Export the layer to file. May or may not work.
|
81
|
-
def export(outfile)
|
82
|
-
export_info(outfile)
|
83
|
-
|
84
|
-
@blend_mode.write(outfile)
|
85
|
-
@file.seek(@blend_mode.num_bytes, IO::SEEK_CUR)
|
86
|
-
|
87
|
-
export_mask_data(outfile)
|
88
|
-
export_blending_ranges(outfile)
|
89
|
-
export_legacy_layer_name(outfile)
|
90
|
-
export_extra_data(outfile)
|
91
|
-
|
92
|
-
outfile.write @file.read(end_of_section - @file.tell)
|
93
|
-
end
|
94
|
-
|
95
64
|
# We just delegate this to a normal method call.
|
96
65
|
def [](val)
|
97
66
|
self.send(val)
|
98
67
|
end
|
99
68
|
|
100
|
-
def parse_channel_image!(header, parse)
|
101
|
-
@image = ChannelImage.new(@file, header, self)
|
102
|
-
parse ? @image.parse : @image.skip
|
103
|
-
end
|
104
|
-
|
105
|
-
# Does this layer represent the start of a folder section?
|
106
|
-
def folder?
|
107
|
-
return false unless @adjustments.has_key?(:section_divider)
|
108
|
-
@adjustments[:section_divider].is_folder
|
109
|
-
end
|
110
|
-
|
111
|
-
# Does this layer represent the end of a folder section?
|
112
|
-
def folder_end?
|
113
|
-
return false unless @adjustments.has_key?(:section_divider)
|
114
|
-
@adjustments[:section_divider].is_hidden
|
115
|
-
end
|
116
|
-
|
117
|
-
# Is this layer visible?
|
118
|
-
def visible?
|
119
|
-
@visible
|
120
|
-
end
|
121
|
-
|
122
|
-
# Is this layer hidden?
|
123
|
-
def hidden?
|
124
|
-
!@visible
|
125
|
-
end
|
126
|
-
|
127
|
-
# Attempt to translate this layer and modify the document.
|
128
|
-
def translate(x=0, y=0)
|
129
|
-
@left += x
|
130
|
-
@right += x
|
131
|
-
@top += y
|
132
|
-
@bottom += y
|
133
|
-
|
134
|
-
@path_components.each{ |p| p.translate(x,y) } if @path_components
|
135
|
-
end
|
136
|
-
|
137
|
-
# Attempt to scale the path components within this layer.
|
138
|
-
def scale_path_components(xr, yr)
|
139
|
-
return unless @path_components
|
140
|
-
|
141
|
-
@path_components.each{ |p| p.scale(xr, yr) }
|
142
|
-
end
|
143
|
-
|
144
|
-
# Helper that exports the text data in this layer, if any.
|
145
|
-
def text
|
146
|
-
return nil unless @adjustments[:type]
|
147
|
-
@adjustments[:type].to_hash
|
148
|
-
end
|
149
|
-
|
150
|
-
# Gets the name of this layer. If the PSD file is from an even remotely
|
151
|
-
# recent version of Photoshop, this data is stored as extra layer info and
|
152
|
-
# as a UTF-16 name. Otherwise, it's stored in a legacy block.
|
153
|
-
def name
|
154
|
-
if @adjustments.has_key?(:name)
|
155
|
-
return @adjustments[:name].data
|
156
|
-
end
|
157
|
-
|
158
|
-
return @legacy_name
|
159
|
-
end
|
160
|
-
|
161
69
|
# We delegate all missing method calls to the extra layer info to make it easier
|
162
70
|
# to access that data.
|
163
71
|
def method_missing(method, *args, &block)
|
164
72
|
return @adjustments[method] if @adjustments.has_key?(method)
|
165
73
|
super
|
166
74
|
end
|
167
|
-
|
168
|
-
private
|
169
|
-
|
170
|
-
def parse_info
|
171
|
-
start_section(:info)
|
172
|
-
|
173
|
-
@top = @file.read_int
|
174
|
-
@left = @file.read_int
|
175
|
-
@bottom = @file.read_int
|
176
|
-
@right = @file.read_int
|
177
|
-
@channels = @file.read_short
|
178
|
-
|
179
|
-
@rows = @bottom - @top
|
180
|
-
@cols = @right - @left
|
181
|
-
|
182
|
-
@channels.times do
|
183
|
-
channel_id = @file.read_short
|
184
|
-
channel_length = @file.read_int
|
185
|
-
|
186
|
-
@channels_info << {id: channel_id, length: channel_length}
|
187
|
-
end
|
188
|
-
|
189
|
-
end_section(:info)
|
190
|
-
end
|
191
|
-
|
192
|
-
def export_info(outfile)
|
193
|
-
[@top, @left, @bottom, @right].each { |val| outfile.write_int(val) }
|
194
|
-
outfile.write_short(@channels)
|
195
|
-
|
196
|
-
@channels_info.each do |channel_info|
|
197
|
-
outfile.write_short channel_info[:id]
|
198
|
-
outfile.write_int channel_info[:length]
|
199
|
-
end
|
200
|
-
|
201
|
-
@file.seek end_of_section(:info)
|
202
|
-
end
|
203
|
-
|
204
|
-
def export_mask_data(outfile)
|
205
|
-
outfile.write @file.read(@mask_end - @mask_begin + 4)
|
206
|
-
end
|
207
|
-
|
208
|
-
def export_blending_ranges(outfile)
|
209
|
-
length = 4 * 2 # greys
|
210
|
-
length += @blending_ranges[:num_channels] * 8
|
211
|
-
outfile.write_int length
|
212
|
-
|
213
|
-
outfile.write_short @blending_ranges[:grey][:source][:black]
|
214
|
-
outfile.write_short @blending_ranges[:grey][:source][:white]
|
215
|
-
outfile.write_short @blending_ranges[:grey][:dest][:black]
|
216
|
-
outfile.write_short @blending_ranges[:grey][:dest][:white]
|
217
|
-
|
218
|
-
@blending_ranges[:num_channels].times do |i|
|
219
|
-
outfile.write_short @blending_ranges[:channels][i][:source][:black]
|
220
|
-
outfile.write_short @blending_ranges[:channels][i][:source][:white]
|
221
|
-
outfile.write_short @blending_ranges[:channels][i][:dest][:black]
|
222
|
-
outfile.write_short @blending_ranges[:channels][i][:dest][:white]
|
223
|
-
end
|
224
|
-
|
225
|
-
@file.seek length + 4, IO::SEEK_CUR
|
226
|
-
end
|
227
|
-
|
228
|
-
def export_legacy_layer_name(outfile)
|
229
|
-
outfile.write @file.read(@legacy_name_end - @legacy_name_start)
|
230
|
-
end
|
231
|
-
|
232
|
-
def export_extra_data(outfile)
|
233
|
-
outfile.write @file.read(@extra_data_end - @extra_data_begin)
|
234
|
-
if @path_components && !@path_components.empty?
|
235
|
-
outfile.seek @vector_mask_begin
|
236
|
-
@file.seek @vector_mask_begin
|
237
|
-
|
238
|
-
write_vector_mask(outfile)
|
239
|
-
@file.seek outfile.tell
|
240
|
-
end
|
241
|
-
end
|
242
|
-
|
243
|
-
def parse_blend_modes
|
244
|
-
@blend_mode = BlendMode.read(@file)
|
245
|
-
|
246
|
-
@blending_mode = @blend_mode.mode
|
247
|
-
@opacity = @blend_mode.opacity
|
248
|
-
@visible = @blend_mode.visible
|
249
|
-
end
|
250
|
-
|
251
|
-
def parse_mask_data
|
252
|
-
@mask_begin = @file.tell
|
253
|
-
@mask = Mask.read(@file)
|
254
|
-
@mask_end = @file.tell
|
255
|
-
end
|
256
|
-
|
257
|
-
def parse_blending_ranges
|
258
|
-
length = @file.read_int
|
259
|
-
|
260
|
-
@blending_ranges[:grey] = {
|
261
|
-
source: {
|
262
|
-
black: @file.read_short,
|
263
|
-
white: @file.read_short
|
264
|
-
},
|
265
|
-
dest: {
|
266
|
-
black: @file.read_short,
|
267
|
-
white: @file.read_short
|
268
|
-
}
|
269
|
-
}
|
270
|
-
|
271
|
-
@blending_ranges[:num_channels] = (length - 8) / 8
|
272
|
-
|
273
|
-
@blending_ranges[:channels] = []
|
274
|
-
@blending_ranges[:num_channels].times do
|
275
|
-
@blending_ranges[:channels] << {
|
276
|
-
source: {
|
277
|
-
black: @file.read_short,
|
278
|
-
white: @file.read_short
|
279
|
-
},
|
280
|
-
dest: {
|
281
|
-
black: @file.read_short,
|
282
|
-
white: @file.read_short
|
283
|
-
}
|
284
|
-
}
|
285
|
-
end
|
286
|
-
end
|
287
|
-
|
288
|
-
# The old school layer names are encoded in MacRoman format,
|
289
|
-
# not UTF-8. Luckily Ruby kicks ass at character conversion.
|
290
|
-
def parse_legacy_layer_name
|
291
|
-
@legacy_name_start = @file.tell
|
292
|
-
|
293
|
-
len = Util.pad4 @file.read(1).bytes.to_a[0]
|
294
|
-
@legacy_name = @file.read_string(len)
|
295
|
-
|
296
|
-
@legacy_name_end = @file.tell
|
297
|
-
end
|
298
|
-
|
299
|
-
# This section is a bit tricky to parse because it represents all of the
|
300
|
-
# extra data that describes this layer.
|
301
|
-
def parse_extra_data
|
302
|
-
@extra_data_begin = @file.tell
|
303
|
-
|
304
|
-
while @file.tell < @layer_end
|
305
|
-
# Signature, don't need
|
306
|
-
@file.seek 4, IO::SEEK_CUR
|
307
|
-
|
308
|
-
# Key, very important
|
309
|
-
key = @file.read_string(4)
|
310
|
-
@info_keys << key
|
311
|
-
|
312
|
-
length = Util.pad2 @file.read_int
|
313
|
-
pos = @file.tell
|
314
|
-
|
315
|
-
info_parsed = false
|
316
|
-
LAYER_INFO.each do |name, info|
|
317
|
-
next unless info.key == key
|
318
|
-
|
319
|
-
PSD.logger.debug "Layer Info: key = #{key}, start = #{pos}, length = #{length}"
|
320
|
-
|
321
|
-
begin
|
322
|
-
i = info.new(@file, length)
|
323
|
-
i.parse
|
324
|
-
|
325
|
-
@adjustments[name] = i
|
326
|
-
info_parsed = true
|
327
|
-
rescue Exception => e
|
328
|
-
PSD.logger.error "Parsing error: key = #{key}, message = #{e.message}"
|
329
|
-
PSD.logger.error e.backtrace.join("\n")
|
330
|
-
end
|
331
|
-
|
332
|
-
break
|
333
|
-
end
|
334
|
-
|
335
|
-
if !info_parsed
|
336
|
-
PSD.logger.debug "Skipping: key = #{key}, pos = #{@file.tell}, length = #{length}"
|
337
|
-
@file.seek pos + length
|
338
|
-
end
|
339
|
-
|
340
|
-
@file.seek pos + length if @file.tell != (pos + length)
|
341
|
-
end
|
342
|
-
|
343
|
-
@extra_data_end = @file.tell
|
344
|
-
end
|
345
|
-
|
346
|
-
def write_vector_mask(outfile)
|
347
|
-
outfile.write @file.read(8)
|
348
|
-
# outfile.write_int 3
|
349
|
-
# outfile.write_int @vector_tag
|
350
|
-
|
351
|
-
@path_components.each{ |pc| pc.write(outfile); @file.seek(26, IO::SEEK_CUR) }
|
352
|
-
end
|
353
75
|
end
|
354
76
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class PSD
|
2
|
+
class Layer
|
3
|
+
module BlendModes
|
4
|
+
attr_reader :blend_mode, :blending_mode, :opacity
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def parse_blend_modes
|
9
|
+
@blend_mode = BlendMode.read(@file)
|
10
|
+
|
11
|
+
@blending_mode = @blend_mode.mode
|
12
|
+
@opacity = @blend_mode.opacity
|
13
|
+
@visible = @blend_mode.visible
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
class PSD
|
2
|
+
class Layer
|
3
|
+
module BlendingRanges
|
4
|
+
attr_reader :blending_ranges
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def parse_blending_ranges
|
9
|
+
length = @file.read_int
|
10
|
+
|
11
|
+
@blending_ranges[:grey] = {
|
12
|
+
source: {
|
13
|
+
black: @file.read_short,
|
14
|
+
white: @file.read_short
|
15
|
+
},
|
16
|
+
dest: {
|
17
|
+
black: @file.read_short,
|
18
|
+
white: @file.read_short
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
@blending_ranges[:num_channels] = (length - 8) / 8
|
23
|
+
|
24
|
+
@blending_ranges[:channels] = []
|
25
|
+
@blending_ranges[:num_channels].times do
|
26
|
+
@blending_ranges[:channels] << {
|
27
|
+
source: {
|
28
|
+
black: @file.read_short,
|
29
|
+
white: @file.read_short
|
30
|
+
},
|
31
|
+
dest: {
|
32
|
+
black: @file.read_short,
|
33
|
+
white: @file.read_short
|
34
|
+
}
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def export_blending_ranges(outfile)
|
40
|
+
length = 4 * 2 # greys
|
41
|
+
length += @blending_ranges[:num_channels] * 8
|
42
|
+
outfile.write_int length
|
43
|
+
|
44
|
+
outfile.write_short @blending_ranges[:grey][:source][:black]
|
45
|
+
outfile.write_short @blending_ranges[:grey][:source][:white]
|
46
|
+
outfile.write_short @blending_ranges[:grey][:dest][:black]
|
47
|
+
outfile.write_short @blending_ranges[:grey][:dest][:white]
|
48
|
+
|
49
|
+
@blending_ranges[:num_channels].times do |i|
|
50
|
+
outfile.write_short @blending_ranges[:channels][i][:source][:black]
|
51
|
+
outfile.write_short @blending_ranges[:channels][i][:source][:white]
|
52
|
+
outfile.write_short @blending_ranges[:channels][i][:dest][:black]
|
53
|
+
outfile.write_short @blending_ranges[:channels][i][:dest][:white]
|
54
|
+
end
|
55
|
+
|
56
|
+
@file.seek length + 4, IO::SEEK_CUR
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class PSD
|
2
|
+
class Layer
|
3
|
+
module Exporting
|
4
|
+
# Export the layer to file. May or may not work.
|
5
|
+
def export(outfile)
|
6
|
+
export_position_and_channels(outfile)
|
7
|
+
|
8
|
+
@blend_mode.write(outfile)
|
9
|
+
@file.seek(@blend_mode.num_bytes, IO::SEEK_CUR)
|
10
|
+
|
11
|
+
export_mask_data(outfile)
|
12
|
+
export_blending_ranges(outfile)
|
13
|
+
export_legacy_layer_name(outfile)
|
14
|
+
export_extra_data(outfile)
|
15
|
+
|
16
|
+
outfile.write @file.read(end_of_section - @file.tell)
|
17
|
+
end
|
18
|
+
|
19
|
+
def write_vector_mask(outfile)
|
20
|
+
outfile.write @file.read(8)
|
21
|
+
# outfile.write_int 3
|
22
|
+
# outfile.write_int @vector_tag
|
23
|
+
|
24
|
+
@path_components.each{ |pc| pc.write(outfile); @file.seek(26, IO::SEEK_CUR) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
class PSD
|
2
|
+
class Layer
|
3
|
+
module Helpers
|
4
|
+
# Does this layer represent the start of a folder section?
|
5
|
+
def folder?
|
6
|
+
if info.has_key?(:section_divider)
|
7
|
+
info[:section_divider].is_folder
|
8
|
+
elsif info.has_key?(:nested_section_divider)
|
9
|
+
info[:nested_section_divider].is_folder
|
10
|
+
else
|
11
|
+
name == "<Layer group>"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Does this layer represent the end of a folder section?
|
16
|
+
def folder_end?
|
17
|
+
if info.has_key?(:section_divider)
|
18
|
+
info[:section_divider].is_hidden
|
19
|
+
elsif info.has_key?(:nested_section_divider)
|
20
|
+
info[:nested_section_divider].is_hidden
|
21
|
+
else
|
22
|
+
name == "</Layer group>"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Is this layer visible?
|
27
|
+
def visible?
|
28
|
+
@visible
|
29
|
+
end
|
30
|
+
|
31
|
+
# Is this layer hidden?
|
32
|
+
def hidden?
|
33
|
+
!@visible
|
34
|
+
end
|
35
|
+
|
36
|
+
# Helper that exports the text data in this layer, if any.
|
37
|
+
def text
|
38
|
+
return nil unless info[:type]
|
39
|
+
info[:type].to_hash
|
40
|
+
end
|
41
|
+
|
42
|
+
def layer_type
|
43
|
+
return 'normal' unless info.has_key?(:section_divider)
|
44
|
+
info[:section_divider].layer_type
|
45
|
+
end
|
46
|
+
|
47
|
+
def fill_opacity
|
48
|
+
return nil unless info.has_key?(:fill_opacity)
|
49
|
+
info[:fill_opacity].enabled
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
class PSD
|
2
|
+
class Layer
|
3
|
+
module Info
|
4
|
+
# All of the extra layer info sections that we know how to parse.
|
5
|
+
LAYER_INFO = {
|
6
|
+
type: TypeTool,
|
7
|
+
legacy_type: LegacyTypeTool,
|
8
|
+
metadata: MetadataSetting,
|
9
|
+
layer_name_source: LayerNameSource,
|
10
|
+
object_effects: ObjectEffects,
|
11
|
+
name: UnicodeName,
|
12
|
+
section_divider: LayerSectionDivider,
|
13
|
+
nested_section_divider: NestedLayerDivider,
|
14
|
+
reference_point: ReferencePoint,
|
15
|
+
layer_id: LayerID,
|
16
|
+
fill_opacity: FillOpacity,
|
17
|
+
placed_layer: PlacedLayer,
|
18
|
+
vector_mask: VectorMask
|
19
|
+
}
|
20
|
+
|
21
|
+
attr_reader :adjustments
|
22
|
+
alias :info :adjustments
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# This section is a bit tricky to parse because it represents all of the
|
27
|
+
# extra data that describes this layer.
|
28
|
+
def parse_layer_info
|
29
|
+
@extra_data_begin = @file.tell
|
30
|
+
|
31
|
+
while @file.tell < @layer_end
|
32
|
+
# Signature, don't need
|
33
|
+
@file.seek 4, IO::SEEK_CUR
|
34
|
+
|
35
|
+
# Key, very important
|
36
|
+
key = @file.read_string(4)
|
37
|
+
@info_keys << key
|
38
|
+
|
39
|
+
length = Util.pad2 @file.read_int
|
40
|
+
pos = @file.tell
|
41
|
+
|
42
|
+
info_parsed = false
|
43
|
+
LAYER_INFO.each do |name, info|
|
44
|
+
next unless info.key == key
|
45
|
+
|
46
|
+
PSD.logger.debug "Layer Info: key = #{key}, start = #{pos}, length = #{length}"
|
47
|
+
|
48
|
+
begin
|
49
|
+
i = info.new(@file, length)
|
50
|
+
i.parse
|
51
|
+
|
52
|
+
@adjustments[name] = i
|
53
|
+
info_parsed = true
|
54
|
+
rescue Exception => e
|
55
|
+
PSD.logger.error "Parsing error: key = #{key}, message = #{e.message}"
|
56
|
+
PSD.logger.error e.backtrace.join("\n")
|
57
|
+
end
|
58
|
+
|
59
|
+
break
|
60
|
+
end
|
61
|
+
|
62
|
+
if !info_parsed
|
63
|
+
PSD.logger.debug "Skipping: key = #{key}, pos = #{@file.tell}, length = #{length}"
|
64
|
+
@file.seek pos + length
|
65
|
+
end
|
66
|
+
|
67
|
+
if @file.tell != (pos + length)
|
68
|
+
PSD.logger.warn "Layer info key #{key} ended at #{@file.tell}, expected #{pos + length}"
|
69
|
+
@file.seek pos + length
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
@extra_data_end = @file.tell
|
74
|
+
end
|
75
|
+
|
76
|
+
def export_extra_data(outfile)
|
77
|
+
outfile.write @file.read(@extra_data_end - @extra_data_begin)
|
78
|
+
if @path_components && !@path_components.empty?
|
79
|
+
outfile.seek @vector_mask_begin
|
80
|
+
@file.seek @vector_mask_begin
|
81
|
+
|
82
|
+
write_vector_mask(outfile)
|
83
|
+
@file.seek outfile.tell
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class PSD
|
2
|
+
class Layer
|
3
|
+
module Mask
|
4
|
+
attr_reader :mask
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def parse_mask_data
|
9
|
+
@mask_begin = @file.tell
|
10
|
+
@mask = PSD::Mask.read(@file)
|
11
|
+
@mask_end = @file.tell
|
12
|
+
end
|
13
|
+
|
14
|
+
def export_mask_data(outfile)
|
15
|
+
outfile.write @file.read(@mask_end - @mask_begin + 4)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class PSD
|
2
|
+
class Layer
|
3
|
+
module Name
|
4
|
+
# Gets the name of this layer. If the PSD file is from an even remotely
|
5
|
+
# recent version of Photoshop, this data is stored as extra layer info and
|
6
|
+
# as a UTF-16 name. Otherwise, it's stored in a legacy block.
|
7
|
+
def name
|
8
|
+
if @adjustments.has_key?(:name)
|
9
|
+
return @adjustments[:name].data
|
10
|
+
end
|
11
|
+
|
12
|
+
return @legacy_name
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
# The old school layer names are encoded in MacRoman format,
|
18
|
+
# not UTF-8. Luckily Ruby kicks ass at character conversion.
|
19
|
+
def parse_legacy_layer_name
|
20
|
+
@legacy_name_start = @file.tell
|
21
|
+
|
22
|
+
len = Util.pad4 @file.read(1).bytes.to_a[0]
|
23
|
+
@legacy_name = @file.read_string(len)
|
24
|
+
|
25
|
+
@legacy_name_end = @file.tell
|
26
|
+
end
|
27
|
+
|
28
|
+
def export_legacy_layer_name(outfile)
|
29
|
+
outfile.write @file.read(@legacy_name_end - @legacy_name_start)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class PSD
|
2
|
+
class Layer
|
3
|
+
module PathComponents
|
4
|
+
# Attempt to translate this layer and modify the document.
|
5
|
+
def translate(x=0, y=0)
|
6
|
+
@left += x
|
7
|
+
@right += x
|
8
|
+
@top += y
|
9
|
+
@bottom += y
|
10
|
+
|
11
|
+
@path_components.each{ |p| p.translate(x,y) } if @path_components
|
12
|
+
end
|
13
|
+
|
14
|
+
# Attempt to scale the path components within this layer.
|
15
|
+
def scale_path_components(xr, yr)
|
16
|
+
return unless @path_components
|
17
|
+
|
18
|
+
@path_components.each{ |p| p.scale(xr, yr) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class PSD
|
2
|
+
class Layer
|
3
|
+
module PositionAndChannels
|
4
|
+
attr_reader :top, :left, :bottom, :right, :cols, :rows
|
5
|
+
attr_reader :channels, :channels_info
|
6
|
+
|
7
|
+
alias :width :cols
|
8
|
+
alias :height :rows
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def parse_position_and_channels
|
13
|
+
start_section(:info)
|
14
|
+
|
15
|
+
@top = @file.read_int
|
16
|
+
@left = @file.read_int
|
17
|
+
@bottom = @file.read_int
|
18
|
+
@right = @file.read_int
|
19
|
+
@channels = @file.read_short
|
20
|
+
|
21
|
+
@rows = @bottom - @top
|
22
|
+
@cols = @right - @left
|
23
|
+
|
24
|
+
@channels.times do
|
25
|
+
channel_id = @file.read_short
|
26
|
+
channel_length = @file.read_int
|
27
|
+
|
28
|
+
@channels_info << {id: channel_id, length: channel_length}
|
29
|
+
end
|
30
|
+
|
31
|
+
end_section(:info)
|
32
|
+
end
|
33
|
+
|
34
|
+
def export_position_and_channels(outfile)
|
35
|
+
[@top, @left, @bottom, @right].each { |val| outfile.write_int(val) }
|
36
|
+
outfile.write_short(@channels)
|
37
|
+
|
38
|
+
@channels_info.each do |channel_info|
|
39
|
+
outfile.write_short channel_info[:id]
|
40
|
+
outfile.write_int channel_info[:length]
|
41
|
+
end
|
42
|
+
|
43
|
+
@file.seek end_of_section(:info)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative '../layer_info'
|
2
|
+
|
3
|
+
class PSD
|
4
|
+
# Not 100% sure what the purpose of this key is, but it seems to exist
|
5
|
+
# whenever the lsct key doesn't. Parsing this like a layer section
|
6
|
+
# divider seems to solve a lot of parsing issues with folders.
|
7
|
+
#
|
8
|
+
# See https://github.com/layervault/psd.rb/issues/38
|
9
|
+
class NestedLayerDivider < LayerInfo
|
10
|
+
@key = 'lsdk'
|
11
|
+
|
12
|
+
attr_reader :is_folder, :is_hidden
|
13
|
+
|
14
|
+
def initialize(file, length)
|
15
|
+
super
|
16
|
+
|
17
|
+
@is_folder = false
|
18
|
+
@is_hidden = false
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse
|
22
|
+
code = @file.read_int
|
23
|
+
|
24
|
+
case code
|
25
|
+
when 1, 2 then @is_folder = true
|
26
|
+
when 3 then @is_hidden = true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -4,7 +4,7 @@ class PSD
|
|
4
4
|
class LayerSectionDivider < LayerInfo
|
5
5
|
@key = 'lsct'
|
6
6
|
|
7
|
-
attr_reader :layer_type, :is_folder, :is_hidden
|
7
|
+
attr_reader :layer_type, :is_folder, :is_hidden, :blend_mode, :sub_type
|
8
8
|
|
9
9
|
SECTION_DIVIDER_TYPES = [
|
10
10
|
"other",
|
@@ -18,6 +18,8 @@ class PSD
|
|
18
18
|
|
19
19
|
@is_folder = false
|
20
20
|
@is_hidden = false
|
21
|
+
@blend_mode = nil
|
22
|
+
@sub_type = nil
|
21
23
|
end
|
22
24
|
|
23
25
|
def parse
|
@@ -29,6 +31,17 @@ class PSD
|
|
29
31
|
when 3 then @is_hidden = true
|
30
32
|
end
|
31
33
|
|
34
|
+
PSD.logger.warn "Section divider is unexpected value: #{code}" if code > 4
|
35
|
+
|
36
|
+
return self unless @length >= 12
|
37
|
+
|
38
|
+
@file.seek 4, IO::SEEK_CUR # sig
|
39
|
+
@blend_mode = @file.read_string(4)
|
40
|
+
|
41
|
+
return self unless @length >= 16
|
42
|
+
|
43
|
+
@sub_type = @file.read_int == 0 ? 'normal' : 'scene group'
|
44
|
+
|
32
45
|
return self
|
33
46
|
end
|
34
47
|
end
|
data/lib/psd/layer_mask.rb
CHANGED
@@ -56,7 +56,7 @@ class PSD
|
|
56
56
|
|
57
57
|
layers.each do |layer|
|
58
58
|
@file.seek 8, IO::SEEK_CUR and next if layer.folder? || layer.folder_end?
|
59
|
-
layer.parse_channel_image
|
59
|
+
layer.parse_channel_image(@header, @options[:parse_layer_images])
|
60
60
|
end
|
61
61
|
end
|
62
62
|
|
data/lib/psd/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: psd
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.3.
|
4
|
+
version: 1.3.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan LeFevre
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-10-
|
12
|
+
date: 2013-10-23 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
@@ -159,8 +159,19 @@ files:
|
|
159
159
|
- lib/psd/image_modes/greyscale.rb
|
160
160
|
- lib/psd/image_modes/rgb.rb
|
161
161
|
- lib/psd/layer.rb
|
162
|
+
- lib/psd/layer/blend_modes.rb
|
163
|
+
- lib/psd/layer/blending_ranges.rb
|
164
|
+
- lib/psd/layer/channel_image.rb
|
165
|
+
- lib/psd/layer/exporting.rb
|
166
|
+
- lib/psd/layer/helpers.rb
|
167
|
+
- lib/psd/layer/info.rb
|
168
|
+
- lib/psd/layer/mask.rb
|
169
|
+
- lib/psd/layer/name.rb
|
170
|
+
- lib/psd/layer/path_components.rb
|
171
|
+
- lib/psd/layer/position_and_channels.rb
|
162
172
|
- lib/psd/layer_info.rb
|
163
173
|
- lib/psd/layer_info/fill_opacity.rb
|
174
|
+
- lib/psd/layer_info/layer_group.rb
|
164
175
|
- lib/psd/layer_info/layer_id.rb
|
165
176
|
- lib/psd/layer_info/layer_name_source.rb
|
166
177
|
- lib/psd/layer_info/layer_section_divider.rb
|