466a75d10faf6df068c4a0a103acf9c5fe77e002
1 #!/usr/bin/env ruby
3 # simplepath.rb
4 # functions for digesting paths into a simple list structure
5 #
6 # Ruby port by MenTaLguY
7 #
8 # Copyright (C) 2005 Aaron Spike <aaron@ekips.org>
9 # Copyright (C) 2006 MenTaLguY <mental@rydia.net>
10 #
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the GNU General Public License
22 # along with this program; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25 require 'strscan'
27 def lexPath(d)
28 # iterator which breaks path data
29 # identifies command and parameter tokens
31 scanner = StringScanner.new(d)
33 delim = /[ \t\r\n,]+/
34 command = /[MLHVCSQTAZmlhvcsqtaz]/
35 parameter = /(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)/
37 until scanner.eos?
38 scanner.skip(delim)
39 if m = scanner.scan(command)
40 yield m, true
41 elsif m = scanner.scan(parameter)
42 yield m, false
43 else
44 #TODO: create new exception
45 raise 'Invalid path data!'
46 end
47 end
48 end
50 PathDef = Struct.new :implicit_next, :param_count, :casts, :coord_types
51 PATHDEFS = {
52 'M' => PathDef['L', 2, [:to_f, :to_f], [:x,:y]],
53 'L' => PathDef['L', 2, [:to_f, :to_f], [:x,:y]],
54 'H' => PathDef['H', 1, [:to_f], [:x]],
55 'V' => PathDef['V', 1, [:to_f], [:y]],
56 'C' => PathDef['C', 6, [:to_f, :to_f, :to_f, :to_f, :to_f, :to_f], [:x,:y,:x,:y,:x,:y]],
57 'S' => PathDef['S', 4, [:to_f, :to_f, :to_f, :to_f], [:x,:y,:x,:y]],
58 'Q' => PathDef['Q', 4, [:to_f, :to_f, :to_f, :to_f], [:x,:y,:x,:y]],
59 'T' => PathDef['T', 2, [:to_f, :to_f], [:x,:y]],
60 'A' => PathDef['A', 7, [:to_f, :to_f, :to_f, :to_i, :to_i, :to_f, :to_f], [0,0,0,0,0,:x,:y]],
61 'Z' => PathDef['L', 0, [], []]
62 }
64 def parsePath(d)
65 # Parse SVG path and return an array of segments.
66 # Removes all shorthand notation.
67 # Converts coordinates to absolute.
69 retval = []
71 command = nil
72 outputCommand = nil
73 params = []
75 pen = [0.0,0.0]
76 subPathStart = pen
77 lastControl = pen
78 lastCommand = nil
80 lexPath(d) do |token, isCommand|
81 if command
82 raise 'Invalid number of parameters' if isCommand
83 else
84 if isCommand
85 raise 'Invalid path, must begin with moveto.' \
86 unless lastCommand or token.upcase == 'M'
87 command = token
88 else
89 #command was omited
90 #use last command's implicit next command
91 raise 'Invalid path, no initial command.' unless lastCommand
92 if lastCommand =~ /[A-Z]/
93 command = PATHDEFS[lastCommand].implicit_next
94 else
95 command = PATHDEFS[lastCommand.upcase].implicit_next.downcase
96 end
97 end
98 outputCommand = command.upcase
99 end
101 unless isCommand
102 param = token.send PATHDEFS[outputCommand].casts[params.length]
103 if command =~ /[a-z]/
104 case PATHDEFS[outputCommand].coord_types[params.length]
105 when :x: param += pen[0]
106 when :y: param += pen[1]
107 end
108 end
109 params.push param
110 end
112 if params.length == PATHDEFS[outputCommand].param_count
114 #Flesh out shortcut notation
115 case outputCommand
116 when 'H','V'
117 case outputCommand
118 when 'H': params.push pen[1]
119 when 'V': params.unshift pen[0]
120 end
121 outputCommand = 'L'
122 when 'S','T'
123 params.unshift(pen[1]+(pen[1]-lastControl[1]))
124 params.unshift(pen[0]+(pen[0]-lastControl[0]))
125 case outputCommand
126 when 'S': outputCommand = 'C'
127 when 'T': outputCommand = 'Q'
128 end
129 end
131 #current values become "last" values
132 case outputCommand
133 when 'M'
134 subPathStart = params[0,2]
135 pen = subPathStart
136 when 'Z'
137 pen = subPathStart
138 else
139 pen = params[-2,2]
140 end
142 case outputCommand
143 when 'Q','C'
144 lastControl = params[-4,2]
145 else
146 lastControl = pen
147 end
149 lastCommand = command
150 retval.push [outputCommand,params]
151 command = nil
152 params = []
153 end
154 end
156 raise 'Unexpected end of path' if command
158 return retval
159 end
161 def formatPath(a)
162 # Format SVG path data from an array
163 a.map { |cmd,params| "#{cmd} #{params.join(' ')}" }.join
164 end
166 def _transformPath(p)
167 p.each do |cmd,params|
168 coord_types = PATHDEFS[cmd].coord_types
169 for i in 0...(params.length)
170 yield params, i, coord_types[i]
171 end
172 end
173 end
175 def translatePath(p, x, y)
176 _transformPath(p) do |params, i, coord_type|
177 case coord_type
178 when :x: params[i] += x
179 when :y: params[i] += y
180 end
181 end
182 end
184 def scalePath(p, x, y)
185 _transformPath(p) do |params, i, coord_type|
186 case coord_type
187 when :x: params[i] *= x
188 when :y: params[i] *= y
189 end
190 end
191 end
193 def rotatePath(p, a, cx = 0, cy = 0)
194 return p if a == 0
195 _transformPath(p) do |params, i, coord_type|
196 if coord_type == :x
197 x = params[i] - cx
198 y = params[i + 1] - cy
199 r = Math.sqrt((x**2) + (y**2))
200 unless r.zero?
201 theta = Math.atan2(y, x) + a
202 params[i] = (r * Math.cos(theta)) + cx
203 params[i + 1] = (r * Math.sin(theta)) + cy
204 end
205 end
206 end
207 end