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