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,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