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
data/lib/psd/header.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
class PSD
|
2
|
+
# Describes the Header for the PSD file, which is the first section of the file.
|
3
|
+
class Header < BinData::Record
|
4
|
+
endian :big
|
5
|
+
|
6
|
+
string :sig, read_length: 4
|
7
|
+
uint16 :version
|
8
|
+
|
9
|
+
# Reserved bytes
|
10
|
+
skip length: 6
|
11
|
+
|
12
|
+
uint16 :channels
|
13
|
+
uint32 :rows
|
14
|
+
uint32 :cols
|
15
|
+
uint16 :depth
|
16
|
+
uint16 :mode
|
17
|
+
|
18
|
+
uint32 :color_data_len
|
19
|
+
skip length: :color_data_len
|
20
|
+
|
21
|
+
# All of the color modes are stored internally as a short from 0-15.
|
22
|
+
# This is a mapping of that value to a human-readable name.
|
23
|
+
MODES = [
|
24
|
+
'Bitmap',
|
25
|
+
'GrayScale',
|
26
|
+
'IndexedColor',
|
27
|
+
'RGBColor',
|
28
|
+
'CMYKColor',
|
29
|
+
'HSLColor',
|
30
|
+
'HSBColor',
|
31
|
+
'Multichannel',
|
32
|
+
'Duotone',
|
33
|
+
'LabColor',
|
34
|
+
'Gray16',
|
35
|
+
'RGB48',
|
36
|
+
'Lab48',
|
37
|
+
'CMYK64',
|
38
|
+
'DeepMultichannel',
|
39
|
+
'Duotone16'
|
40
|
+
]
|
41
|
+
|
42
|
+
# Get the human-readable color mode name.
|
43
|
+
def mode_name
|
44
|
+
if mode >= 0 && mode <= 15
|
45
|
+
MODES[mode]
|
46
|
+
else
|
47
|
+
"(#{mode})"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Width of the entire document in pixels.
|
52
|
+
def width
|
53
|
+
cols
|
54
|
+
end
|
55
|
+
|
56
|
+
# Height of the entire document in pixels.
|
57
|
+
def height
|
58
|
+
rows
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/psd/helpers.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
class PSD
|
2
|
+
# Various helper methods that make accessing PSD data easier since it's
|
3
|
+
# split up among various sections.
|
4
|
+
module Helpers
|
5
|
+
# Width of the entire PSD document, in pixels.
|
6
|
+
def width
|
7
|
+
header.cols
|
8
|
+
end
|
9
|
+
|
10
|
+
# Height of the entire PSD document, in pixels.
|
11
|
+
def height
|
12
|
+
header.rows
|
13
|
+
end
|
14
|
+
|
15
|
+
# All of the layers in this document, including section divider layers.
|
16
|
+
def layers
|
17
|
+
layer_mask.layers
|
18
|
+
end
|
19
|
+
|
20
|
+
# All of the layers, but filters out the section dividers.
|
21
|
+
def actual_layers
|
22
|
+
layers.delete_if { |l| l.folder? || l.folder_end? }
|
23
|
+
end
|
24
|
+
|
25
|
+
# All of the folders in the document.
|
26
|
+
def folders
|
27
|
+
layers.select { |l| l.folder? }
|
28
|
+
end
|
29
|
+
|
30
|
+
# Constructs a tree of the current document for easy traversal and data access.
|
31
|
+
def tree
|
32
|
+
@root ||= PSD::Node::Root.new(self)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/psd/image.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
class PSD
|
2
|
+
# Parses the full preview image at the end of the PSD document.
|
3
|
+
class Image
|
4
|
+
include Format::RAW
|
5
|
+
include Format::RLE
|
6
|
+
include Mode::CMYK
|
7
|
+
include Mode::Greyscale
|
8
|
+
include Mode::RGB
|
9
|
+
include Export::PNG
|
10
|
+
|
11
|
+
# All of the possible compression formats Photoshop uses.
|
12
|
+
COMPRESSIONS = [
|
13
|
+
'Raw',
|
14
|
+
'RLE',
|
15
|
+
'ZIP',
|
16
|
+
'ZIPPrediction'
|
17
|
+
]
|
18
|
+
|
19
|
+
# Each color channel is represented by a unique ID
|
20
|
+
CHANNEL_INFO = [
|
21
|
+
{id: 0},
|
22
|
+
{id: 1},
|
23
|
+
{id: 2},
|
24
|
+
{id: -1}
|
25
|
+
]
|
26
|
+
|
27
|
+
# Store a reference to the file and the header. We also do a few simple calculations
|
28
|
+
# to figure out the number of pixels in the image and the length of each channel.
|
29
|
+
def initialize(file, header)
|
30
|
+
@file = file
|
31
|
+
@header = header
|
32
|
+
|
33
|
+
@num_pixels = width * height
|
34
|
+
@num_pixels *= 2 if depth == 16
|
35
|
+
|
36
|
+
calculate_length
|
37
|
+
@channel_data = {} # Using a Hash over an NArray, because NArray has problems w/ Ruby 2.0. Hashes are faster than Arrays
|
38
|
+
|
39
|
+
@start_pos = @file.tell
|
40
|
+
@end_pos = @start_pos + @length
|
41
|
+
|
42
|
+
@pixel_data = []
|
43
|
+
end
|
44
|
+
|
45
|
+
# Begins parsing the image by first figuring out the compression format used, and then
|
46
|
+
# by reading the image data.
|
47
|
+
def parse
|
48
|
+
@compression = parse_compression!
|
49
|
+
|
50
|
+
# ZIP not implemented
|
51
|
+
if [2, 3].include?(@compression)
|
52
|
+
@file.seek @end_pos and return
|
53
|
+
end
|
54
|
+
|
55
|
+
parse_image_data!
|
56
|
+
|
57
|
+
return self
|
58
|
+
end
|
59
|
+
|
60
|
+
# We delegate a few useful methods to the header.
|
61
|
+
[:height, :width, :channels, :depth, :mode].each do |attribute|
|
62
|
+
define_method attribute do
|
63
|
+
@header.send(attribute)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def calculate_length
|
70
|
+
@length = case depth
|
71
|
+
when 1 then (width + 7) / 8 * height
|
72
|
+
when 16 then width * height * 2
|
73
|
+
else width * height
|
74
|
+
end
|
75
|
+
|
76
|
+
@channel_length = @length
|
77
|
+
@length *= channels
|
78
|
+
end
|
79
|
+
|
80
|
+
def parse_compression!
|
81
|
+
@file.read_short
|
82
|
+
end
|
83
|
+
|
84
|
+
def parse_image_data!
|
85
|
+
case @compression
|
86
|
+
when 0 then parse_raw!
|
87
|
+
when 1 then parse_rle!
|
88
|
+
when 2, 3 then parse_zip!
|
89
|
+
else @file.seek(@end_pos)
|
90
|
+
end
|
91
|
+
|
92
|
+
process_image_data
|
93
|
+
end
|
94
|
+
|
95
|
+
def process_image_data
|
96
|
+
case mode
|
97
|
+
when 1 then combine_greyscale_channel
|
98
|
+
when 3 then combine_rgb_channel
|
99
|
+
when 4 then combine_cmyk_channel
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def pixel_step
|
104
|
+
depth == 8 ? 1 : 2
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'chunky_png'
|
2
|
+
|
3
|
+
class PSD::Image
|
4
|
+
module Export
|
5
|
+
# PNG image export. This is the default export format.
|
6
|
+
module PNG
|
7
|
+
# Load the image pixels into a PNG file and return a reference to the
|
8
|
+
# data.
|
9
|
+
def to_png
|
10
|
+
png = ChunkyPNG::Image.new(width, height, ChunkyPNG::Color::TRANSPARENT)
|
11
|
+
|
12
|
+
i = 0
|
13
|
+
height.times do |y|
|
14
|
+
width.times do |x|
|
15
|
+
png[x,y] = ChunkyPNG::Color.rgba(
|
16
|
+
@pixel_data[i],
|
17
|
+
@pixel_data[i+1],
|
18
|
+
@pixel_data[i+2],
|
19
|
+
@pixel_data[i+3]
|
20
|
+
)
|
21
|
+
|
22
|
+
i += 4
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
png
|
27
|
+
end
|
28
|
+
alias :export :to_png
|
29
|
+
|
30
|
+
# Saves the PNG data to disk.
|
31
|
+
def save_as_png(file)
|
32
|
+
to_png.save(file)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
class PSD::Image
|
2
|
+
module Format
|
3
|
+
# Parses an RLE compressed image
|
4
|
+
module RLE
|
5
|
+
private
|
6
|
+
|
7
|
+
def parse_rle!
|
8
|
+
@byte_counts = parse_byte_counts!
|
9
|
+
parse_channel_data!
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse_byte_counts!
|
13
|
+
byte_counts = []
|
14
|
+
channels.times do |i|
|
15
|
+
height.times do |j|
|
16
|
+
byte_counts << @file.read_short
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
return byte_counts
|
21
|
+
end
|
22
|
+
|
23
|
+
def parse_channel_data!
|
24
|
+
chan_pos = 0
|
25
|
+
line_index = 0
|
26
|
+
|
27
|
+
channels.times do |i|
|
28
|
+
chan_pos = decode_rle_channel(chan_pos, line_index)
|
29
|
+
line_index += height
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def decode_rle_channel(chan_pos, line_index)
|
34
|
+
height.times do |j|
|
35
|
+
byte_count = @byte_counts[line_index]
|
36
|
+
line_index += 1
|
37
|
+
start = @file.tell
|
38
|
+
|
39
|
+
while @file.tell < start + byte_count
|
40
|
+
len = @file.read(1).bytes.to_a[0]
|
41
|
+
|
42
|
+
if len < 128
|
43
|
+
len += 1
|
44
|
+
(chan_pos...chan_pos+len).each do |k|
|
45
|
+
@channel_data[k] = @file.read(1).bytes.to_a[0]
|
46
|
+
end
|
47
|
+
|
48
|
+
chan_pos += len
|
49
|
+
elsif len > 128
|
50
|
+
len ^= 0xff
|
51
|
+
len += 2
|
52
|
+
|
53
|
+
val = @file.read(1).bytes.to_a[0]
|
54
|
+
(chan_pos...chan_pos+len).each do |k|
|
55
|
+
@channel_data[k] = val
|
56
|
+
end
|
57
|
+
|
58
|
+
chan_pos += len
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
return chan_pos
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class PSD::Image::Mode
|
2
|
+
module CMYK
|
3
|
+
private
|
4
|
+
|
5
|
+
def combine_cmyk_channel
|
6
|
+
(0...@num_pixels).step(pixel_step) do |i|
|
7
|
+
if channels == 5
|
8
|
+
a = @channel_data[i]
|
9
|
+
c = @channel_data[i + @channel_length]
|
10
|
+
m = @channel_data[i + @channel_length * 2]
|
11
|
+
y = @channel_data[i + @channel_length * 3]
|
12
|
+
k = @channel_data[i + @channel_length * 4]
|
13
|
+
else
|
14
|
+
a = 255
|
15
|
+
c = @channel_data[i]
|
16
|
+
m = @channel_data[i + @channel_length]
|
17
|
+
y = @channel_data[i + @channel_length * 2]
|
18
|
+
k = @channel_data[i + @channel_length * 3]
|
19
|
+
end
|
20
|
+
|
21
|
+
rgb = PSD::Color.cmyk_to_rgb(255 - c, 255 - m, 255 - y, 255 - k)
|
22
|
+
|
23
|
+
@pixel_data.push *rgb.values, a
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class PSD::Image::Mode
|
2
|
+
module Greyscale
|
3
|
+
private
|
4
|
+
|
5
|
+
def combine_greyscale8_channel
|
6
|
+
if channels == 2
|
7
|
+
# We have an alpha channel
|
8
|
+
@num_pixels.times do |i|
|
9
|
+
alpha = @channel_data[i]
|
10
|
+
grey = @channel_data[@channel_length + i]
|
11
|
+
|
12
|
+
@pixel_data.push grey, grey, grey, alpha
|
13
|
+
end
|
14
|
+
else
|
15
|
+
@num_pixels.times do |i|
|
16
|
+
@pixel_data.push *([@channel_data[i]] * 3), 255
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def combine_greyscale_channel
|
22
|
+
if channels == 2
|
23
|
+
(0...@num_pixels).step(pixel_step) do |i|
|
24
|
+
alpha = @channel_data[i]
|
25
|
+
grey = @channel_data[@channel_length + i]
|
26
|
+
|
27
|
+
@pixel_data.push grey, grey, grey, alpha
|
28
|
+
end
|
29
|
+
else
|
30
|
+
(0...@num_pixels).step(pixel_step) do |i|
|
31
|
+
@pixel_data.push *([@channel_data[i]] * 3), 255
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class PSD::Image::Mode
|
2
|
+
# Combines the channel data from the image into RGB pixel values
|
3
|
+
module RGB
|
4
|
+
private
|
5
|
+
|
6
|
+
def combine_rgb_channel
|
7
|
+
(0...@num_pixels).step(pixel_step) do |i|
|
8
|
+
pixel = {r: 0, g: 0, b: 0, a: 255}
|
9
|
+
|
10
|
+
PSD::Image::CHANNEL_INFO[0...channels].each_with_index do |chan, index|
|
11
|
+
val = @channel_data[i + (@channel_length * index)]
|
12
|
+
|
13
|
+
case chan[:id]
|
14
|
+
when -1 then pixel[:a] = val
|
15
|
+
when 0 then pixel[:r] = val
|
16
|
+
when 1 then pixel[:g] = val
|
17
|
+
when 2 then pixel[:b] = val
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
@pixel_data.push *pixel.values
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/psd/layer.rb
ADDED
@@ -0,0 +1,342 @@
|
|
1
|
+
class PSD
|
2
|
+
# Represents a single layer and all of the data associated with
|
3
|
+
# that layer.
|
4
|
+
class Layer
|
5
|
+
include Section
|
6
|
+
|
7
|
+
attr_reader :id, :mask, :blending_ranges, :adjustments, :channels_info
|
8
|
+
attr_reader :blend_mode, :layer_type, :blending_mode, :opacity, :fill_opacity
|
9
|
+
attr_reader :channels, :image
|
10
|
+
|
11
|
+
attr_accessor :group_layer
|
12
|
+
attr_accessor :top, :left, :bottom, :right, :rows, :cols, :ref_x, :ref_y, :node, :file
|
13
|
+
|
14
|
+
alias :info :adjustments
|
15
|
+
alias :width :cols
|
16
|
+
alias :height :rows
|
17
|
+
|
18
|
+
# All of the extra layer info sections that we know how to parse.
|
19
|
+
LAYER_INFO = {
|
20
|
+
type: TypeTool,
|
21
|
+
legacy_type: LegacyTypeTool,
|
22
|
+
layer_name_source: LayerNameSource,
|
23
|
+
object_effects: ObjectEffects,
|
24
|
+
name: UnicodeName,
|
25
|
+
section_divider: LayerSectionDivider,
|
26
|
+
reference_point: ReferencePoint,
|
27
|
+
layer_id: LayerID,
|
28
|
+
fill_opacity: FillOpacity,
|
29
|
+
placed_layer: PlacedLayer,
|
30
|
+
vector_mask: VectorMask
|
31
|
+
}
|
32
|
+
|
33
|
+
# Initializes all of the defaults for the layer.
|
34
|
+
def initialize(file)
|
35
|
+
@file = file
|
36
|
+
@image = nil
|
37
|
+
@mask = {}
|
38
|
+
@blending_ranges = {}
|
39
|
+
@adjustments = {}
|
40
|
+
@channels_info = []
|
41
|
+
@blend_mode = {}
|
42
|
+
@group_layer = nil
|
43
|
+
|
44
|
+
@layer_type = 'normal'
|
45
|
+
@blending_mode = 'normal'
|
46
|
+
@opacity = 255
|
47
|
+
@fill_opacity = 255
|
48
|
+
|
49
|
+
# Just used for tracking which layer adjustments we're parsing.
|
50
|
+
# Not essential.
|
51
|
+
@info_keys = []
|
52
|
+
end
|
53
|
+
|
54
|
+
# Parse the layer and all of it's sub-sections.
|
55
|
+
def parse(index=nil)
|
56
|
+
start_section
|
57
|
+
|
58
|
+
@idx = index
|
59
|
+
|
60
|
+
parse_info
|
61
|
+
parse_blend_modes
|
62
|
+
|
63
|
+
extra_len = @file.read_int
|
64
|
+
@layer_end = @file.tell + extra_len
|
65
|
+
|
66
|
+
parse_mask_data
|
67
|
+
parse_blending_ranges
|
68
|
+
parse_legacy_layer_name
|
69
|
+
parse_extra_data
|
70
|
+
|
71
|
+
@file.seek @layer_end # Skip over any filler zeros
|
72
|
+
|
73
|
+
end_section
|
74
|
+
return self
|
75
|
+
end
|
76
|
+
|
77
|
+
# Export the layer to file. May or may not work.
|
78
|
+
def export(outfile)
|
79
|
+
export_info(outfile)
|
80
|
+
|
81
|
+
@blend_mode.write(outfile)
|
82
|
+
@file.seek(@blend_mode.num_bytes, IO::SEEK_CUR)
|
83
|
+
|
84
|
+
export_mask_data(outfile)
|
85
|
+
export_blending_ranges(outfile)
|
86
|
+
export_legacy_layer_name(outfile)
|
87
|
+
export_extra_data(outfile)
|
88
|
+
|
89
|
+
outfile.write @file.read(end_of_section - @file.tell)
|
90
|
+
end
|
91
|
+
|
92
|
+
# We just delegate this to a normal method call.
|
93
|
+
def [](val)
|
94
|
+
self.send(val)
|
95
|
+
end
|
96
|
+
|
97
|
+
def parse_channel_image!(header) #:nodoc:
|
98
|
+
# @image = ChannelImage.new(@file, header, self)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Does this layer represent the start of a folder section?
|
102
|
+
def folder?
|
103
|
+
return false unless @adjustments.has_key?(:section_divider)
|
104
|
+
@adjustments[:section_divider].is_folder
|
105
|
+
end
|
106
|
+
|
107
|
+
# Does this layer represent the end of a folder section?
|
108
|
+
def folder_end?
|
109
|
+
return false unless @adjustments.has_key?(:section_divider)
|
110
|
+
@adjustments[:section_divider].is_hidden
|
111
|
+
end
|
112
|
+
|
113
|
+
# Is this layer visible?
|
114
|
+
def visible?
|
115
|
+
@visible
|
116
|
+
end
|
117
|
+
|
118
|
+
# Is this layer hidden?
|
119
|
+
def hidden?
|
120
|
+
!@visible
|
121
|
+
end
|
122
|
+
|
123
|
+
# Attempt to translate this layer and modify the document.
|
124
|
+
def translate(x=0, y=0)
|
125
|
+
@left += x
|
126
|
+
@right += x
|
127
|
+
@top += y
|
128
|
+
@bottom += y
|
129
|
+
|
130
|
+
@path_components.each{ |p| p.translate(x,y) } if @path_components
|
131
|
+
end
|
132
|
+
|
133
|
+
# Attempt to scale the path components within this layer.
|
134
|
+
def scale_path_components(xr, yr)
|
135
|
+
return unless @path_components
|
136
|
+
|
137
|
+
@path_components.each{ |p| p.scale(xr, yr) }
|
138
|
+
end
|
139
|
+
|
140
|
+
# Helper that exports the text data in this layer, if any.
|
141
|
+
def text
|
142
|
+
return nil unless @adjustments[:type]
|
143
|
+
@adjustments[:type].to_hash
|
144
|
+
end
|
145
|
+
|
146
|
+
# Gets the name of this layer. If the PSD file is from an even remotely
|
147
|
+
# recent version of Photoshop, this data is stored as extra layer info and
|
148
|
+
# as a UTF-16 name. Otherwise, it's stored in a legacy block.
|
149
|
+
def name
|
150
|
+
if @adjustments.has_key?(:name)
|
151
|
+
return @adjustments[:name].data
|
152
|
+
end
|
153
|
+
|
154
|
+
return @legacy_name
|
155
|
+
end
|
156
|
+
|
157
|
+
# We delegate all missing method calls to the extra layer info to make it easier
|
158
|
+
# to access that data.
|
159
|
+
def method_missing(method, *args, &block)
|
160
|
+
return @adjustments[method] if @adjustments.has_key?(method)
|
161
|
+
super
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
def parse_info
|
167
|
+
start_section(:info)
|
168
|
+
|
169
|
+
@top = @file.read_int
|
170
|
+
@left = @file.read_int
|
171
|
+
@bottom = @file.read_int
|
172
|
+
@right = @file.read_int
|
173
|
+
@channels = @file.read_short
|
174
|
+
|
175
|
+
@rows = @bottom - @top
|
176
|
+
@cols = @right - @left
|
177
|
+
|
178
|
+
@channels.times do
|
179
|
+
channel_id = @file.read_short
|
180
|
+
channel_length = @file.read_int
|
181
|
+
|
182
|
+
@channels_info << {id: channel_id, length: channel_length}
|
183
|
+
end
|
184
|
+
|
185
|
+
end_section(:info)
|
186
|
+
end
|
187
|
+
|
188
|
+
def export_info(outfile)
|
189
|
+
[@top, @left, @bottom, @right].each { |val| outfile.write_int(val) }
|
190
|
+
outfile.write_short(@channels)
|
191
|
+
|
192
|
+
@channels_info.each do |channel_info|
|
193
|
+
outfile.write_short channel_info[:id]
|
194
|
+
outfile.write_int channel_info[:length]
|
195
|
+
end
|
196
|
+
|
197
|
+
@file.seek end_of_section(:info)
|
198
|
+
end
|
199
|
+
|
200
|
+
def export_mask_data(outfile)
|
201
|
+
outfile.write @file.read(@mask_end - @mask_begin + 4)
|
202
|
+
end
|
203
|
+
|
204
|
+
def export_blending_ranges(outfile)
|
205
|
+
length = 4 * 2 # greys
|
206
|
+
length += @blending_ranges[:num_channels] * 8
|
207
|
+
outfile.write_int length
|
208
|
+
|
209
|
+
outfile.write_short @blending_ranges[:grey][:source][:black]
|
210
|
+
outfile.write_short @blending_ranges[:grey][:source][:white]
|
211
|
+
outfile.write_short @blending_ranges[:grey][:dest][:black]
|
212
|
+
outfile.write_short @blending_ranges[:grey][:dest][:white]
|
213
|
+
|
214
|
+
@blending_ranges[:num_channels].times do |i|
|
215
|
+
outfile.write_short @blending_ranges[:channels][i][:source][:black]
|
216
|
+
outfile.write_short @blending_ranges[:channels][i][:source][:white]
|
217
|
+
outfile.write_short @blending_ranges[:channels][i][:dest][:black]
|
218
|
+
outfile.write_short @blending_ranges[:channels][i][:dest][:white]
|
219
|
+
end
|
220
|
+
|
221
|
+
@file.seek length + 4, IO::SEEK_CUR
|
222
|
+
end
|
223
|
+
|
224
|
+
def export_legacy_layer_name(outfile)
|
225
|
+
outfile.write @file.read(@legacy_name_end - @legacy_name_start)
|
226
|
+
end
|
227
|
+
|
228
|
+
def export_extra_data(outfile)
|
229
|
+
outfile.write @file.read(@extra_data_end - @extra_data_begin)
|
230
|
+
if @path_components && !@path_components.empty?
|
231
|
+
outfile.seek @vector_mask_begin
|
232
|
+
@file.seek @vector_mask_begin
|
233
|
+
|
234
|
+
write_vector_mask(outfile)
|
235
|
+
@file.seek outfile.tell
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def parse_blend_modes
|
240
|
+
@blend_mode = BlendMode.read(@file)
|
241
|
+
|
242
|
+
@blending_mode = @blend_mode.mode
|
243
|
+
@opacity = @blend_mode.opacity
|
244
|
+
@visible = @blend_mode.visible
|
245
|
+
end
|
246
|
+
|
247
|
+
def parse_mask_data
|
248
|
+
@mask_begin = @file.tell
|
249
|
+
@mask = Mask.read(@file)
|
250
|
+
@mask_end = @file.tell
|
251
|
+
end
|
252
|
+
|
253
|
+
def parse_blending_ranges
|
254
|
+
length = @file.read_int
|
255
|
+
|
256
|
+
@blending_ranges[:grey] = {
|
257
|
+
source: {
|
258
|
+
black: @file.read_short,
|
259
|
+
white: @file.read_short
|
260
|
+
},
|
261
|
+
dest: {
|
262
|
+
black: @file.read_short,
|
263
|
+
white: @file.read_short
|
264
|
+
}
|
265
|
+
}
|
266
|
+
|
267
|
+
@blending_ranges[:num_channels] = (length - 8) / 8
|
268
|
+
|
269
|
+
@blending_ranges[:channels] = []
|
270
|
+
@blending_ranges[:num_channels].times do
|
271
|
+
@blending_ranges[:channels] << {
|
272
|
+
source: {
|
273
|
+
black: @file.read_short,
|
274
|
+
white: @file.read_short
|
275
|
+
},
|
276
|
+
dest: {
|
277
|
+
black: @file.read_short,
|
278
|
+
white: @file.read_short
|
279
|
+
}
|
280
|
+
}
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# The old school layer names are encoded in MacRoman format,
|
285
|
+
# not UTF-8. Luckily Ruby kicks ass at character conversion.
|
286
|
+
def parse_legacy_layer_name
|
287
|
+
@legacy_name_start = @file.tell
|
288
|
+
len = Util.pad4 @file.read(1).unpack('C')[0]
|
289
|
+
@legacy_name = @file.read(len).encode('UTF-8', 'MacRoman').delete("\000")
|
290
|
+
@legacy_name_end = @file.tell
|
291
|
+
end
|
292
|
+
|
293
|
+
# This section is a bit tricky to parse because it represents all of the
|
294
|
+
# extra data that describes this layer.
|
295
|
+
def parse_extra_data
|
296
|
+
@extra_data_begin = @file.tell
|
297
|
+
|
298
|
+
while @file.tell < @layer_end
|
299
|
+
# Signature, don't need
|
300
|
+
@file.seek 4, IO::SEEK_CUR
|
301
|
+
|
302
|
+
# Key, very important
|
303
|
+
key = @file.read(4).unpack('A4')[0]
|
304
|
+
@info_keys << key
|
305
|
+
|
306
|
+
length = Util.pad2 @file.read_int
|
307
|
+
pos = @file.tell
|
308
|
+
|
309
|
+
info_parsed = false
|
310
|
+
LAYER_INFO.each do |name, info|
|
311
|
+
next unless info.key == key
|
312
|
+
|
313
|
+
i = info.new(@file, length)
|
314
|
+
i.parse
|
315
|
+
|
316
|
+
@adjustments[name] = i
|
317
|
+
info_parsed = true
|
318
|
+
break
|
319
|
+
end
|
320
|
+
|
321
|
+
if !info_parsed
|
322
|
+
PSD.keys << key
|
323
|
+
# puts "SKIPPING #{key}, length = #{length}"
|
324
|
+
@file.seek length, IO::SEEK_CUR
|
325
|
+
end
|
326
|
+
|
327
|
+
@file.seek pos + length if @file.tell != (pos + length)
|
328
|
+
end
|
329
|
+
|
330
|
+
# puts "Layer = #{name}, Parsed = #{@info_keys - PSD.keys.uniq}, Unparsed = #{PSD.keys.uniq - @info_keys}"
|
331
|
+
@extra_data_end = @file.tell
|
332
|
+
end
|
333
|
+
|
334
|
+
def write_vector_mask(outfile)
|
335
|
+
outfile.write @file.read(8)
|
336
|
+
# outfile.write_int 3
|
337
|
+
# outfile.write_int @vector_tag
|
338
|
+
|
339
|
+
@path_components.each{ |pc| pc.write(outfile); @file.seek(26, IO::SEEK_CUR) }
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|