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