psd 1.3.2 → 1.3.3
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/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
|