ruby-psd 0.0.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/ruby-psd.rb +249 -0
- metadata +8 -7
- data/lib/main.rb +0 -0
data/lib/ruby-psd.rb
ADDED
@@ -0,0 +1,249 @@
|
|
1
|
+
class RubyPSD
|
2
|
+
|
3
|
+
# Official documents of PSD File Format is here.
|
4
|
+
# http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/PhotoshopFileFormats.htm#50577409_20023
|
5
|
+
|
6
|
+
def initialize(path)
|
7
|
+
@path = path
|
8
|
+
@width = 0
|
9
|
+
@height = 0
|
10
|
+
@layers = []
|
11
|
+
end
|
12
|
+
attr_accessor :width, :height, :layers
|
13
|
+
|
14
|
+
def get_file_header
|
15
|
+
[
|
16
|
+
[0x38, 0x42, 0x50, 0x53], # Signature[4] : 8BPS
|
17
|
+
[0x00, 0x01], #Version[2] : always it's must be 1
|
18
|
+
[0x00, 0x00, 0x00, 0x00, 0x00, 0x00], #Reserved[6]
|
19
|
+
[0x00, 0x04], # The number of channels in the image, Usually it's RGBA so it will be 4
|
20
|
+
num2bytes(@height, 4), # Height[4]
|
21
|
+
num2bytes(@width, 4), # Width[4]
|
22
|
+
[0x00, 0x08], #Depth[2]: the number of bits per channel. Supported values are 1, 8, 16 and 32. opted 8bit as default.
|
23
|
+
[0x00, 0x03] #ColorMode: opted RGB Mode(3) as default.
|
24
|
+
].flatten.pack("C*")
|
25
|
+
end
|
26
|
+
|
27
|
+
def get_color_mode_data
|
28
|
+
# I'm not sure about this specs so I makes it as only 4 byte length of zero
|
29
|
+
[0x00, 0x00, 0x00, 0x00].pack("C*")
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_image_resources
|
33
|
+
# I'm not sure about this specs so I makes it as only 4 byte length of zero
|
34
|
+
[0x00, 0x00, 0x00, 0x00].pack("C*")
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_layer_and_mask_information
|
38
|
+
|
39
|
+
layer_info = get_layer_info
|
40
|
+
global_layer_info = get_global_layer_info
|
41
|
+
additional_layer_info = get_additional_layer_info
|
42
|
+
size_of_layer_and_mask_information = layer_info.size + global_layer_info.size + additional_layer_info.size
|
43
|
+
|
44
|
+
[
|
45
|
+
num2bytes(size_of_layer_and_mask_information, 4),
|
46
|
+
layer_info,
|
47
|
+
global_layer_info,
|
48
|
+
additional_layer_info
|
49
|
+
].flatten.pack("C*")
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_layer_info
|
54
|
+
|
55
|
+
_LAYER_COUNT_LENGTH = 2
|
56
|
+
layer_counts = get_layer_counts
|
57
|
+
layer_records = get_layer_records
|
58
|
+
channel_image_data = get_channel_image_data(layer_counts)
|
59
|
+
|
60
|
+
size_of_layer_info = _LAYER_COUNT_LENGTH + layer_records.size + channel_image_data.size
|
61
|
+
|
62
|
+
[
|
63
|
+
num2bytes(size_of_layer_info, 4),
|
64
|
+
num2bytes(layer_counts, 2),
|
65
|
+
layer_records,
|
66
|
+
channel_image_data
|
67
|
+
].flatten
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
def get_layer_counts
|
72
|
+
# 1 is the count of default art layer named "background"
|
73
|
+
1 + (get_key_nums @layers) * 2
|
74
|
+
end
|
75
|
+
|
76
|
+
def get_key_nums(layers)
|
77
|
+
ret = layers.size
|
78
|
+
layers.each do |v|
|
79
|
+
if v.class == Array
|
80
|
+
ret += get_key_nums(v) - 1
|
81
|
+
end
|
82
|
+
end
|
83
|
+
ret
|
84
|
+
end
|
85
|
+
|
86
|
+
def get_layer_records
|
87
|
+
art_layer_record = get_art_layer_record
|
88
|
+
layers_array = convert_layers_to_array(@layers)
|
89
|
+
group_layer_records = layers_array.map do |name|
|
90
|
+
get_group_layer_record name
|
91
|
+
end
|
92
|
+
[art_layer_record, group_layer_records].flatten
|
93
|
+
end
|
94
|
+
|
95
|
+
def get_art_layer_record
|
96
|
+
get_layer_record "background", []
|
97
|
+
end
|
98
|
+
|
99
|
+
def convert_layers_to_array(arr)
|
100
|
+
def pick_up_from_array(ar, prev = nil)
|
101
|
+
ret = []
|
102
|
+
ar.each_with_index do |a, i|
|
103
|
+
if a.class == Array
|
104
|
+
ret += pick_up_from_array(a)
|
105
|
+
if prev != nil and ar[i+1].class != Array
|
106
|
+
ret << "</Layer group>"
|
107
|
+
prev = nil
|
108
|
+
end
|
109
|
+
else
|
110
|
+
if prev != nil
|
111
|
+
ret << "</Layer group>"
|
112
|
+
end
|
113
|
+
ret << a
|
114
|
+
prev = a
|
115
|
+
end
|
116
|
+
end
|
117
|
+
ret << "</Layer group>" if prev != nil
|
118
|
+
ret
|
119
|
+
end
|
120
|
+
(pick_up_from_array arr).flatten.reverse
|
121
|
+
end
|
122
|
+
|
123
|
+
def get_group_layer_record(name)
|
124
|
+
if name == "</Layer group>"
|
125
|
+
additional_data = [
|
126
|
+
[0x38, 0x42, 0x49, 0x4D], # Blend mode signature: '8BIM'
|
127
|
+
[0x6c, 0x73, 0x63, 0x74], # Section divider setting 'lscr'
|
128
|
+
[0x00, 0x00, 0x00, 0x04], # Length of this additional record
|
129
|
+
[0x00, 0x00, 0x00, 0x03] # Type. This is 3 = bounding section divider.
|
130
|
+
]
|
131
|
+
else
|
132
|
+
additional_data = [
|
133
|
+
[0x38, 0x42, 0x49, 0x4D], # Blend mode signature: '8BIM'
|
134
|
+
[0x6c, 0x73, 0x63, 0x74], # Section divider setting 'lscr'
|
135
|
+
[0x00, 0x00, 0x00, 0x0c], # Length of this additional record. It might be 12.
|
136
|
+
[0x00, 0x00, 0x00, 0x01], # Type. This is 1 = bounding section divider.
|
137
|
+
# Following is only present if length = 12
|
138
|
+
[0x38, 0x42, 0x49, 0x4D], # Blend mode signature: '8BIM'
|
139
|
+
[0x70, 0x61, 0x73, 0x73], # Blend mode key: 'pass'
|
140
|
+
]
|
141
|
+
end
|
142
|
+
get_layer_record name, additional_data.flatten
|
143
|
+
end
|
144
|
+
|
145
|
+
def get_layer_record (name, additional_data)
|
146
|
+
|
147
|
+
pascal_name = get_pascal_name(name, 4)
|
148
|
+
size_of_additional_data = 8 + pascal_name.size + additional_data.size
|
149
|
+
[
|
150
|
+
[0x00] * 16, #Rectangle containing the contents of the layer
|
151
|
+
[0x00, 0x04], #Number of channels in the layer ( Usually it will be 4 as RGBA )
|
152
|
+
# Channel information. Six bytes per channel, consisting of: 2 bytes for Channel ID
|
153
|
+
# 4 bytes for length of corresponding channel data
|
154
|
+
[0xFF, 0xFF] + [0x00, 0x00, 0x00, 0x02], #-1 = transparency
|
155
|
+
[0x00, 0x00] + [0x00, 0x00, 0x00, 0x02], #0 = red
|
156
|
+
[0x00, 0x01] + [0x00, 0x00, 0x00, 0x02], #1 = green
|
157
|
+
[0x00, 0x02] + [0x00, 0x00, 0x00, 0x02], #2 = blue
|
158
|
+
[0x38, 0x42, 0x49, 0x4D], # Blend mode signature: '8BIM'
|
159
|
+
[0x6E, 0x6F, 0x72, 0x6D], # Blend mode key: 'norm'
|
160
|
+
[0xFF], #Opacity. 0 = transparent ... 255 = opaque
|
161
|
+
[0x00], #Clipping: 0 = base, 1 = non-base
|
162
|
+
[0x08], #Flags: [00001000]
|
163
|
+
# bit 0 = transparency protected;
|
164
|
+
# bit 1 = visible;
|
165
|
+
# bit 2 = obsolete;
|
166
|
+
# bit 3 = 1 for Photoshop 5.0 and later, tells if bit 4 has useful information;
|
167
|
+
# bit 4 = pixel data irrelevant to appearance of document
|
168
|
+
[0x00], # Just Filler (zero)
|
169
|
+
num2bytes(size_of_additional_data, 4),
|
170
|
+
[0x00] * 4, # Layer mask data. It won't be.
|
171
|
+
[0x00] * 4, # Layer blending ranges. It won't be.
|
172
|
+
pascal_name,
|
173
|
+
additional_data
|
174
|
+
].flatten
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
def get_pascal_name(name, padding)
|
179
|
+
arr = name.unpack("C*")
|
180
|
+
arr = [arr.size] + arr
|
181
|
+
if ((arr.size % padding) > 0)
|
182
|
+
arr += ([0] * (padding - (arr.size % padding)))
|
183
|
+
end
|
184
|
+
arr
|
185
|
+
end
|
186
|
+
|
187
|
+
def get_channel_image_data(layer_count)
|
188
|
+
[0x00] * 8 * layer_count
|
189
|
+
end
|
190
|
+
|
191
|
+
def get_global_layer_info
|
192
|
+
[0x00] * 4
|
193
|
+
end
|
194
|
+
|
195
|
+
def get_additional_layer_info
|
196
|
+
[]
|
197
|
+
end
|
198
|
+
|
199
|
+
def get_image_data
|
200
|
+
image_data = generate_image_data
|
201
|
+
[
|
202
|
+
[0x00, 0x01],
|
203
|
+
image_data # Compression method: 1 = RLE compressed the image data starts with the byte
|
204
|
+
].flatten.pack("C*")
|
205
|
+
end
|
206
|
+
|
207
|
+
def generate_image_data
|
208
|
+
|
209
|
+
packet = get_packet_bytes(@width)
|
210
|
+
channels = 4
|
211
|
+
[
|
212
|
+
[0x00, 0x00] * @height * channels,
|
213
|
+
packet * @height * channels
|
214
|
+
]
|
215
|
+
|
216
|
+
end
|
217
|
+
|
218
|
+
def get_packet_bytes(width)
|
219
|
+
divided = width / 128
|
220
|
+
remainder = width % 128
|
221
|
+
ret = []
|
222
|
+
[divided, remainder]
|
223
|
+
divided.times do
|
224
|
+
ret << [0x101 - 128, 0xFF]
|
225
|
+
end
|
226
|
+
ret << [0x101 - remainder, 0xFF]
|
227
|
+
ret.flatten
|
228
|
+
end
|
229
|
+
|
230
|
+
def generate
|
231
|
+
psd = open @path, "w"
|
232
|
+
psd.print get_file_header
|
233
|
+
psd.print get_color_mode_data
|
234
|
+
psd.print get_image_resources
|
235
|
+
psd.print get_layer_and_mask_information
|
236
|
+
psd.print get_image_data
|
237
|
+
psd.close
|
238
|
+
end
|
239
|
+
|
240
|
+
def output_psd
|
241
|
+
end
|
242
|
+
|
243
|
+
def num2bytes (num, size)
|
244
|
+
num.to_s(16).rjust(size*2,"0").unpack("a2"*(size)).map{|a| "0x#{a}".to_i(16)}
|
245
|
+
end
|
246
|
+
|
247
|
+
end
|
248
|
+
|
249
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-psd
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,16 +9,17 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-02-
|
12
|
+
date: 2013-02-28 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
|
-
description:
|
15
|
-
email:
|
14
|
+
description: Generating psd skeleton file simple and by scriping of Ruby
|
15
|
+
email:
|
16
|
+
- shunter1112@gmail.com
|
16
17
|
executables: []
|
17
18
|
extensions: []
|
18
19
|
extra_rdoc_files: []
|
19
20
|
files:
|
20
|
-
- lib/
|
21
|
-
homepage: http://
|
21
|
+
- lib/ruby-psd.rb
|
22
|
+
homepage: http://github.com/shunter1112/ruby-psd
|
22
23
|
licenses: []
|
23
24
|
post_install_message:
|
24
25
|
rdoc_options: []
|
@@ -41,5 +42,5 @@ rubyforge_project:
|
|
41
42
|
rubygems_version: 1.8.25
|
42
43
|
signing_key:
|
43
44
|
specification_version: 3
|
44
|
-
summary:
|
45
|
+
summary: Generating psd skeleton file simple and by scriping of Ruby
|
45
46
|
test_files: []
|
data/lib/main.rb
DELETED
File without changes
|