psd 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/Gemfile +2 -0
- data/Guardfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +161 -0
- data/Rakefile +1 -0
- data/circle.yml +6 -0
- data/examples/export.rb +7 -0
- data/examples/export_image.rb +12 -0
- data/examples/export_text_data.rb +13 -0
- data/examples/images/example-cmyk.psd +0 -0
- data/examples/images/example-greyscale.psd +0 -0
- data/examples/images/example.psd +0 -0
- data/examples/images/example16.psd +0 -0
- data/examples/parse.rb +31 -0
- data/examples/path.rb +7 -0
- data/examples/tree.rb +8 -0
- data/examples/unimplemented_info.rb +9 -0
- data/lib/psd.rb +145 -0
- data/lib/psd/blend_mode.rb +75 -0
- data/lib/psd/channel_image.rb +9 -0
- data/lib/psd/color.rb +125 -0
- data/lib/psd/descriptor.rb +172 -0
- data/lib/psd/file.rb +99 -0
- data/lib/psd/header.rb +61 -0
- data/lib/psd/helpers.rb +35 -0
- data/lib/psd/image.rb +107 -0
- data/lib/psd/image_exports/png.rb +36 -0
- data/lib/psd/image_formats/raw.rb +14 -0
- data/lib/psd/image_formats/rle.rb +67 -0
- data/lib/psd/image_modes/cmyk.rb +27 -0
- data/lib/psd/image_modes/greyscale.rb +36 -0
- data/lib/psd/image_modes/rgb.rb +25 -0
- data/lib/psd/layer.rb +342 -0
- data/lib/psd/layer_info.rb +22 -0
- data/lib/psd/layer_info/fill_opacity.rb +13 -0
- data/lib/psd/layer_info/layer_id.rb +13 -0
- data/lib/psd/layer_info/layer_name_source.rb +12 -0
- data/lib/psd/layer_info/layer_section_divider.rb +35 -0
- data/lib/psd/layer_info/legacy_typetool.rb +88 -0
- data/lib/psd/layer_info/object_effects.rb +16 -0
- data/lib/psd/layer_info/placed_layer.rb +14 -0
- data/lib/psd/layer_info/reference_point.rb +16 -0
- data/lib/psd/layer_info/typetool.rb +127 -0
- data/lib/psd/layer_info/unicode_name.rb +18 -0
- data/lib/psd/layer_info/vector_mask.rb +25 -0
- data/lib/psd/layer_mask.rb +106 -0
- data/lib/psd/mask.rb +45 -0
- data/lib/psd/node.rb +51 -0
- data/lib/psd/node_exporting.rb +20 -0
- data/lib/psd/node_group.rb +67 -0
- data/lib/psd/node_layer.rb +71 -0
- data/lib/psd/node_root.rb +78 -0
- data/lib/psd/nodes/ancestry.rb +82 -0
- data/lib/psd/nodes/has_children.rb +13 -0
- data/lib/psd/nodes/lock_to_origin.rb +7 -0
- data/lib/psd/nodes/parse_layers.rb +18 -0
- data/lib/psd/nodes/search.rb +28 -0
- data/lib/psd/pascal_string.rb +14 -0
- data/lib/psd/path_record.rb +180 -0
- data/lib/psd/resource.rb +27 -0
- data/lib/psd/resources.rb +47 -0
- data/lib/psd/section.rb +26 -0
- data/lib/psd/util.rb +17 -0
- data/lib/psd/version.rb +3 -0
- data/psd.gemspec +30 -0
- data/spec/files/example.psd +0 -0
- data/spec/files/one_layer.psd +0 -0
- data/spec/files/path.psd +0 -0
- data/spec/files/simplest.psd +0 -0
- data/spec/files/text.psd +0 -0
- data/spec/hierarchy_spec.rb +86 -0
- data/spec/identity_spec.rb +34 -0
- data/spec/parsing_spec.rb +134 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/text_spec.rb +12 -0
- 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
|
data/lib/psd/resource.rb
ADDED
@@ -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
|
data/lib/psd/section.rb
ADDED
@@ -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
|
data/lib/psd/util.rb
ADDED
@@ -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
|
data/lib/psd/version.rb
ADDED
data/psd.gemspec
ADDED
@@ -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
|
data/spec/files/path.psd
ADDED
Binary file
|
Binary file
|
data/spec/files/text.psd
ADDED
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
|