psd-enginedata 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +2 -0
  4. data/Guardfile +9 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +112 -0
  7. data/Rakefile +1 -0
  8. data/lib/psd/enginedata.rb +93 -0
  9. data/lib/psd/enginedata/document_helpers.rb +46 -0
  10. data/lib/psd/enginedata/errors.rb +6 -0
  11. data/lib/psd/enginedata/export.rb +8 -0
  12. data/lib/psd/enginedata/exporters/css.rb +65 -0
  13. data/lib/psd/enginedata/instruction.rb +39 -0
  14. data/lib/psd/enginedata/instructions/boolean.rb +14 -0
  15. data/lib/psd/enginedata/instructions/hash_end.rb +18 -0
  16. data/lib/psd/enginedata/instructions/hash_start.rb +16 -0
  17. data/lib/psd/enginedata/instructions/multi_line_array_end.rb +16 -0
  18. data/lib/psd/enginedata/instructions/multi_line_array_start.rb +16 -0
  19. data/lib/psd/enginedata/instructions/noop.rb +10 -0
  20. data/lib/psd/enginedata/instructions/number.rb +14 -0
  21. data/lib/psd/enginedata/instructions/number_with_decimal.rb +14 -0
  22. data/lib/psd/enginedata/instructions/property.rb +14 -0
  23. data/lib/psd/enginedata/instructions/property_with_data.rb +21 -0
  24. data/lib/psd/enginedata/instructions/single_line_array.rb +21 -0
  25. data/lib/psd/enginedata/instructions/string.rb +14 -0
  26. data/lib/psd/enginedata/node.rb +11 -0
  27. data/lib/psd/enginedata/text.rb +49 -0
  28. data/lib/psd/enginedata/version.rb +6 -0
  29. data/psd-enginedata.gemspec +29 -0
  30. data/spec/files/enginedata +1622 -0
  31. data/spec/files/enginedata_simple +26 -0
  32. data/spec/integration/export_spec.rb +35 -0
  33. data/spec/integration/parser_spec.rb +24 -0
  34. data/spec/spec_helper.rb +17 -0
  35. metadata +198 -0
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,9 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
9
+
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,6 @@
1
+ # -*- encoding : utf-8 -*-
2
+ class PSD
3
+ class EngineData #:nodoc:
4
+ class TokenNotFound < StandardError; end
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ # -*- encoding : utf-8 -*-
2
+ class PSD
3
+ class EngineData
4
+ module Export #:nodoc:
5
+ include CSS
6
+ end
7
+ end
8
+ 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