From f8e92c7dd713929ea5450b2e1f110914e0998827 Mon Sep 17 00:00:00 2001 From: Fangzhe Chang Date: Wed, 21 Feb 2024 13:25:20 -0500 Subject: [PATCH 1/9] parser: support multiple operations in transform string --- parser.go | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/parser.go b/parser.go index d577985..8b4f553 100644 --- a/parser.go +++ b/parser.go @@ -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 } } } From 63ac4cd3bd441603b6a8396ce7f2fd22645ed22e Mon Sep 17 00:00:00 2001 From: fangzhechang Date: Thu, 22 Feb 2024 12:04:04 -0500 Subject: [PATCH 2/9] parser_test: add unit test TestTransform --- parser_test.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/parser_test.go b/parser_test.go index 44a64a6..f01a1d6 100644 --- a/parser_test.go +++ b/parser_test.go @@ -29,3 +29,40 @@ func TestParse(t *testing.T) { is.NoErr(err) is.NotNil(svg) } + +func TestTransform(t *testing.T) { + content := ` + + + + + + + ` + 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)") + } +} From fcd4324c7dcca6945866d4bc384ce96d3a7df77b Mon Sep 17 00:00:00 2001 From: fangzhechang Date: Tue, 27 Feb 2024 10:41:21 -0500 Subject: [PATCH 3/9] path: fix parsing multiple curves in curveto command --- path.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/path.go b/path.go index 15c9249..0477d4e 100644 --- a/path.go +++ b/path.go @@ -773,10 +773,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 { @@ -790,6 +787,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] @@ -812,10 +810,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 { @@ -832,6 +827,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 From ae9cc8cdeacd6dfbed8eb1855a6c116d48676c21 Mon Sep 17 00:00:00 2001 From: fangzhechang Date: Tue, 27 Feb 2024 10:50:45 -0500 Subject: [PATCH 4/9] parser_test: add unit test Test2Curves --- parser_test.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/parser_test.go b/parser_test.go index f01a1d6..2d3cedf 100644 --- a/parser_test.go +++ b/parser_test.go @@ -66,3 +66,33 @@ func TestTransform(t *testing.T) { t.Error("mismatch expected transform matrix(0.1 0 0 -0.1 0 630)") } } + +func Test2Curves(t *testing.T) { + content := ` + + + + + ` + 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) + } +} From e03c5523b2f3aaa71d34c1013b14f2f75e1d43fb Mon Sep 17 00:00:00 2001 From: fangzhechang Date: Tue, 27 Feb 2024 11:26:09 -0500 Subject: [PATCH 5/9] path: fix applying tranform multiple times in parseCurveToRelDI bug: x+tuples[j*3][0] mixed transformed x and untransformed tuples[j*3][0], effecitvely making x transformed (e.g., scale by -1) twice. This calls for similar fix in parseCurveToRel(). --- path.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/path.go b/path.go index 0477d4e..13d2ead 100644 --- a/path.go +++ b/path.go @@ -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, @@ -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 From d342199ec9412a4b366ce04ee32e9965cab2d041 Mon Sep 17 00:00:00 2001 From: fangzhechang Date: Tue, 27 Feb 2024 11:31:00 -0500 Subject: [PATCH 6/9] parser_test: add unit tests for parseCurveToRelDI fix --- parser_test.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/parser_test.go b/parser_test.go index 2d3cedf..99d7fa2 100644 --- a/parser_test.go +++ b/parser_test.go @@ -1,6 +1,7 @@ package svg import ( + "fmt" "strings" "testing" @@ -96,3 +97,69 @@ func Test2Curves(t *testing.T) { 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 := ` + + + + + + + ` + 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 := ` + + + + + + + ` + 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) + } +} From 9f9e0bd3fb982942b1803d2a702095910318ebfc Mon Sep 17 00:00:00 2001 From: fangzhechang Date: Thu, 29 Feb 2024 09:29:07 -0500 Subject: [PATCH 7/9] drawinginstruction: add String method --- drawinginstruction.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/drawinginstruction.go b/drawinginstruction.go index 322158d..6e39c25 100644 --- a/drawinginstruction.go +++ b/drawinginstruction.go @@ -1,5 +1,7 @@ package svg +import "fmt" + // InstructionType tells our path drawing library which function it has // to call type InstructionType int @@ -37,3 +39,39 @@ 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 "" +} From 3384a3850385e3ab7e7db82efd75f597b498fa6c Mon Sep 17 00:00:00 2001 From: fangzhechang Date: Thu, 29 Feb 2024 09:50:50 -0500 Subject: [PATCH 8/9] drawinginstruction: add function PathStringFromDrawingInstructions --- drawinginstruction.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/drawinginstruction.go b/drawinginstruction.go index 6e39c25..80284de 100644 --- a/drawinginstruction.go +++ b/drawinginstruction.go @@ -75,3 +75,24 @@ func (di *DrawingInstruction) String() string { } return "" } + +// PathStringFromDrawingInstructions converts drawing instructions obtained +// from svg element back into 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 `` +} From 93b06a8fa0ffb922e64708c871b91a5b7331deb3 Mon Sep 17 00:00:00 2001 From: fangzhechang Date: Wed, 6 Mar 2024 11:16:09 -0500 Subject: [PATCH 9/9] parser_test: add unit test TestParsedPathString --- parser_test.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/parser_test.go b/parser_test.go index 99d7fa2..969edc2 100644 --- a/parser_test.go +++ b/parser_test.go @@ -2,6 +2,7 @@ package svg import ( "fmt" + "os" "strings" "testing" @@ -163,3 +164,58 @@ func TestPathRelTranslate(t *testing.T) { t.Fatalf("expect %v: got %v", expect, got) } } + +func TestParsedPathString(t *testing.T) { + header := ` + + + ` + path := ` + + + ` + tail := ` + ` + 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") +}