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,28 @@
1
+ class PSD
2
+ class Node
3
+ module Search
4
+ # Searches the tree structure for a node at the given path. The path is
5
+ # defined by the layer/folder names. Because the PSD format does not
6
+ # require unique layer/folder names, we always return an array of all
7
+ # found nodes.
8
+ def children_at_path(path, opts={})
9
+ path = path.split('/').delete_if { |p| p == "" } unless path.is_a?(Array)
10
+
11
+ query = path.shift
12
+ matches = children.select do |c|
13
+ if opts[:case_sensitive]
14
+ c.name == query
15
+ else
16
+ c.name.downcase == query.downcase
17
+ end
18
+ end
19
+
20
+ if path.length == 0
21
+ return matches
22
+ else
23
+ return matches.map { |m| m.children_at_path(path, opts) }.flatten
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,14 @@
1
+ # Helper class that parses a pascal string, which is a
2
+ # string that has it's length prepended to it.
3
+ class PascalString < BinData::Record
4
+ uint8 :len, value: lambda { data.length }
5
+ string :data, read_length: :len
6
+
7
+ def get
8
+ self.data
9
+ end
10
+
11
+ def set(v)
12
+ self.data = v
13
+ end
14
+ end
@@ -0,0 +1,180 @@
1
+ class PSD
2
+ # Parses a vector path
3
+ class PathRecord
4
+ attr_accessor :layer
5
+
6
+ # Facade to make it easier to parse the path record.
7
+ def self.read(layer)
8
+ pr = PSD::PathRecord.new(layer.file)
9
+ pr.layer = layer
10
+ pr
11
+ end
12
+
13
+ # Reads the record type and begins parsing accordingly.
14
+ def initialize(file)
15
+ @file = file
16
+
17
+ @record_type = @file.read_short
18
+
19
+ case @record_type
20
+ when 0, 3 then read_path_record
21
+ when 1, 2, 4, 5 then read_bezier_point
22
+ when 7 then read_clipboard_record
23
+ when 8 then read_initial_fill
24
+ else @file.seek(24, IO::SEEK_CUR)
25
+ end
26
+ end
27
+
28
+ # Writes out the path to file.
29
+ def write(outfile)
30
+ outfile.write_short @record_type
31
+ case @record_type
32
+ when 0 then write_path_record(outfile)
33
+ when 3 then write_path_record(outfile)
34
+ when 1 then write_bezier_point(outfile)
35
+ when 2 then write_bezier_point(outfile)
36
+ when 4 then write_bezier_point(outfile)
37
+ when 5 then write_bezier_point(outfile)
38
+ when 7 then write_clipboard_record(outfile)
39
+ when 8 then write_initial_fill(outfile)
40
+ else outfile.seek(24, IO::SEEK_CUR)
41
+ end
42
+ end
43
+
44
+ # Exports the path record to an easier to work with hash.
45
+ def to_hash
46
+ case @record_type
47
+ when 0, 3
48
+ {
49
+ num_points: @num_points
50
+ }
51
+ when 1, 2, 4, 5
52
+ {
53
+ linked: @linked,
54
+ preceding: {
55
+ vert: @preceding_vert,
56
+ horiz: @preceding_horiz
57
+ },
58
+ anchor: {
59
+ vert: @anchor_vert,
60
+ horiz: @anchor_horiz
61
+ },
62
+ leaving: {
63
+ vert: @leaving_vert,
64
+ horiz: @leaving_horiz
65
+ }
66
+ }
67
+ when 7
68
+ {
69
+ clipboard: {
70
+ top: @clipboard_top,
71
+ left: @clipboard_left,
72
+ bottom: @clipboard_bottom,
73
+ right: @clipboard_right,
74
+ resolution: @clipboard_resolution
75
+ }
76
+ }
77
+ when 8
78
+ {
79
+ initial_fill: @initial_fill
80
+ }
81
+ else
82
+ {}
83
+ end.merge({ record_type: @record_type })
84
+ end
85
+
86
+ # Attempts to translate the path
87
+ def translate(x=0, y=0)
88
+ return unless is_bezier_point?
89
+
90
+ document_width, document_height = @layer.document_dimensions
91
+ translate_x_ratio = x.to_f / document_width.to_f
92
+ translate_y_ratio = y.to_f / document_height.to_f
93
+
94
+ @preceding_vert += translate_y_ratio
95
+ @preceding_horiz += translate_x_ratio
96
+ @anchor_vert += translate_y_ratio
97
+ @anchor_horiz += translate_x_ratio
98
+ @leaving_vert += translate_y_ratio
99
+ @leaving_horiz += translate_x_ratio
100
+ end
101
+
102
+ # Attempts to scale the path
103
+ def scale(xr, yr)
104
+ return unless is_bezier_point?
105
+
106
+ @preceding_vert *= yr
107
+ @preceding_horiz *= xr
108
+ @anchor_vert *= yr
109
+ @anchor_horiz *= xr
110
+ @leaving_vert *= yr
111
+ @leaving_horiz *= xr
112
+ end
113
+
114
+ # Is this record a bezier point?
115
+ def is_bezier_point?
116
+ [1,2,4,5].include? @record_type
117
+ end
118
+
119
+ private
120
+
121
+ def read_path_record
122
+ @num_points = @file.read_short
123
+ @file.seek(22, IO::SEEK_CUR)
124
+ end
125
+
126
+ def write_path_record(file)
127
+ file.write_short @num_points
128
+ file.seek(22, IO::SEEK_CUR)
129
+ end
130
+
131
+ def read_bezier_point
132
+ @linked = [1,4].include? @record_type
133
+
134
+ @preceding_vert = @file.read_path_number
135
+ @preceding_horiz = @file.read_path_number
136
+
137
+ @anchor_vert = @file.read_path_number
138
+ @anchor_horiz = @file.read_path_number
139
+
140
+ @leaving_vert = @file.read_path_number
141
+ @leaving_horiz = @file.read_path_number
142
+ end
143
+
144
+ def write_bezier_point(outfile)
145
+ outfile.write_path_number @preceding_vert
146
+ outfile.write_path_number @preceding_horiz
147
+ outfile.write_path_number @anchor_vert
148
+ outfile.write_path_number @anchor_horiz
149
+ outfile.write_path_number @leaving_vert
150
+ outfile.write_path_number @leaving_horiz
151
+ end
152
+
153
+ def read_clipboard_record
154
+ @clipboard_top = @file.read_path_number
155
+ @clipboard_left = @file.read_path_number
156
+ @clipboard_bottom = @file.read_path_number
157
+ @clipboard_right = @file.read_path_number
158
+ @clipboard_resolution = @file.read_path_number
159
+ @file.seek(4, IO::SEEK_CUR)
160
+ end
161
+
162
+ def write_clipboard_record(file)
163
+ [@clipboard_top, @clipboard_left, @clipboard_bottom,
164
+ @clipboard_right, @clipboard_resolution].each do |point|
165
+ file.write_path_number point
166
+ end
167
+ file.seek(4, IO::SEEK_CUR)
168
+ end
169
+
170
+ def read_initial_fill
171
+ @initial_fill = @file.read_short
172
+ @file.seek(22, IO::SEEK_CUR)
173
+ end
174
+
175
+ def write_initial_fill(file)
176
+ file.write_short @initial_fill
177
+ file.seek(22, IO::SEEK_CUR)
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,27 @@
1
+ class PSD
2
+ # Definition for a single Resource record.
3
+ #
4
+ # Most of the resources are options/preferences set by the user
5
+ # or automatically by Photoshop.
6
+ class Resource < BinData::Record
7
+ endian :big
8
+
9
+ string :type, read_length: 4
10
+ uint16 :id
11
+ uint8 :name_len
12
+ stringz :name, read_length: :name_length
13
+ uint32 :res_size
14
+
15
+ skip length: :resource_size
16
+
17
+ #---
18
+ # Really weird padding business
19
+ def name_length
20
+ Util.pad2(name_len + 1) - 1
21
+ end
22
+
23
+ def resource_size
24
+ Util.pad2(res_size)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,47 @@
1
+ class PSD
2
+ # Parses and reads all of the Resource records in the document.
3
+ class Resources
4
+ include Section
5
+
6
+ attr_reader :resources
7
+ alias :data :resources
8
+
9
+ def initialize(file)
10
+ @file = file
11
+ @resources = []
12
+ @length = nil
13
+ end
14
+
15
+ # Parses each Resource and stores them.
16
+ def parse
17
+ start_section
18
+
19
+ n = length
20
+ start = @file.tell
21
+
22
+ while n > 0
23
+ pos = @file.tell
24
+ @resources << PSD::Resource.read(@file)
25
+ n -= @file.tell - pos
26
+ end
27
+
28
+ unless n == 0
29
+ @file.seek start + length
30
+ end
31
+
32
+ end_section
33
+ return @resources
34
+ end
35
+
36
+ def skip
37
+ @file.seek length, IO::SEEK_CUR
38
+ end
39
+
40
+ private
41
+
42
+ def length
43
+ return @length unless @length.nil?
44
+ @length = @file.read_int
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,26 @@
1
+ class PSD
2
+ # Helper that lets us track the beginning and ending locations
3
+ # of each section. This is for debug and error catching purposes,
4
+ # primarily.
5
+ module Section
6
+ attr_reader :section_start, :section_end
7
+
8
+ def start_section(section=:all)
9
+ @section_start = {} unless @section_start
10
+ @section_start[section] = @file.tell
11
+ end
12
+
13
+ def end_section(section=:all)
14
+ @section_end = {} unless @section_end
15
+ @section_end[section] = @file.tell
16
+ end
17
+
18
+ def start_of_section(section=:all)
19
+ @section_start[section]
20
+ end
21
+
22
+ def end_of_section(section=:all)
23
+ @section_end[section]
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,17 @@
1
+ class PSD
2
+ class Util
3
+ # Ensures value is a multiple of 2
4
+ def self.pad2(i)
5
+ ((i + 1) / 2) * 2
6
+ end
7
+
8
+ # Ensures value is a multiple of 4
9
+ def self.pad4(i)
10
+ i - (i.modulo(4)) + 3
11
+ end
12
+
13
+ def self.clamp(num, min, max)
14
+ [min, num, max].sort[1]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ class PSD
2
+ VERSION = "0.3.2"
3
+ end
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'psd/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "psd"
8
+ gem.version = PSD::VERSION
9
+ gem.authors = ["Ryan LeFevre", "Kelly Sutton"]
10
+ gem.email = ["ryan@layervault.com", "kelly@layervault.com"]
11
+ gem.description = %q{Parse Photoshop save files with ease}
12
+ gem.summary = %q{General purpose library for parsing Photoshop save files}
13
+ gem.homepage = "http://cosmos.layervault.com/psdrb.html"
14
+ gem.license = 'MIT'
15
+
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.require_paths = ["lib"]
20
+
21
+ gem.add_dependency "bindata"
22
+ gem.add_dependency "chunky_png"
23
+ gem.add_dependency 'psd-enginedata'
24
+
25
+ gem.test_files = Dir.glob("spec/**/*")
26
+ gem.add_development_dependency 'rspec'
27
+ gem.add_development_dependency 'guard'
28
+ gem.add_development_dependency 'guard-rspec'
29
+ gem.add_development_dependency 'rb-fsevent', '~> 0.9'
30
+ end
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,86 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Hierarchy" do
4
+ before(:each) do
5
+ @psd = PSD.new('spec/files/example.psd')
6
+ end
7
+
8
+ it "should parse tree" do
9
+ @psd.parse!
10
+
11
+ tree = @psd.tree.to_hash
12
+ tree.should include :children
13
+ tree[:children].length.should == 3
14
+ end
15
+
16
+ describe "Ancestry" do
17
+ before(:each) do
18
+ @psd.parse!
19
+ @tree = @psd.tree
20
+ end
21
+
22
+ it "should provide tree traversal methods" do
23
+ @tree.respond_to?(:root).should be_true
24
+ @tree.respond_to?(:siblings).should be_true
25
+ @tree.respond_to?(:descendants).should be_true
26
+ @tree.respond_to?(:subtree).should be_true
27
+ end
28
+
29
+ it "should properly identify the root node" do
30
+ @tree.root?.should be_true
31
+ @tree.root.should == @tree
32
+ @tree.children.last.root.should == @tree
33
+ end
34
+
35
+ it "should retrieve all descendants of a node" do
36
+ @tree.descendants.size.should == 12
37
+ @tree.descendant_layers.size.should == 9
38
+ @tree.descendant_groups.size.should == 3
39
+ @tree.descendants.first.should_not == @tree
40
+ end
41
+
42
+ it "should retreive the entire subtree of a node" do
43
+ @tree.subtree.size.should == 13
44
+ @tree.subtree_layers.size.should == 9
45
+ @tree.subtree_groups.size.should == 3
46
+ @tree.subtree.first.should == @tree
47
+ end
48
+
49
+ it "should properly identify the existence of children" do
50
+ @tree.has_children?.should be_true
51
+ @tree.is_childless?.should be_false
52
+ @tree.descendant_layers.first.has_children?.should be_false
53
+ @tree.descendant_layers.first.is_childless?.should be_true
54
+ end
55
+
56
+ it "should retrieve all siblings of a node" do
57
+ @tree.children.first.siblings.should == @tree.children
58
+ @tree.children.first.siblings.should include @tree.children.first
59
+ @tree.children.first.has_siblings?.should be_true
60
+ @tree.children.first.is_only_child?.should be_false
61
+ end
62
+
63
+ it "should properly calculate node depth" do
64
+ @tree.depth.should == 0
65
+ @tree.descendant_layers.last.depth.should == 2
66
+ @tree.children.first.depth.should == 1
67
+ end
68
+
69
+ describe "Searching" do
70
+ it "should find a node given a path" do
71
+ @tree.children_at_path('Version A/Matte').is_a?(Array).should be_true
72
+ @tree.children_at_path('Version A/Matte').size.should == 1
73
+ @tree.children_at_path('Version A/Matte').first.is_a?(PSD::Node::Layer).should be_true
74
+ end
75
+
76
+ it "should ignore leading slashes" do
77
+ @tree.children_at_path('/Version A/Matte').size.should == 1
78
+ end
79
+
80
+ it "should return an empty array when a node is not found" do
81
+ @tree.children_at_path('LOLWUTOMGBBQSAUCE').is_a?(Array).should be_true
82
+ @tree.children_at_path('LOLWUTOMGBBQSAUCE').size.should == 0
83
+ end
84
+ end
85
+ end
86
+ end