398 lines
9.7 KiB
Go
398 lines
9.7 KiB
Go
// Copyright 2017 The oksvg Authors. All rights reserved.
|
|
// created: 2/12/2017 by S.R.Wiley
|
|
//
|
|
// utils.go implements translation of an SVG2.0 path into a rasterx Path.
|
|
|
|
package oksvg
|
|
|
|
import (
|
|
"errors"
|
|
"log"
|
|
"math"
|
|
"unicode"
|
|
|
|
"github.com/srwiley/rasterx"
|
|
|
|
"golang.org/x/image/math/fixed"
|
|
)
|
|
|
|
type (
|
|
// ErrorMode is the for setting how the parser reacts to unparsed elements
|
|
ErrorMode uint8
|
|
// PathCursor is used to parse SVG format path strings into a rasterx Path
|
|
PathCursor struct {
|
|
rasterx.Path
|
|
placeX, placeY float64
|
|
cntlPtX, cntlPtY float64
|
|
pathStartX, pathStartY float64
|
|
points []float64
|
|
lastKey uint8
|
|
ErrorMode ErrorMode
|
|
inPath bool
|
|
}
|
|
)
|
|
|
|
const (
|
|
// IgnoreErrorMode skips un-parsed SVG elements.
|
|
IgnoreErrorMode ErrorMode = iota
|
|
|
|
// WarnErrorMode outputs a warning when an un-parsed SVG element is found.
|
|
WarnErrorMode
|
|
|
|
// StrictErrorMode causes an error when an un-parsed SVG element is found.
|
|
StrictErrorMode
|
|
)
|
|
|
|
var (
|
|
errParamMismatch = errors.New("param mismatch")
|
|
errCommandUnknown = errors.New("unknown command")
|
|
errZeroLengthID = errors.New("zero length id")
|
|
)
|
|
|
|
// ReadFloat reads a floating point value and adds it to the cursor's points slice.
|
|
func (c *PathCursor) ReadFloat(numStr string) error {
|
|
last := 0
|
|
isFirst := true
|
|
for i, n := range numStr {
|
|
if n == '.' {
|
|
if isFirst {
|
|
isFirst = false
|
|
continue
|
|
}
|
|
f, err := parseFloat(numStr[last:i], 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.points = append(c.points, f)
|
|
last = i
|
|
}
|
|
}
|
|
f, err := parseFloat(numStr[last:], 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.points = append(c.points, f)
|
|
return nil
|
|
}
|
|
|
|
// GetPoints reads a set of floating point values from the SVG format number string,
|
|
// and add them to the cursor's points slice.
|
|
func (c *PathCursor) GetPoints(dataPoints string) error {
|
|
lastIndex := -1
|
|
c.points = c.points[0:0]
|
|
lr := ' '
|
|
for i, r := range dataPoints {
|
|
if !unicode.IsNumber(r) && r != '.' && !(r == '-' && lr == 'e') && r != 'e' {
|
|
if lastIndex != -1 {
|
|
if err := c.ReadFloat(dataPoints[lastIndex:i]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if r == '-' {
|
|
lastIndex = i
|
|
} else {
|
|
lastIndex = -1
|
|
}
|
|
} else if lastIndex == -1 {
|
|
lastIndex = i
|
|
}
|
|
lr = r
|
|
}
|
|
if lastIndex != -1 && lastIndex != len(dataPoints) {
|
|
if err := c.ReadFloat(dataPoints[lastIndex:]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EllipseAt adds a path of an elipse centered at cx, cy of radius rx and ry
|
|
// to the PathCursor
|
|
func (c *PathCursor) EllipseAt(cx, cy, rx, ry float64) {
|
|
c.placeX, c.placeY = cx+rx, cy
|
|
c.points = c.points[0:0]
|
|
c.points = append(c.points, rx, ry, 0.0, 1.0, 0.0, c.placeX, c.placeY)
|
|
c.Path.Start(fixed.Point26_6{
|
|
X: fixed.Int26_6(c.placeX * 64),
|
|
Y: fixed.Int26_6(c.placeY * 64)})
|
|
c.placeX, c.placeY = rasterx.AddArc(c.points, cx, cy, c.placeX, c.placeY, &c.Path)
|
|
c.Path.Stop(true)
|
|
}
|
|
|
|
// AddArcFromA adds a path of an arc element to the cursor path to the PathCursor
|
|
func (c *PathCursor) AddArcFromA(points []float64) {
|
|
cx, cy := rasterx.FindEllipseCenter(&points[0], &points[1], points[2]*math.Pi/180, c.placeX,
|
|
c.placeY, points[5], points[6], points[4] == 0, points[3] == 0)
|
|
c.placeX, c.placeY = rasterx.AddArc(c.points, cx, cy, c.placeX, c.placeY, &c.Path)
|
|
}
|
|
|
|
// CompilePath translates the svgPath description string into a rasterx path.
|
|
// All valid SVG path elements are interpreted to rasterx equivalents.
|
|
// The resulting path element is stored in the PathCursor.
|
|
func (c *PathCursor) CompilePath(svgPath string) error {
|
|
c.init()
|
|
lastIndex := -1
|
|
for i, v := range svgPath {
|
|
if unicode.IsLetter(v) && v != 'e' {
|
|
if lastIndex != -1 {
|
|
if err := c.addSeg(svgPath[lastIndex:i]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
lastIndex = i
|
|
}
|
|
}
|
|
if lastIndex != -1 {
|
|
if err := c.addSeg(svgPath[lastIndex:]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func reflect(px, py, rx, ry float64) (x, y float64) {
|
|
return px*2 - rx, py*2 - ry
|
|
}
|
|
|
|
func (c *PathCursor) valsToAbs(last float64) {
|
|
for i := 0; i < len(c.points); i++ {
|
|
last += c.points[i]
|
|
c.points[i] = last
|
|
}
|
|
}
|
|
|
|
func (c *PathCursor) pointsToAbs(sz int) {
|
|
lastX := c.placeX
|
|
lastY := c.placeY
|
|
for j := 0; j < len(c.points); j += sz {
|
|
for i := 0; i < sz; i += 2 {
|
|
c.points[i+j] += lastX
|
|
c.points[i+1+j] += lastY
|
|
}
|
|
lastX = c.points[(j+sz)-2]
|
|
lastY = c.points[(j+sz)-1]
|
|
}
|
|
}
|
|
|
|
func (c *PathCursor) hasSetsOrMore(sz int, rel bool) bool {
|
|
if !(len(c.points) >= sz && len(c.points)%sz == 0) {
|
|
return false
|
|
}
|
|
if rel {
|
|
c.pointsToAbs(sz)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (c *PathCursor) reflectControlQuad() {
|
|
switch c.lastKey {
|
|
case 'q', 'Q', 'T', 't':
|
|
c.cntlPtX, c.cntlPtY = reflect(c.placeX, c.placeY, c.cntlPtX, c.cntlPtY)
|
|
default:
|
|
c.cntlPtX, c.cntlPtY = c.placeX, c.placeY
|
|
}
|
|
}
|
|
|
|
func (c *PathCursor) reflectControlCube() {
|
|
switch c.lastKey {
|
|
case 'c', 'C', 's', 'S':
|
|
c.cntlPtX, c.cntlPtY = reflect(c.placeX, c.placeY, c.cntlPtX, c.cntlPtY)
|
|
default:
|
|
c.cntlPtX, c.cntlPtY = c.placeX, c.placeY
|
|
}
|
|
}
|
|
|
|
// addSeg decodes an SVG seqment string into equivalent raster path commands saved
|
|
// in the cursor's Path
|
|
func (c *PathCursor) addSeg(segString string) error {
|
|
// Parse the string describing the numeric points in SVG format
|
|
if err := c.GetPoints(segString[1:]); err != nil {
|
|
return err
|
|
}
|
|
l := len(c.points)
|
|
k := segString[0]
|
|
rel := false
|
|
switch k {
|
|
case 'z':
|
|
fallthrough
|
|
case 'Z':
|
|
if len(c.points) != 0 {
|
|
return errParamMismatch
|
|
}
|
|
if c.inPath {
|
|
c.Path.Stop(true)
|
|
c.placeX = c.pathStartX
|
|
c.placeY = c.pathStartY
|
|
c.inPath = false
|
|
}
|
|
case 'm':
|
|
rel = true
|
|
fallthrough
|
|
case 'M':
|
|
if !c.hasSetsOrMore(2, rel) {
|
|
return errParamMismatch
|
|
}
|
|
c.pathStartX, c.pathStartY = c.points[0], c.points[1]
|
|
c.inPath = true
|
|
c.Path.Start(fixed.Point26_6{X: fixed.Int26_6((c.pathStartX) * 64), Y: fixed.Int26_6((c.pathStartY) * 64)})
|
|
for i := 2; i < l-1; i += 2 {
|
|
c.Path.Line(fixed.Point26_6{
|
|
X: fixed.Int26_6((c.points[i]) * 64),
|
|
Y: fixed.Int26_6((c.points[i+1]) * 64)})
|
|
}
|
|
c.placeX = c.points[l-2]
|
|
c.placeY = c.points[l-1]
|
|
case 'l':
|
|
rel = true
|
|
fallthrough
|
|
case 'L':
|
|
if !c.hasSetsOrMore(2, rel) {
|
|
return errParamMismatch
|
|
}
|
|
for i := 0; i < l-1; i += 2 {
|
|
c.Path.Line(fixed.Point26_6{
|
|
X: fixed.Int26_6((c.points[i]) * 64),
|
|
Y: fixed.Int26_6((c.points[i+1]) * 64)})
|
|
}
|
|
c.placeX = c.points[l-2]
|
|
c.placeY = c.points[l-1]
|
|
case 'v':
|
|
c.valsToAbs(c.placeY)
|
|
fallthrough
|
|
case 'V':
|
|
if !c.hasSetsOrMore(1, false) {
|
|
return errParamMismatch
|
|
}
|
|
for _, p := range c.points {
|
|
c.Path.Line(fixed.Point26_6{
|
|
X: fixed.Int26_6((c.placeX) * 64),
|
|
Y: fixed.Int26_6((p) * 64)})
|
|
}
|
|
c.placeY = c.points[l-1]
|
|
case 'h':
|
|
c.valsToAbs(c.placeX)
|
|
fallthrough
|
|
case 'H':
|
|
if !c.hasSetsOrMore(1, false) {
|
|
return errParamMismatch
|
|
}
|
|
for _, p := range c.points {
|
|
c.Path.Line(fixed.Point26_6{
|
|
X: fixed.Int26_6((p) * 64),
|
|
Y: fixed.Int26_6((c.placeY) * 64)})
|
|
}
|
|
c.placeX = c.points[l-1]
|
|
case 'q':
|
|
rel = true
|
|
fallthrough
|
|
case 'Q':
|
|
if !c.hasSetsOrMore(4, rel) {
|
|
return errParamMismatch
|
|
}
|
|
for i := 0; i < l-3; i += 4 {
|
|
c.Path.QuadBezier(
|
|
fixed.Point26_6{
|
|
X: fixed.Int26_6((c.points[i]) * 64),
|
|
Y: fixed.Int26_6((c.points[i+1]) * 64)},
|
|
fixed.Point26_6{
|
|
X: fixed.Int26_6((c.points[i+2]) * 64),
|
|
Y: fixed.Int26_6((c.points[i+3]) * 64)})
|
|
}
|
|
c.cntlPtX, c.cntlPtY = c.points[l-4], c.points[l-3]
|
|
c.placeX = c.points[l-2]
|
|
c.placeY = c.points[l-1]
|
|
case 't':
|
|
rel = true
|
|
fallthrough
|
|
case 'T':
|
|
if !c.hasSetsOrMore(2, rel) {
|
|
return errParamMismatch
|
|
}
|
|
for i := 0; i < l-1; i += 2 {
|
|
c.reflectControlQuad()
|
|
c.Path.QuadBezier(
|
|
fixed.Point26_6{
|
|
X: fixed.Int26_6((c.cntlPtX) * 64),
|
|
Y: fixed.Int26_6((c.cntlPtY) * 64)},
|
|
fixed.Point26_6{
|
|
X: fixed.Int26_6((c.points[i]) * 64),
|
|
Y: fixed.Int26_6((c.points[i+1]) * 64)})
|
|
c.lastKey = k
|
|
c.placeX = c.points[i]
|
|
c.placeY = c.points[i+1]
|
|
}
|
|
case 'c':
|
|
rel = true
|
|
fallthrough
|
|
case 'C':
|
|
if !c.hasSetsOrMore(6, rel) {
|
|
return errParamMismatch
|
|
}
|
|
for i := 0; i < l-5; i += 6 {
|
|
c.Path.CubeBezier(
|
|
fixed.Point26_6{
|
|
X: fixed.Int26_6((c.points[i]) * 64),
|
|
Y: fixed.Int26_6((c.points[i+1]) * 64)},
|
|
fixed.Point26_6{
|
|
X: fixed.Int26_6((c.points[i+2]) * 64),
|
|
Y: fixed.Int26_6((c.points[i+3]) * 64)},
|
|
fixed.Point26_6{
|
|
X: fixed.Int26_6((c.points[i+4]) * 64),
|
|
Y: fixed.Int26_6((c.points[i+5]) * 64)})
|
|
}
|
|
c.cntlPtX, c.cntlPtY = c.points[l-4], c.points[l-3]
|
|
c.placeX = c.points[l-2]
|
|
c.placeY = c.points[l-1]
|
|
case 's':
|
|
rel = true
|
|
fallthrough
|
|
case 'S':
|
|
if !c.hasSetsOrMore(4, rel) {
|
|
return errParamMismatch
|
|
}
|
|
for i := 0; i < l-3; i += 4 {
|
|
c.reflectControlCube()
|
|
c.Path.CubeBezier(fixed.Point26_6{
|
|
X: fixed.Int26_6((c.cntlPtX) * 64), Y: fixed.Int26_6((c.cntlPtY) * 64)},
|
|
fixed.Point26_6{
|
|
X: fixed.Int26_6((c.points[i]) * 64), Y: fixed.Int26_6((c.points[i+1]) * 64)},
|
|
fixed.Point26_6{
|
|
X: fixed.Int26_6((c.points[i+2]) * 64), Y: fixed.Int26_6((c.points[i+3]) * 64)})
|
|
c.lastKey = k
|
|
c.cntlPtX, c.cntlPtY = c.points[i], c.points[i+1]
|
|
c.placeX = c.points[i+2]
|
|
c.placeY = c.points[i+3]
|
|
}
|
|
case 'a', 'A':
|
|
if !c.hasSetsOrMore(7, false) {
|
|
return errParamMismatch
|
|
}
|
|
for i := 0; i < l-6; i += 7 {
|
|
if k == 'a' {
|
|
c.points[i+5] += c.placeX
|
|
c.points[i+6] += c.placeY
|
|
}
|
|
c.AddArcFromA(c.points[i:])
|
|
}
|
|
default:
|
|
if c.ErrorMode == StrictErrorMode {
|
|
return errCommandUnknown
|
|
}
|
|
if c.ErrorMode == WarnErrorMode {
|
|
log.Println("Ignoring svg command " + string(k))
|
|
}
|
|
}
|
|
// So we know how to extend some segment types
|
|
c.lastKey = k
|
|
return nil
|
|
}
|
|
|
|
func (c *PathCursor) init() {
|
|
c.placeX = 0.0
|
|
c.placeY = 0.0
|
|
c.points = c.points[0:0]
|
|
c.lastKey = ' '
|
|
c.Path.Clear()
|
|
c.inPath = false
|
|
}
|