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/mask.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
class PSD
|
2
|
+
# Represents the mask for a single layer
|
3
|
+
class Mask < BinData::Record
|
4
|
+
endian :big
|
5
|
+
|
6
|
+
int32 :mask_size
|
7
|
+
|
8
|
+
int32 :top, onlyif: :has_data?
|
9
|
+
int32 :left, onlyif: :has_data?
|
10
|
+
int32 :bottom, onlyif: :has_data?
|
11
|
+
int32 :right, onlyif: :has_data?
|
12
|
+
int8 :default_color, onlyif: :has_data?
|
13
|
+
bit8 :flags, onlyif: :has_data?
|
14
|
+
|
15
|
+
skip length: 2, onlyif: lambda { mask_size == 20 }
|
16
|
+
skip length: 18, onlyif: lambda { mask_size > 0 && mask_size != 20}
|
17
|
+
|
18
|
+
# Is there a mask defined?
|
19
|
+
def has_data?
|
20
|
+
mask_size > 0
|
21
|
+
end
|
22
|
+
|
23
|
+
# Width of the mask
|
24
|
+
def width
|
25
|
+
right - left
|
26
|
+
end
|
27
|
+
|
28
|
+
# Height of the mask
|
29
|
+
def height
|
30
|
+
bottom - top
|
31
|
+
end
|
32
|
+
|
33
|
+
def relative
|
34
|
+
flags & 0x01
|
35
|
+
end
|
36
|
+
|
37
|
+
def disabled
|
38
|
+
(flags & (0x01 << 1)) > 0
|
39
|
+
end
|
40
|
+
|
41
|
+
def invert
|
42
|
+
(flags & (0x01 << 2)) > 0
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/psd/node.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require_relative 'nodes/ancestry'
|
2
|
+
require_relative 'nodes/search'
|
3
|
+
|
4
|
+
# Internal structure to help us build trees of a Photoshop documents.
|
5
|
+
# A lot of method names borrowed from the Ruby ancestry gem.
|
6
|
+
class PSD
|
7
|
+
class Node
|
8
|
+
include Ancestry
|
9
|
+
include Search
|
10
|
+
|
11
|
+
# Default properties that all nodes contain
|
12
|
+
PROPERTIES = [:name, :left, :right, :top, :bottom, :height, :width]
|
13
|
+
|
14
|
+
attr_accessor :parent, :children
|
15
|
+
|
16
|
+
def initialize(layers=[])
|
17
|
+
@children = []
|
18
|
+
layers.each do |layer|
|
19
|
+
layer.parent = self
|
20
|
+
@children << layer
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def hidden?
|
25
|
+
!@layer.visible?
|
26
|
+
end
|
27
|
+
|
28
|
+
def visible?
|
29
|
+
@layer.visible?
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_hash
|
33
|
+
hash = {
|
34
|
+
type: nil,
|
35
|
+
visible: visible?,
|
36
|
+
opacity: @layer.opacity / 255.0,
|
37
|
+
blending_mode: @layer.blending_mode
|
38
|
+
}
|
39
|
+
|
40
|
+
PROPERTIES.each do |p|
|
41
|
+
hash[p] = self.send(p)
|
42
|
+
end
|
43
|
+
|
44
|
+
hash
|
45
|
+
end
|
46
|
+
|
47
|
+
def document_dimensions
|
48
|
+
@parent.document_dimensions
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class PSD
|
2
|
+
module NodeExporting #:nodoc:
|
3
|
+
def export_node(node, path)
|
4
|
+
hide_all_nodes
|
5
|
+
node.show!
|
6
|
+
node.lock_to_origin
|
7
|
+
|
8
|
+
width_difference_factor = @header.cols.to_f / node.width
|
9
|
+
height_difference_factor = @header.rows.to_f / node.height
|
10
|
+
@header.cols, @header.rows = node.width, node.height
|
11
|
+
|
12
|
+
node.scale_path_components(width_difference_factor, height_difference_factor)
|
13
|
+
export path
|
14
|
+
end
|
15
|
+
|
16
|
+
def hide_all_nodes
|
17
|
+
tree.descendants.map(&:hide!)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require_relative 'node'
|
2
|
+
|
3
|
+
class PSD::Node
|
4
|
+
# Represents a group, or folder, in the PSD document. It can have
|
5
|
+
# zero or more children nodes.
|
6
|
+
class Group < PSD::Node
|
7
|
+
include PSD::HasChildren
|
8
|
+
include PSD::Node::ParseLayers
|
9
|
+
include PSD::Node::LockToOrigin
|
10
|
+
|
11
|
+
attr_reader :name, :top, :left, :bottom, :right
|
12
|
+
|
13
|
+
# Parses the descendant tree structure and figures out the bounds
|
14
|
+
# of the layers within this folder.
|
15
|
+
def initialize(folder)
|
16
|
+
@name = folder[:name]
|
17
|
+
@layer = folder[:layer]
|
18
|
+
parse_layers(folder[:layers])
|
19
|
+
get_dimensions
|
20
|
+
end
|
21
|
+
|
22
|
+
# Calculated height of this folder.
|
23
|
+
def rows
|
24
|
+
@right - @left
|
25
|
+
end
|
26
|
+
alias :height :rows
|
27
|
+
|
28
|
+
# Calculated width of this folder.
|
29
|
+
def cols
|
30
|
+
@bottom - @top
|
31
|
+
end
|
32
|
+
alias :width :cols
|
33
|
+
|
34
|
+
# Attempt to translate this folder and all of the descendants.
|
35
|
+
def translate(x=0, y=0)
|
36
|
+
@children.each{ |c| c.translate(x,y) }
|
37
|
+
end
|
38
|
+
|
39
|
+
# Attempt to hide all children of this layer.
|
40
|
+
def hide!
|
41
|
+
@children.each{ |c| c.hide! }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Attempt to show all children of this layer.
|
45
|
+
def show!
|
46
|
+
@children.each{ |c| c.show! }
|
47
|
+
end
|
48
|
+
|
49
|
+
# Export this layer and it's children to a hash recursively.
|
50
|
+
def to_hash
|
51
|
+
super.merge({
|
52
|
+
type: :group,
|
53
|
+
visible: visible?,
|
54
|
+
children: children.map(&:to_hash)
|
55
|
+
})
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def get_dimensions
|
61
|
+
@left = @children.map(&:left).min || 0
|
62
|
+
@top = @children.map(&:top).min || 0
|
63
|
+
@bottom = @children.map(&:bottom).max || 0
|
64
|
+
@right = @children.map(&:right).max || 0
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require_relative 'node'
|
2
|
+
|
3
|
+
class PSD::Node
|
4
|
+
class Layer < PSD::Node
|
5
|
+
include PSD::Node::LockToOrigin
|
6
|
+
|
7
|
+
attr_reader :layer
|
8
|
+
|
9
|
+
# Stores a reference to the PSD::Layer
|
10
|
+
def initialize(layer)
|
11
|
+
super([])
|
12
|
+
|
13
|
+
@layer = layer
|
14
|
+
layer.node = self
|
15
|
+
end
|
16
|
+
|
17
|
+
# Delegates some methods to the PSD::Layer
|
18
|
+
(PROPERTIES + [:text, :ref_x, :ref_y]).each do |meth|
|
19
|
+
define_method meth do
|
20
|
+
@layer.send(meth)
|
21
|
+
end
|
22
|
+
|
23
|
+
define_method "#{meth}=" do |val|
|
24
|
+
@layer.send("#{meth}=", val)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# If the method is missing, we blindly send it to the layer.
|
29
|
+
# The layer handles the case in which the method doesn't exist.
|
30
|
+
def method_missing(method, *args, &block)
|
31
|
+
layer.send(method, *args, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Attempt to translate the layer.
|
35
|
+
def translate(x=0, y=0)
|
36
|
+
@layer.translate x, y
|
37
|
+
end
|
38
|
+
|
39
|
+
# Attempt to scale the path components of the layer.
|
40
|
+
def scale_path_components(xr, yr)
|
41
|
+
@layer.scale_path_components(xr, yr)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Tries to hide the layer by moving it way off canvas.
|
45
|
+
def hide!
|
46
|
+
# TODO actually mess with the blend modes instead of
|
47
|
+
# just putting things way off canvas
|
48
|
+
return if @hidden_by_kelly
|
49
|
+
translate(100000, 10000)
|
50
|
+
@hidden_by_kelly = true
|
51
|
+
end
|
52
|
+
|
53
|
+
# Tries to re-show the canvas by moving it back to it's original position.
|
54
|
+
def show!
|
55
|
+
if @hidden_by_kelly
|
56
|
+
translate(-100000, -10000)
|
57
|
+
@hidden_by_kelly = false
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Exports this layer to a Hash.
|
62
|
+
def to_hash
|
63
|
+
super.merge({
|
64
|
+
type: :layer,
|
65
|
+
text: @layer.text,
|
66
|
+
ref_x: @layer.reference_point.x,
|
67
|
+
ref_y: @layer.reference_point.y
|
68
|
+
})
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require_relative 'node'
|
2
|
+
|
3
|
+
class PSD::Node
|
4
|
+
# Represents the root node of a Photoshop document
|
5
|
+
class Root < PSD::Node
|
6
|
+
include PSD::HasChildren
|
7
|
+
include PSD::Node::ParseLayers
|
8
|
+
|
9
|
+
attr_reader :children
|
10
|
+
|
11
|
+
# Stores a reference to the parsed PSD and builds the
|
12
|
+
# tree hierarchy.
|
13
|
+
def initialize(psd)
|
14
|
+
@psd = psd
|
15
|
+
build_hierarchy
|
16
|
+
end
|
17
|
+
|
18
|
+
# Recursively exports the hierarchy to a Hash
|
19
|
+
def to_hash
|
20
|
+
{
|
21
|
+
children: children.map(&:to_hash),
|
22
|
+
document: {
|
23
|
+
width: document_width,
|
24
|
+
height: document_height
|
25
|
+
}
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the width and height of the entire PSD document.
|
30
|
+
def document_dimensions
|
31
|
+
[@psd.header.width, @psd.header.height]
|
32
|
+
end
|
33
|
+
|
34
|
+
# The width of the full PSD document as defined in the header.
|
35
|
+
def document_width
|
36
|
+
@psd.header.width.to_i
|
37
|
+
end
|
38
|
+
|
39
|
+
# The height of the full PSD document as defined in the header.
|
40
|
+
def document_height
|
41
|
+
@psd.header.height.to_i
|
42
|
+
end
|
43
|
+
|
44
|
+
# The root node has no name since it's not an actual layer or group.
|
45
|
+
def name
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
|
49
|
+
# The depth of the root node is always 0.
|
50
|
+
def depth
|
51
|
+
0
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def build_hierarchy
|
57
|
+
@children = []
|
58
|
+
result = { layers: [] }
|
59
|
+
parseStack = []
|
60
|
+
|
61
|
+
# First we build the hierarchy
|
62
|
+
@psd.layers.each do |layer|
|
63
|
+
if layer.folder?
|
64
|
+
parseStack << result
|
65
|
+
result = { name: layer.name, layer: layer, layers: [] }
|
66
|
+
elsif layer.folder_end?
|
67
|
+
temp = result
|
68
|
+
result = parseStack.pop
|
69
|
+
result[:layers] << temp
|
70
|
+
else
|
71
|
+
result[:layers] << layer
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
parse_layers(result[:layers])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
class PSD
|
2
|
+
class Node
|
3
|
+
# Collection of methods to help in traversing the PSD tree structure.
|
4
|
+
module Ancestry
|
5
|
+
# Returns the root node
|
6
|
+
def root
|
7
|
+
return self if is_root?
|
8
|
+
return parent.root
|
9
|
+
end
|
10
|
+
|
11
|
+
# Is this node the root node?
|
12
|
+
def root?
|
13
|
+
self.is_a?(PSD::Node::Root)
|
14
|
+
end
|
15
|
+
alias :is_root? :root?
|
16
|
+
|
17
|
+
# Returns all ancestors in the path of this node. This
|
18
|
+
# does NOT return the root node.
|
19
|
+
def ancestors
|
20
|
+
return [] if parent.nil? || parent.is_root?
|
21
|
+
return parent.ancestors + [parent]
|
22
|
+
end
|
23
|
+
|
24
|
+
# Does this node have any children nodes?
|
25
|
+
def has_children?
|
26
|
+
children.length > 0
|
27
|
+
end
|
28
|
+
|
29
|
+
# Inverse of has_children?
|
30
|
+
def is_childless?
|
31
|
+
!has_children?
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns all sibling nodes including the current node. Can also
|
35
|
+
# be thought of as all children of the parent of this node.
|
36
|
+
def siblings
|
37
|
+
return [] if parent.nil?
|
38
|
+
parent.children
|
39
|
+
end
|
40
|
+
|
41
|
+
# Does this node have any siblings?
|
42
|
+
def has_siblings?
|
43
|
+
siblings.length > 1
|
44
|
+
end
|
45
|
+
|
46
|
+
# Is this node the only descendant of its parent?
|
47
|
+
def is_only_child?
|
48
|
+
siblings.length == 1
|
49
|
+
end
|
50
|
+
|
51
|
+
# Recursively get all descendant nodes, not including this node.
|
52
|
+
def descendants
|
53
|
+
children + children.map(&:children).flatten
|
54
|
+
end
|
55
|
+
|
56
|
+
# Same as descendants, except it includes this node.
|
57
|
+
def subtree
|
58
|
+
[self] + descendants
|
59
|
+
end
|
60
|
+
|
61
|
+
# Depth from the root node. Root depth is 0.
|
62
|
+
def depth
|
63
|
+
return ancestors.length + 1
|
64
|
+
end
|
65
|
+
|
66
|
+
def method_missing(method, *args, &block)
|
67
|
+
test = /^(.+)_(layers|groups)$/.match(method)
|
68
|
+
if test
|
69
|
+
m = self.respond_to?(test[1]) ? test[1] : "#{test[1]}s"
|
70
|
+
self.send(m).select &method("#{test[2]}_only")
|
71
|
+
else
|
72
|
+
super
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def layers_only(d); d.is_a?(PSD::Node::Layer); end
|
79
|
+
def groups_only(d); d.is_a?(PSD::Node::Group); end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class PSD
|
2
|
+
module HasChildren
|
3
|
+
# Returns all group/folder children of this node.
|
4
|
+
def groups
|
5
|
+
@children.select{ |c| c.is_a?(PSD::Group) }
|
6
|
+
end
|
7
|
+
|
8
|
+
# Returns all layer children of this node.
|
9
|
+
def layers
|
10
|
+
@children.select{ |c| c.is_a?(PSD::Layer) }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class PSD::Node
|
2
|
+
module ParseLayers
|
3
|
+
# Organizes the flat layer structure into tree nodes.
|
4
|
+
def parse_layers(layers)
|
5
|
+
@children = []
|
6
|
+
layers.each do |layer|
|
7
|
+
if layer.is_a?(Hash)
|
8
|
+
node = PSD::Node::Group.new(layer)
|
9
|
+
elsif layer.is_a?(PSD::Layer)
|
10
|
+
node = PSD::Node::Layer.new(layer)
|
11
|
+
end
|
12
|
+
|
13
|
+
node.parent = self
|
14
|
+
@children << node
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|