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