path_cursor.go

  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}