Skip to content

Commit

Permalink
Merge pull request #15 from fangzhechang/master
Browse files Browse the repository at this point in the history
Handle multiple operations in a same transform string
  • Loading branch information
rustyoz authored Apr 15, 2024
2 parents 488fa3e + 93b06a8 commit f965005
Show file tree
Hide file tree
Showing 4 changed files with 286 additions and 23 deletions.
59 changes: 59 additions & 0 deletions drawinginstruction.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package svg

import "fmt"

// InstructionType tells our path drawing library which function it has
// to call
type InstructionType int
Expand Down Expand Up @@ -37,3 +39,60 @@ type DrawingInstruction struct {
StrokeLineCap *string
StrokeLineJoin *string
}

func (di *DrawingInstruction) String() string {
switch di.Kind {
case MoveInstruction:
return fmt.Sprintf("M%v %v", di.M[0], di.M[1])
case CircleInstruction:
return fmt.Sprintf("circle R=%v", *di.Radius)
case CurveInstruction:
c := di.CurvePoints
return fmt.Sprintf("C%v %v %v %v %v %v",
c.C1[0], c.C1[1], c.C2[0], c.C2[1], c.T[0], c.T[1])
case LineInstruction:
return fmt.Sprintf("L%v %v", di.M[0], di.M[1])
case CloseInstruction:
return "Z"
case PaintInstruction:
pt := ""
if di.Fill != nil {
pt = fmt.Sprintf("%vfill=\"%v\" ", pt, *di.Fill)
}
if di.Stroke != nil {
pt = fmt.Sprintf("%vstroke=\"%v\" ", pt, *di.Stroke)
}
if di.StrokeWidth != nil {
pt = fmt.Sprintf("%vstroke-width=\"%v\" ", pt, *di.StrokeWidth)
}
if di.StrokeLineCap != nil {
pt = fmt.Sprintf("%vstroke-linecap=\"%v\" ", pt, *di.StrokeLineCap)
}
if di.StrokeLineJoin != nil {
pt = fmt.Sprintf("%vstroke-linejoin=\"%v\" ", pt, *di.StrokeLineJoin)
}
return pt
}
return ""
}

// PathStringFromDrawingInstructions converts drawing instructions obtained
// from svg <path/> element back into <path/> form
func PathStringFromDrawingInstructions(dis []*DrawingInstruction) string {
data := " "
sep := ""
var paint *DrawingInstruction
for _, di := range dis {
if di.Kind == PaintInstruction {
paint = di
} else {
data += sep + di.String()
sep = " "
}
}
pt := ""
if paint != nil {
pt = paint.String()
}
return `<path d="` + data + `" ` + pt + `/>`
}
27 changes: 21 additions & 6 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,24 +54,39 @@ func parseTuple(l *gl.Lexer) (Tuple, error) {
}

func parseTransform(tstring string) (mt.Transform, error) {
var x *mt.Transform
lexer, _ := gl.Lex("tlexer", tstring)
for {
i := lexer.NextItem()
var t mt.Transform
var err error
switch i.Type {
case gl.ItemEOS:
return mt.Identity(),
fmt.Errorf("transform parse failed")
if x == nil {
return mt.Identity(),
fmt.Errorf("transform parse failed")
}
return *x, nil
case gl.ItemWord:
switch i.Value {
case "matrix":
return parseMatrix(lexer)
t, err = parseMatrix(lexer)
case "translate":
return parseTranslate(lexer)
t, err = parseTranslate(lexer)
case "rotate":
return parseRotate(lexer)
t, err = parseRotate(lexer)
case "scale":
return parseScale(lexer)
t, err = parseScale(lexer)
}
case gl.ItemWSP:
continue
}
if err != nil {
return t, err
} else if x == nil {
x = &t
} else {
x.MultiplyWith(t) // see https://www.w3.org/TR/SVGTiny12
}
}
}
Expand Down
190 changes: 190 additions & 0 deletions parser_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package svg

import (
"fmt"
"os"
"strings"
"testing"

Expand Down Expand Up @@ -29,3 +31,191 @@ func TestParse(t *testing.T) {
is.NoErr(err)
is.NotNil(svg)
}

func TestTransform(t *testing.T) {
content := `<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="684.000000pt" height="630.000000pt" viewBox="0 0 684.000000 630.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,630.000000) scale(0.100000,-0.100000)"
fill="#ff0000" stroke="none">
<path d="M1705 6274 c-758 -115 -1377 -637 -1614 -1360 -74 -227 -86 -311 -86
-624 0 -248 2 -286 23 -389 64 -315 210 -626 414 -880 34 -42 715 -731 1515
-1531 l1453 -1455 1444 1445 c794 795 1473 1481 1510 1524 98 117 189 259 261
409 233 479 267 1011 99 1516 -240 718 -861 1235 -1615 1345 -138 21 -430 21
-564 1 -213 -32 -397 -87 -580 -174 -188 -90 -319 -177 -478 -316 l-77 -66
-77 66 c-309 270 -657 431 -1054 489 -132 20 -447 20 -574 0z"/>
</g>
</svg>
`
s, err := ParseSvg(content, "transformed heart", 0)
if err != nil {
t.Fatalf("cannot parse svg %v", content)
}
if len(s.Groups) < 1 {
t.Fatal("group not found")
}
g := s.Groups[0]
t.Logf("original: transform=\"%v\"", g.TransformString)
m := *g.Transform
a, c, e := m[0][0], m[0][1], m[0][2]
b, d, f := m[1][0], m[1][1], m[1][2]
// see https://www.w3.org/TR/SVGTiny12 for [a b c d e f] vector notation
t.Logf("accumulated: transform=\"matrix(%v %v %v %v %v %v)\"", a, b, c, d, e, f)
if !(a == 0.1 && d == -0.1 && f == 630) {
t.Error("mismatch expected transform matrix(0.1 0 0 -0.1 0 630)")
}
}

func Test2Curves(t *testing.T) {
content := `<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
viewBox="0 -450 1000 1000">
<path fill="none" stroke="red" stroke-width="5"
d="M100 200 C25 100 400 100 400 200 400 100 775 100 700 200 L400 450z" />
</svg>
`
s, err := ParseSvg(content, "heart", 1)
if err != nil {
t.Fatalf("cannot parse svg %v", content)
}
dis, _ := s.ParseDrawingInstructions()
strux := []*DrawingInstruction{}
for di := range dis {
strux = append(strux, di)
}
curveIdx := 2
di := strux[curveIdx]
if di.Kind != CurveInstruction {
t.Fatalf("expect curve drawing instructions, got %v", di)
}
p := di.CurvePoints // 400 100 775 100 700 200
if !(p.C1[0] == 400 && p.C2[0] == 775 && p.T[0] == 700) {
t.Fatalf("expect [400 100] [775 100] [700 200], got %v %v %v", *p.C1, *p.C2, *p.T)
}
}

func TestPathRelScale(t *testing.T) {
content := `<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
viewBox="0 -600 800 500">
<g transform="scale(1,-1)">
<path fill="none" stroke="red" stroke-width="5"
d="M100 200 c-75 -100 300 -100 300 0 0 -100 375 -100 300 0 l-300 350z" />
</g>
</svg>
`
s, err := ParseSvg(content, "", 1)
if err != nil {
t.Fatalf("cannot parse svg %v", content)
}
dis, _ := s.ParseDrawingInstructions()
strux := []*DrawingInstruction{}
for di := range dis {
strux = append(strux, di)
}
curveIdx := 1 // Move Curve*2 Line Close Paint
di := strux[curveIdx]
if di.Kind != CurveInstruction {
t.Fatalf("expect curve (c) instruction at %v, got %v", curveIdx, di)
}
if di.CurvePoints.T[1] != -200 { // [100+300, 200+0] scale by [1, -1]
t.Fatalf("expect 1st curve terminating at [400, -200], got %v",
*di.CurvePoints.T)
}
}

func TestPathRelTranslate(t *testing.T) {
content := `<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
viewBox="0 400 600 600">
<g fill="#ff0000" stroke="none" transform="translate(0,200)">
<path d="M170 627 c-75 -11 -137 -63 -161 -136 -7 -22 -8 -31 -8
-62 0 -24 0.2 -28 2 -38 z"/>
</g>
</svg>
`
s, err := ParseSvg(content, "", 1)
if err != nil {
t.Fatalf("cannot parse svg %v", content)
}
dis, errChan := s.ParseDrawingInstructions()
for e := range errChan {
t.Fatalf("parse drawing instruction: %v", e)
}
strux := []*DrawingInstruction{}
for di := range dis {
strux = append(strux, di)
}
c1 := strux[1]
c3 := strux[3]
if c1.CurvePoints.C1[1] != 816 || c3.CurvePoints.T[1] != 591 {
expect := "C95 816 33 764 9 691 C2 669 1 660 1 629 C1 605 1.2 601 3 591"
got := fmt.Sprintf("C%v %v ... %v", c1.CurvePoints.C1[0],
c1.CurvePoints.C1[1], c3.CurvePoints.T[1])
t.Fatalf("expect %v: got %v", expect, got)
}
}

func TestParsedPathString(t *testing.T) {
header := `<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="684pt" height="630pt" viewBox="0 0 684 630"
preserveAspectRatio="xMidYMid meet">
`
path := `<g transform="translate(0,630) scale(0.1,-0.1)"
fill="#ff0000" stroke="none">
<path d="M1705 6274 c-758 -115 -1377 -637 -1614 -1360 -74 -227 -86 -311 -86
-624 0 -248 2 -286 23 -389 64 -315 210 -626 414 -880 34 -42 715 -731 1515
-1531 l1453 -1455 1444 1445 c794 795 1473 1481 1510 1524 98 117 189 259 261
409 233 479 267 1011 99 1516 -240 718 -861 1235 -1615 1345 -138 21 -430 21
-564 1 -213 -32 -397 -87 -580 -174 -188 -90 -319 -177 -478 -316 l-77 -66
-77 66 c-309 270 -657 431 -1054 489 -132 20 -447 20 -574 0z"/>
</g>
`
tail := `</svg>
`
content := header + path + tail
s, err := ParseSvg(content, "heart", 1)
if err != nil {
t.Fatalf("cannot parse svg %v", content)
}
dis, errChan := s.ParseDrawingInstructions()
for e := range errChan {
t.Fatalf("drawing instruction error: %v", e)
}
strux := []*DrawingInstruction{}
for di := range dis {
strux = append(strux, di)
}
tmpdir := os.TempDir() + string(os.PathSeparator)
f0 := tmpdir + "heart.svg"
f, err := os.Create(f0)
if err != nil {
t.Errorf("error %v", err)
}
f.WriteString(content)
f.Close()
t.Logf("original shape in %v", f0)

parsed := PathStringFromDrawingInstructions(strux)
f1 := tmpdir + "parsedheart.svg"
f, err = os.Create(f1)
if err != nil {
t.Errorf("error %v", err)
}
f.WriteString(header + parsed + tail)
f.Close()
t.Logf("parsed shape in %v", f1)
t.Log("Please check consistency of above files, with web browser or eog")
}
33 changes: 16 additions & 17 deletions path.go
Original file line number Diff line number Diff line change
Expand Up @@ -695,12 +695,19 @@ func (pdp *pathDescriptionParser) parseCurveToRelDI() error {
tuples = append(tuples, t)
pdp.lex.ConsumeWhiteSpace()
}
x, y := pdp.transform.Apply(pdp.x, pdp.y)

for j := 0; j < len(tuples)/3; j++ { // convert to absolute
j3 := j * 3
for i := 0; i < 3; i++ {
tuples[j3+i][0] += pdp.x
tuples[j3+i][1] += pdp.y
}
t := tuples[j3+2]
pdp.x, pdp.y = t[0], t[1]
}
for j := 0; j < len(tuples)/3; j++ {
c1x, c1y := pdp.transform.Apply(x+tuples[j*3][0], y+tuples[j*3][1])
c2x, c2y := pdp.transform.Apply(x+tuples[j*3+1][0], y+tuples[j*3+1][1])
tx, ty := pdp.transform.Apply(x+tuples[j*3+2][0], y+tuples[j*3+2][1])
c1x, c1y := pdp.transform.Apply(tuples[j*3][0], tuples[j*3][1])
c2x, c2y := pdp.transform.Apply(tuples[j*3+1][0], tuples[j*3+1][1])
tx, ty := pdp.transform.Apply(tuples[j*3+2][0], tuples[j*3+2][1])

pdp.p.instructions <- &DrawingInstruction{
Kind: CurveInstruction,
Expand All @@ -709,10 +716,6 @@ func (pdp *pathDescriptionParser) parseCurveToRelDI() error {
T: &Tuple{tx, ty},
},
}

pdp.x += tuples[j*3+2][0]
pdp.y += tuples[j*3+2][1]
x, y = pdp.transform.Apply(pdp.x, pdp.y)
}

return nil
Expand Down Expand Up @@ -773,10 +776,7 @@ func (pdp *pathDescriptionParser) parseCurveToRel() error {
}

func (pdp *pathDescriptionParser) parseCurveToAbsDI() error {
var (
tuples []Tuple
instrTuples []Tuple
)
var tuples []Tuple

pdp.lex.ConsumeWhiteSpace()
for pdp.lex.PeekItem().Type == gl.ItemNumber {
Expand All @@ -790,6 +790,7 @@ func (pdp *pathDescriptionParser) parseCurveToAbsDI() error {
}

for j := 0; j < len(tuples)/3; j++ {
instrTuples := []Tuple{}
for _, nt := range tuples[j*3 : (j+1)*3] {
pdp.x = nt[0]
pdp.y = nt[1]
Expand All @@ -812,10 +813,7 @@ func (pdp *pathDescriptionParser) parseCurveToAbsDI() error {
}

func (pdp *pathDescriptionParser) parseCurveToAbs() error {
var (
tuples []Tuple
instrTuples []Tuple
)
var tuples []Tuple

pdp.lex.ConsumeWhiteSpace()
for pdp.lex.PeekItem().Type == gl.ItemNumber {
Expand All @@ -832,6 +830,7 @@ func (pdp *pathDescriptionParser) parseCurveToAbs() error {
pdp.currentsegment.addPoint([2]float64{x, y})

for j := 0; j < len(tuples)/3; j++ {
instrTuples := []Tuple{}
var cb cubicBezier
cb.controlpoints[0][0] = pdp.x
cb.controlpoints[0][1] = pdp.y
Expand Down

0 comments on commit f965005

Please sign in to comment.