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