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