879 lines
19 KiB
Go
879 lines
19 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
type SVGDocument struct {
|
|
Width float64
|
|
Height float64
|
|
ViewBox [4]float64
|
|
Elements []SVGElement
|
|
Groups []SVGGroup
|
|
Warnings []string
|
|
RawSVG []byte
|
|
}
|
|
|
|
type SVGElement struct {
|
|
Type string
|
|
PathData string
|
|
Segments []PathSegment
|
|
Transform [6]float64
|
|
Fill string
|
|
Stroke string
|
|
StrokeW float64
|
|
GroupID int
|
|
BBox Bounds
|
|
}
|
|
|
|
type PathSegment struct {
|
|
Command byte
|
|
Args []float64
|
|
}
|
|
|
|
type SVGGroup struct {
|
|
ID string
|
|
Label string
|
|
Transform [6]float64
|
|
Children []int
|
|
Visible bool
|
|
}
|
|
|
|
func ParseSVG(path string) (*SVGDocument, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read SVG: %v", err)
|
|
}
|
|
|
|
doc := &SVGDocument{
|
|
RawSVG: data,
|
|
}
|
|
|
|
decoder := xml.NewDecoder(strings.NewReader(string(data)))
|
|
var groupStack []int
|
|
currentGroupID := -1
|
|
|
|
for {
|
|
tok, err := decoder.Token()
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
switch t := tok.(type) {
|
|
case xml.StartElement:
|
|
switch t.Name.Local {
|
|
case "svg":
|
|
doc.parseSVGRoot(t)
|
|
|
|
case "g":
|
|
g := parseGroupAttrs(t)
|
|
gid := len(doc.Groups)
|
|
doc.Groups = append(doc.Groups, g)
|
|
if currentGroupID >= 0 {
|
|
doc.Groups[currentGroupID].Children = append(doc.Groups[currentGroupID].Children, gid)
|
|
}
|
|
groupStack = append(groupStack, currentGroupID)
|
|
currentGroupID = gid
|
|
|
|
case "path":
|
|
el := doc.parsePathElement(t, currentGroupID)
|
|
doc.Elements = append(doc.Elements, el)
|
|
|
|
case "rect":
|
|
el := doc.parseRectElement(t, currentGroupID)
|
|
doc.Elements = append(doc.Elements, el)
|
|
|
|
case "circle":
|
|
el := doc.parseCircleElement(t, currentGroupID)
|
|
doc.Elements = append(doc.Elements, el)
|
|
|
|
case "ellipse":
|
|
el := doc.parseEllipseElement(t, currentGroupID)
|
|
doc.Elements = append(doc.Elements, el)
|
|
|
|
case "line":
|
|
el := doc.parseLineElement(t, currentGroupID)
|
|
doc.Elements = append(doc.Elements, el)
|
|
|
|
case "polyline":
|
|
el := doc.parsePolyElement(t, currentGroupID, "polyline")
|
|
doc.Elements = append(doc.Elements, el)
|
|
|
|
case "polygon":
|
|
el := doc.parsePolyElement(t, currentGroupID, "polygon")
|
|
doc.Elements = append(doc.Elements, el)
|
|
|
|
case "text":
|
|
doc.Warnings = appendUnique(doc.Warnings, "Convert text to paths before importing")
|
|
decoder.Skip()
|
|
|
|
case "image":
|
|
doc.Warnings = appendUnique(doc.Warnings, "Embedded raster images (<image>) are not supported")
|
|
decoder.Skip()
|
|
}
|
|
|
|
case xml.EndElement:
|
|
if t.Name.Local == "g" && len(groupStack) > 0 {
|
|
currentGroupID = groupStack[len(groupStack)-1]
|
|
groupStack = groupStack[:len(groupStack)-1]
|
|
}
|
|
}
|
|
}
|
|
|
|
for i := range doc.Elements {
|
|
doc.Elements[i].BBox = computeElementBBox(&doc.Elements[i])
|
|
}
|
|
|
|
return doc, nil
|
|
}
|
|
|
|
// identityTransform returns [a,b,c,d,e,f] for the identity matrix
|
|
func identityTransform() [6]float64 {
|
|
return [6]float64{1, 0, 0, 1, 0, 0}
|
|
}
|
|
|
|
func (doc *SVGDocument) parseSVGRoot(el xml.StartElement) {
|
|
for _, a := range el.Attr {
|
|
switch a.Name.Local {
|
|
case "width":
|
|
doc.Width = parseLengthMM(a.Value)
|
|
case "height":
|
|
doc.Height = parseLengthMM(a.Value)
|
|
case "viewBox":
|
|
parts := splitFloats(a.Value)
|
|
if len(parts) >= 4 {
|
|
doc.ViewBox = [4]float64{parts[0], parts[1], parts[2], parts[3]}
|
|
}
|
|
case "version":
|
|
if a.Value != "" && a.Value != "1.1" {
|
|
doc.Warnings = appendUnique(doc.Warnings, "SVG version is "+a.Value+"; for best results re-export as plain SVG 1.1 from Inkscape")
|
|
}
|
|
}
|
|
}
|
|
|
|
if doc.Width == 0 && doc.ViewBox[2] > 0 {
|
|
doc.Width = doc.ViewBox[2] * (25.4 / 96.0)
|
|
}
|
|
if doc.Height == 0 && doc.ViewBox[3] > 0 {
|
|
doc.Height = doc.ViewBox[3] * (25.4 / 96.0)
|
|
}
|
|
}
|
|
|
|
func parseGroupAttrs(el xml.StartElement) SVGGroup {
|
|
g := SVGGroup{
|
|
Transform: identityTransform(),
|
|
Visible: true,
|
|
}
|
|
for _, a := range el.Attr {
|
|
switch {
|
|
case a.Name.Local == "id":
|
|
g.ID = a.Value
|
|
case a.Name.Local == "transform":
|
|
g.Transform = parseTransform(a.Value)
|
|
case a.Name.Local == "label" && a.Name.Space == "http://www.inkscape.org/namespaces/inkscape":
|
|
g.Label = a.Value
|
|
case a.Name.Local == "style":
|
|
if strings.Contains(a.Value, "display:none") || strings.Contains(a.Value, "display: none") {
|
|
g.Visible = false
|
|
}
|
|
case a.Name.Local == "display":
|
|
if a.Value == "none" {
|
|
g.Visible = false
|
|
}
|
|
}
|
|
}
|
|
return g
|
|
}
|
|
|
|
func (doc *SVGDocument) baseElement(el xml.StartElement, groupID int) SVGElement {
|
|
e := SVGElement{
|
|
Transform: identityTransform(),
|
|
GroupID: groupID,
|
|
}
|
|
for _, a := range el.Attr {
|
|
switch a.Name.Local {
|
|
case "transform":
|
|
e.Transform = parseTransform(a.Value)
|
|
case "fill":
|
|
e.Fill = a.Value
|
|
case "stroke":
|
|
e.Stroke = a.Value
|
|
case "stroke-width":
|
|
e.StrokeW, _ = strconv.ParseFloat(a.Value, 64)
|
|
case "style":
|
|
e.parseStyleAttr(a.Value)
|
|
}
|
|
}
|
|
return e
|
|
}
|
|
|
|
func (e *SVGElement) parseStyleAttr(style string) {
|
|
for _, part := range strings.Split(style, ";") {
|
|
kv := strings.SplitN(strings.TrimSpace(part), ":", 2)
|
|
if len(kv) != 2 {
|
|
continue
|
|
}
|
|
k, v := strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1])
|
|
switch k {
|
|
case "fill":
|
|
e.Fill = v
|
|
case "stroke":
|
|
e.Stroke = v
|
|
case "stroke-width":
|
|
e.StrokeW, _ = strconv.ParseFloat(v, 64)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (doc *SVGDocument) parsePathElement(el xml.StartElement, groupID int) SVGElement {
|
|
e := doc.baseElement(el, groupID)
|
|
e.Type = "path"
|
|
for _, a := range el.Attr {
|
|
if a.Name.Local == "d" {
|
|
e.PathData = a.Value
|
|
e.Segments = parsePath(a.Value)
|
|
}
|
|
}
|
|
return e
|
|
}
|
|
|
|
func (doc *SVGDocument) parseRectElement(el xml.StartElement, groupID int) SVGElement {
|
|
e := doc.baseElement(el, groupID)
|
|
e.Type = "rect"
|
|
var x, y, w, h, rx, ry float64
|
|
for _, a := range el.Attr {
|
|
switch a.Name.Local {
|
|
case "x":
|
|
x, _ = strconv.ParseFloat(a.Value, 64)
|
|
case "y":
|
|
y, _ = strconv.ParseFloat(a.Value, 64)
|
|
case "width":
|
|
w, _ = strconv.ParseFloat(a.Value, 64)
|
|
case "height":
|
|
h, _ = strconv.ParseFloat(a.Value, 64)
|
|
case "rx":
|
|
rx, _ = strconv.ParseFloat(a.Value, 64)
|
|
case "ry":
|
|
ry, _ = strconv.ParseFloat(a.Value, 64)
|
|
}
|
|
}
|
|
e.Segments = rectToPath(x, y, w, h, rx, ry)
|
|
return e
|
|
}
|
|
|
|
func (doc *SVGDocument) parseCircleElement(el xml.StartElement, groupID int) SVGElement {
|
|
e := doc.baseElement(el, groupID)
|
|
e.Type = "circle"
|
|
var cx, cy, r float64
|
|
for _, a := range el.Attr {
|
|
switch a.Name.Local {
|
|
case "cx":
|
|
cx, _ = strconv.ParseFloat(a.Value, 64)
|
|
case "cy":
|
|
cy, _ = strconv.ParseFloat(a.Value, 64)
|
|
case "r":
|
|
r, _ = strconv.ParseFloat(a.Value, 64)
|
|
}
|
|
}
|
|
e.Segments = circleToPath(cx, cy, r)
|
|
return e
|
|
}
|
|
|
|
func (doc *SVGDocument) parseEllipseElement(el xml.StartElement, groupID int) SVGElement {
|
|
e := doc.baseElement(el, groupID)
|
|
e.Type = "ellipse"
|
|
var cx, cy, rx, ry float64
|
|
for _, a := range el.Attr {
|
|
switch a.Name.Local {
|
|
case "cx":
|
|
cx, _ = strconv.ParseFloat(a.Value, 64)
|
|
case "cy":
|
|
cy, _ = strconv.ParseFloat(a.Value, 64)
|
|
case "rx":
|
|
rx, _ = strconv.ParseFloat(a.Value, 64)
|
|
case "ry":
|
|
ry, _ = strconv.ParseFloat(a.Value, 64)
|
|
}
|
|
}
|
|
e.Segments = ellipseToPath(cx, cy, rx, ry)
|
|
return e
|
|
}
|
|
|
|
func (doc *SVGDocument) parseLineElement(el xml.StartElement, groupID int) SVGElement {
|
|
e := doc.baseElement(el, groupID)
|
|
e.Type = "line"
|
|
var x1, y1, x2, y2 float64
|
|
for _, a := range el.Attr {
|
|
switch a.Name.Local {
|
|
case "x1":
|
|
x1, _ = strconv.ParseFloat(a.Value, 64)
|
|
case "y1":
|
|
y1, _ = strconv.ParseFloat(a.Value, 64)
|
|
case "x2":
|
|
x2, _ = strconv.ParseFloat(a.Value, 64)
|
|
case "y2":
|
|
y2, _ = strconv.ParseFloat(a.Value, 64)
|
|
}
|
|
}
|
|
e.Segments = lineToPath(x1, y1, x2, y2)
|
|
return e
|
|
}
|
|
|
|
func (doc *SVGDocument) parsePolyElement(el xml.StartElement, groupID int, typ string) SVGElement {
|
|
e := doc.baseElement(el, groupID)
|
|
e.Type = typ
|
|
for _, a := range el.Attr {
|
|
if a.Name.Local == "points" {
|
|
pts := splitFloats(a.Value)
|
|
if typ == "polygon" {
|
|
e.Segments = polygonToPath(pts)
|
|
} else {
|
|
e.Segments = polylineToPath(pts)
|
|
}
|
|
}
|
|
}
|
|
return e
|
|
}
|
|
|
|
// Shape-to-path converters
|
|
|
|
func rectToPath(x, y, w, h, rx, ry float64) []PathSegment {
|
|
if rx == 0 && ry == 0 {
|
|
return []PathSegment{
|
|
{Command: 'M', Args: []float64{x, y}},
|
|
{Command: 'L', Args: []float64{x + w, y}},
|
|
{Command: 'L', Args: []float64{x + w, y + h}},
|
|
{Command: 'L', Args: []float64{x, y + h}},
|
|
{Command: 'Z'},
|
|
}
|
|
}
|
|
if rx == 0 {
|
|
rx = ry
|
|
}
|
|
if ry == 0 {
|
|
ry = rx
|
|
}
|
|
if rx > w/2 {
|
|
rx = w / 2
|
|
}
|
|
if ry > h/2 {
|
|
ry = h / 2
|
|
}
|
|
return []PathSegment{
|
|
{Command: 'M', Args: []float64{x + rx, y}},
|
|
{Command: 'L', Args: []float64{x + w - rx, y}},
|
|
{Command: 'A', Args: []float64{rx, ry, 0, 0, 1, x + w, y + ry}},
|
|
{Command: 'L', Args: []float64{x + w, y + h - ry}},
|
|
{Command: 'A', Args: []float64{rx, ry, 0, 0, 1, x + w - rx, y + h}},
|
|
{Command: 'L', Args: []float64{x + rx, y + h}},
|
|
{Command: 'A', Args: []float64{rx, ry, 0, 0, 1, x, y + h - ry}},
|
|
{Command: 'L', Args: []float64{x, y + ry}},
|
|
{Command: 'A', Args: []float64{rx, ry, 0, 0, 1, x + rx, y}},
|
|
{Command: 'Z'},
|
|
}
|
|
}
|
|
|
|
func circleToPath(cx, cy, r float64) []PathSegment {
|
|
return ellipseToPath(cx, cy, r, r)
|
|
}
|
|
|
|
func ellipseToPath(cx, cy, rx, ry float64) []PathSegment {
|
|
return []PathSegment{
|
|
{Command: 'M', Args: []float64{cx - rx, cy}},
|
|
{Command: 'A', Args: []float64{rx, ry, 0, 1, 0, cx + rx, cy}},
|
|
{Command: 'A', Args: []float64{rx, ry, 0, 1, 0, cx - rx, cy}},
|
|
{Command: 'Z'},
|
|
}
|
|
}
|
|
|
|
func lineToPath(x1, y1, x2, y2 float64) []PathSegment {
|
|
return []PathSegment{
|
|
{Command: 'M', Args: []float64{x1, y1}},
|
|
{Command: 'L', Args: []float64{x2, y2}},
|
|
}
|
|
}
|
|
|
|
func polylineToPath(coords []float64) []PathSegment {
|
|
if len(coords) < 4 {
|
|
return nil
|
|
}
|
|
segs := []PathSegment{
|
|
{Command: 'M', Args: []float64{coords[0], coords[1]}},
|
|
}
|
|
for i := 2; i+1 < len(coords); i += 2 {
|
|
segs = append(segs, PathSegment{Command: 'L', Args: []float64{coords[i], coords[i+1]}})
|
|
}
|
|
return segs
|
|
}
|
|
|
|
func polygonToPath(coords []float64) []PathSegment {
|
|
segs := polylineToPath(coords)
|
|
if len(segs) > 0 {
|
|
segs = append(segs, PathSegment{Command: 'Z'})
|
|
}
|
|
return segs
|
|
}
|
|
|
|
// Transform parsing
|
|
|
|
var transformFuncRe = regexp.MustCompile(`(\w+)\s*\(([^)]*)\)`)
|
|
|
|
func parseTransform(attr string) [6]float64 {
|
|
result := identityTransform()
|
|
matches := transformFuncRe.FindAllStringSubmatch(attr, -1)
|
|
for _, m := range matches {
|
|
fn := m[1]
|
|
args := splitFloats(m[2])
|
|
var t [6]float64
|
|
switch fn {
|
|
case "matrix":
|
|
if len(args) >= 6 {
|
|
t = [6]float64{args[0], args[1], args[2], args[3], args[4], args[5]}
|
|
} else {
|
|
continue
|
|
}
|
|
case "translate":
|
|
tx := 0.0
|
|
ty := 0.0
|
|
if len(args) >= 1 {
|
|
tx = args[0]
|
|
}
|
|
if len(args) >= 2 {
|
|
ty = args[1]
|
|
}
|
|
t = [6]float64{1, 0, 0, 1, tx, ty}
|
|
case "scale":
|
|
sx := 1.0
|
|
sy := 1.0
|
|
if len(args) >= 1 {
|
|
sx = args[0]
|
|
}
|
|
if len(args) >= 2 {
|
|
sy = args[1]
|
|
} else {
|
|
sy = sx
|
|
}
|
|
t = [6]float64{sx, 0, 0, sy, 0, 0}
|
|
case "rotate":
|
|
if len(args) < 1 {
|
|
continue
|
|
}
|
|
angle := args[0] * math.Pi / 180.0
|
|
c, s := math.Cos(angle), math.Sin(angle)
|
|
if len(args) >= 3 {
|
|
cx, cy := args[1], args[2]
|
|
t = [6]float64{c, s, -s, c, cx - c*cx + s*cy, cy - s*cx - c*cy}
|
|
} else {
|
|
t = [6]float64{c, s, -s, c, 0, 0}
|
|
}
|
|
case "skewX":
|
|
if len(args) < 1 {
|
|
continue
|
|
}
|
|
t = [6]float64{1, 0, math.Tan(args[0] * math.Pi / 180.0), 1, 0, 0}
|
|
case "skewY":
|
|
if len(args) < 1 {
|
|
continue
|
|
}
|
|
t = [6]float64{1, math.Tan(args[0] * math.Pi / 180.0), 0, 1, 0, 0}
|
|
default:
|
|
continue
|
|
}
|
|
result = composeTransforms(result, t)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func composeTransforms(a, b [6]float64) [6]float64 {
|
|
return [6]float64{
|
|
a[0]*b[0] + a[2]*b[1],
|
|
a[1]*b[0] + a[3]*b[1],
|
|
a[0]*b[2] + a[2]*b[3],
|
|
a[1]*b[2] + a[3]*b[3],
|
|
a[0]*b[4] + a[2]*b[5] + a[4],
|
|
a[1]*b[4] + a[3]*b[5] + a[5],
|
|
}
|
|
}
|
|
|
|
// Path d attribute parser
|
|
|
|
func parsePath(d string) []PathSegment {
|
|
var segments []PathSegment
|
|
tokens := tokenizePath(d)
|
|
if len(tokens) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var curX, curY float64
|
|
var startX, startY float64
|
|
var lastCmd byte
|
|
|
|
i := 0
|
|
for i < len(tokens) {
|
|
tok := tokens[i]
|
|
cmd := byte(0)
|
|
|
|
if len(tok) == 1 && isPathCommand(tok[0]) {
|
|
cmd = tok[0]
|
|
i++
|
|
} else if lastCmd != 0 {
|
|
cmd = lastCmd
|
|
if cmd == 'M' {
|
|
cmd = 'L'
|
|
} else if cmd == 'm' {
|
|
cmd = 'l'
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
|
|
rel := cmd >= 'a' && cmd <= 'z'
|
|
upper := cmd
|
|
if rel {
|
|
upper = cmd - 32
|
|
}
|
|
|
|
switch upper {
|
|
case 'M':
|
|
if i+1 >= len(tokens) {
|
|
break
|
|
}
|
|
x, y := parseF(tokens[i]), parseF(tokens[i+1])
|
|
i += 2
|
|
if rel {
|
|
x += curX
|
|
y += curY
|
|
}
|
|
curX, curY = x, y
|
|
startX, startY = x, y
|
|
segments = append(segments, PathSegment{Command: 'M', Args: []float64{x, y}})
|
|
|
|
case 'L':
|
|
if i+1 >= len(tokens) {
|
|
break
|
|
}
|
|
x, y := parseF(tokens[i]), parseF(tokens[i+1])
|
|
i += 2
|
|
if rel {
|
|
x += curX
|
|
y += curY
|
|
}
|
|
curX, curY = x, y
|
|
segments = append(segments, PathSegment{Command: 'L', Args: []float64{x, y}})
|
|
|
|
case 'H':
|
|
if i >= len(tokens) {
|
|
break
|
|
}
|
|
x := parseF(tokens[i])
|
|
i++
|
|
if rel {
|
|
x += curX
|
|
}
|
|
curX = x
|
|
segments = append(segments, PathSegment{Command: 'L', Args: []float64{curX, curY}})
|
|
|
|
case 'V':
|
|
if i >= len(tokens) {
|
|
break
|
|
}
|
|
y := parseF(tokens[i])
|
|
i++
|
|
if rel {
|
|
y += curY
|
|
}
|
|
curY = y
|
|
segments = append(segments, PathSegment{Command: 'L', Args: []float64{curX, curY}})
|
|
|
|
case 'C':
|
|
if i+5 >= len(tokens) {
|
|
break
|
|
}
|
|
args := make([]float64, 6)
|
|
for j := 0; j < 6; j++ {
|
|
args[j] = parseF(tokens[i+j])
|
|
}
|
|
i += 6
|
|
if rel {
|
|
for j := 0; j < 6; j += 2 {
|
|
args[j] += curX
|
|
args[j+1] += curY
|
|
}
|
|
}
|
|
curX, curY = args[4], args[5]
|
|
segments = append(segments, PathSegment{Command: 'C', Args: args})
|
|
|
|
case 'S':
|
|
if i+3 >= len(tokens) {
|
|
break
|
|
}
|
|
args := make([]float64, 4)
|
|
for j := 0; j < 4; j++ {
|
|
args[j] = parseF(tokens[i+j])
|
|
}
|
|
i += 4
|
|
if rel {
|
|
for j := 0; j < 4; j += 2 {
|
|
args[j] += curX
|
|
args[j+1] += curY
|
|
}
|
|
}
|
|
curX, curY = args[2], args[3]
|
|
segments = append(segments, PathSegment{Command: 'S', Args: args})
|
|
|
|
case 'Q':
|
|
if i+3 >= len(tokens) {
|
|
break
|
|
}
|
|
args := make([]float64, 4)
|
|
for j := 0; j < 4; j++ {
|
|
args[j] = parseF(tokens[i+j])
|
|
}
|
|
i += 4
|
|
if rel {
|
|
for j := 0; j < 4; j += 2 {
|
|
args[j] += curX
|
|
args[j+1] += curY
|
|
}
|
|
}
|
|
curX, curY = args[2], args[3]
|
|
segments = append(segments, PathSegment{Command: 'Q', Args: args})
|
|
|
|
case 'T':
|
|
if i+1 >= len(tokens) {
|
|
break
|
|
}
|
|
x, y := parseF(tokens[i]), parseF(tokens[i+1])
|
|
i += 2
|
|
if rel {
|
|
x += curX
|
|
y += curY
|
|
}
|
|
curX, curY = x, y
|
|
segments = append(segments, PathSegment{Command: 'T', Args: []float64{x, y}})
|
|
|
|
case 'A':
|
|
if i+6 >= len(tokens) {
|
|
break
|
|
}
|
|
rx := parseF(tokens[i])
|
|
ry := parseF(tokens[i+1])
|
|
rot := parseF(tokens[i+2])
|
|
largeArc := parseF(tokens[i+3])
|
|
sweep := parseF(tokens[i+4])
|
|
x := parseF(tokens[i+5])
|
|
y := parseF(tokens[i+6])
|
|
i += 7
|
|
if rel {
|
|
x += curX
|
|
y += curY
|
|
}
|
|
curX, curY = x, y
|
|
segments = append(segments, PathSegment{Command: 'A', Args: []float64{rx, ry, rot, largeArc, sweep, x, y}})
|
|
|
|
case 'Z':
|
|
curX, curY = startX, startY
|
|
segments = append(segments, PathSegment{Command: 'Z'})
|
|
|
|
default:
|
|
i++
|
|
}
|
|
|
|
lastCmd = cmd
|
|
}
|
|
|
|
return segments
|
|
}
|
|
|
|
func isPathCommand(c byte) bool {
|
|
return strings.ContainsRune("MmLlHhVvCcSsQqTtAaZz", rune(c))
|
|
}
|
|
|
|
func tokenizePath(d string) []string {
|
|
var tokens []string
|
|
var buf strings.Builder
|
|
flush := func() {
|
|
s := buf.String()
|
|
if s != "" {
|
|
tokens = append(tokens, s)
|
|
buf.Reset()
|
|
}
|
|
}
|
|
|
|
for i := 0; i < len(d); i++ {
|
|
c := d[i]
|
|
if isPathCommand(c) {
|
|
flush()
|
|
tokens = append(tokens, string(c))
|
|
} else if c == ',' || c == ' ' || c == '\t' || c == '\n' || c == '\r' {
|
|
flush()
|
|
} else if c == '-' && buf.Len() > 0 {
|
|
// Negative sign starts new number unless after 'e'/'E' (exponent)
|
|
s := buf.String()
|
|
lastChar := s[len(s)-1]
|
|
if lastChar != 'e' && lastChar != 'E' {
|
|
flush()
|
|
}
|
|
buf.WriteByte(c)
|
|
} else if c == '.' && strings.Contains(buf.String(), ".") {
|
|
flush()
|
|
buf.WriteByte(c)
|
|
} else {
|
|
buf.WriteByte(c)
|
|
}
|
|
}
|
|
flush()
|
|
return tokens
|
|
}
|
|
|
|
func parseF(s string) float64 {
|
|
v, _ := strconv.ParseFloat(s, 64)
|
|
return v
|
|
}
|
|
|
|
// Unit conversion
|
|
|
|
func parseLengthMM(s string) float64 {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return 0
|
|
}
|
|
unit := ""
|
|
numStr := s
|
|
for _, u := range []string{"mm", "cm", "in", "pt", "px"} {
|
|
if strings.HasSuffix(s, u) {
|
|
unit = u
|
|
numStr = strings.TrimSpace(s[:len(s)-len(u)])
|
|
break
|
|
}
|
|
}
|
|
// Strip non-numeric trailing chars
|
|
numStr = strings.TrimRightFunc(numStr, func(r rune) bool {
|
|
return !unicode.IsDigit(r) && r != '.' && r != '-' && r != '+'
|
|
})
|
|
v, _ := strconv.ParseFloat(numStr, 64)
|
|
|
|
switch unit {
|
|
case "mm":
|
|
return v
|
|
case "cm":
|
|
return v * 10.0
|
|
case "in":
|
|
return v * 25.4
|
|
case "pt":
|
|
return v * (25.4 / 72.0)
|
|
case "px", "":
|
|
return v * (25.4 / 96.0)
|
|
}
|
|
return v * (25.4 / 96.0)
|
|
}
|
|
|
|
// Bounding box computation (approximate, from segments)
|
|
|
|
func computeElementBBox(el *SVGElement) Bounds {
|
|
b := Bounds{MinX: math.MaxFloat64, MinY: math.MaxFloat64, MaxX: -math.MaxFloat64, MaxY: -math.MaxFloat64}
|
|
|
|
expandPt := func(x, y float64) {
|
|
tx := el.Transform[0]*x + el.Transform[2]*y + el.Transform[4]
|
|
ty := el.Transform[1]*x + el.Transform[3]*y + el.Transform[5]
|
|
if tx < b.MinX {
|
|
b.MinX = tx
|
|
}
|
|
if tx > b.MaxX {
|
|
b.MaxX = tx
|
|
}
|
|
if ty < b.MinY {
|
|
b.MinY = ty
|
|
}
|
|
if ty > b.MaxY {
|
|
b.MaxY = ty
|
|
}
|
|
}
|
|
|
|
for _, seg := range el.Segments {
|
|
switch seg.Command {
|
|
case 'M', 'L', 'T':
|
|
if len(seg.Args) >= 2 {
|
|
expandPt(seg.Args[0], seg.Args[1])
|
|
}
|
|
case 'C':
|
|
if len(seg.Args) >= 6 {
|
|
expandPt(seg.Args[0], seg.Args[1])
|
|
expandPt(seg.Args[2], seg.Args[3])
|
|
expandPt(seg.Args[4], seg.Args[5])
|
|
}
|
|
case 'S', 'Q':
|
|
if len(seg.Args) >= 4 {
|
|
expandPt(seg.Args[0], seg.Args[1])
|
|
expandPt(seg.Args[2], seg.Args[3])
|
|
}
|
|
case 'A':
|
|
if len(seg.Args) >= 7 {
|
|
expandPt(seg.Args[5], seg.Args[6])
|
|
expandPt(seg.Args[5]-seg.Args[0], seg.Args[6]-seg.Args[1])
|
|
expandPt(seg.Args[5]+seg.Args[0], seg.Args[6]+seg.Args[1])
|
|
}
|
|
}
|
|
}
|
|
|
|
if b.MinX == math.MaxFloat64 {
|
|
return Bounds{}
|
|
}
|
|
return b
|
|
}
|
|
|
|
// Helpers
|
|
|
|
func splitFloats(s string) []float64 {
|
|
s = strings.Map(func(r rune) rune {
|
|
if r == ',' {
|
|
return ' '
|
|
}
|
|
return r
|
|
}, s)
|
|
var result []float64
|
|
for _, part := range strings.Fields(s) {
|
|
v, err := strconv.ParseFloat(part, 64)
|
|
if err == nil {
|
|
result = append(result, v)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func appendUnique(warnings []string, msg string) []string {
|
|
for _, w := range warnings {
|
|
if w == msg {
|
|
return warnings
|
|
}
|
|
}
|
|
return append(warnings, msg)
|
|
}
|
|
|
|
func (doc *SVGDocument) LayerCount() int {
|
|
count := 0
|
|
for _, g := range doc.Groups {
|
|
if g.Label != "" {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func (doc *SVGDocument) VisibleLayerNames() []string {
|
|
var names []string
|
|
for _, g := range doc.Groups {
|
|
if g.Label != "" && g.Visible {
|
|
names = append(names, g.Label)
|
|
}
|
|
}
|
|
return names
|
|
}
|