diff --git a/drawinginstruction.go b/drawinginstruction.go index 322158d..80284de 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,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 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 `` +} 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 } } } diff --git a/parser_test.go b/parser_test.go index 44a64a6..969edc2 100644 --- a/parser_test.go +++ b/parser_test.go @@ -1,6 +1,8 @@ package svg import ( + "fmt" + "os" "strings" "testing" @@ -29,3 +31,191 @@ 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)") + } +} + +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) + } +} + +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) + } +} + +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") +} diff --git a/path.go b/path.go index 15c9249..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 @@ -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 { @@ -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] @@ -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 { @@ -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