psd 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
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