Skip to content

Commit f965005

Browse files
authored
Merge pull request #15 from fangzhechang/master
Handle multiple operations in a same transform string
2 parents 488fa3e + 93b06a8 commit f965005

File tree

4 files changed

+286
-23
lines changed

4 files changed

+286
-23
lines changed

drawinginstruction.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package svg
22

3+
import "fmt"
4+
35
// InstructionType tells our path drawing library which function it has
46
// to call
57
type InstructionType int
@@ -37,3 +39,60 @@ type DrawingInstruction struct {
3739
StrokeLineCap *string
3840
StrokeLineJoin *string
3941
}
42+
43+
func (di *DrawingInstruction) String() string {
44+
switch di.Kind {
45+
case MoveInstruction:
46+
return fmt.Sprintf("M%v %v", di.M[0], di.M[1])
47+
case CircleInstruction:
48+
return fmt.Sprintf("circle R=%v", *di.Radius)
49+
case CurveInstruction:
50+
c := di.CurvePoints
51+
return fmt.Sprintf("C%v %v %v %v %v %v",
52+
c.C1[0], c.C1[1], c.C2[0], c.C2[1], c.T[0], c.T[1])
53+
case LineInstruction:
54+
return fmt.Sprintf("L%v %v", di.M[0], di.M[1])
55+
case CloseInstruction:
56+
return "Z"
57+
case PaintInstruction:
58+
pt := ""
59+
if di.Fill != nil {
60+
pt = fmt.Sprintf("%vfill=\"%v\" ", pt, *di.Fill)
61+
}
62+
if di.Stroke != nil {
63+
pt = fmt.Sprintf("%vstroke=\"%v\" ", pt, *di.Stroke)
64+
}
65+
if di.StrokeWidth != nil {
66+
pt = fmt.Sprintf("%vstroke-width=\"%v\" ", pt, *di.StrokeWidth)
67+
}
68+
if di.StrokeLineCap != nil {
69+
pt = fmt.Sprintf("%vstroke-linecap=\"%v\" ", pt, *di.StrokeLineCap)
70+
}
71+
if di.StrokeLineJoin != nil {
72+
pt = fmt.Sprintf("%vstroke-linejoin=\"%v\" ", pt, *di.StrokeLineJoin)
73+
}
74+
return pt
75+
}
76+
return ""
77+
}
78+
79+
// PathStringFromDrawingInstructions converts drawing instructions obtained
80+
// from svg <path/> element back into <path/> form
81+
func PathStringFromDrawingInstructions(dis []*DrawingInstruction) string {
82+
data := " "
83+
sep := ""
84+
var paint *DrawingInstruction
85+
for _, di := range dis {
86+
if di.Kind == PaintInstruction {
87+
paint = di
88+
} else {
89+
data += sep + di.String()
90+
sep = " "
91+
}
92+
}
93+
pt := ""
94+
if paint != nil {
95+
pt = paint.String()
96+
}
97+
return `<path d="` + data + `" ` + pt + `/>`
98+
}

parser.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,24 +54,39 @@ func parseTuple(l *gl.Lexer) (Tuple, error) {
5454
}
5555

5656
func parseTransform(tstring string) (mt.Transform, error) {
57+
var x *mt.Transform
5758
lexer, _ := gl.Lex("tlexer", tstring)
5859
for {
5960
i := lexer.NextItem()
61+
var t mt.Transform
62+
var err error
6063
switch i.Type {
6164
case gl.ItemEOS:
62-
return mt.Identity(),
63-
fmt.Errorf("transform parse failed")
65+
if x == nil {
66+
return mt.Identity(),
67+
fmt.Errorf("transform parse failed")
68+
}
69+
return *x, nil
6470
case gl.ItemWord:
6571
switch i.Value {
6672
case "matrix":
67-
return parseMatrix(lexer)
73+
t, err = parseMatrix(lexer)
6874
case "translate":
69-
return parseTranslate(lexer)
75+
t, err = parseTranslate(lexer)
7076
case "rotate":
71-
return parseRotate(lexer)
77+
t, err = parseRotate(lexer)
7278
case "scale":
73-
return parseScale(lexer)
79+
t, err = parseScale(lexer)
7480
}
81+
case gl.ItemWSP:
82+
continue
83+
}
84+
if err != nil {
85+
return t, err
86+
} else if x == nil {
87+
x = &t
88+
} else {
89+
x.MultiplyWith(t) // see https://www.w3.org/TR/SVGTiny12
7590
}
7691
}
7792
}

parser_test.go

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package svg
22

33
import (
4+
"fmt"
5+
"os"
46
"strings"
57
"testing"
68

@@ -29,3 +31,191 @@ func TestParse(t *testing.T) {
2931
is.NoErr(err)
3032
is.NotNil(svg)
3133
}
34+
35+
func TestTransform(t *testing.T) {
36+
content := `<?xml version="1.0" standalone="no"?>
37+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
38+
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
39+
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
40+
width="684.000000pt" height="630.000000pt" viewBox="0 0 684.000000 630.000000"
41+
preserveAspectRatio="xMidYMid meet">
42+
<g transform="translate(0.000000,630.000000) scale(0.100000,-0.100000)"
43+
fill="#ff0000" stroke="none">
44+
<path d="M1705 6274 c-758 -115 -1377 -637 -1614 -1360 -74 -227 -86 -311 -86
45+
-624 0 -248 2 -286 23 -389 64 -315 210 -626 414 -880 34 -42 715 -731 1515
46+
-1531 l1453 -1455 1444 1445 c794 795 1473 1481 1510 1524 98 117 189 259 261
47+
409 233 479 267 1011 99 1516 -240 718 -861 1235 -1615 1345 -138 21 -430 21
48+
-564 1 -213 -32 -397 -87 -580 -174 -188 -90 -319 -177 -478 -316 l-77 -66
49+
-77 66 c-309 270 -657 431 -1054 489 -132 20 -447 20 -574 0z"/>
50+
</g>
51+
</svg>
52+
`
53+
s, err := ParseSvg(content, "transformed heart", 0)
54+
if err != nil {
55+
t.Fatalf("cannot parse svg %v", content)
56+
}
57+
if len(s.Groups) < 1 {
58+
t.Fatal("group not found")
59+
}
60+
g := s.Groups[0]
61+
t.Logf("original: transform=\"%v\"", g.TransformString)
62+
m := *g.Transform
63+
a, c, e := m[0][0], m[0][1], m[0][2]
64+
b, d, f := m[1][0], m[1][1], m[1][2]
65+
// see https://www.w3.org/TR/SVGTiny12 for [a b c d e f] vector notation
66+
t.Logf("accumulated: transform=\"matrix(%v %v %v %v %v %v)\"", a, b, c, d, e, f)
67+
if !(a == 0.1 && d == -0.1 && f == 630) {
68+
t.Error("mismatch expected transform matrix(0.1 0 0 -0.1 0 630)")
69+
}
70+
}
71+
72+
func Test2Curves(t *testing.T) {
73+
content := `<?xml version="1.0" standalone="no"?>
74+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
75+
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
76+
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
77+
viewBox="0 -450 1000 1000">
78+
<path fill="none" stroke="red" stroke-width="5"
79+
d="M100 200 C25 100 400 100 400 200 400 100 775 100 700 200 L400 450z" />
80+
</svg>
81+
`
82+
s, err := ParseSvg(content, "heart", 1)
83+
if err != nil {
84+
t.Fatalf("cannot parse svg %v", content)
85+
}
86+
dis, _ := s.ParseDrawingInstructions()
87+
strux := []*DrawingInstruction{}
88+
for di := range dis {
89+
strux = append(strux, di)
90+
}
91+
curveIdx := 2
92+
di := strux[curveIdx]
93+
if di.Kind != CurveInstruction {
94+
t.Fatalf("expect curve drawing instructions, got %v", di)
95+
}
96+
p := di.CurvePoints // 400 100 775 100 700 200
97+
if !(p.C1[0] == 400 && p.C2[0] == 775 && p.T[0] == 700) {
98+
t.Fatalf("expect [400 100] [775 100] [700 200], got %v %v %v", *p.C1, *p.C2, *p.T)
99+
}
100+
}
101+
102+
func TestPathRelScale(t *testing.T) {
103+
content := `<?xml version="1.0" standalone="no"?>
104+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
105+
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
106+
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
107+
viewBox="0 -600 800 500">
108+
<g transform="scale(1,-1)">
109+
<path fill="none" stroke="red" stroke-width="5"
110+
d="M100 200 c-75 -100 300 -100 300 0 0 -100 375 -100 300 0 l-300 350z" />
111+
</g>
112+
</svg>
113+
`
114+
s, err := ParseSvg(content, "", 1)
115+
if err != nil {
116+
t.Fatalf("cannot parse svg %v", content)
117+
}
118+
dis, _ := s.ParseDrawingInstructions()
119+
strux := []*DrawingInstruction{}
120+
for di := range dis {
121+
strux = append(strux, di)
122+
}
123+
curveIdx := 1 // Move Curve*2 Line Close Paint
124+
di := strux[curveIdx]
125+
if di.Kind != CurveInstruction {
126+
t.Fatalf("expect curve (c) instruction at %v, got %v", curveIdx, di)
127+
}
128+
if di.CurvePoints.T[1] != -200 { // [100+300, 200+0] scale by [1, -1]
129+
t.Fatalf("expect 1st curve terminating at [400, -200], got %v",
130+
*di.CurvePoints.T)
131+
}
132+
}
133+
134+
func TestPathRelTranslate(t *testing.T) {
135+
content := `<?xml version="1.0" standalone="no"?>
136+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
137+
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
138+
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
139+
viewBox="0 400 600 600">
140+
<g fill="#ff0000" stroke="none" transform="translate(0,200)">
141+
<path d="M170 627 c-75 -11 -137 -63 -161 -136 -7 -22 -8 -31 -8
142+
-62 0 -24 0.2 -28 2 -38 z"/>
143+
</g>
144+
</svg>
145+
`
146+
s, err := ParseSvg(content, "", 1)
147+
if err != nil {
148+
t.Fatalf("cannot parse svg %v", content)
149+
}
150+
dis, errChan := s.ParseDrawingInstructions()
151+
for e := range errChan {
152+
t.Fatalf("parse drawing instruction: %v", e)
153+
}
154+
strux := []*DrawingInstruction{}
155+
for di := range dis {
156+
strux = append(strux, di)
157+
}
158+
c1 := strux[1]
159+
c3 := strux[3]
160+
if c1.CurvePoints.C1[1] != 816 || c3.CurvePoints.T[1] != 591 {
161+
expect := "C95 816 33 764 9 691 C2 669 1 660 1 629 C1 605 1.2 601 3 591"
162+
got := fmt.Sprintf("C%v %v ... %v", c1.CurvePoints.C1[0],
163+
c1.CurvePoints.C1[1], c3.CurvePoints.T[1])
164+
t.Fatalf("expect %v: got %v", expect, got)
165+
}
166+
}
167+
168+
func TestParsedPathString(t *testing.T) {
169+
header := `<?xml version="1.0" standalone="no"?>
170+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
171+
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
172+
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
173+
width="684pt" height="630pt" viewBox="0 0 684 630"
174+
preserveAspectRatio="xMidYMid meet">
175+
`
176+
path := `<g transform="translate(0,630) scale(0.1,-0.1)"
177+
fill="#ff0000" stroke="none">
178+
<path d="M1705 6274 c-758 -115 -1377 -637 -1614 -1360 -74 -227 -86 -311 -86
179+
-624 0 -248 2 -286 23 -389 64 -315 210 -626 414 -880 34 -42 715 -731 1515
180+
-1531 l1453 -1455 1444 1445 c794 795 1473 1481 1510 1524 98 117 189 259 261
181+
409 233 479 267 1011 99 1516 -240 718 -861 1235 -1615 1345 -138 21 -430 21
182+
-564 1 -213 -32 -397 -87 -580 -174 -188 -90 -319 -177 -478 -316 l-77 -66
183+
-77 66 c-309 270 -657 431 -1054 489 -132 20 -447 20 -574 0z"/>
184+
</g>
185+
`
186+
tail := `</svg>
187+
`
188+
content := header + path + tail
189+
s, err := ParseSvg(content, "heart", 1)
190+
if err != nil {
191+
t.Fatalf("cannot parse svg %v", content)
192+
}
193+
dis, errChan := s.ParseDrawingInstructions()
194+
for e := range errChan {
195+
t.Fatalf("drawing instruction error: %v", e)
196+
}
197+
strux := []*DrawingInstruction{}
198+
for di := range dis {
199+
strux = append(strux, di)
200+
}
201+
tmpdir := os.TempDir() + string(os.PathSeparator)
202+
f0 := tmpdir + "heart.svg"
203+
f, err := os.Create(f0)
204+
if err != nil {
205+
t.Errorf("error %v", err)
206+
}
207+
f.WriteString(content)
208+
f.Close()
209+
t.Logf("original shape in %v", f0)
210+
211+
parsed := PathStringFromDrawingInstructions(strux)
212+
f1 := tmpdir + "parsedheart.svg"
213+
f, err = os.Create(f1)
214+
if err != nil {
215+
t.Errorf("error %v", err)
216+
}
217+
f.WriteString(header + parsed + tail)
218+
f.Close()
219+
t.Logf("parsed shape in %v", f1)
220+
t.Log("Please check consistency of above files, with web browser or eog")
221+
}

path.go

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -695,12 +695,19 @@ func (pdp *pathDescriptionParser) parseCurveToRelDI() error {
695695
tuples = append(tuples, t)
696696
pdp.lex.ConsumeWhiteSpace()
697697
}
698-
x, y := pdp.transform.Apply(pdp.x, pdp.y)
699-
698+
for j := 0; j < len(tuples)/3; j++ { // convert to absolute
699+
j3 := j * 3
700+
for i := 0; i < 3; i++ {
701+
tuples[j3+i][0] += pdp.x
702+
tuples[j3+i][1] += pdp.y
703+
}
704+
t := tuples[j3+2]
705+
pdp.x, pdp.y = t[0], t[1]
706+
}
700707
for j := 0; j < len(tuples)/3; j++ {
701-
c1x, c1y := pdp.transform.Apply(x+tuples[j*3][0], y+tuples[j*3][1])
702-
c2x, c2y := pdp.transform.Apply(x+tuples[j*3+1][0], y+tuples[j*3+1][1])
703-
tx, ty := pdp.transform.Apply(x+tuples[j*3+2][0], y+tuples[j*3+2][1])
708+
c1x, c1y := pdp.transform.Apply(tuples[j*3][0], tuples[j*3][1])
709+
c2x, c2y := pdp.transform.Apply(tuples[j*3+1][0], tuples[j*3+1][1])
710+
tx, ty := pdp.transform.Apply(tuples[j*3+2][0], tuples[j*3+2][1])
704711

705712
pdp.p.instructions <- &DrawingInstruction{
706713
Kind: CurveInstruction,
@@ -709,10 +716,6 @@ func (pdp *pathDescriptionParser) parseCurveToRelDI() error {
709716
T: &Tuple{tx, ty},
710717
},
711718
}
712-
713-
pdp.x += tuples[j*3+2][0]
714-
pdp.y += tuples[j*3+2][1]
715-
x, y = pdp.transform.Apply(pdp.x, pdp.y)
716719
}
717720

718721
return nil
@@ -773,10 +776,7 @@ func (pdp *pathDescriptionParser) parseCurveToRel() error {
773776
}
774777

775778
func (pdp *pathDescriptionParser) parseCurveToAbsDI() error {
776-
var (
777-
tuples []Tuple
778-
instrTuples []Tuple
779-
)
779+
var tuples []Tuple
780780

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

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

814815
func (pdp *pathDescriptionParser) parseCurveToAbs() error {
815-
var (
816-
tuples []Tuple
817-
instrTuples []Tuple
818-
)
816+
var tuples []Tuple
819817

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

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

0 commit comments

Comments
 (0)