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,22 @@
1
+ class PSD
2
+ # Parent class for all of the extra layer info.
3
+ class LayerInfo
4
+ attr_reader :data
5
+
6
+ # The value of the key as used in the PSD format.
7
+ class << self; attr_accessor :key; end
8
+ @key = ""
9
+
10
+ def initialize(file, length)
11
+ @file = file
12
+ @length = length
13
+ @section_end = @file.tell + @length
14
+ @data = {}
15
+ end
16
+
17
+ # Override this - default seeks to end of section
18
+ def parse
19
+ @file.seek @section_end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ require_relative '../layer_info'
2
+
3
+ class PSD
4
+ class FillOpacity < LayerInfo
5
+ @key = 'iOpa'
6
+
7
+ attr_reader :enabled
8
+
9
+ def parse
10
+ @enabled = @file.read_boolean
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require_relative '../layer_info'
2
+
3
+ class PSD
4
+ class LayerID < LayerInfo
5
+ @key = 'lyid'
6
+
7
+ attr_reader :id
8
+
9
+ def parse
10
+ @id = @file.read_int
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ require_relative '../layer_info'
2
+
3
+ class PSD
4
+ class LayerNameSource < LayerInfo
5
+ @key = 'lnsr'
6
+
7
+ def parse
8
+ @data = @file.read_int
9
+ return self
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,35 @@
1
+ require_relative '../layer_info'
2
+
3
+ class PSD
4
+ class LayerSectionDivider < LayerInfo
5
+ @key = 'lsct'
6
+
7
+ attr_reader :layer_type, :is_folder, :is_hidden
8
+
9
+ SECTION_DIVIDER_TYPES = [
10
+ "other",
11
+ "open folder",
12
+ "closed folder",
13
+ "bounding section divider"
14
+ ]
15
+
16
+ def initialize(file, length)
17
+ super
18
+
19
+ @is_folder = false
20
+ @is_hidden = false
21
+ end
22
+
23
+ def parse
24
+ code = @file.read_int
25
+ @layer_type = SECTION_DIVIDER_TYPES[code]
26
+
27
+ case code
28
+ when 1, 2 then @is_folder = true
29
+ when 3 then @is_hidden = true
30
+ end
31
+
32
+ return self
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,88 @@
1
+ require_relative '../layer_info'
2
+ require_relative 'typetool'
3
+
4
+ class PSD
5
+ class LegacyTypeTool < TypeTool
6
+ @key = 'tySh'
7
+
8
+ def parse
9
+ version = @file.read_short
10
+ parse_transform_info
11
+
12
+ # Font Information
13
+ version = @file.read_short
14
+
15
+ faces_count = @file.read_short
16
+ @data[:face] = []
17
+
18
+ faces_count.times do |i|
19
+ @data[:face][i] = {}
20
+ @data[:face][i][:mark] = @file.read_short
21
+ @data[:face][i][:font_type] = @file.read_int
22
+ @data[:face][i][:font_name] = PascalString.read(@file)
23
+ @data[:face][i][:font_family_name] = PascalString.read(@file)
24
+ @data[:face][i][:font_style_name] = PascalString.read(@file)
25
+ @data[:face][i][:script] = @file.read_short
26
+ @data[:face][i][:number_axes_vector] = @file.read_int
27
+ @data[:face][i][:vector] = []
28
+
29
+ @data[:face][i][:number_axes_vector].times do |j|
30
+ @data[:face][i][:vector] << @file.read_int
31
+ end
32
+ end
33
+
34
+ # Style Information
35
+ styles_count = @file.read_short
36
+ @data[:style] = []
37
+
38
+ styles_count.times do |i|
39
+ @data[:style][i] = {}
40
+ @data[:style][i][:mark] = @file.read_short
41
+ @data[:style][i][:face_mark] = @file.read_short
42
+ @data[:style][i][:size] = @file.read_int
43
+ @data[:style][i][:tracking] = @file.read_int
44
+ @data[:style][i][:kerning] = @file.read_int
45
+ @data[:style][i][:leading] = @file.read_int
46
+ @data[:style][i][:base_shift] = @file.read_int
47
+ @data[:style][i][:auto_kern] = @file.read_boolean
48
+
49
+ # Bleh
50
+ @file.read 1
51
+
52
+ @data[:style][i][:rotate] = @file.read_boolean
53
+ end
54
+
55
+ # Text information
56
+ @data[:type] = @file.read_short
57
+ @data[:scaling_factor] = @file.read_int
58
+ @data[:character_count] = @file.read_int
59
+ @data[:horz_place] = @file.read_int
60
+ @data[:vert_place] = @file.read_int
61
+ @data[:select_start] = @file.read_int
62
+ @data[:select_end] = @file.read_int
63
+
64
+ lines_count = @file.read_short
65
+ @data[:line] = []
66
+
67
+ lines_count.times do |i|
68
+ @data[:line][i] = {}
69
+ @data[:line][i][:char_count] = @file.read_int
70
+ @data[:line][i][:orientation] = @file.read_short
71
+ @data[:line][i][:alignment] = @file.read_short
72
+ @data[:line][i][:actual_char] = @file.read_short
73
+ @data[:line][i][:style] = @file.read_short
74
+ end
75
+
76
+ # Color information
77
+ @data[:color] = @file.read_space_color
78
+ @data[:antialias] = @file.read_boolean
79
+
80
+ return self
81
+ end
82
+
83
+ # Not sure where this is stored right now
84
+ def text_value
85
+ ""
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,16 @@
1
+ require_relative '../layer_info'
2
+
3
+ class PSD
4
+ class ObjectEffects < LayerInfo
5
+ @key = 'lfx2'
6
+
7
+ def parse
8
+ version = @file.read_int
9
+ descriptor_version = @file.read_int
10
+
11
+ @data = Descriptor.new(@file).parse
12
+
13
+ return self
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ require_relative '../layer_info'
2
+
3
+ class PSD
4
+ class PlacedLayer < LayerInfo
5
+ @key = 'SoLd'
6
+
7
+ def parse
8
+ # Useless id/version info
9
+ @file.seek 12, IO::SEEK_CUR
10
+
11
+ @data = Descriptor.new(@file).parse
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ require_relative '../layer_info'
2
+
3
+ class PSD
4
+ class ReferencePoint < LayerInfo
5
+ @key = 'fxrp'
6
+
7
+ attr_reader :x, :y
8
+
9
+ def parse
10
+ @x = @file.read_double
11
+ @y = @file.read_double
12
+
13
+ return self
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,127 @@
1
+ require_relative '../layer_info'
2
+
3
+ class PSD
4
+ # Parses and provides information about text areas within layers in
5
+ # the document.
6
+ class TypeTool < LayerInfo
7
+ @key = 'TySh'
8
+
9
+ # Parse all of the text data in the layer.
10
+ def parse
11
+ version = @file.read_short
12
+ parse_transform_info
13
+
14
+ text_version = @file.read_short
15
+ descriptor_version = @file.read_int
16
+
17
+ @data[:text] = Descriptor.new(@file).parse
18
+ @data[:text]['EngineData']
19
+ .encode!('UTF-8', 'MacRoman')
20
+ .delete!("\000")
21
+
22
+ @data[:engine_data] = nil
23
+ begin
24
+ parser.parse!
25
+ @data[:engine_data] = parser.result
26
+ rescue Exception => e
27
+ puts e.message
28
+ end
29
+
30
+ warpVersion = @file.read_short
31
+ descriptor_version = @file.read_int
32
+
33
+ @data[:warp] = Descriptor.new(@file).parse
34
+ [:left, :top, :right, :bottom].each do |pos|
35
+ @data[pos] = @file.read_int
36
+ end
37
+
38
+ return self
39
+ end
40
+
41
+ # Extracts the text within the text area. In the event that psd-enginedata fails
42
+ # for some reason, we attempt to extract the text using some rough regex.
43
+ def text_value
44
+ if engine_data.nil?
45
+ # Something went wrong, lets hack our way through.
46
+ /\/Text \(˛ˇ(.*)\)$/.match(@data[:text]['EngineData'])[1].gsub /\r/, "\n"
47
+ else
48
+ engine_data.EngineDict.Editor.Text
49
+ end
50
+ end
51
+ alias :to_s :text_value
52
+
53
+ # Gets all of the basic font information for this text area. This assumes that
54
+ # the first font is the only one you want.
55
+ def font
56
+ {
57
+ name: fonts.first,
58
+ sizes: sizes,
59
+ colors: colors,
60
+ css: parser.to_css
61
+ }
62
+ end
63
+
64
+ # Returns all fonts listed for this layer, since fonts are defined on a
65
+ # per-character basis.
66
+ def fonts
67
+ return [] if engine_data.nil?
68
+ engine_data.ResourceDict.FontSet.map(&:Name)
69
+ end
70
+
71
+ # Return all font sizes for this layer.
72
+ def sizes
73
+ return [] if engine_data.nil?
74
+ engine_data.EngineDict.StyleRun.RunArray.map do |r|
75
+ r.StyleSheet.StyleSheetData.FontSize
76
+ end.uniq
77
+ end
78
+
79
+ # Return all colors used for text in this layer. The colors are returned in RGB
80
+ # format as an array of arrays.
81
+ #
82
+ # => [[255, 0, 0], [0, 0, 255]]
83
+ def colors
84
+ return [] if engine_data.nil?
85
+ engine_data.EngineDict.StyleRun.RunArray.map do |r|
86
+ next unless r.StyleSheet.StyleSheetData.key?('FillColor')
87
+ r.StyleSheet.StyleSheetData.FillColor.Values.map do |v|
88
+ (v * 255).round
89
+ end
90
+ end.uniq
91
+ end
92
+
93
+ def engine_data
94
+ @data[:engine_data]
95
+ end
96
+
97
+ def parser
98
+ @parser ||= PSD::EngineData.new(@data[:text]['EngineData'])
99
+ end
100
+
101
+ def to_hash
102
+ {
103
+ value: text_value,
104
+ font: font,
105
+ left: left,
106
+ top: top,
107
+ right: right,
108
+ bottom: bottom,
109
+ transform: transform
110
+ }
111
+ end
112
+
113
+ def method_missing(method, *args, &block)
114
+ return @data[method] if @data.has_key?(method)
115
+ return super
116
+ end
117
+
118
+ private
119
+
120
+ def parse_transform_info
121
+ @data[:transform] = {}
122
+ [:xx, :xy, :yx, :yy, :tx, :ty].each do |t|
123
+ @data[:transform][t] = @file.read_double
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,18 @@
1
+ require_relative '../layer_info'
2
+
3
+ class PSD
4
+ class UnicodeName < LayerInfo
5
+ @key = 'luni'
6
+
7
+ def parse
8
+ pos = @file.tell
9
+ len = @file.read_int * 2
10
+ @data = @file.read(len).unpack("A#{len}")[0].encode('UTF-8').delete("\000")
11
+
12
+ # The name seems to be padded with null bytes. This is the easiest solution.
13
+ @file.seek pos + @length
14
+
15
+ return self
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,25 @@
1
+ require_relative '../layer_info'
2
+
3
+ class PSD
4
+ class VectorMask < LayerInfo
5
+ @key = 'vmsk'
6
+
7
+ attr_reader :invert, :not_link, :disable, :paths
8
+
9
+ def parse
10
+ version = @file.read_int
11
+ tag = @file.read_int
12
+
13
+ @invert = tag & 0x01
14
+ @not_link = (tag & (0x01 << 1)) > 0
15
+ @disable = (tag & (0x01 << 2)) > 0
16
+
17
+ num_records = (@length - 8) / 26
18
+
19
+ @paths = []
20
+ num_records.times do
21
+ @paths << PathRecord.new(@file)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,106 @@
1
+ class PSD
2
+ # Covers parsing the global mask and controls parsing of all the
3
+ # layers/folders in the document.
4
+ class LayerMask
5
+ include Section
6
+
7
+ attr_reader :layers, :global_mask
8
+
9
+ # Store a reference to the file and the header and initialize the defaults.
10
+ def initialize(file, header)
11
+ @file = file
12
+ @header = header
13
+
14
+ @layers = []
15
+ @merged_alpha = false
16
+ @global_mask = nil
17
+ @extras = []
18
+ end
19
+
20
+ # Allows us to skip this section because it starts with the length of the section
21
+ # stored as an integer.
22
+ def skip
23
+ @file.seek @file.read_int, IO::SEEK_CUR
24
+ return self
25
+ end
26
+
27
+ # Parse this section, including all of the layers and folders. Once implemented, this
28
+ # will also trigger parsing of the channel images for each layer.
29
+ def parse
30
+ start_section
31
+
32
+ mask_size = @file.read_int
33
+ finish = @file.tell + mask_size
34
+
35
+ return self if mask_size <= 0
36
+
37
+ layer_info_size = Util.pad2(@file.read_int)
38
+
39
+ if layer_info_size > 0
40
+ layer_count = @file.read_short
41
+
42
+ if layer_count < 0
43
+ layer_count = layer_count.abs
44
+ @merged_alpha = true
45
+ end
46
+
47
+ if layer_count * (18 + 6 * @header.channels) > layer_info_size
48
+ raise "Unlikely number of layers parsed: #{layer_count}"
49
+ end
50
+
51
+ @layer_section_start = @file.tell
52
+ layer_count.times do
53
+ @layers << PSD::Layer.new(@file).parse
54
+ end
55
+
56
+ layers.each do |layer|
57
+ @file.seek 8, IO::SEEK_CUR and next if layer.folder? || layer.folder_end?
58
+
59
+ layer.parse_channel_image!(@header)
60
+ end
61
+ end
62
+
63
+ # Layers are parsed in reverse order
64
+ layers.reverse!
65
+ group_layers
66
+
67
+ # Temporarily seek to the end of this section
68
+ @file.seek finish
69
+ end_section
70
+
71
+ return self
72
+ end
73
+
74
+ # Export the mask and all the children layers to a file.
75
+ def export(outfile)
76
+ if @layers.size == 0
77
+ # No data, just read whatever's here.
78
+ return outfile.write @file.read(@section_end[:all] - start_of_section)
79
+ end
80
+
81
+ # Read the initial mask data since it won't change
82
+ outfile.write @file.read(@layer_section_start - @file.tell)
83
+
84
+ @layers.reverse.each do |layer|
85
+ layer.export(outfile)
86
+ end
87
+
88
+ outfile.write @file.read(end_of_section - @file.tell)
89
+ end
90
+
91
+ private
92
+
93
+ def group_layers
94
+ group_layer = nil
95
+ layers.each do |layer|
96
+ if layer.folder?
97
+ group_layer = layer
98
+ elsif layer.folder_end?
99
+ group_layer = nil
100
+ else
101
+ layer.group_layer = layer
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end