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.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +2 -0
  5. data/Guardfile +8 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +161 -0
  8. data/Rakefile +1 -0
  9. data/circle.yml +6 -0
  10. data/examples/export.rb +7 -0
  11. data/examples/export_image.rb +12 -0
  12. data/examples/export_text_data.rb +13 -0
  13. data/examples/images/example-cmyk.psd +0 -0
  14. data/examples/images/example-greyscale.psd +0 -0
  15. data/examples/images/example.psd +0 -0
  16. data/examples/images/example16.psd +0 -0
  17. data/examples/parse.rb +31 -0
  18. data/examples/path.rb +7 -0
  19. data/examples/tree.rb +8 -0
  20. data/examples/unimplemented_info.rb +9 -0
  21. data/lib/psd.rb +145 -0
  22. data/lib/psd/blend_mode.rb +75 -0
  23. data/lib/psd/channel_image.rb +9 -0
  24. data/lib/psd/color.rb +125 -0
  25. data/lib/psd/descriptor.rb +172 -0
  26. data/lib/psd/file.rb +99 -0
  27. data/lib/psd/header.rb +61 -0
  28. data/lib/psd/helpers.rb +35 -0
  29. data/lib/psd/image.rb +107 -0
  30. data/lib/psd/image_exports/png.rb +36 -0
  31. data/lib/psd/image_formats/raw.rb +14 -0
  32. data/lib/psd/image_formats/rle.rb +67 -0
  33. data/lib/psd/image_modes/cmyk.rb +27 -0
  34. data/lib/psd/image_modes/greyscale.rb +36 -0
  35. data/lib/psd/image_modes/rgb.rb +25 -0
  36. data/lib/psd/layer.rb +342 -0
  37. data/lib/psd/layer_info.rb +22 -0
  38. data/lib/psd/layer_info/fill_opacity.rb +13 -0
  39. data/lib/psd/layer_info/layer_id.rb +13 -0
  40. data/lib/psd/layer_info/layer_name_source.rb +12 -0
  41. data/lib/psd/layer_info/layer_section_divider.rb +35 -0
  42. data/lib/psd/layer_info/legacy_typetool.rb +88 -0
  43. data/lib/psd/layer_info/object_effects.rb +16 -0
  44. data/lib/psd/layer_info/placed_layer.rb +14 -0
  45. data/lib/psd/layer_info/reference_point.rb +16 -0
  46. data/lib/psd/layer_info/typetool.rb +127 -0
  47. data/lib/psd/layer_info/unicode_name.rb +18 -0
  48. data/lib/psd/layer_info/vector_mask.rb +25 -0
  49. data/lib/psd/layer_mask.rb +106 -0
  50. data/lib/psd/mask.rb +45 -0
  51. data/lib/psd/node.rb +51 -0
  52. data/lib/psd/node_exporting.rb +20 -0
  53. data/lib/psd/node_group.rb +67 -0
  54. data/lib/psd/node_layer.rb +71 -0
  55. data/lib/psd/node_root.rb +78 -0
  56. data/lib/psd/nodes/ancestry.rb +82 -0
  57. data/lib/psd/nodes/has_children.rb +13 -0
  58. data/lib/psd/nodes/lock_to_origin.rb +7 -0
  59. data/lib/psd/nodes/parse_layers.rb +18 -0
  60. data/lib/psd/nodes/search.rb +28 -0
  61. data/lib/psd/pascal_string.rb +14 -0
  62. data/lib/psd/path_record.rb +180 -0
  63. data/lib/psd/resource.rb +27 -0
  64. data/lib/psd/resources.rb +47 -0
  65. data/lib/psd/section.rb +26 -0
  66. data/lib/psd/util.rb +17 -0
  67. data/lib/psd/version.rb +3 -0
  68. data/psd.gemspec +30 -0
  69. data/spec/files/example.psd +0 -0
  70. data/spec/files/one_layer.psd +0 -0
  71. data/spec/files/path.psd +0 -0
  72. data/spec/files/simplest.psd +0 -0
  73. data/spec/files/text.psd +0 -0
  74. data/spec/hierarchy_spec.rb +86 -0
  75. data/spec/identity_spec.rb +34 -0
  76. data/spec/parsing_spec.rb +134 -0
  77. data/spec/spec_helper.rb +13 -0
  78. data/spec/text_spec.rb +12 -0
  79. 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
@@ -0,0 +1,9 @@
1
+ require_relative 'image'
2
+
3
+ class PSD
4
+ # Represents an image for a single layer, which differs slightly in format from
5
+ # the full size preview image.
6
+ class ChannelImage < Image
7
+
8
+ end
9
+ end
@@ -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
@@ -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