psd 0.3.2
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 +7 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/Gemfile +2 -0
- data/Guardfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +161 -0
- data/Rakefile +1 -0
- data/circle.yml +6 -0
- data/examples/export.rb +7 -0
- data/examples/export_image.rb +12 -0
- data/examples/export_text_data.rb +13 -0
- data/examples/images/example-cmyk.psd +0 -0
- data/examples/images/example-greyscale.psd +0 -0
- data/examples/images/example.psd +0 -0
- data/examples/images/example16.psd +0 -0
- data/examples/parse.rb +31 -0
- data/examples/path.rb +7 -0
- data/examples/tree.rb +8 -0
- data/examples/unimplemented_info.rb +9 -0
- data/lib/psd.rb +145 -0
- data/lib/psd/blend_mode.rb +75 -0
- data/lib/psd/channel_image.rb +9 -0
- data/lib/psd/color.rb +125 -0
- data/lib/psd/descriptor.rb +172 -0
- data/lib/psd/file.rb +99 -0
- data/lib/psd/header.rb +61 -0
- data/lib/psd/helpers.rb +35 -0
- data/lib/psd/image.rb +107 -0
- data/lib/psd/image_exports/png.rb +36 -0
- data/lib/psd/image_formats/raw.rb +14 -0
- data/lib/psd/image_formats/rle.rb +67 -0
- data/lib/psd/image_modes/cmyk.rb +27 -0
- data/lib/psd/image_modes/greyscale.rb +36 -0
- data/lib/psd/image_modes/rgb.rb +25 -0
- data/lib/psd/layer.rb +342 -0
- data/lib/psd/layer_info.rb +22 -0
- data/lib/psd/layer_info/fill_opacity.rb +13 -0
- data/lib/psd/layer_info/layer_id.rb +13 -0
- data/lib/psd/layer_info/layer_name_source.rb +12 -0
- data/lib/psd/layer_info/layer_section_divider.rb +35 -0
- data/lib/psd/layer_info/legacy_typetool.rb +88 -0
- data/lib/psd/layer_info/object_effects.rb +16 -0
- data/lib/psd/layer_info/placed_layer.rb +14 -0
- data/lib/psd/layer_info/reference_point.rb +16 -0
- data/lib/psd/layer_info/typetool.rb +127 -0
- data/lib/psd/layer_info/unicode_name.rb +18 -0
- data/lib/psd/layer_info/vector_mask.rb +25 -0
- data/lib/psd/layer_mask.rb +106 -0
- data/lib/psd/mask.rb +45 -0
- data/lib/psd/node.rb +51 -0
- data/lib/psd/node_exporting.rb +20 -0
- data/lib/psd/node_group.rb +67 -0
- data/lib/psd/node_layer.rb +71 -0
- data/lib/psd/node_root.rb +78 -0
- data/lib/psd/nodes/ancestry.rb +82 -0
- data/lib/psd/nodes/has_children.rb +13 -0
- data/lib/psd/nodes/lock_to_origin.rb +7 -0
- data/lib/psd/nodes/parse_layers.rb +18 -0
- data/lib/psd/nodes/search.rb +28 -0
- data/lib/psd/pascal_string.rb +14 -0
- data/lib/psd/path_record.rb +180 -0
- data/lib/psd/resource.rb +27 -0
- data/lib/psd/resources.rb +47 -0
- data/lib/psd/section.rb +26 -0
- data/lib/psd/util.rb +17 -0
- data/lib/psd/version.rb +3 -0
- data/psd.gemspec +30 -0
- data/spec/files/example.psd +0 -0
- data/spec/files/one_layer.psd +0 -0
- data/spec/files/path.psd +0 -0
- data/spec/files/simplest.psd +0 -0
- data/spec/files/text.psd +0 -0
- data/spec/hierarchy_spec.rb +86 -0
- data/spec/identity_spec.rb +34 -0
- data/spec/parsing_spec.rb +134 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/text_spec.rb +12 -0
- metadata +231 -0
@@ -0,0 +1,75 @@
|
|
1
|
+
class PSD
|
2
|
+
# Describes the blend mode for a single layer or folder.
|
3
|
+
class BlendMode < BinData::Record
|
4
|
+
endian :big
|
5
|
+
|
6
|
+
string :sig, read_length: 4
|
7
|
+
string :blend_key, read_length: 4, trim_value: true
|
8
|
+
uint8 :opacity
|
9
|
+
uint8 :clipping
|
10
|
+
bit8 :flags
|
11
|
+
skip length: 1
|
12
|
+
|
13
|
+
# All of the blend modes are stored in the PSD file with a specific key.
|
14
|
+
# This is the mapping of that key to its readable name.
|
15
|
+
BLEND_MODES = {
|
16
|
+
norm: 'normal',
|
17
|
+
dark: 'darken',
|
18
|
+
lite: 'lighten',
|
19
|
+
hue: 'hue',
|
20
|
+
sat: 'saturation',
|
21
|
+
colr: 'color',
|
22
|
+
lum: 'luminosity',
|
23
|
+
mul: 'multiply',
|
24
|
+
scrn: 'screen',
|
25
|
+
diss: 'dissolve',
|
26
|
+
over: 'overlay',
|
27
|
+
hLit: 'hard light',
|
28
|
+
sLit: 'soft light',
|
29
|
+
diff: 'difference',
|
30
|
+
smud: 'exclusion',
|
31
|
+
div: 'color dodge',
|
32
|
+
idiv: 'color burn',
|
33
|
+
lbrn: 'linear burn',
|
34
|
+
lddg: 'linear dodge',
|
35
|
+
vLit: 'vivid light',
|
36
|
+
lLit: 'linear light',
|
37
|
+
pLit: 'pin light',
|
38
|
+
hMix: 'hard mix'
|
39
|
+
}
|
40
|
+
|
41
|
+
# Get the readable name for this blend mode.
|
42
|
+
def mode
|
43
|
+
BLEND_MODES[blend_key.to_sym]
|
44
|
+
end
|
45
|
+
|
46
|
+
# Set the blend mode with the readable name.
|
47
|
+
def mode=(val)
|
48
|
+
blend_key = BLEND_MODES.invert[val.downcase]
|
49
|
+
end
|
50
|
+
|
51
|
+
# Opacity is stored as an integer between 0-255. This converts the opacity
|
52
|
+
# to a percentage value to match the Photoshop interface.
|
53
|
+
def opacity_percentage
|
54
|
+
opacity * 100 / 255
|
55
|
+
end
|
56
|
+
|
57
|
+
def transparency_protected
|
58
|
+
flags & 0x01
|
59
|
+
end
|
60
|
+
|
61
|
+
# Is this layer/folder visible?
|
62
|
+
def visible
|
63
|
+
!((flags & (0x01 << 1)) > 0)
|
64
|
+
end
|
65
|
+
|
66
|
+
def obsolete
|
67
|
+
(flags & (0x01 << 2)) > 0
|
68
|
+
end
|
69
|
+
|
70
|
+
def pixel_data_irrelevant
|
71
|
+
return nil unless (flags & (0x01 << 3)) > 0
|
72
|
+
(flags & (0x01 << 4)) > 0
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/psd/color.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
class PSD
|
2
|
+
# Various color conversion methods. All color values are stored in the PSD
|
3
|
+
# document in the color space as defined by the user instead of a normalized
|
4
|
+
# value of some kind. This means that we have to do all the conversion ourselves
|
5
|
+
# for each color space.
|
6
|
+
class Color
|
7
|
+
instance_eval do
|
8
|
+
# This is a relic of libpsd that will likely go away in a future version. It
|
9
|
+
# stored the entire color value in a 32-bit address space for speed.
|
10
|
+
def color_space_to_argb(color_space, color_component)
|
11
|
+
color = case color_space
|
12
|
+
when 0
|
13
|
+
rgb_to_color *color_component
|
14
|
+
when 1
|
15
|
+
hsb_to_color color_component[0],
|
16
|
+
color_component[1] / 100.0, color_component[2] / 100.0
|
17
|
+
when 2
|
18
|
+
cmyk_to_color color_component[0] / 100.0,
|
19
|
+
color_component[1] / 100.0, color_component[2] / 100.0,
|
20
|
+
color_component[3] / 100.0
|
21
|
+
when 7
|
22
|
+
lab_to_color *color_component
|
23
|
+
else
|
24
|
+
0x00FFFFFF
|
25
|
+
end
|
26
|
+
|
27
|
+
color_to_argb(color)
|
28
|
+
end
|
29
|
+
|
30
|
+
def color_to_argb(color)
|
31
|
+
[
|
32
|
+
(color) >> 24,
|
33
|
+
((color) & 0x00FF0000) >> 16,
|
34
|
+
((color) & 0x0000FF00) >> 8,
|
35
|
+
(color) & 0x000000FF
|
36
|
+
]
|
37
|
+
end
|
38
|
+
|
39
|
+
def rgb_to_color(*args)
|
40
|
+
argb_to_color(255, *args)
|
41
|
+
end
|
42
|
+
|
43
|
+
def argb_to_color(a, r, g, b)
|
44
|
+
(a << 24) | (r << 16) | (g << 8) | b
|
45
|
+
end
|
46
|
+
|
47
|
+
def hsb_to_color(*args)
|
48
|
+
ahsb_to_color(255, *args)
|
49
|
+
end
|
50
|
+
|
51
|
+
def ahsb_to_color(alpha, hue, saturation, brightness)
|
52
|
+
if saturation == 0.0
|
53
|
+
b = g = r = (255 * brightness).to_i
|
54
|
+
else
|
55
|
+
if brightness <= 0.5
|
56
|
+
m2 = brightness * (1 + saturation)
|
57
|
+
else
|
58
|
+
m2 = brightness + saturation - brightness * saturation
|
59
|
+
end
|
60
|
+
|
61
|
+
m1 = 2 * brightness - m2
|
62
|
+
r = hue_to_color(hue + 120, m1, m2)
|
63
|
+
g = hue_to_color(hue, m1, m2)
|
64
|
+
b = hue_to_color(hue - 120, m1, m2)
|
65
|
+
end
|
66
|
+
|
67
|
+
argb_to_color alpha, r, g, b
|
68
|
+
end
|
69
|
+
|
70
|
+
def hue_to_color(hue, m1, m2)
|
71
|
+
hue = (hue % 360).to_i
|
72
|
+
if hue < 60
|
73
|
+
v = m1 + (m2 - m1) * hue / 60
|
74
|
+
elsif hue < 180
|
75
|
+
v = m2
|
76
|
+
elsif hue < 240
|
77
|
+
v = m1 + (m2 - m1) * (240 - hue) / 60
|
78
|
+
else
|
79
|
+
v = m1
|
80
|
+
end
|
81
|
+
|
82
|
+
(v * 255).to_i
|
83
|
+
end
|
84
|
+
|
85
|
+
def cmyk_to_color(c, m, y, k)
|
86
|
+
r = 1 - (c * (1 - k) + k) * 255
|
87
|
+
g = 1 - (m * (1 - k) + k) * 255
|
88
|
+
b = 1 - (y * (1 - k) + k) * 255
|
89
|
+
|
90
|
+
r = [0, r, 255].sort[1]
|
91
|
+
g = [0, g, 255].sort[1]
|
92
|
+
b = [0, b, 255].sort[1]
|
93
|
+
|
94
|
+
rgb_to_color r, g, b
|
95
|
+
end
|
96
|
+
|
97
|
+
def lab_to_color(*args)
|
98
|
+
alab_to_color(255, *args)
|
99
|
+
end
|
100
|
+
|
101
|
+
def alab_to_color(alpha, l, a, b)
|
102
|
+
xyz = lab_to_xyz(l, a, b)
|
103
|
+
axyz_to_color alpha, xyz[:x], xyz[:y], xyz[:z]
|
104
|
+
end
|
105
|
+
|
106
|
+
def lab_to_xyz(l, a, b)
|
107
|
+
y = (l + 16) / 116
|
108
|
+
x = y + (a / 500)
|
109
|
+
z = y - (b / 200)
|
110
|
+
|
111
|
+
x, y, z = [x, y, z].map do |n|
|
112
|
+
n**3 > 0.008856 ? n**3 : (n - 16 / 116) / 7.787
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def cmyk_to_rgb(c, m, y, k)
|
117
|
+
Hash[{
|
118
|
+
r: (65535 - (c * (255 - k) + (k << 8))) >> 8,
|
119
|
+
g: (65535 - (m * (255 - k) + (k << 8))) >> 8,
|
120
|
+
b: (65535 - (y * (255 - k) + (k << 8))) >> 8
|
121
|
+
}.map { |k, v| [k, Util.clamp(v, 0, 255)] }]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
class PSD
|
2
|
+
# A descriptor is a block of data that describes a complex data structure of some kind.
|
3
|
+
# It was added sometime around Photoshop 5.0 and it superceded a few legacy things such
|
4
|
+
# as layer names and type data.
|
5
|
+
class Descriptor
|
6
|
+
# Store a reference to our file and initialize our data structure.
|
7
|
+
def initialize(file)
|
8
|
+
@file = file
|
9
|
+
@data = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
# Parse the descriptor. Descriptors always start with a class identifier, followed by
|
13
|
+
# a variable number of items in the descriptor. We return the Hash that represents
|
14
|
+
# the full data structure.
|
15
|
+
def parse
|
16
|
+
@data[:class] = parse_class
|
17
|
+
|
18
|
+
num_items = @file.read_int
|
19
|
+
num_items.times do |i|
|
20
|
+
id, value = parse_key_item
|
21
|
+
@data[id] = value
|
22
|
+
end
|
23
|
+
|
24
|
+
@data
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def parse_class
|
30
|
+
{
|
31
|
+
name: @file.read_unicode_string,
|
32
|
+
id: parse_id
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def parse_id
|
37
|
+
len = @file.read_int
|
38
|
+
len == 0 ? @file.read_int : @file.read_string(len)
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse_key_item
|
42
|
+
id = parse_id
|
43
|
+
value = parse_item
|
44
|
+
|
45
|
+
return id, value
|
46
|
+
end
|
47
|
+
|
48
|
+
def parse_item(type = nil)
|
49
|
+
type = @file.read_string(4) if type.nil?
|
50
|
+
|
51
|
+
value = case type
|
52
|
+
when 'bool' then parse_boolean
|
53
|
+
when 'type', 'GlbC' then parse_class
|
54
|
+
when 'Objc', 'GlbO' then parse
|
55
|
+
when 'doub' then parse_double
|
56
|
+
when 'enum' then parse_enum
|
57
|
+
when 'alis' then parse_alias
|
58
|
+
when 'Pth' then parse_file_path
|
59
|
+
when 'long' then parse_integer
|
60
|
+
when 'comp' then parse_large_integer
|
61
|
+
when 'VlLs' then parse_list
|
62
|
+
when 'ObAr' then parse_object_array
|
63
|
+
when 'tdta' then parse_raw_data
|
64
|
+
when 'obj ' then parse_reference
|
65
|
+
when 'TEXT' then @file.read_unicode_string
|
66
|
+
when 'UntF' then parse_unit_double
|
67
|
+
end
|
68
|
+
|
69
|
+
return value
|
70
|
+
end
|
71
|
+
|
72
|
+
def parse_boolean; @file.read_boolean; end
|
73
|
+
def parse_double; @file.read_double; end
|
74
|
+
def parse_integer; @file.read_int; end
|
75
|
+
def parse_large_integer; @file.read_longlong; end
|
76
|
+
def parse_identifier; @file.read_int; end
|
77
|
+
def parse_index; @file.read_int; end
|
78
|
+
def parse_offset; @file.read_int; end
|
79
|
+
def parse_property; parse_id; end
|
80
|
+
|
81
|
+
# Discard the first ID becasue it's the same as the key
|
82
|
+
# parsed from the Key/Item. Also, YOLO.
|
83
|
+
def parse_enum
|
84
|
+
parse_id
|
85
|
+
parse_id
|
86
|
+
end
|
87
|
+
|
88
|
+
def parse_alias
|
89
|
+
len = @file.read_int
|
90
|
+
@file.read_string len
|
91
|
+
end
|
92
|
+
|
93
|
+
def parse_file_path
|
94
|
+
len = @file.read_int
|
95
|
+
|
96
|
+
# Little-endian, because fuck the world.
|
97
|
+
sig = @file.read_string(4)
|
98
|
+
path_size = @file.read('l<')
|
99
|
+
num_chars = @file.read('l<')
|
100
|
+
|
101
|
+
path = @file.read_unicode_string(num_chars)
|
102
|
+
|
103
|
+
{sig: sig, path: path}
|
104
|
+
end
|
105
|
+
|
106
|
+
def parse_list
|
107
|
+
count = @file.read_int
|
108
|
+
items = []
|
109
|
+
|
110
|
+
count.times do |i|
|
111
|
+
items << parse_item
|
112
|
+
end
|
113
|
+
|
114
|
+
return items
|
115
|
+
end
|
116
|
+
|
117
|
+
def parse_object_array
|
118
|
+
count = @file.read_int
|
119
|
+
klass = parse_class
|
120
|
+
items_in_obj = @file.read_int
|
121
|
+
|
122
|
+
obj = []
|
123
|
+
count.times do |i|
|
124
|
+
item = []
|
125
|
+
items_in_obj.times do |j|
|
126
|
+
item << parse_object_array
|
127
|
+
end
|
128
|
+
|
129
|
+
obj << item
|
130
|
+
end
|
131
|
+
|
132
|
+
return obj
|
133
|
+
end
|
134
|
+
|
135
|
+
def parse_raw_data
|
136
|
+
len = @file.read_int
|
137
|
+
@file.read(len)
|
138
|
+
end
|
139
|
+
|
140
|
+
def parse_reference
|
141
|
+
form = @file.read_string(4)
|
142
|
+
klass = parse_class
|
143
|
+
|
144
|
+
case form
|
145
|
+
when 'Clss' then nil
|
146
|
+
when 'Enmr' then parse_enum
|
147
|
+
when 'Idnt' then parse_identifier
|
148
|
+
when 'indx' then parse_index
|
149
|
+
when 'name' then @file.read_unicode_string
|
150
|
+
when 'rele' then parse_offset
|
151
|
+
when 'prop' then parse_property
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def parse_unit_double
|
156
|
+
unit_id = @file.read_string(4)
|
157
|
+
unit = case unit_id
|
158
|
+
when '#Ang' then 'Angle'
|
159
|
+
when '#Rsl' then 'Density'
|
160
|
+
when '#Rlt' then 'Distance'
|
161
|
+
when '#Nne' then 'None'
|
162
|
+
when '#Prc' then 'Percent'
|
163
|
+
when '#Pxl' then 'Pixels'
|
164
|
+
when '#Mlm' then 'Millimeters'
|
165
|
+
when '#Pnt' then 'Points'
|
166
|
+
end
|
167
|
+
|
168
|
+
value = @file.read_double
|
169
|
+
{id: unit_id, unit: unit, value: value}
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
data/lib/psd/file.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
class PSD
|
2
|
+
# An extension of the built-in Ruby File class that adds numerous helpers for
|
3
|
+
# reading/writing binary data.
|
4
|
+
class File < ::File
|
5
|
+
# All of the formats and their pack codes that we want to be able to convert into
|
6
|
+
# methods for easy reading/writing.
|
7
|
+
FORMATS = {
|
8
|
+
ulonglong: {
|
9
|
+
length: 8,
|
10
|
+
code: 'Q>'
|
11
|
+
},
|
12
|
+
longlong: {
|
13
|
+
length: 8,
|
14
|
+
code: 'q>'
|
15
|
+
},
|
16
|
+
double: {
|
17
|
+
length: 8,
|
18
|
+
code: 'G'
|
19
|
+
},
|
20
|
+
float: {
|
21
|
+
length: 4,
|
22
|
+
code: 'F'
|
23
|
+
},
|
24
|
+
uint: {
|
25
|
+
length: 4,
|
26
|
+
code: 'L>'
|
27
|
+
},
|
28
|
+
int: {
|
29
|
+
length: 4,
|
30
|
+
code: 'l>'
|
31
|
+
},
|
32
|
+
ushort: {
|
33
|
+
length: 2,
|
34
|
+
code: 'S>'
|
35
|
+
},
|
36
|
+
short: {
|
37
|
+
length: 2,
|
38
|
+
code: 's>'
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
FORMATS.each do |format, info|
|
43
|
+
define_method "read_#{format}" do
|
44
|
+
read(info[:length]).unpack(info[:code])[0]
|
45
|
+
end
|
46
|
+
|
47
|
+
define_method "write_#{format}" do |val|
|
48
|
+
write [val].pack(info[:code])
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Adobe's lovely signed 32-bit fixed-point number with 8bits.24bits
|
53
|
+
# http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/PhotoshopFileFormats.htm#50577409_17587
|
54
|
+
def read_path_number
|
55
|
+
read(1).unpack('c*')[0].to_f +
|
56
|
+
(read(3).unpack('B*')[0].to_i(2).to_f / (2 ** 24)).to_f # pre-decimal point
|
57
|
+
end
|
58
|
+
|
59
|
+
def write_path_number(num)
|
60
|
+
write [num.to_i].pack('C')
|
61
|
+
|
62
|
+
# Now for the fun part.
|
63
|
+
# We first convert the decimal to be a whole number representing a
|
64
|
+
# fraction with the denominator of 2^24
|
65
|
+
# Next, we write that number as a 24-bit integer to the file
|
66
|
+
binary_numerator = ((num - num.to_i).abs * 2 ** 24).to_i
|
67
|
+
write [binary_numerator >> 16].pack('C')
|
68
|
+
write [binary_numerator >> 8].pack('C')
|
69
|
+
write [binary_numerator >> 0].pack('C')
|
70
|
+
end
|
71
|
+
|
72
|
+
# Reads a string of the given length and converts it to UTF-8 from the internally used MacRoman encoding.
|
73
|
+
def read_string(length)
|
74
|
+
read(length).encode('UTF-8', 'MacRoman').delete("\000")
|
75
|
+
end
|
76
|
+
|
77
|
+
# Reads a unicode string, which is double the length of a normal string and encoded as UTF-16.
|
78
|
+
def read_unicode_string(length=nil)
|
79
|
+
length ||= read_int if length.nil?
|
80
|
+
!length.nil? && length > 0 ? read(length * 2).encode('UTF-8', 'UTF-16BE').delete("\000") : ''
|
81
|
+
end
|
82
|
+
|
83
|
+
# Reads a boolean value.
|
84
|
+
def read_boolean
|
85
|
+
read(1)[0] != 0
|
86
|
+
end
|
87
|
+
|
88
|
+
# Reads a 32-bit color space value.
|
89
|
+
def read_space_color
|
90
|
+
color_space = read_short
|
91
|
+
color_component = []
|
92
|
+
4.times do |i|
|
93
|
+
color_component.push(read_short >> 8)
|
94
|
+
end
|
95
|
+
|
96
|
+
Color.color_space_to_argb(color_space, color_component)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|