1// Copyright 2017 The oksvg Authors. All rights reserved.
2// created: 2/12/2017 by S.R.Wiley
3//
4// utils.go implements translation of an SVG2.0 path into a rasterx Path.
5
6package oksvg
7
8import (
9 "errors"
10 "log"
11 "math"
12 "unicode"
13
14 "github.com/srwiley/rasterx"
15
16 "golang.org/x/image/math/fixed"
17)
18
19type (
20 // ErrorMode is the for setting how the parser reacts to unparsed elements
21 ErrorMode uint8
22 // PathCursor is used to parse SVG format path strings into a rasterx Path
23 PathCursor struct {
24 rasterx.Path
25 placeX, placeY float64
26 cntlPtX, cntlPtY float64
27 pathStartX, pathStartY float64
28 points []float64
29 lastKey uint8
30 ErrorMode ErrorMode
31 inPath bool
32 }
33)
34
35const (
36 // IgnoreErrorMode skips un-parsed SVG elements.
37 IgnoreErrorMode ErrorMode = iota
38
39 // WarnErrorMode outputs a warning when an un-parsed SVG element is found.
40 WarnErrorMode
41
42 // StrictErrorMode causes an error when an un-parsed SVG element is found.
43 StrictErrorMode
44)
45
46var (
47 errParamMismatch = errors.New("param mismatch")
48 errCommandUnknown = errors.New("unknown command")
49 errZeroLengthID = errors.New("zero length id")
50)
51
52// ReadFloat reads a floating point value and adds it to the cursor's points slice.
53func (c *PathCursor) ReadFloat(numStr string) error {
54 last := 0
55 isFirst := true
56 for i, n := range numStr {
57 if n == '.' {
58 if isFirst {
59 isFirst = false
60 continue
61 }
62 f, err := parseFloat(numStr[last:i], 64)
63 if err != nil {
64 return err
65 }
66 c.points = append(c.points, f)
67 last = i
68 }
69 }
70 f, err := parseFloat(numStr[last:], 64)
71 if err != nil {
72 return err
73 }
74 c.points = append(c.points, f)
75 return nil
76}
77
78// GetPoints reads a set of floating point values from the SVG format number string,
79// and add them to the cursor's points slice.
80func (c *PathCursor) GetPoints(dataPoints string) error {
81 lastIndex := -1
82 c.points = c.points[0:0]
83 lr := ' '
84 for i, r := range dataPoints {
85 if !unicode.IsNumber(r) && r != '.' && !(r == '-' && lr == 'e') && r != 'e' {
86 if lastIndex != -1 {
87 if err := c.ReadFloat(dataPoints[lastIndex:i]); err != nil {
88 return err
89 }
90 }
91 if r == '-' {
92 lastIndex = i
93 } else {
94 lastIndex = -1
95 }
96 } else if lastIndex == -1 {
97 lastIndex = i
98 }
99 lr = r
100 }
101 if lastIndex != -1 && lastIndex != len(dataPoints) {
102 if err := c.ReadFloat(dataPoints[lastIndex:]); err != nil {
103 return err
104 }
105 }
106 return nil
107}
108
109// EllipseAt adds a path of an elipse centered at cx, cy of radius rx and ry
110// to the PathCursor
111func (c *PathCursor) EllipseAt(cx, cy, rx, ry float64) {
112 c.placeX, c.placeY = cx+rx, cy
113 c.points = c.points[0:0]
114 c.points = append(c.points, rx, ry, 0.0, 1.0, 0.0, c.placeX, c.placeY)
115 c.Path.Start(fixed.Point26_6{
116 X: fixed.Int26_6(c.placeX * 64),
117 Y: fixed.Int26_6(c.placeY * 64)})
118 c.placeX, c.placeY = rasterx.AddArc(c.points, cx, cy, c.placeX, c.placeY, &c.Path)
119 c.Path.Stop(true)
120}
121
122// AddArcFromA adds a path of an arc element to the cursor path to the PathCursor
123func (c *PathCursor) AddArcFromA(points []float64) {
124 cx, cy := rasterx.FindEllipseCenter(&points[0], &points[1], points[2]*math.Pi/180, c.placeX,
125 c.placeY, points[5], points[6], points[4] == 0, points[3] == 0)
126 c.placeX, c.placeY = rasterx.AddArc(c.points, cx, cy, c.placeX, c.placeY, &c.Path)
127}
128
129// CompilePath translates the svgPath description string into a rasterx path.
130// All valid SVG path elements are interpreted to rasterx equivalents.
131// The resulting path element is stored in the PathCursor.
132func (c *PathCursor) CompilePath(svgPath string) error {
133 c.init()
134 lastIndex := -1
135 for i, v := range svgPath {
136 if unicode.IsLetter(v) && v != 'e' {
137 if lastIndex != -1 {
138 if err := c.addSeg(svgPath[lastIndex:i]); err != nil {
139 return err
140 }
141 }
142 lastIndex = i
143 }
144 }
145 if lastIndex != -1 {
146 if err := c.addSeg(svgPath[lastIndex:]); err != nil {
147 return err
148 }
149 }
150 return nil
151}
152
153func reflect(px, py, rx, ry float64) (x, y float64) {
154 return px*2 - rx, py*2 - ry
155}
156
157func (c *PathCursor) valsToAbs(last float64) {
158 for i := 0; i < len(c.points); i++ {
159 last += c.points[i]
160 c.points[i] = last
161 }
162}
163
164func (c *PathCursor) pointsToAbs(sz int) {
165 lastX := c.placeX
166 lastY := c.placeY
167 for j := 0; j < len(c.points); j += sz {
168 for i := 0; i < sz; i += 2 {
169 c.points[i+j] += lastX
170 c.points[i+1+j] += lastY
171 }
172 lastX = c.points[(j+sz)-2]
173 lastY = c.points[(j+sz)-1]
174 }
175}
176
177func (c *PathCursor) hasSetsOrMore(sz int, rel bool) bool {
178 if !(len(c.points) >= sz && len(c.points)%sz == 0) {
179 return false
180 }
181 if rel {
182 c.pointsToAbs(sz)
183 }
184 return true
185}
186
187func (c *PathCursor) reflectControlQuad() {
188 switch c.lastKey {
189 case 'q', 'Q', 'T', 't':
190 c.cntlPtX, c.cntlPtY = reflect(c.placeX, c.placeY, c.cntlPtX, c.cntlPtY)
191 default:
192 c.cntlPtX, c.cntlPtY = c.placeX, c.placeY
193 }
194}
195
196func (c *PathCursor) reflectControlCube() {
197 switch c.lastKey {
198 case 'c', 'C', 's', 'S':
199 c.cntlPtX, c.cntlPtY = reflect(c.placeX, c.placeY, c.cntlPtX, c.cntlPtY)
200 default:
201 c.cntlPtX, c.cntlPtY = c.placeX, c.placeY
202 }
203}
204
205// addSeg decodes an SVG seqment string into equivalent raster path commands saved
206// in the cursor's Path
207func (c *PathCursor) addSeg(segString string) error {
208 // Parse the string describing the numeric points in SVG format
209 if err := c.GetPoints(segString[1:]); err != nil {
210 return err
211 }
212 l := len(c.points)
213 k := segString[0]
214 rel := false
215 switch k {
216 case 'z':
217 fallthrough
218 case 'Z':
219 if len(c.points) != 0 {
220 return errParamMismatch
221 }
222 if c.inPath {
223 c.Path.Stop(true)
224 c.placeX = c.pathStartX
225 c.placeY = c.pathStartY
226 c.inPath = false
227 }
228 case 'm':
229 rel = true
230 fallthrough
231 case 'M':
232 if !c.hasSetsOrMore(2, rel) {
233 return errParamMismatch
234 }
235 c.pathStartX, c.pathStartY = c.points[0], c.points[1]
236 c.inPath = true
237 c.Path.Start(fixed.Point26_6{X: fixed.Int26_6((c.pathStartX) * 64), Y: fixed.Int26_6((c.pathStartY) * 64)})
238 for i := 2; i < l-1; i += 2 {
239 c.Path.Line(fixed.Point26_6{
240 X: fixed.Int26_6((c.points[i]) * 64),
241 Y: fixed.Int26_6((c.points[i+1]) * 64)})
242 }
243 c.placeX = c.points[l-2]
244 c.placeY = c.points[l-1]
245 case 'l':
246 rel = true
247 fallthrough
248 case 'L':
249 if !c.hasSetsOrMore(2, rel) {
250 return errParamMismatch
251 }
252 for i := 0; i < l-1; i += 2 {
253 c.Path.Line(fixed.Point26_6{
254 X: fixed.Int26_6((c.points[i]) * 64),
255 Y: fixed.Int26_6((c.points[i+1]) * 64)})
256 }
257 c.placeX = c.points[l-2]
258 c.placeY = c.points[l-1]
259 case 'v':
260 c.valsToAbs(c.placeY)
261 fallthrough
262 case 'V':
263 if !c.hasSetsOrMore(1, false) {
264 return errParamMismatch
265 }
266 for _, p := range c.points {
267 c.Path.Line(fixed.Point26_6{
268 X: fixed.Int26_6((c.placeX) * 64),
269 Y: fixed.Int26_6((p) * 64)})
270 }
271 c.placeY = c.points[l-1]
272 case 'h':
273 c.valsToAbs(c.placeX)
274 fallthrough
275 case 'H':
276 if !c.hasSetsOrMore(1, false) {
277 return errParamMismatch
278 }
279 for _, p := range c.points {
280 c.Path.Line(fixed.Point26_6{
281 X: fixed.Int26_6((p) * 64),
282 Y: fixed.Int26_6((c.placeY) * 64)})
283 }
284 c.placeX = c.points[l-1]
285 case 'q':
286 rel = true
287 fallthrough
288 case 'Q':
289 if !c.hasSetsOrMore(4, rel) {
290 return errParamMismatch
291 }
292 for i := 0; i < l-3; i += 4 {
293 c.Path.QuadBezier(
294 fixed.Point26_6{
295 X: fixed.Int26_6((c.points[i]) * 64),
296 Y: fixed.Int26_6((c.points[i+1]) * 64)},
297 fixed.Point26_6{
298 X: fixed.Int26_6((c.points[i+2]) * 64),
299 Y: fixed.Int26_6((c.points[i+3]) * 64)})
300 }
301 c.cntlPtX, c.cntlPtY = c.points[l-4], c.points[l-3]
302 c.placeX = c.points[l-2]
303 c.placeY = c.points[l-1]
304 case 't':
305 rel = true
306 fallthrough
307 case 'T':
308 if !c.hasSetsOrMore(2, rel) {
309 return errParamMismatch
310 }
311 for i := 0; i < l-1; i += 2 {
312 c.reflectControlQuad()
313 c.Path.QuadBezier(
314 fixed.Point26_6{
315 X: fixed.Int26_6((c.cntlPtX) * 64),
316 Y: fixed.Int26_6((c.cntlPtY) * 64)},
317 fixed.Point26_6{
318 X: fixed.Int26_6((c.points[i]) * 64),
319 Y: fixed.Int26_6((c.points[i+1]) * 64)})
320 c.lastKey = k
321 c.placeX = c.points[i]
322 c.placeY = c.points[i+1]
323 }
324 case 'c':
325 rel = true
326 fallthrough
327 case 'C':
328 if !c.hasSetsOrMore(6, rel) {
329 return errParamMismatch
330 }
331 for i := 0; i < l-5; i += 6 {
332 c.Path.CubeBezier(
333 fixed.Point26_6{
334 X: fixed.Int26_6((c.points[i]) * 64),
335 Y: fixed.Int26_6((c.points[i+1]) * 64)},
336 fixed.Point26_6{
337 X: fixed.Int26_6((c.points[i+2]) * 64),
338 Y: fixed.Int26_6((c.points[i+3]) * 64)},
339 fixed.Point26_6{
340 X: fixed.Int26_6((c.points[i+4]) * 64),
341 Y: fixed.Int26_6((c.points[i+5]) * 64)})
342 }
343 c.cntlPtX, c.cntlPtY = c.points[l-4], c.points[l-3]
344 c.placeX = c.points[l-2]
345 c.placeY = c.points[l-1]
346 case 's':
347 rel = true
348 fallthrough
349 case 'S':
350 if !c.hasSetsOrMore(4, rel) {
351 return errParamMismatch
352 }
353 for i := 0; i < l-3; i += 4 {
354 c.reflectControlCube()
355 c.Path.CubeBezier(fixed.Point26_6{
356 X: fixed.Int26_6((c.cntlPtX) * 64), Y: fixed.Int26_6((c.cntlPtY) * 64)},
357 fixed.Point26_6{
358 X: fixed.Int26_6((c.points[i]) * 64), Y: fixed.Int26_6((c.points[i+1]) * 64)},
359 fixed.Point26_6{
360 X: fixed.Int26_6((c.points[i+2]) * 64), Y: fixed.Int26_6((c.points[i+3]) * 64)})
361 c.lastKey = k
362 c.cntlPtX, c.cntlPtY = c.points[i], c.points[i+1]
363 c.placeX = c.points[i+2]
364 c.placeY = c.points[i+3]
365 }
366 case 'a', 'A':
367 if !c.hasSetsOrMore(7, false) {
368 return errParamMismatch
369 }
370 for i := 0; i < l-6; i += 7 {
371 if k == 'a' {
372 c.points[i+5] += c.placeX
373 c.points[i+6] += c.placeY
374 }
375 c.AddArcFromA(c.points[i:])
376 }
377 default:
378 if c.ErrorMode == StrictErrorMode {
379 return errCommandUnknown
380 }
381 if c.ErrorMode == WarnErrorMode {
382 log.Println("Ignoring svg command " + string(k))
383 }
384 }
385 // So we know how to extend some segment types
386 c.lastKey = k
387 return nil
388}
389
390func (c *PathCursor) init() {
391 c.placeX = 0.0
392 c.placeY = 0.0
393 c.points = c.points[0:0]
394 c.lastKey = ' '
395 c.Path.Clear()
396 c.inPath = false
397}