psd-enginedata 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/Gemfile +2 -0
- data/Guardfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +112 -0
- data/Rakefile +1 -0
- data/lib/psd/enginedata.rb +93 -0
- data/lib/psd/enginedata/document_helpers.rb +46 -0
- data/lib/psd/enginedata/errors.rb +6 -0
- data/lib/psd/enginedata/export.rb +8 -0
- data/lib/psd/enginedata/exporters/css.rb +65 -0
- data/lib/psd/enginedata/instruction.rb +39 -0
- data/lib/psd/enginedata/instructions/boolean.rb +14 -0
- data/lib/psd/enginedata/instructions/hash_end.rb +18 -0
- data/lib/psd/enginedata/instructions/hash_start.rb +16 -0
- data/lib/psd/enginedata/instructions/multi_line_array_end.rb +16 -0
- data/lib/psd/enginedata/instructions/multi_line_array_start.rb +16 -0
- data/lib/psd/enginedata/instructions/noop.rb +10 -0
- data/lib/psd/enginedata/instructions/number.rb +14 -0
- data/lib/psd/enginedata/instructions/number_with_decimal.rb +14 -0
- data/lib/psd/enginedata/instructions/property.rb +14 -0
- data/lib/psd/enginedata/instructions/property_with_data.rb +21 -0
- data/lib/psd/enginedata/instructions/single_line_array.rb +21 -0
- data/lib/psd/enginedata/instructions/string.rb +14 -0
- data/lib/psd/enginedata/node.rb +11 -0
- data/lib/psd/enginedata/text.rb +49 -0
- data/lib/psd/enginedata/version.rb +6 -0
- data/psd-enginedata.gemspec +29 -0
- data/spec/files/enginedata +1622 -0
- data/spec/files/enginedata_simple +26 -0
- data/spec/integration/export_spec.rb +35 -0
- data/spec/integration/parser_spec.rb +24 -0
- data/spec/spec_helper.rb +17 -0
- metadata +198 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 LayerVault
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
# PSD EngineData
|
2
|
+
|
3
|
+
Adobe thought it would be cool to create their own markup language for text data embedded within a Photoshop document. I've taken the liberty of calling it EngineData since that is the name used in the descriptor embedded within PSD files. This is a general purpose parser for that data.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'psd-enginedata'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install psd-enginedata
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
``` ruby
|
22
|
+
# From File
|
23
|
+
parser = PSD::EngineData.load('path/to/file')
|
24
|
+
|
25
|
+
# From string
|
26
|
+
parser = PSD::EngineData.new(text)
|
27
|
+
|
28
|
+
# Parse it
|
29
|
+
parser.parse!
|
30
|
+
|
31
|
+
# Use it
|
32
|
+
puts parser.result.Editor.Text
|
33
|
+
```
|
34
|
+
|
35
|
+
## File Spec
|
36
|
+
|
37
|
+
The EngineData format uses certain characters to denote various data types. All newlines are denoted with a UNIX newline (\n) and it is not indentation-aware (although it seems to be stored with pretty indentation in the PSD file).
|
38
|
+
|
39
|
+
```
|
40
|
+
<<
|
41
|
+
/EngineDict
|
42
|
+
<<
|
43
|
+
/Editor
|
44
|
+
<<
|
45
|
+
/Text (˛ˇMake a change and save.)
|
46
|
+
>>
|
47
|
+
>>
|
48
|
+
/Font
|
49
|
+
<<
|
50
|
+
/Name (˛ˇHelveticaNeue-Light)
|
51
|
+
/FillColor
|
52
|
+
<<
|
53
|
+
/Type 1
|
54
|
+
/Values [ 1.0 0.0 0.0 0.0 ]
|
55
|
+
>>
|
56
|
+
/StyleSheetSet [
|
57
|
+
<<
|
58
|
+
/Name (˛ˇNormal RGB)
|
59
|
+
>>
|
60
|
+
]
|
61
|
+
>>
|
62
|
+
>>
|
63
|
+
```
|
64
|
+
|
65
|
+
There are 6 data types available to use:
|
66
|
+
|
67
|
+
* Dictionary/Hash
|
68
|
+
* Array (single or multi-line)
|
69
|
+
* Float
|
70
|
+
* Integer
|
71
|
+
* String
|
72
|
+
* Boolean
|
73
|
+
|
74
|
+
Altogether we have a non-turing complete markup language with some interesting quirks that was written in 1998 with the release of Photoshop 5.0 since it included the ability to edit text areas after they were created (before that, they were rasterized immediately).
|
75
|
+
|
76
|
+
### Property Names
|
77
|
+
|
78
|
+
Property names are started with a `/` followed by an alphanumeric word. The property value either comes after a space or a new line.
|
79
|
+
|
80
|
+
### Dictionaries/Hashes
|
81
|
+
|
82
|
+
Dictionaries are denoted by a property name (unless at the root), followed by a new line and the `<<` instruction. The end of a dictionary is denoted by the `>>` instruction.
|
83
|
+
|
84
|
+
A dictionary can contain any type of data including more dictionaries for nesting purposes. All values within a dictionary must have a property followed by a value of some kind.
|
85
|
+
|
86
|
+
### Arrays
|
87
|
+
|
88
|
+
Arrays come in two flavors: single-line or multi-line. Both are denoted by square brackets `[ ]`.
|
89
|
+
|
90
|
+
Single-line arrays can only hold number values (doubles or integers). While it wouldn't be out of the question for it to hold string or boolean values, this does not seem to appear in any tested Photoshop documents so we're going to assume it's not a part of the specification. Values in single-line arrays are **space delimited**. There seems to be a space between the opening and closing square brackets and the data, but we assume it's optional.
|
91
|
+
|
92
|
+
Multi-line arrays can contain any type of data. Values in multi-line arrays are newline-denoted. The opening bracket for multi-line arrays must occur on the same line as the property to which it belongs.
|
93
|
+
|
94
|
+
### Floats/Integers
|
95
|
+
|
96
|
+
Basically, numbers can be expressed with or without a decimal point. Floats are of the form `1.76`, but the non-fractional digits to the left of the decimal point are optional. Integers are just numbers with no commas or decimal points.
|
97
|
+
|
98
|
+
### Strings
|
99
|
+
|
100
|
+
Strings are delimited by parentheses with a cedilla-breve (thanks [Rob](http://github.com/robgrant) for the terminology) at the start of the string. Since operations are UNIX newline (\n) delimited within the document, new lines in strings are denoted by carriage returns (\r).
|
101
|
+
|
102
|
+
### Booleans
|
103
|
+
|
104
|
+
Booleans are simply denoted by `true` or `false`. Cool.
|
105
|
+
|
106
|
+
## Contributing
|
107
|
+
|
108
|
+
1. Fork it
|
109
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
110
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
111
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
112
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'hashie'
|
3
|
+
|
4
|
+
dir_root = File.dirname(File.absolute_path(__FILE__)) + '/enginedata'
|
5
|
+
[
|
6
|
+
'/instructions/*',
|
7
|
+
'/exporters/*',
|
8
|
+
'/**/*'
|
9
|
+
].each do |path|
|
10
|
+
Dir.glob(dir_root + path) { |file| require file if File.file?(file) }
|
11
|
+
end
|
12
|
+
|
13
|
+
class PSD
|
14
|
+
# General purpose parser for the text data markup present within PSD documents.
|
15
|
+
class EngineData
|
16
|
+
attr_reader :text
|
17
|
+
attr_accessor :property_stack, :node_stack, :property, :node
|
18
|
+
alias :result :node
|
19
|
+
|
20
|
+
include DocumentHelpers
|
21
|
+
include Export
|
22
|
+
|
23
|
+
# All of the instructions as defined by the EngineData spec.
|
24
|
+
INSTRUCTIONS = [
|
25
|
+
Instruction::HashStart,
|
26
|
+
Instruction::HashEnd,
|
27
|
+
Instruction::SingleLineArray,
|
28
|
+
Instruction::MultiLineArrayStart,
|
29
|
+
Instruction::MultiLineArrayEnd,
|
30
|
+
Instruction::Property,
|
31
|
+
Instruction::PropertyWithData,
|
32
|
+
Instruction::String,
|
33
|
+
Instruction::NumberWithDecimal,
|
34
|
+
Instruction::Number,
|
35
|
+
Instruction::Boolean,
|
36
|
+
Instruction::Noop
|
37
|
+
]
|
38
|
+
|
39
|
+
# Read a file containing EngineData markup and initialize a new instance.
|
40
|
+
def self.load(file)
|
41
|
+
self.new File.read(file)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Create a new Text instance and initialize our parsing stacks.
|
45
|
+
def initialize(text)
|
46
|
+
@text = Text.new(text)
|
47
|
+
|
48
|
+
@property_stack = []
|
49
|
+
@node_stack = []
|
50
|
+
|
51
|
+
@property = :root
|
52
|
+
@node = nil
|
53
|
+
|
54
|
+
@parsed = false
|
55
|
+
end
|
56
|
+
|
57
|
+
# Parses the full document.
|
58
|
+
def parse!
|
59
|
+
return if parsed?
|
60
|
+
|
61
|
+
while true
|
62
|
+
line = @text.current
|
63
|
+
@parsed = true and return if line.nil?
|
64
|
+
|
65
|
+
parse_tokens(line)
|
66
|
+
@text.next!
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Has the document been parsed yet?
|
71
|
+
def parsed?
|
72
|
+
@parsed
|
73
|
+
end
|
74
|
+
|
75
|
+
# Go through each instruction until a token match is found, then parse the
|
76
|
+
# matches.
|
77
|
+
def parse_tokens(text)
|
78
|
+
INSTRUCTIONS.each do |inst|
|
79
|
+
match = inst.match(text)
|
80
|
+
return inst.new(self, text).execute! if match
|
81
|
+
end
|
82
|
+
|
83
|
+
# This is a hack for the Japanese character rules that the format embeds
|
84
|
+
match = Instruction::String.match(text + @text.next)
|
85
|
+
if match
|
86
|
+
text += @text.next!
|
87
|
+
return Instruction::String.new(self, text).execute!
|
88
|
+
end
|
89
|
+
|
90
|
+
raise TokenNotFound.new("Text = #{text.dump}, Line = #{@text.line + 1}")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
class PSD
|
3
|
+
class EngineData
|
4
|
+
# A collection of helper methods that are used to manipulate the internal
|
5
|
+
# data structure while parsing.
|
6
|
+
module DocumentHelpers
|
7
|
+
# Pushes a property and node onto the parsing stack.
|
8
|
+
def stack_push(property = nil, node = nil)
|
9
|
+
node = @node if node.nil?
|
10
|
+
property = @property if property.nil?
|
11
|
+
|
12
|
+
@node_stack.push node
|
13
|
+
@property_stack.push property
|
14
|
+
end
|
15
|
+
|
16
|
+
# Pops a property and node from the parsing stack
|
17
|
+
def stack_pop
|
18
|
+
return @property_stack.pop, @node_stack.pop
|
19
|
+
end
|
20
|
+
|
21
|
+
# Sets the current active node
|
22
|
+
def set_node(node)
|
23
|
+
@node = node
|
24
|
+
end
|
25
|
+
|
26
|
+
# Creates a new node
|
27
|
+
def reset_node
|
28
|
+
@node = Node.new
|
29
|
+
end
|
30
|
+
|
31
|
+
# Sets the current active property
|
32
|
+
def set_property(property = nil)
|
33
|
+
@property = property
|
34
|
+
end
|
35
|
+
|
36
|
+
# Updates a node with a given property and child node.
|
37
|
+
def update_node(property, node)
|
38
|
+
if node.is_a?(PSD::EngineData::Node)
|
39
|
+
node[property] = @node
|
40
|
+
elsif node.is_a?(Array)
|
41
|
+
node.push @node
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
class PSD
|
3
|
+
class EngineData
|
4
|
+
module Export
|
5
|
+
# Exports the document to a CSS string.
|
6
|
+
module CSS
|
7
|
+
# Creates the CSS string and returns it. Each property is newline separated
|
8
|
+
# and not all properties may be present depending on the document.
|
9
|
+
#
|
10
|
+
# Colors are returned in rgba() format and fonts may include some internal
|
11
|
+
# Photoshop fonts.
|
12
|
+
def to_css
|
13
|
+
parse! unless parsed?
|
14
|
+
|
15
|
+
definition = {}
|
16
|
+
definition.merge! font_family
|
17
|
+
definition.merge! font_size
|
18
|
+
definition.merge! font_color
|
19
|
+
|
20
|
+
css = []
|
21
|
+
definition.each do |k, v|
|
22
|
+
css << "#{k}: #{v};"
|
23
|
+
end
|
24
|
+
|
25
|
+
css.join("\n")
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def font_family
|
31
|
+
font = result.ResourceDict.FontSet.map { |f| %{"#{f.Name}"} }.join(', ')
|
32
|
+
|
33
|
+
{
|
34
|
+
'font-family' => font
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def font_size
|
39
|
+
{
|
40
|
+
'font-size' => "#{styles.FontSize}pt"
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def font_color
|
45
|
+
return {} unless styles.key?('FillColor')
|
46
|
+
|
47
|
+
color = styles.FillColor.Values.map { |c| (c * 255).round }
|
48
|
+
if color.length == 3
|
49
|
+
alpha = 255
|
50
|
+
else
|
51
|
+
alpha = color.shift
|
52
|
+
end
|
53
|
+
|
54
|
+
{
|
55
|
+
'color' => "rgba(#{color.join(', ')}, #{alpha})"
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def styles
|
60
|
+
result.EngineDict.StyleRun.RunArray[0].StyleSheet.StyleSheetData
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
class PSD
|
3
|
+
class EngineData
|
4
|
+
# A single instruction as defined by the EngineData spec.
|
5
|
+
class Instruction
|
6
|
+
# The regex for the token, defaulted to nil. Override this.
|
7
|
+
def self.token; end
|
8
|
+
|
9
|
+
# Checks to see if the given text is a match for this token.
|
10
|
+
def self.match(text)
|
11
|
+
token.match(text)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Stores a reference to the EngineData document and the current
|
15
|
+
# String being parsed.
|
16
|
+
def initialize(document, text)
|
17
|
+
@document = document
|
18
|
+
@text = text
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns the regex match to the current string.
|
22
|
+
def match
|
23
|
+
self.class.match @text
|
24
|
+
end
|
25
|
+
|
26
|
+
# Once matched, we execute the instruction and apply the changes
|
27
|
+
# to the parsed data.
|
28
|
+
def execute!; end
|
29
|
+
|
30
|
+
def method_missing(method, *args, &block)
|
31
|
+
if @document.respond_to?(method)
|
32
|
+
return @document.send(method, *args)
|
33
|
+
end
|
34
|
+
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|