From 96883adea4f0e19d87409e41dec0588d73790eea Mon Sep 17 00:00:00 2001 From: Drahoslav Date: Sat, 16 Dec 2017 19:26:34 +0100 Subject: [PATCH 01/21] Initial commit for svg context Satisfy draw2dc.GrapghicContext using empty methods --- draw2dsvg/doc.go | 11 +++ draw2dsvg/fileutil.go | 24 ++++++ draw2dsvg/gc.go | 182 +++++++++++++++++++++++++++++++++++++++++ draw2dsvg/test_test.go | 31 +++++++ 4 files changed, 248 insertions(+) create mode 100644 draw2dsvg/doc.go create mode 100644 draw2dsvg/fileutil.go create mode 100644 draw2dsvg/gc.go create mode 100644 draw2dsvg/test_test.go diff --git a/draw2dsvg/doc.go b/draw2dsvg/doc.go new file mode 100644 index 0000000..f07708f --- /dev/null +++ b/draw2dsvg/doc.go @@ -0,0 +1,11 @@ +// Copyright 2015 The draw2d Authors. All rights reserved. +// created: 16/12/2017 by Drahoslav Bednář + +// Package draw2svg provides a graphic context that can draw +// vector graphics and text on svg file using the svgo package. +// +// Quick Start +// The following Go code geneartes a simple drawing and saves it +// to a svg document: +// // TODO +package draw2dsvg diff --git a/draw2dsvg/fileutil.go b/draw2dsvg/fileutil.go new file mode 100644 index 0000000..e243b62 --- /dev/null +++ b/draw2dsvg/fileutil.go @@ -0,0 +1,24 @@ +package draw2dsvg + +import ( + "os" + "bytes" + "errors" + svgo "github.com/ajstarks/svgo/float" +) + +func SaveToSvgFile(filePath string, svg *svgo.SVG) error { + f, err := os.Create(filePath) + if err != nil { + return err + } + defer f.Close() + + if b, ok := svg.Writer.(*bytes.Buffer); ok { + bytes.NewBuffer(b.Bytes()).WriteTo(f) // clone buffer to make multiple writes possible + } else { + return errors.New("Svg has not been not created from with NewSvg (dow not have byte.Buffer as its Writer)") + } + + return nil +} \ No newline at end of file diff --git a/draw2dsvg/gc.go b/draw2dsvg/gc.go new file mode 100644 index 0000000..ffcb4b7 --- /dev/null +++ b/draw2dsvg/gc.go @@ -0,0 +1,182 @@ +// Copyright 2015 The draw2d Authors. All rights reserved. +// created: 16/12/2017 by Drahoslav Bednář + +package draw2dsvg + +import ( + "image" + "bytes" + "image/color" + "github.com/llgcode/draw2d" + "github.com/llgcode/draw2d/draw2dbase" + svgo "github.com/ajstarks/svgo/float" +) + +const ( +) + +var ( +) + +func NewSvg() *svgo.SVG { + return svgo.New(&bytes.Buffer{}) +} + +// GraphicContext implements the draw2d.GraphicContext interface +// It provides draw2d with a svg backend (based on svgo) +type GraphicContext struct { + *draw2dbase.StackGraphicContext + svg *svgo.SVG +} + +func NewGraphicContext(svg *svgo.SVG) *GraphicContext { + gc := &GraphicContext{draw2dbase.NewStackGraphicContext(), svg} + return gc +} + +// TODO implement all following methods + +// BeginPath creates a new path +func (gc *GraphicContext) BeginPath() { + +} +// GetPath copies the current path, then returns it +func (gc *GraphicContext) GetPath() draw2d.Path { + return draw2d.Path{} +} +// GetMatrixTransform returns the current transformation matrix +func (gc *GraphicContext) GetMatrixTransform() draw2d.Matrix { + return draw2d.Matrix{} +} +// SetMatrixTransform sets the current transformation matrix +func (gc *GraphicContext) SetMatrixTransform(tr draw2d.Matrix) { + +} +// ComposeMatrixTransform composes the current transformation matrix with tr +func (gc *GraphicContext) ComposeMatrixTransform(tr draw2d.Matrix) { + +} +// Rotate applies a rotation to the current transformation matrix. angle is in radian. +func (gc *GraphicContext) Rotate(angle float64) { + +} +// Translate applies a translation to the current transformation matrix. +func (gc *GraphicContext) Translate(tx, ty float64) { + +} +// Scale applies a scale to the current transformation matrix. +func (gc *GraphicContext) Scale(sx, sy float64) { + +} +// SetStrokeColor sets the current stroke color +func (gc *GraphicContext) SetStrokeColor(c color.Color) { + +} +// SetFillColor sets the current fill color +func (gc *GraphicContext) SetFillColor(c color.Color) { + +} +// SetFillRule sets the current fill rule +func (gc *GraphicContext) SetFillRule(f draw2d.FillRule) { + +} +// SetLineWidth sets the current line width +func (gc *GraphicContext) SetLineWidth(lineWidth float64) { + +} +// SetLineCap sets the current line cap +func (gc *GraphicContext) SetLineCap(cap draw2d.LineCap) { + +} +// SetLineJoin sets the current line join +func (gc *GraphicContext) SetLineJoin(join draw2d.LineJoin) { + +} +// SetLineDash sets the current dash +func (gc *GraphicContext) SetLineDash(dash []float64, dashOffset float64) { + +} +// SetFontSize sets the current font size +func (gc *GraphicContext) SetFontSize(fontSize float64) { + +} +// GetFontSize gets the current font size +func (gc *GraphicContext) GetFontSize() float64 { + return 0 +} +// SetFontData sets the current FontData +func (gc *GraphicContext) SetFontData(fontData draw2d.FontData) { + +} +// GetFontData gets the current FontData +func (gc *GraphicContext) GetFontData() draw2d.FontData { + return draw2d.FontData{} +} +// GetFontName gets the current FontData as a string +func (gc *GraphicContext) GetFontName() string { + return "" +} +// DrawImage draws the raster image in the current canvas +func (gc *GraphicContext) DrawImage(image image.Image) { + +} +// Save the context and push it to the context stack +func (gc *GraphicContext) Save() { + +} +// Restore remove the current context and restore the last one +func (gc *GraphicContext) Restore() { + +} +// Clear fills the current canvas with a default transparent color +func (gc *GraphicContext) Clear() { + +} +// ClearRect fills the specified rectangle with a default transparent color +func (gc *GraphicContext) ClearRect(x1, y1, x2, y2 int) { + +} +// SetDPI sets the current DPI +func (gc *GraphicContext) SetDPI(dpi int) { + +} +// GetDPI gets the current DPI +func (gc *GraphicContext) GetDPI() int { + return 0 +} +// GetStringBounds gets pixel bounds(dimensions) of given string +func (gc *GraphicContext) GetStringBounds(s string) (left, top, right, bottom float64) { + return 0, 0, 0, 0 +} +// CreateStringPath creates a path from the string s at x, y +func (gc *GraphicContext) CreateStringPath(text string, x, y float64) (cursor float64) { + return 0 +} +// FillString draws the text at point (0, 0) +func (gc *GraphicContext) FillString(text string) (cursor float64) { + return 0 +} +// FillStringAt draws the text at the specified point (x, y) +func (gc *GraphicContext) FillStringAt(text string, x, y float64) (cursor float64) { + return 0 +} +// StrokeString draws the contour of the text at point (0, 0) +func (gc *GraphicContext) StrokeString(text string) (cursor float64) { + return 0 +} +// StrokeStringAt draws the contour of the text at point (x, y) +func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64) { + return 0 +} +// Stroke strokes the paths with the color specified by SetStrokeColor +func (gc *GraphicContext) Stroke(paths ...*draw2d.Path) { + +} +// Fill fills the paths with the color specified by SetFillColor +func (gc *GraphicContext) Fill(paths ...*draw2d.Path) { + +} +// FillStroke first fills the paths and than strokes them +func (gc *GraphicContext) FillStroke(paths ...*draw2d.Path) { + +} \ No newline at end of file diff --git a/draw2dsvg/test_test.go b/draw2dsvg/test_test.go new file mode 100644 index 0000000..1b5b3de --- /dev/null +++ b/draw2dsvg/test_test.go @@ -0,0 +1,31 @@ +// Copyright 2015 The draw2d Authors. All rights reserved. +// created: 16/12/2017 by Drahoslav Bednář + +// Package draw2dsvg_test gives test coverage with the command: +// go test -cover ./... | grep -v "no test" +// (It should be run from its parent draw2d directory.) +package draw2dsvg_test + +import ( + "testing" + "github.com/llgcode/draw2d" + "github.com/llgcode/draw2d/draw2dsvg" +) + +type sample func(gc draw2d.GraphicContext, ext string) (string, error) + +func test(t *testing.T, draw sample) { + // Initialize the graphic context on an pdf document + dest := draw2dsvg.NewSvg() + gc := draw2dsvg.NewGraphicContext(dest) + // Draw sample + output, err := draw(gc, "svg") + if err != nil { + t.Errorf("Drawing %q failed: %v", output, err) + return + } + err = draw2dsvg.SaveToSvgFile(output, dest) + if err != nil { + t.Errorf("Saving %q failed: %v", output, err) + } +} From 295a8365b3ca762bf86e98737c07dc35967bc463 Mon Sep 17 00:00:00 2001 From: Drahoslav Date: Thu, 21 Dec 2017 15:52:26 +0100 Subject: [PATCH 02/21] Remove svgo dependency --- draw2dsvg/fileutil.go | 11 +++-------- draw2dsvg/gc.go | 13 +++++++------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/draw2dsvg/fileutil.go b/draw2dsvg/fileutil.go index e243b62..e864c83 100644 --- a/draw2dsvg/fileutil.go +++ b/draw2dsvg/fileutil.go @@ -3,22 +3,17 @@ package draw2dsvg import ( "os" "bytes" - "errors" - svgo "github.com/ajstarks/svgo/float" + _ "errors" ) -func SaveToSvgFile(filePath string, svg *svgo.SVG) error { +func SaveToSvgFile(filePath string, svg *SVG) error { f, err := os.Create(filePath) if err != nil { return err } defer f.Close() - if b, ok := svg.Writer.(*bytes.Buffer); ok { - bytes.NewBuffer(b.Bytes()).WriteTo(f) // clone buffer to make multiple writes possible - } else { - return errors.New("Svg has not been not created from with NewSvg (dow not have byte.Buffer as its Writer)") - } + bytes.NewBuffer((*bytes.Buffer)(svg).Bytes()).WriteTo(f) // clone buffer to make multiple writes possible return nil } \ No newline at end of file diff --git a/draw2dsvg/gc.go b/draw2dsvg/gc.go index ffcb4b7..c3596d8 100644 --- a/draw2dsvg/gc.go +++ b/draw2dsvg/gc.go @@ -9,7 +9,6 @@ import ( "image/color" "github.com/llgcode/draw2d" "github.com/llgcode/draw2d/draw2dbase" - svgo "github.com/ajstarks/svgo/float" ) const ( @@ -18,18 +17,20 @@ const ( var ( ) -func NewSvg() *svgo.SVG { - return svgo.New(&bytes.Buffer{}) +type SVG bytes.Buffer + +func NewSvg() *SVG { + return &SVG{} } // GraphicContext implements the draw2d.GraphicContext interface -// It provides draw2d with a svg backend (based on svgo) +// It provides draw2d with a svg backend type GraphicContext struct { *draw2dbase.StackGraphicContext - svg *svgo.SVG + svg *SVG } -func NewGraphicContext(svg *svgo.SVG) *GraphicContext { +func NewGraphicContext(svg *SVG) *GraphicContext { gc := &GraphicContext{draw2dbase.NewStackGraphicContext(), svg} return gc } From cdf301b7bec72db36466e346e6ad8a9a4d6a7456 Mon Sep 17 00:00:00 2001 From: Drahoslav Date: Thu, 21 Dec 2017 18:18:29 +0100 Subject: [PATCH 03/21] Use encoding/xml to encode svg --- draw2dsvg/fileutil.go | 11 ++++++--- draw2dsvg/gc.go | 8 +++--- draw2dsvg/svg.go | 28 +++++++++++++++++++++ draw2dsvg/xml_test.go | 57 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 8 deletions(-) create mode 100644 draw2dsvg/svg.go create mode 100644 draw2dsvg/xml_test.go diff --git a/draw2dsvg/fileutil.go b/draw2dsvg/fileutil.go index e864c83..ed28f03 100644 --- a/draw2dsvg/fileutil.go +++ b/draw2dsvg/fileutil.go @@ -2,18 +2,21 @@ package draw2dsvg import ( "os" - "bytes" + "encoding/xml" _ "errors" ) -func SaveToSvgFile(filePath string, svg *SVG) error { +func SaveToSvgFile(filePath string, svg *Svg) error { f, err := os.Create(filePath) if err != nil { return err } defer f.Close() - bytes.NewBuffer((*bytes.Buffer)(svg).Bytes()).WriteTo(f) // clone buffer to make multiple writes possible + f.Write([]byte(xml.Header)) + encoder := xml.NewEncoder(f) + encoder.Indent("", "\t") + err = encoder.Encode(svg) - return nil + return err } \ No newline at end of file diff --git a/draw2dsvg/gc.go b/draw2dsvg/gc.go index c3596d8..6ff10be 100644 --- a/draw2dsvg/gc.go +++ b/draw2dsvg/gc.go @@ -19,18 +19,18 @@ var ( type SVG bytes.Buffer -func NewSvg() *SVG { - return &SVG{} +func NewSvg() *Svg { + return &Svg{Xmlns: "http://www.w3.org/2000/svg"} } // GraphicContext implements the draw2d.GraphicContext interface // It provides draw2d with a svg backend type GraphicContext struct { *draw2dbase.StackGraphicContext - svg *SVG + svg *Svg } -func NewGraphicContext(svg *SVG) *GraphicContext { +func NewGraphicContext(svg *Svg) *GraphicContext { gc := &GraphicContext{draw2dbase.NewStackGraphicContext(), svg} return gc } diff --git a/draw2dsvg/svg.go b/draw2dsvg/svg.go new file mode 100644 index 0000000..9e99d0e --- /dev/null +++ b/draw2dsvg/svg.go @@ -0,0 +1,28 @@ +// Copyright 2015 The draw2d Authors. All rights reserved. +// created: 16/12/2017 by Drahoslav Bednář +package draw2dsvg + +import ( + "encoding/xml" +) + +type Svg struct { + XMLName xml.Name `xml:"svg"` + Xmlns string `xml:"xmlns,attr"` + Groups []Group `xml:"g"` +} + +type Group struct { + Groups []Group `xml:"g"` + Paths []Path `xml:"path"` + Texts []Text `xml:"text"` +} + +type Path struct { + Data string `xml:"d,attr"` +} + +type Text struct { + Text string `xml:",innerxml"` + Style string `xml:",attr,omitempty"` +} \ No newline at end of file diff --git a/draw2dsvg/xml_test.go b/draw2dsvg/xml_test.go new file mode 100644 index 0000000..4be778b --- /dev/null +++ b/draw2dsvg/xml_test.go @@ -0,0 +1,57 @@ +// Copyright 2015 The draw2d Authors. All rights reserved. +// created: 16/12/2017 by Drahoslav Bednář + +// Package draw2dsvg_test gives test coverage with the command: +// go test -cover ./... | grep -v "no test" +// (It should be run from its parent draw2d directory.) +package draw2dsvg + +import ( + "testing" + "encoding/xml" +) + +// Test basic encoding of svg/xml elements +func TestXml(t *testing.T) { + + svg := NewSvg() + svg.Groups = []Group{Group{ + Groups: []Group{ + Group{}, // nested groups + Group{}, + }, + Texts: []Text{ + Text{Text: "Hello"}, // text + Text{Text: "world", Style: "opacity: 0.5"}, // text with style + }, + Paths: []Path{ + Path{Data: "M100,200 C100,100 250,100 250,200 S400,300 400,200"}, // simple path + Path{}, // empty path + }, + }} + + expectedOut := ` + + + + + + Hello + world + +` + + out, err := xml.MarshalIndent(svg, "", " ") + + if err != nil { + t.Error(err) + } + if string(out) != expectedOut { + t.Errorf("svg output is not as expected\n"+ + "got:\n%s\n\n"+ + "want:\n%s\n", + string(out), + expectedOut, + ) + } +} From bd7567e3314111c55a44cade88daacb9638f7ac3 Mon Sep 17 00:00:00 2001 From: Drahoslav Date: Fri, 22 Dec 2017 09:59:31 +0100 Subject: [PATCH 04/21] Add samples test - no outputs yet --- draw2dsvg/samples_test.go | 65 +++++++++++++++++++++++++++++++++++++++ draw2dsvg/svg.go | 1 + draw2dsvg/test_test.go | 1 + samples/samples.go | 4 +-- 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 draw2dsvg/samples_test.go diff --git a/draw2dsvg/samples_test.go b/draw2dsvg/samples_test.go new file mode 100644 index 0000000..aec6573 --- /dev/null +++ b/draw2dsvg/samples_test.go @@ -0,0 +1,65 @@ +// Copyright 2015 The draw2d Authors. All rights reserved. +// created: 26/06/2015 by Stani Michiels +// See also test_test.go + +package draw2dsvg_test + +import ( + "testing" + + "github.com/llgcode/draw2d" + "github.com/llgcode/draw2d/samples/android" + "github.com/llgcode/draw2d/samples/frameimage" + "github.com/llgcode/draw2d/samples/geometry" + "github.com/llgcode/draw2d/samples/gopher" + "github.com/llgcode/draw2d/samples/gopher2" + "github.com/llgcode/draw2d/samples/helloworld" + "github.com/llgcode/draw2d/samples/line" + "github.com/llgcode/draw2d/samples/linecapjoin" + "github.com/llgcode/draw2d/samples/postscript" +) + +func TestSampleAndroid(t *testing.T) { + test(t, android.Main) +} + +// TODO: FillString: w (width) is incorrect +func TestSampleGeometry(t *testing.T) { + // Set the global folder for searching fonts + // The pdf backend needs for every ttf file its corresponding + // json/.z file which is generated by gofpdf/makefont. + draw2d.SetFontFolder("../resource/font") + test(t, geometry.Main) +} + +func TestSampleGopher(t *testing.T) { + test(t, gopher.Main) +} + +func TestSampleGopher2(t *testing.T) { + test(t, gopher2.Main) +} + +func TestSampleHelloWorld(t *testing.T) { + // Set the global folder for searching fonts + // The pdf backend needs for every ttf file its corresponding + // json/.z file which is generated by gofpdf/makefont. + draw2d.SetFontFolder("../resource/font") + test(t, helloworld.Main) +} + +func TestSampleFrameImage(t *testing.T) { + test(t, frameimage.Main) +} + +func TestSampleLine(t *testing.T) { + test(t, line.Main) +} + +func TestSampleLineCap(t *testing.T) { + test(t, linecapjoin.Main) +} + +func TestSamplePostscript(t *testing.T) { + test(t, postscript.Main) +} diff --git a/draw2dsvg/svg.go b/draw2dsvg/svg.go index 9e99d0e..e69071b 100644 --- a/draw2dsvg/svg.go +++ b/draw2dsvg/svg.go @@ -1,5 +1,6 @@ // Copyright 2015 The draw2d Authors. All rights reserved. // created: 16/12/2017 by Drahoslav Bednář + package draw2dsvg import ( diff --git a/draw2dsvg/test_test.go b/draw2dsvg/test_test.go index 1b5b3de..d1bd328 100644 --- a/draw2dsvg/test_test.go +++ b/draw2dsvg/test_test.go @@ -8,6 +8,7 @@ package draw2dsvg_test import ( "testing" + "github.com/llgcode/draw2d" "github.com/llgcode/draw2d/draw2dsvg" ) diff --git a/samples/samples.go b/samples/samples.go index ff30559..c76b608 100644 --- a/samples/samples.go +++ b/samples/samples.go @@ -8,7 +8,7 @@ import "fmt" // Resource returns a resource filename for testing. func Resource(folder, filename, ext string) string { var root string - if ext == "pdf" { + if ext == "pdf" || ext == "svg" { root = "../" } return fmt.Sprintf("%sresource/%s/%s", root, folder, filename) @@ -17,7 +17,7 @@ func Resource(folder, filename, ext string) string { // Output returns the output filename for testing. func Output(name, ext string) string { var root string - if ext == "pdf" { + if ext == "pdf" || ext == "svg" { root = "../" } return fmt.Sprintf("%soutput/samples/%s.%s", root, name, ext) From ca83e2422228ce0e0a5e745c16ea5b90f82c6f1e Mon Sep 17 00:00:00 2001 From: Drahoslav Date: Fri, 22 Dec 2017 22:40:56 +0100 Subject: [PATCH 05/21] Basic path implementation --- draw2dsvg/gc.go | 168 +++++++++++++++++++++++------------------- draw2dsvg/svg.go | 15 +++- draw2dsvg/xml_test.go | 4 +- 3 files changed, 108 insertions(+), 79 deletions(-) diff --git a/draw2dsvg/gc.go b/draw2dsvg/gc.go index 6ff10be..b7f175d 100644 --- a/draw2dsvg/gc.go +++ b/draw2dsvg/gc.go @@ -4,9 +4,11 @@ package draw2dsvg import ( + "fmt" "image" - "bytes" "image/color" + "strings" + "bytes" "github.com/llgcode/draw2d" "github.com/llgcode/draw2d/draw2dbase" ) @@ -17,6 +19,12 @@ const ( var ( ) +type drawType int +const ( + filled drawType = 1 << iota + stroked +) + type SVG bytes.Buffer func NewSvg() *Svg { @@ -35,76 +43,100 @@ func NewGraphicContext(svg *Svg) *GraphicContext { return gc } -// TODO implement all following methods +// Clear fills the current canvas with a default transparent color +func (gc *GraphicContext) Clear() { + gc.svg.Groups = nil + gc.svg.Groups = append(gc.svg.Groups, Group{ + // TODO add background color? + }) +} -// BeginPath creates a new path -func (gc *GraphicContext) BeginPath() { +// Stroke strokes the paths with the color specified by SetStrokeColor +func (gc *GraphicContext) Stroke(paths ...*draw2d.Path) { + gc.drawPaths(stroked, paths...) + gc.Current.Path.Clear() +} +// Fill fills the paths with the color specified by SetFillColor +func (gc *GraphicContext) Fill(paths ...*draw2d.Path) { + gc.drawPaths(filled, paths...) + gc.Current.Path.Clear() +} +// FillStroke first fills the paths and than strokes them +func (gc *GraphicContext) FillStroke(paths ...*draw2d.Path) { + gc.drawPaths(filled | stroked, paths...) + gc.Current.Path.Clear() +} -} -// GetPath copies the current path, then returns it -func (gc *GraphicContext) GetPath() draw2d.Path { - return draw2d.Path{} -} -// GetMatrixTransform returns the current transformation matrix -func (gc *GraphicContext) GetMatrixTransform() draw2d.Matrix { - return draw2d.Matrix{} -} -// SetMatrixTransform sets the current transformation matrix -func (gc *GraphicContext) SetMatrixTransform(tr draw2d.Matrix) { +func (gc *GraphicContext) drawPaths (drawType drawType, paths ...*draw2d.Path) { + paths = append(paths, gc.Current.Path) -} -// ComposeMatrixTransform composes the current transformation matrix with tr -func (gc *GraphicContext) ComposeMatrixTransform(tr draw2d.Matrix) { + svgPaths := make([]Path, len(paths)) -} -// Rotate applies a rotation to the current transformation matrix. angle is in radian. -func (gc *GraphicContext) Rotate(angle float64) { + for i, path := range paths { + svgPaths[i].Desc = toSvgPathDesc(path) + if drawType & stroked == stroked { + svgPaths[i].Stroke = toSvgRGBA(gc.Current.StrokeColor) + } + if drawType & filled == filled { + svgPaths[i].Fill = toSvgRGBA(gc.Current.FillColor) + } + } + gc.svg.Groups = append(gc.svg.Groups, Group{ + Paths: svgPaths, + }) } -// Translate applies a translation to the current transformation matrix. -func (gc *GraphicContext) Translate(tx, ty float64) { +func toSvgRGBA (c color.Color) string { // TODO move elsewhere + r, g, b, a := c.RGBA() + return fmt.Sprintf("rgba(%v, %v, %v, %v)", r>>8, g>>8, b>>8, float64(a>>8)/255) } -// Scale applies a scale to the current transformation matrix. -func (gc *GraphicContext) Scale(sx, sy float64) { +func toSvgPathDesc (p *draw2d.Path) string { // TODO move elsewhere + parts := make([]string, len(p.Components)) + i := 0 + for j, cmp := range p.Components { + switch cmp { + case draw2d.MoveToCmp: + parts[j] = fmt.Sprintf("M %v %v", p.Points[i], p.Points[i+1]) + i += 2 + case draw2d.LineToCmp: + parts[j] = fmt.Sprintf("L %v %v", p.Points[i], p.Points[i+1]) + i += 2 + case draw2d.QuadCurveToCmp: + parts[j] = fmt.Sprintf("Q %v %v %v %v", p.Points[i], p.Points[i+1], p.Points[i+2], p.Points[i+3]) + i += 4 + case draw2d.CubicCurveToCmp: + parts[j] = fmt.Sprintf("C %v %v %v %v %v %v", p.Points[i], p.Points[i+1], p.Points[i+2], p.Points[i+3], p.Points[i+4], p.Points[i+5]) + i += 6 + case draw2d.ArcToCmp: + large := 0 + sweep := 0 + if p.Points[i+4] - p.Points[i+5] > 0 { // TODO this is probably not correct + large = 1 + sweep = 1 + } + // rx ry x-axis-rotation large-arc-flag sweep-flag x y + parts[j] = fmt.Sprintf("A %v %v %v %v %v %v %v", + p.Points[i+2], // rx + p.Points[i+3], // ry + 0, // x-axis-rotation + large, // large-arc-flag + sweep, // // sweep-flag + p.Points[i], // x // TODO this is center of arc not an endpont + p.Points[i+1], // y // TODO -//- + ) + i += 6 + case draw2d.CloseCmp: + parts[j] = "Z" + } + } + println("parts", parts) + return strings.Join(parts, " ") } -// SetStrokeColor sets the current stroke color -func (gc *GraphicContext) SetStrokeColor(c color.Color) { +/////////////////////////////////////// +// TODO implement following methods (or remove if not neccesary) -} -// SetFillColor sets the current fill color -func (gc *GraphicContext) SetFillColor(c color.Color) { - -} -// SetFillRule sets the current fill rule -func (gc *GraphicContext) SetFillRule(f draw2d.FillRule) { - -} -// SetLineWidth sets the current line width -func (gc *GraphicContext) SetLineWidth(lineWidth float64) { - -} -// SetLineCap sets the current line cap -func (gc *GraphicContext) SetLineCap(cap draw2d.LineCap) { - -} -// SetLineJoin sets the current line join -func (gc *GraphicContext) SetLineJoin(join draw2d.LineJoin) { - -} -// SetLineDash sets the current dash -func (gc *GraphicContext) SetLineDash(dash []float64, dashOffset float64) { - -} -// SetFontSize sets the current font size -func (gc *GraphicContext) SetFontSize(fontSize float64) { - -} -// GetFontSize gets the current font size -func (gc *GraphicContext) GetFontSize() float64 { - return 0 -} // SetFontData sets the current FontData func (gc *GraphicContext) SetFontData(fontData draw2d.FontData) { @@ -128,10 +160,6 @@ func (gc *GraphicContext) Save() { // Restore remove the current context and restore the last one func (gc *GraphicContext) Restore() { -} -// Clear fills the current canvas with a default transparent color -func (gc *GraphicContext) Clear() { - } // ClearRect fills the specified rectangle with a default transparent color func (gc *GraphicContext) ClearRect(x1, y1, x2, y2 int) { @@ -169,15 +197,3 @@ func (gc *GraphicContext) StrokeString(text string) (cursor float64) { func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64) { return 0 } -// Stroke strokes the paths with the color specified by SetStrokeColor -func (gc *GraphicContext) Stroke(paths ...*draw2d.Path) { - -} -// Fill fills the paths with the color specified by SetFillColor -func (gc *GraphicContext) Fill(paths ...*draw2d.Path) { - -} -// FillStroke first fills the paths and than strokes them -func (gc *GraphicContext) FillStroke(paths ...*draw2d.Path) { - -} \ No newline at end of file diff --git a/draw2dsvg/svg.go b/draw2dsvg/svg.go index e69071b..9e70575 100644 --- a/draw2dsvg/svg.go +++ b/draw2dsvg/svg.go @@ -7,6 +7,8 @@ import ( "encoding/xml" ) +/* svg elements */ + type Svg struct { XMLName xml.Name `xml:"svg"` Xmlns string `xml:"xmlns,attr"` @@ -14,16 +16,27 @@ type Svg struct { } type Group struct { + FillStroke Groups []Group `xml:"g"` Paths []Path `xml:"path"` Texts []Text `xml:"text"` } type Path struct { - Data string `xml:"d,attr"` + FillStroke + Desc string `xml:"d,attr"` } type Text struct { + FillStroke Text string `xml:",innerxml"` Style string `xml:",attr,omitempty"` +} + + +/* shared attrs */ + +type FillStroke struct { + Fill string `xml:"fill,attr,omitempty"` + Stroke string `xml:"stroke,attr,omitempty"` } \ No newline at end of file diff --git a/draw2dsvg/xml_test.go b/draw2dsvg/xml_test.go index 4be778b..0206a11 100644 --- a/draw2dsvg/xml_test.go +++ b/draw2dsvg/xml_test.go @@ -13,7 +13,7 @@ import ( // Test basic encoding of svg/xml elements func TestXml(t *testing.T) { - + svg := NewSvg() svg.Groups = []Group{Group{ Groups: []Group{ @@ -25,7 +25,7 @@ func TestXml(t *testing.T) { Text{Text: "world", Style: "opacity: 0.5"}, // text with style }, Paths: []Path{ - Path{Data: "M100,200 C100,100 250,100 250,200 S400,300 400,200"}, // simple path + Path{Desc: "M100,200 C100,100 250,100 250,200 S400,300 400,200"}, // simple path Path{}, // empty path }, }} From 6c0a15c624164853af39eee1ada0ff715d1a774a Mon Sep 17 00:00:00 2001 From: Drahoslav Date: Sun, 24 Dec 2017 12:32:29 +0100 Subject: [PATCH 06/21] Use line-{width,cap,join} attributes in svg context --- draw2d.go | 16 ++++++++++++++++ draw2dsvg/gc.go | 13 ++++++++++++- draw2dsvg/svg.go | 3 +++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/draw2d.go b/draw2d.go index 92a7344..5905a4d 100644 --- a/draw2d.go +++ b/draw2d.go @@ -128,6 +128,14 @@ const ( SquareCap ) +func (cap LineCap) String() string { + return map[LineCap]string{ + RoundCap: "round", + ButtCap: "cap", + SquareCap: "square", + }[cap] +} + // LineJoin is the style of segments joint type LineJoin int @@ -140,6 +148,14 @@ const ( MiterJoin ) +func (join LineJoin) String() string { + return map[LineJoin]string{ + RoundJoin: "round", + BevelJoin: "bevel", + MiterJoin: "miter", + }[join] +} + // StrokeStyle keeps stroke style attributes // that is used by the Stroke method of a Drawer type StrokeStyle struct { diff --git a/draw2dsvg/gc.go b/draw2dsvg/gc.go index b7f175d..a542de2 100644 --- a/draw2dsvg/gc.go +++ b/draw2dsvg/gc.go @@ -76,9 +76,16 @@ func (gc *GraphicContext) drawPaths (drawType drawType, paths ...*draw2d.Path) { svgPaths[i].Desc = toSvgPathDesc(path) if drawType & stroked == stroked { svgPaths[i].Stroke = toSvgRGBA(gc.Current.StrokeColor) + svgPaths[i].StrokeWidth = toSvgLength(gc.Current.LineWidth) + svgPaths[i].StrokeLinecap = gc.Current.Cap.String() + svgPaths[i].StrokeLinejoin = gc.Current.Join.String() + } else { + svgPaths[i].Stroke = "none" } if drawType & filled == filled { svgPaths[i].Fill = toSvgRGBA(gc.Current.FillColor) + } else { + svgPaths[i].Fill = "none" } } @@ -89,7 +96,11 @@ func (gc *GraphicContext) drawPaths (drawType drawType, paths ...*draw2d.Path) { func toSvgRGBA (c color.Color) string { // TODO move elsewhere r, g, b, a := c.RGBA() - return fmt.Sprintf("rgba(%v, %v, %v, %v)", r>>8, g>>8, b>>8, float64(a>>8)/255) + return fmt.Sprintf("rgba(%v, %v, %v, %.3f)", r>>8, g>>8, b>>8, float64(a>>8)/255) +} + +func toSvgLength (l float64) string { + return fmt.Sprintf("%.4f", l) } func toSvgPathDesc (p *draw2d.Path) string { // TODO move elsewhere diff --git a/draw2dsvg/svg.go b/draw2dsvg/svg.go index 9e70575..b2dfbb3 100644 --- a/draw2dsvg/svg.go +++ b/draw2dsvg/svg.go @@ -39,4 +39,7 @@ type Text struct { type FillStroke struct { Fill string `xml:"fill,attr,omitempty"` Stroke string `xml:"stroke,attr,omitempty"` + StrokeWidth string `xml:"stroke-width,attr,omitempty"` + StrokeLinecap string `xml:"stroke-linecap,attr,omitempty"` + StrokeLinejoin string `xml:"stroke-linejoin,attr,omitempty"` } \ No newline at end of file From 41d8a21ba22696c8173c54ed09f3a9f902b5b30f Mon Sep 17 00:00:00 2001 From: Drahoslav Date: Sun, 24 Dec 2017 12:38:59 +0100 Subject: [PATCH 07/21] Improve toSvgPathDesc some toArcs paths still does not work correctly --- draw2dsvg/gc.go | 105 ++++++++++++++++++++++++++++++------------------ 1 file changed, 67 insertions(+), 38 deletions(-) diff --git a/draw2dsvg/gc.go b/draw2dsvg/gc.go index a542de2..212bd49 100644 --- a/draw2dsvg/gc.go +++ b/draw2dsvg/gc.go @@ -4,22 +4,22 @@ package draw2dsvg import ( - "fmt" - "image" - "image/color" - "strings" "bytes" + "fmt" "github.com/llgcode/draw2d" "github.com/llgcode/draw2d/draw2dbase" + "image" + "image/color" + "math" + "strings" ) -const ( -) +const () -var ( -) +var () type drawType int + const ( filled drawType = 1 << iota stroked @@ -47,7 +47,7 @@ func NewGraphicContext(svg *Svg) *GraphicContext { func (gc *GraphicContext) Clear() { gc.svg.Groups = nil gc.svg.Groups = append(gc.svg.Groups, Group{ - // TODO add background color? + // TODO add background color? }) } @@ -56,25 +56,27 @@ func (gc *GraphicContext) Stroke(paths ...*draw2d.Path) { gc.drawPaths(stroked, paths...) gc.Current.Path.Clear() } + // Fill fills the paths with the color specified by SetFillColor func (gc *GraphicContext) Fill(paths ...*draw2d.Path) { gc.drawPaths(filled, paths...) gc.Current.Path.Clear() } + // FillStroke first fills the paths and than strokes them func (gc *GraphicContext) FillStroke(paths ...*draw2d.Path) { - gc.drawPaths(filled | stroked, paths...) + gc.drawPaths(filled|stroked, paths...) gc.Current.Path.Clear() } -func (gc *GraphicContext) drawPaths (drawType drawType, paths ...*draw2d.Path) { +func (gc *GraphicContext) drawPaths(drawType drawType, paths ...*draw2d.Path) { paths = append(paths, gc.Current.Path) svgPaths := make([]Path, len(paths)) for i, path := range paths { svgPaths[i].Desc = toSvgPathDesc(path) - if drawType & stroked == stroked { + if drawType&stroked == stroked { svgPaths[i].Stroke = toSvgRGBA(gc.Current.StrokeColor) svgPaths[i].StrokeWidth = toSvgLength(gc.Current.LineWidth) svgPaths[i].StrokeLinecap = gc.Current.Cap.String() @@ -82,7 +84,7 @@ func (gc *GraphicContext) drawPaths (drawType drawType, paths ...*draw2d.Path) { } else { svgPaths[i].Stroke = "none" } - if drawType & filled == filled { + if drawType&filled == filled { svgPaths[i].Fill = toSvgRGBA(gc.Current.FillColor) } else { svgPaths[i].Fill = "none" @@ -94,57 +96,70 @@ func (gc *GraphicContext) drawPaths (drawType drawType, paths ...*draw2d.Path) { }) } -func toSvgRGBA (c color.Color) string { // TODO move elsewhere +func toSvgRGBA(c color.Color) string { // TODO move elsewhere r, g, b, a := c.RGBA() return fmt.Sprintf("rgba(%v, %v, %v, %.3f)", r>>8, g>>8, b>>8, float64(a>>8)/255) } -func toSvgLength (l float64) string { +func toSvgLength(l float64) string { return fmt.Sprintf("%.4f", l) } -func toSvgPathDesc (p *draw2d.Path) string { // TODO move elsewhere +func toSvgPathDesc(p *draw2d.Path) string { // TODO move elsewhere parts := make([]string, len(p.Components)) - i := 0 - for j, cmp := range p.Components { + ps := p.Points + for i, cmp := range p.Components { switch cmp { case draw2d.MoveToCmp: - parts[j] = fmt.Sprintf("M %v %v", p.Points[i], p.Points[i+1]) - i += 2 + parts[i] = fmt.Sprintf("M %.4f,%.4f", ps[0], ps[1]) + ps = ps[2:] case draw2d.LineToCmp: - parts[j] = fmt.Sprintf("L %v %v", p.Points[i], p.Points[i+1]) - i += 2 + parts[i] = fmt.Sprintf("L %.4f,%.4f", ps[0], ps[1]) + ps = ps[2:] case draw2d.QuadCurveToCmp: - parts[j] = fmt.Sprintf("Q %v %v %v %v", p.Points[i], p.Points[i+1], p.Points[i+2], p.Points[i+3]) - i += 4 + parts[i] = fmt.Sprintf("Q %.4f,%.4f %.4f,%.4f", ps[0], ps[1], ps[2], ps[3]) + ps = ps[4:] case draw2d.CubicCurveToCmp: - parts[j] = fmt.Sprintf("C %v %v %v %v %v %v", p.Points[i], p.Points[i+1], p.Points[i+2], p.Points[i+3], p.Points[i+4], p.Points[i+5]) - i += 6 + parts[i] = fmt.Sprintf("C %.4f,%.4f %.4f,%.4f %.4f,%.4f", ps[0], ps[1], ps[2], ps[3], ps[4], ps[5]) + ps = ps[6:] case draw2d.ArcToCmp: + cx, cy := ps[0], ps[1] // center + rx, ry := ps[2], ps[3] // radii + fi := ps[4] + ps[5] // startAngle + angle + + // compute endpoint + sinfi, cosfi := math.Sincos(fi) + nom := math.Hypot(ry*cosfi, rx*sinfi) + x := cx + (rx*ry*cosfi)/nom + y := cy + (rx*ry*sinfi)/nom + x += 0.001 // dirty hack to ensure whole arc is drawn if start point equals endpoint + y += 0.001 + + // compute large and sweep flags large := 0 sweep := 0 - if p.Points[i+4] - p.Points[i+5] > 0 { // TODO this is probably not correct + if math.Abs(ps[5]) > math.Pi { large = 1 + } + if !math.Signbit(ps[5]) { sweep = 1 } + // rx ry x-axis-rotation large-arc-flag sweep-flag x y - parts[j] = fmt.Sprintf("A %v %v %v %v %v %v %v", - p.Points[i+2], // rx - p.Points[i+3], // ry - 0, // x-axis-rotation - large, // large-arc-flag - sweep, // // sweep-flag - p.Points[i], // x // TODO this is center of arc not an endpont - p.Points[i+1], // y // TODO -//- + parts[i] = fmt.Sprintf("A %.4f %.4f %v %v %v %.4f %.4f", + rx, ry, + 0, + large, sweep, + x, y, ) - i += 6 + ps = ps[6:] case draw2d.CloseCmp: - parts[j] = "Z" + parts[i] = "Z" } } - println("parts", parts) return strings.Join(parts, " ") } + /////////////////////////////////////// // TODO implement following methods (or remove if not neccesary) @@ -152,58 +167,72 @@ func toSvgPathDesc (p *draw2d.Path) string { // TODO move elsewhere func (gc *GraphicContext) SetFontData(fontData draw2d.FontData) { } + // GetFontData gets the current FontData func (gc *GraphicContext) GetFontData() draw2d.FontData { return draw2d.FontData{} } + // GetFontName gets the current FontData as a string func (gc *GraphicContext) GetFontName() string { return "" } + // DrawImage draws the raster image in the current canvas func (gc *GraphicContext) DrawImage(image image.Image) { } + // Save the context and push it to the context stack func (gc *GraphicContext) Save() { } + // Restore remove the current context and restore the last one func (gc *GraphicContext) Restore() { } + // ClearRect fills the specified rectangle with a default transparent color func (gc *GraphicContext) ClearRect(x1, y1, x2, y2 int) { } + // SetDPI sets the current DPI func (gc *GraphicContext) SetDPI(dpi int) { } + // GetDPI gets the current DPI func (gc *GraphicContext) GetDPI() int { return 0 } + // GetStringBounds gets pixel bounds(dimensions) of given string func (gc *GraphicContext) GetStringBounds(s string) (left, top, right, bottom float64) { return 0, 0, 0, 0 } + // CreateStringPath creates a path from the string s at x, y func (gc *GraphicContext) CreateStringPath(text string, x, y float64) (cursor float64) { return 0 } + // FillString draws the text at point (0, 0) func (gc *GraphicContext) FillString(text string) (cursor float64) { return 0 } + // FillStringAt draws the text at the specified point (x, y) func (gc *GraphicContext) FillStringAt(text string, x, y float64) (cursor float64) { return 0 } + // StrokeString draws the contour of the text at point (0, 0) func (gc *GraphicContext) StrokeString(text string) (cursor float64) { return 0 } + // StrokeStringAt draws the contour of the text at point (x, y) func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64) { return 0 From 0b3b26d85f72612f8bedc21eb8004476929aa77e Mon Sep 17 00:00:00 2001 From: Drahoslav Date: Sun, 24 Dec 2017 12:44:25 +0100 Subject: [PATCH 08/21] Gofmt --- draw2d.go | 4 ++-- draw2dsvg/gc.go | 11 +++++++++-- font.go | 8 +++----- sync_test.go | 1 - 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/draw2d.go b/draw2d.go index 5905a4d..590a97e 100644 --- a/draw2d.go +++ b/draw2d.go @@ -130,8 +130,8 @@ const ( func (cap LineCap) String() string { return map[LineCap]string{ - RoundCap: "round", - ButtCap: "cap", + RoundCap: "round", + ButtCap: "cap", SquareCap: "square", }[cap] } diff --git a/draw2dsvg/gc.go b/draw2dsvg/gc.go index 212bd49..fbaf6f4 100644 --- a/draw2dsvg/gc.go +++ b/draw2dsvg/gc.go @@ -132,8 +132,6 @@ func toSvgPathDesc(p *draw2d.Path) string { // TODO move elsewhere nom := math.Hypot(ry*cosfi, rx*sinfi) x := cx + (rx*ry*cosfi)/nom y := cy + (rx*ry*sinfi)/nom - x += 0.001 // dirty hack to ensure whole arc is drawn if start point equals endpoint - y += 0.001 // compute large and sweep flags large := 0 @@ -144,6 +142,15 @@ func toSvgPathDesc(p *draw2d.Path) string { // TODO move elsewhere if !math.Signbit(ps[5]) { sweep = 1 } + // dirty hack to ensure whole arc is drawn + // if start point equals end point + if sweep == 1 { + x += 0.001 * sinfi + y += 0.001 * -cosfi + } else { + x += 0.001 * sinfi + y += 0.001 * cosfi + } // rx ry x-axis-rotation large-arc-flag sweep-flag x y parts[i] = fmt.Sprintf("A %.4f %.4f %v %v %v %.4f %.4f", diff --git a/font.go b/font.go index 90ef9a1..f5c9a04 100644 --- a/font.go +++ b/font.go @@ -8,8 +8,8 @@ import ( "log" "path/filepath" - "sync" "github.com/golang/freetype/truetype" + "sync" ) // FontStyle defines bold and italic styles for the font @@ -125,7 +125,7 @@ type FolderFontCache struct { namer FontFileNamer } -// NewFolderFontCache creates FolderFontCache +// NewFolderFontCache creates FolderFontCache func NewFolderFontCache(folder string) *FolderFontCache { return &FolderFontCache{ fonts: make(map[string]*truetype.Font), @@ -168,9 +168,7 @@ type SyncFolderFontCache struct { namer FontFileNamer } - - -// NewSyncFolderFontCache creates SyncFolderFontCache +// NewSyncFolderFontCache creates SyncFolderFontCache func NewSyncFolderFontCache(folder string) *SyncFolderFontCache { return &SyncFolderFontCache{ fonts: make(map[string]*truetype.Font), diff --git a/sync_test.go b/sync_test.go index 6c5afd8..8580a81 100644 --- a/sync_test.go +++ b/sync_test.go @@ -2,7 +2,6 @@ package draw2d_test - import ( "fmt" "github.com/llgcode/draw2d" From 484fe1caef5c3caa428bc0fbeaa443381aa59ad5 Mon Sep 17 00:00:00 2001 From: Drahoslav Date: Sun, 24 Dec 2017 14:46:58 +0100 Subject: [PATCH 09/21] Use dasharray and dashoffset attributes in svg context --- draw2dsvg/gc.go | 12 ++++++++++++ draw2dsvg/svg.go | 25 +++++++++++++------------ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/draw2dsvg/gc.go b/draw2dsvg/gc.go index fbaf6f4..ccee40c 100644 --- a/draw2dsvg/gc.go +++ b/draw2dsvg/gc.go @@ -81,6 +81,10 @@ func (gc *GraphicContext) drawPaths(drawType drawType, paths ...*draw2d.Path) { svgPaths[i].StrokeWidth = toSvgLength(gc.Current.LineWidth) svgPaths[i].StrokeLinecap = gc.Current.Cap.String() svgPaths[i].StrokeLinejoin = gc.Current.Join.String() + if len(gc.Current.Dash) > 0 { + svgPaths[i].StrokeDasharray = toSvgArray(gc.Current.Dash) + svgPaths[i].StrokeDashoffset = toSvgLength(gc.Current.DashOffset) + } } else { svgPaths[i].Stroke = "none" } @@ -105,6 +109,14 @@ func toSvgLength(l float64) string { return fmt.Sprintf("%.4f", l) } +func toSvgArray(nums []float64) string { + arr := make([]string, len(nums)) + for i, num := range nums { + arr[i] = fmt.Sprintf("%.4f", num) + } + return strings.Join(arr, ",") +} + func toSvgPathDesc(p *draw2d.Path) string { // TODO move elsewhere parts := make([]string, len(p.Components)) ps := p.Points diff --git a/draw2dsvg/svg.go b/draw2dsvg/svg.go index b2dfbb3..0913bdb 100644 --- a/draw2dsvg/svg.go +++ b/draw2dsvg/svg.go @@ -11,15 +11,15 @@ import ( type Svg struct { XMLName xml.Name `xml:"svg"` - Xmlns string `xml:"xmlns,attr"` - Groups []Group `xml:"g"` + Xmlns string `xml:"xmlns,attr"` + Groups []Group `xml:"g"` } type Group struct { FillStroke Groups []Group `xml:"g"` - Paths []Path `xml:"path"` - Texts []Text `xml:"text"` + Paths []Path `xml:"path"` + Texts []Text `xml:"text"` } type Path struct { @@ -29,17 +29,18 @@ type Path struct { type Text struct { FillStroke - Text string `xml:",innerxml"` + Text string `xml:",innerxml"` Style string `xml:",attr,omitempty"` } - /* shared attrs */ type FillStroke struct { - Fill string `xml:"fill,attr,omitempty"` - Stroke string `xml:"stroke,attr,omitempty"` - StrokeWidth string `xml:"stroke-width,attr,omitempty"` - StrokeLinecap string `xml:"stroke-linecap,attr,omitempty"` - StrokeLinejoin string `xml:"stroke-linejoin,attr,omitempty"` -} \ No newline at end of file + Fill string `xml:"fill,attr,omitempty"` + Stroke string `xml:"stroke,attr,omitempty"` + StrokeWidth string `xml:"stroke-width,attr,omitempty"` + StrokeLinecap string `xml:"stroke-linecap,attr,omitempty"` + StrokeLinejoin string `xml:"stroke-linejoin,attr,omitempty"` + StrokeDasharray string `xml:"stroke-dasharray,attr,omitempty"` + StrokeDashoffset string `xml:"stroke-dashoffset,attr,omitempty"` +} From d297a025cd79fef445c671118adc78ef86f0c665 Mon Sep 17 00:00:00 2001 From: Drahoslav Date: Sun, 24 Dec 2017 15:31:09 +0100 Subject: [PATCH 10/21] Do minor refactoring of svg context --- draw2dsvg/converters.go | 91 ++++++++++++++++++++++++++++++ draw2dsvg/gc.go | 122 +++++++--------------------------------- draw2dsvg/svg.go | 3 +- draw2dsvg/xml_test.go | 10 ++-- 4 files changed, 119 insertions(+), 107 deletions(-) create mode 100644 draw2dsvg/converters.go diff --git a/draw2dsvg/converters.go b/draw2dsvg/converters.go new file mode 100644 index 0000000..1564f96 --- /dev/null +++ b/draw2dsvg/converters.go @@ -0,0 +1,91 @@ +// Copyright 2015 The draw2d Authors. All rights reserved. +// created: 16/12/2017 by Drahoslav Bednářpackage draw2dsvg + +package draw2dsvg + +import ( + "fmt" + "github.com/llgcode/draw2d" + "image/color" + "math" + "strings" +) + +func toSvgRGBA(c color.Color) string { + r, g, b, a := c.RGBA() + return fmt.Sprintf("rgba(%v, %v, %v, %.3f)", r>>8, g>>8, b>>8, float64(a>>8)/255) +} + +func toSvgLength(l float64) string { + return fmt.Sprintf("%.4f", l) +} + +func toSvgArray(nums []float64) string { + arr := make([]string, len(nums)) + for i, num := range nums { + arr[i] = fmt.Sprintf("%.4f", num) + } + return strings.Join(arr, ",") +} + +func toSvgPathDesc(p *draw2d.Path) string { + parts := make([]string, len(p.Components)) + ps := p.Points + for i, cmp := range p.Components { + switch cmp { + case draw2d.MoveToCmp: + parts[i] = fmt.Sprintf("M %.4f,%.4f", ps[0], ps[1]) + ps = ps[2:] + case draw2d.LineToCmp: + parts[i] = fmt.Sprintf("L %.4f,%.4f", ps[0], ps[1]) + ps = ps[2:] + case draw2d.QuadCurveToCmp: + parts[i] = fmt.Sprintf("Q %.4f,%.4f %.4f,%.4f", ps[0], ps[1], ps[2], ps[3]) + ps = ps[4:] + case draw2d.CubicCurveToCmp: + parts[i] = fmt.Sprintf("C %.4f,%.4f %.4f,%.4f %.4f,%.4f", ps[0], ps[1], ps[2], ps[3], ps[4], ps[5]) + ps = ps[6:] + case draw2d.ArcToCmp: + cx, cy := ps[0], ps[1] // center + rx, ry := ps[2], ps[3] // radii + fi := ps[4] + ps[5] // startAngle + angle + + // compute endpoint + sinfi, cosfi := math.Sincos(fi) + nom := math.Hypot(ry*cosfi, rx*sinfi) + x := cx + (rx*ry*cosfi)/nom + y := cy + (rx*ry*sinfi)/nom + + // compute large and sweep flags + large := 0 + sweep := 0 + if math.Abs(ps[5]) > math.Pi { + large = 1 + } + if !math.Signbit(ps[5]) { + sweep = 1 + } + // dirty hack to ensure whole arc is drawn + // if start point equals end point + if sweep == 1 { + x += 0.001 * sinfi + y += 0.001 * -cosfi + } else { + x += 0.001 * sinfi + y += 0.001 * cosfi + } + + // rx ry x-axis-rotation large-arc-flag sweep-flag x y + parts[i] = fmt.Sprintf("A %.4f %.4f %v %v %v %.4f %.4f", + rx, ry, + 0, + large, sweep, + x, y, + ) + ps = ps[6:] + case draw2d.CloseCmp: + parts[i] = "Z" + } + } + return strings.Join(parts, " ") +} diff --git a/draw2dsvg/gc.go b/draw2dsvg/gc.go index ccee40c..1d741b5 100644 --- a/draw2dsvg/gc.go +++ b/draw2dsvg/gc.go @@ -5,13 +5,9 @@ package draw2dsvg import ( "bytes" - "fmt" "github.com/llgcode/draw2d" "github.com/llgcode/draw2d/draw2dbase" "image" - "image/color" - "math" - "strings" ) const () @@ -28,7 +24,10 @@ const ( type SVG bytes.Buffer func NewSvg() *Svg { - return &Svg{Xmlns: "http://www.w3.org/2000/svg"} + return &Svg{ + Xmlns: "http://www.w3.org/2000/svg", + FillStroke: FillStroke{Fill: "none", Stroke: "none"}, + } } // GraphicContext implements the draw2d.GraphicContext interface @@ -74,109 +73,30 @@ func (gc *GraphicContext) drawPaths(drawType drawType, paths ...*draw2d.Path) { svgPaths := make([]Path, len(paths)) + group := Group{ + Paths: svgPaths, + } + for i, path := range paths { svgPaths[i].Desc = toSvgPathDesc(path) - if drawType&stroked == stroked { - svgPaths[i].Stroke = toSvgRGBA(gc.Current.StrokeColor) - svgPaths[i].StrokeWidth = toSvgLength(gc.Current.LineWidth) - svgPaths[i].StrokeLinecap = gc.Current.Cap.String() - svgPaths[i].StrokeLinejoin = gc.Current.Join.String() - if len(gc.Current.Dash) > 0 { - svgPaths[i].StrokeDasharray = toSvgArray(gc.Current.Dash) - svgPaths[i].StrokeDashoffset = toSvgLength(gc.Current.DashOffset) - } - } else { - svgPaths[i].Stroke = "none" - } - if drawType&filled == filled { - svgPaths[i].Fill = toSvgRGBA(gc.Current.FillColor) - } else { - svgPaths[i].Fill = "none" + } + + if drawType&stroked == stroked { + group.Stroke = toSvgRGBA(gc.Current.StrokeColor) + group.StrokeWidth = toSvgLength(gc.Current.LineWidth) + group.StrokeLinecap = gc.Current.Cap.String() + group.StrokeLinejoin = gc.Current.Join.String() + if len(gc.Current.Dash) > 0 { + group.StrokeDasharray = toSvgArray(gc.Current.Dash) + group.StrokeDashoffset = toSvgLength(gc.Current.DashOffset) } } - gc.svg.Groups = append(gc.svg.Groups, Group{ - Paths: svgPaths, - }) -} - -func toSvgRGBA(c color.Color) string { // TODO move elsewhere - r, g, b, a := c.RGBA() - return fmt.Sprintf("rgba(%v, %v, %v, %.3f)", r>>8, g>>8, b>>8, float64(a>>8)/255) -} - -func toSvgLength(l float64) string { - return fmt.Sprintf("%.4f", l) -} - -func toSvgArray(nums []float64) string { - arr := make([]string, len(nums)) - for i, num := range nums { - arr[i] = fmt.Sprintf("%.4f", num) + if drawType&filled == filled { + group.Fill = toSvgRGBA(gc.Current.FillColor) } - return strings.Join(arr, ",") -} -func toSvgPathDesc(p *draw2d.Path) string { // TODO move elsewhere - parts := make([]string, len(p.Components)) - ps := p.Points - for i, cmp := range p.Components { - switch cmp { - case draw2d.MoveToCmp: - parts[i] = fmt.Sprintf("M %.4f,%.4f", ps[0], ps[1]) - ps = ps[2:] - case draw2d.LineToCmp: - parts[i] = fmt.Sprintf("L %.4f,%.4f", ps[0], ps[1]) - ps = ps[2:] - case draw2d.QuadCurveToCmp: - parts[i] = fmt.Sprintf("Q %.4f,%.4f %.4f,%.4f", ps[0], ps[1], ps[2], ps[3]) - ps = ps[4:] - case draw2d.CubicCurveToCmp: - parts[i] = fmt.Sprintf("C %.4f,%.4f %.4f,%.4f %.4f,%.4f", ps[0], ps[1], ps[2], ps[3], ps[4], ps[5]) - ps = ps[6:] - case draw2d.ArcToCmp: - cx, cy := ps[0], ps[1] // center - rx, ry := ps[2], ps[3] // radii - fi := ps[4] + ps[5] // startAngle + angle - - // compute endpoint - sinfi, cosfi := math.Sincos(fi) - nom := math.Hypot(ry*cosfi, rx*sinfi) - x := cx + (rx*ry*cosfi)/nom - y := cy + (rx*ry*sinfi)/nom - - // compute large and sweep flags - large := 0 - sweep := 0 - if math.Abs(ps[5]) > math.Pi { - large = 1 - } - if !math.Signbit(ps[5]) { - sweep = 1 - } - // dirty hack to ensure whole arc is drawn - // if start point equals end point - if sweep == 1 { - x += 0.001 * sinfi - y += 0.001 * -cosfi - } else { - x += 0.001 * sinfi - y += 0.001 * cosfi - } - - // rx ry x-axis-rotation large-arc-flag sweep-flag x y - parts[i] = fmt.Sprintf("A %.4f %.4f %v %v %v %.4f %.4f", - rx, ry, - 0, - large, sweep, - x, y, - ) - ps = ps[6:] - case draw2d.CloseCmp: - parts[i] = "Z" - } - } - return strings.Join(parts, " ") + gc.svg.Groups = append(gc.svg.Groups, group) } /////////////////////////////////////// diff --git a/draw2dsvg/svg.go b/draw2dsvg/svg.go index 0913bdb..025847a 100644 --- a/draw2dsvg/svg.go +++ b/draw2dsvg/svg.go @@ -13,6 +13,7 @@ type Svg struct { XMLName xml.Name `xml:"svg"` Xmlns string `xml:"xmlns,attr"` Groups []Group `xml:"g"` + FillStroke } type Group struct { @@ -30,7 +31,7 @@ type Path struct { type Text struct { FillStroke Text string `xml:",innerxml"` - Style string `xml:",attr,omitempty"` + Style string `xml:"style,attr,omitempty"` } /* shared attrs */ diff --git a/draw2dsvg/xml_test.go b/draw2dsvg/xml_test.go index 0206a11..db22895 100644 --- a/draw2dsvg/xml_test.go +++ b/draw2dsvg/xml_test.go @@ -7,8 +7,8 @@ package draw2dsvg import ( - "testing" "encoding/xml" + "testing" ) // Test basic encoding of svg/xml elements @@ -17,11 +17,11 @@ func TestXml(t *testing.T) { svg := NewSvg() svg.Groups = []Group{Group{ Groups: []Group{ - Group{}, // nested groups + Group{}, // nested groups Group{}, }, Texts: []Text{ - Text{Text: "Hello"}, // text + Text{Text: "Hello"}, // text Text{Text: "world", Style: "opacity: 0.5"}, // text with style }, Paths: []Path{ @@ -30,14 +30,14 @@ func TestXml(t *testing.T) { }, }} - expectedOut := ` + expectedOut := ` Hello - world + world ` From 6d31bfac59e8bff86014f7d458ee1a8ace77b92a Mon Sep 17 00:00:00 2001 From: Drahoslav Date: Sun, 24 Dec 2017 16:05:15 +0100 Subject: [PATCH 11/21] Use fill-rule attribute in svg context --- draw2dsvg/converters.go | 7 +++++++ draw2dsvg/gc.go | 16 +++++++++++----- draw2dsvg/svg.go | 4 +++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/draw2dsvg/converters.go b/draw2dsvg/converters.go index 1564f96..7b5c6a3 100644 --- a/draw2dsvg/converters.go +++ b/draw2dsvg/converters.go @@ -28,6 +28,13 @@ func toSvgArray(nums []float64) string { return strings.Join(arr, ",") } +func toSvgFillRule(rule draw2d.FillRule) string { + return map[draw2d.FillRule]string{ + draw2d.FillRuleEvenOdd: "evenodd", + draw2d.FillRuleWinding: "nonzero", + }[rule] +} + func toSvgPathDesc(p *draw2d.Path) string { parts := make([]string, len(p.Components)) ps := p.Points diff --git a/draw2dsvg/gc.go b/draw2dsvg/gc.go index 1d741b5..3ef6adb 100644 --- a/draw2dsvg/gc.go +++ b/draw2dsvg/gc.go @@ -8,6 +8,7 @@ import ( "github.com/llgcode/draw2d" "github.com/llgcode/draw2d/draw2dbase" "image" + "strings" ) const () @@ -71,15 +72,17 @@ func (gc *GraphicContext) FillStroke(paths ...*draw2d.Path) { func (gc *GraphicContext) drawPaths(drawType drawType, paths ...*draw2d.Path) { paths = append(paths, gc.Current.Path) - svgPaths := make([]Path, len(paths)) + svgPath := Path{} + group := Group{} - group := Group{ - Paths: svgPaths, - } + svgPathsDesc := make([]string, len(paths)) + // multiple pathes has to be joined to single svg path description + // because fill-rule wont work for whole group for i, path := range paths { - svgPaths[i].Desc = toSvgPathDesc(path) + svgPathsDesc[i] = toSvgPathDesc(path) } + svgPath.Desc = strings.Join(svgPathsDesc, " ") if drawType&stroked == stroked { group.Stroke = toSvgRGBA(gc.Current.StrokeColor) @@ -94,8 +97,11 @@ func (gc *GraphicContext) drawPaths(drawType drawType, paths ...*draw2d.Path) { if drawType&filled == filled { group.Fill = toSvgRGBA(gc.Current.FillColor) + group.FillRule = toSvgFillRule(gc.Current.FillRule) } + group.Paths = []Path{svgPath} + gc.svg.Groups = append(gc.svg.Groups, group) } diff --git a/draw2dsvg/svg.go b/draw2dsvg/svg.go index 025847a..ec16e1b 100644 --- a/draw2dsvg/svg.go +++ b/draw2dsvg/svg.go @@ -37,7 +37,9 @@ type Text struct { /* shared attrs */ type FillStroke struct { - Fill string `xml:"fill,attr,omitempty"` + Fill string `xml:"fill,attr,omitempty"` + FillRule string `xml:"fill-rule,attr,omitempty"` + Stroke string `xml:"stroke,attr,omitempty"` StrokeWidth string `xml:"stroke-width,attr,omitempty"` StrokeLinecap string `xml:"stroke-linecap,attr,omitempty"` From 215a761ccb998d630ccad2bd1687c7d4355ab3a7 Mon Sep 17 00:00:00 2001 From: Drahoslav Date: Tue, 26 Dec 2017 14:25:28 +0100 Subject: [PATCH 12/21] Use transformations in svg context also few minor changes in to svg transformation functions --- draw2dsvg/converters.go | 24 +++++++++++++---- draw2dsvg/gc.go | 59 ++++++++++++++++++++++++----------------- draw2dsvg/svg.go | 7 ++--- 3 files changed, 57 insertions(+), 33 deletions(-) diff --git a/draw2dsvg/converters.go b/draw2dsvg/converters.go index 7b5c6a3..59d5064 100644 --- a/draw2dsvg/converters.go +++ b/draw2dsvg/converters.go @@ -13,7 +13,11 @@ import ( func toSvgRGBA(c color.Color) string { r, g, b, a := c.RGBA() - return fmt.Sprintf("rgba(%v, %v, %v, %.3f)", r>>8, g>>8, b>>8, float64(a>>8)/255) + r, g, b, a = r>>8, g>>8, b>>8, a>>8 + if a == 255 { + return fmt.Sprintf("#%02X%02X%02X", r, g, b) + } + return fmt.Sprintf("rgba(%v,%v,%v,%.3f)", r, g, b, float64(a)/255) } func toSvgLength(l float64) string { @@ -84,10 +88,7 @@ func toSvgPathDesc(p *draw2d.Path) string { // rx ry x-axis-rotation large-arc-flag sweep-flag x y parts[i] = fmt.Sprintf("A %.4f %.4f %v %v %v %.4f %.4f", - rx, ry, - 0, - large, sweep, - x, y, + rx, ry, 0, large, sweep, x, y, ) ps = ps[6:] case draw2d.CloseCmp: @@ -96,3 +97,16 @@ func toSvgPathDesc(p *draw2d.Path) string { } return strings.Join(parts, " ") } + +func toSvgTransform(mat draw2d.Matrix) string { + if mat.IsIdentity() { + return "" + } + if mat.IsTranslation() { + x, y := mat.GetTranslation() + return fmt.Sprintf("translate(%f,%f)", x, y) + } + return fmt.Sprintf("matrix(%f,%f,%f,%f,%f,%f)", + mat[0], mat[1], mat[2], mat[3], mat[4], mat[5], + ) +} diff --git a/draw2dsvg/gc.go b/draw2dsvg/gc.go index 3ef6adb..1daa5fe 100644 --- a/draw2dsvg/gc.go +++ b/draw2dsvg/gc.go @@ -70,38 +70,45 @@ func (gc *GraphicContext) FillStroke(paths ...*draw2d.Path) { } func (gc *GraphicContext) drawPaths(drawType drawType, paths ...*draw2d.Path) { - paths = append(paths, gc.Current.Path) - + // create elements svgPath := Path{} group := Group{} - svgPathsDesc := make([]string, len(paths)) - - // multiple pathes has to be joined to single svg path description - // because fill-rule wont work for whole group - for i, path := range paths { - svgPathsDesc[i] = toSvgPathDesc(path) - } - svgPath.Desc = strings.Join(svgPathsDesc, " ") - - if drawType&stroked == stroked { - group.Stroke = toSvgRGBA(gc.Current.StrokeColor) - group.StrokeWidth = toSvgLength(gc.Current.LineWidth) - group.StrokeLinecap = gc.Current.Cap.String() - group.StrokeLinejoin = gc.Current.Join.String() - if len(gc.Current.Dash) > 0 { - group.StrokeDasharray = toSvgArray(gc.Current.Dash) - group.StrokeDashoffset = toSvgLength(gc.Current.DashOffset) + // set attrs to path + { + paths = append(paths, gc.Current.Path) + svgPathsDesc := make([]string, len(paths)) + // multiple pathes has to be joined to single svg path description + // because fill-rule wont work for whole group as excepted + for i, path := range paths { + svgPathsDesc[i] = toSvgPathDesc(path) } + svgPath.Desc = strings.Join(svgPathsDesc, " ") } - if drawType&filled == filled { - group.Fill = toSvgRGBA(gc.Current.FillColor) - group.FillRule = toSvgFillRule(gc.Current.FillRule) + // set attrs to group + { + if drawType&stroked == stroked { + group.Stroke = toSvgRGBA(gc.Current.StrokeColor) + group.StrokeWidth = toSvgLength(gc.Current.LineWidth) + group.StrokeLinecap = gc.Current.Cap.String() + group.StrokeLinejoin = gc.Current.Join.String() + if len(gc.Current.Dash) > 0 { + group.StrokeDasharray = toSvgArray(gc.Current.Dash) + group.StrokeDashoffset = toSvgLength(gc.Current.DashOffset) + } + } + + if drawType&filled == filled { + group.Fill = toSvgRGBA(gc.Current.FillColor) + group.FillRule = toSvgFillRule(gc.Current.FillRule) + } + + group.Transform = toSvgTransform(gc.Current.Tr) } + // link elements group.Paths = []Path{svgPath} - gc.svg.Groups = append(gc.svg.Groups, group) } @@ -130,12 +137,14 @@ func (gc *GraphicContext) DrawImage(image image.Image) { // Save the context and push it to the context stack func (gc *GraphicContext) Save() { - + gc.StackGraphicContext.Save() + // TODO use common transformation group for multiple elements } // Restore remove the current context and restore the last one func (gc *GraphicContext) Restore() { - + gc.StackGraphicContext.Restore() + // TODO use common transformation group for multiple elements } // ClearRect fills the specified rectangle with a default transparent color diff --git a/draw2dsvg/svg.go b/draw2dsvg/svg.go index ec16e1b..e9a414a 100644 --- a/draw2dsvg/svg.go +++ b/draw2dsvg/svg.go @@ -18,9 +18,10 @@ type Svg struct { type Group struct { FillStroke - Groups []Group `xml:"g"` - Paths []Path `xml:"path"` - Texts []Text `xml:"text"` + Transform string `xml:"transform,attr,omitempty"` + Groups []Group `xml:"g"` + Paths []Path `xml:"path"` + Texts []Text `xml:"text"` } type Path struct { From 90f962641f712a4f9c72c31a109f02815732f671 Mon Sep 17 00:00:00 2001 From: Drahoslav Date: Tue, 26 Dec 2017 19:38:19 +0100 Subject: [PATCH 13/21] Implement basic text drawing in svg context fonts, bounds and string paths not yet implemented --- draw2dsvg/gc.go | 156 ++++++++++++++++++++++++------------------ draw2dsvg/svg.go | 16 +++-- draw2dsvg/xml_test.go | 20 +++--- 3 files changed, 109 insertions(+), 83 deletions(-) diff --git a/draw2dsvg/gc.go b/draw2dsvg/gc.go index 1daa5fe..94d9abe 100644 --- a/draw2dsvg/gc.go +++ b/draw2dsvg/gc.go @@ -46,9 +46,6 @@ func NewGraphicContext(svg *Svg) *GraphicContext { // Clear fills the current canvas with a default transparent color func (gc *GraphicContext) Clear() { gc.svg.Groups = nil - gc.svg.Groups = append(gc.svg.Groups, Group{ - // TODO add background color? - }) } // Stroke strokes the paths with the color specified by SetStrokeColor @@ -69,47 +66,102 @@ func (gc *GraphicContext) FillStroke(paths ...*draw2d.Path) { gc.Current.Path.Clear() } +// FillString draws the text at point (0, 0) +func (gc *GraphicContext) FillString(text string) (cursor float64) { + return gc.FillStringAt(text, 0, 0) +} + +// FillStringAt draws the text at the specified point (x, y) +func (gc *GraphicContext) FillStringAt(text string, x, y float64) (cursor float64) { + return gc.drawString(text, filled, x, y) +} + +// StrokeString draws the contour of the text at point (0, 0) +func (gc *GraphicContext) StrokeString(text string) (cursor float64) { + return gc.StrokeStringAt(text, 0, 0) +} + +// StrokeStringAt draws the contour of the text at point (x, y) +func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64) { + return gc.drawString(text, stroked, x, y) +} + +// Save the context and push it to the context stack +func (gc *GraphicContext) Save() { + gc.StackGraphicContext.Save() + // TODO use common transformation group for multiple elements +} + +// Restore remove the current context and restore the last one +func (gc *GraphicContext) Restore() { + gc.StackGraphicContext.Restore() + // TODO use common transformation group for multiple elements +} + +// private funcitons + func (gc *GraphicContext) drawPaths(drawType drawType, paths ...*draw2d.Path) { // create elements svgPath := Path{} + group := gc.newGroup(drawType) + + // set attrs to path element + paths = append(paths, gc.Current.Path) + svgPathsDesc := make([]string, len(paths)) + // multiple pathes has to be joined to single svg path description + // because fill-rule wont work for whole group as excepted + for i, path := range paths { + svgPathsDesc[i] = toSvgPathDesc(path) + } + svgPath.Desc = strings.Join(svgPathsDesc, " ") + + // link to group + group.Paths = []*Path{&svgPath} +} + +func (gc *GraphicContext) drawString(text string, drawType drawType, x, y float64) float64 { + // create elements + svgText := Text{} + group := gc.newGroup(drawType) + + // set attrs to text element + svgText.Text = text + svgText.X = x + svgText.Y = y + // TODO set font + + // link to group + (*group).Texts = []*Text{&svgText} + return 0 +} + +// Creates new group from current context +// append it to svg and return +func (gc *GraphicContext) newGroup(drawType drawType) *Group { group := Group{} - - // set attrs to path - { - paths = append(paths, gc.Current.Path) - svgPathsDesc := make([]string, len(paths)) - // multiple pathes has to be joined to single svg path description - // because fill-rule wont work for whole group as excepted - for i, path := range paths { - svgPathsDesc[i] = toSvgPathDesc(path) - } - svgPath.Desc = strings.Join(svgPathsDesc, " ") - } - // set attrs to group - { - if drawType&stroked == stroked { - group.Stroke = toSvgRGBA(gc.Current.StrokeColor) - group.StrokeWidth = toSvgLength(gc.Current.LineWidth) - group.StrokeLinecap = gc.Current.Cap.String() - group.StrokeLinejoin = gc.Current.Join.String() - if len(gc.Current.Dash) > 0 { - group.StrokeDasharray = toSvgArray(gc.Current.Dash) - group.StrokeDashoffset = toSvgLength(gc.Current.DashOffset) - } + if drawType&stroked == stroked { + group.Stroke = toSvgRGBA(gc.Current.StrokeColor) + group.StrokeWidth = toSvgLength(gc.Current.LineWidth) + group.StrokeLinecap = gc.Current.Cap.String() + group.StrokeLinejoin = gc.Current.Join.String() + if len(gc.Current.Dash) > 0 { + group.StrokeDasharray = toSvgArray(gc.Current.Dash) + group.StrokeDashoffset = toSvgLength(gc.Current.DashOffset) } - - if drawType&filled == filled { - group.Fill = toSvgRGBA(gc.Current.FillColor) - group.FillRule = toSvgFillRule(gc.Current.FillRule) - } - - group.Transform = toSvgTransform(gc.Current.Tr) } - // link elements - group.Paths = []Path{svgPath} - gc.svg.Groups = append(gc.svg.Groups, group) + if drawType&filled == filled { + group.Fill = toSvgRGBA(gc.Current.FillColor) + group.FillRule = toSvgFillRule(gc.Current.FillRule) + } + + group.Transform = toSvgTransform(gc.Current.Tr) + + // link + gc.svg.Groups = append(gc.svg.Groups, &group) + + return &group } /////////////////////////////////////// @@ -135,18 +187,6 @@ func (gc *GraphicContext) DrawImage(image image.Image) { } -// Save the context and push it to the context stack -func (gc *GraphicContext) Save() { - gc.StackGraphicContext.Save() - // TODO use common transformation group for multiple elements -} - -// Restore remove the current context and restore the last one -func (gc *GraphicContext) Restore() { - gc.StackGraphicContext.Restore() - // TODO use common transformation group for multiple elements -} - // ClearRect fills the specified rectangle with a default transparent color func (gc *GraphicContext) ClearRect(x1, y1, x2, y2 int) { @@ -171,23 +211,3 @@ func (gc *GraphicContext) GetStringBounds(s string) (left, top, right, bottom fl func (gc *GraphicContext) CreateStringPath(text string, x, y float64) (cursor float64) { return 0 } - -// FillString draws the text at point (0, 0) -func (gc *GraphicContext) FillString(text string) (cursor float64) { - return 0 -} - -// FillStringAt draws the text at the specified point (x, y) -func (gc *GraphicContext) FillStringAt(text string, x, y float64) (cursor float64) { - return 0 -} - -// StrokeString draws the contour of the text at point (0, 0) -func (gc *GraphicContext) StrokeString(text string) (cursor float64) { - return 0 -} - -// StrokeStringAt draws the contour of the text at point (x, y) -func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64) { - return 0 -} diff --git a/draw2dsvg/svg.go b/draw2dsvg/svg.go index e9a414a..4a22cc6 100644 --- a/draw2dsvg/svg.go +++ b/draw2dsvg/svg.go @@ -12,16 +12,16 @@ import ( type Svg struct { XMLName xml.Name `xml:"svg"` Xmlns string `xml:"xmlns,attr"` - Groups []Group `xml:"g"` + Groups []*Group `xml:"g"` FillStroke } type Group struct { FillStroke - Transform string `xml:"transform,attr,omitempty"` - Groups []Group `xml:"g"` - Paths []Path `xml:"path"` - Texts []Text `xml:"text"` + Transform string `xml:"transform,attr,omitempty"` + Groups []*Group `xml:"g"` + Paths []*Path `xml:"path"` + Texts []*Text `xml:"text"` } type Path struct { @@ -31,12 +31,18 @@ type Path struct { type Text struct { FillStroke + Position Text string `xml:",innerxml"` Style string `xml:"style,attr,omitempty"` } /* shared attrs */ +type Position struct { + X float64 `xml:"x,attr,omitempty"` + Y float64 `xml:"y,attr,omitempty"` +} + type FillStroke struct { Fill string `xml:"fill,attr,omitempty"` FillRule string `xml:"fill-rule,attr,omitempty"` diff --git a/draw2dsvg/xml_test.go b/draw2dsvg/xml_test.go index db22895..b0760e7 100644 --- a/draw2dsvg/xml_test.go +++ b/draw2dsvg/xml_test.go @@ -15,18 +15,18 @@ import ( func TestXml(t *testing.T) { svg := NewSvg() - svg.Groups = []Group{Group{ - Groups: []Group{ - Group{}, // nested groups - Group{}, + svg.Groups = []*Group{&Group{ + Groups: []*Group{ + &Group{}, // nested groups + &Group{}, }, - Texts: []Text{ - Text{Text: "Hello"}, // text - Text{Text: "world", Style: "opacity: 0.5"}, // text with style + Texts: []*Text{ + &Text{Text: "Hello"}, // text + &Text{Text: "world", Style: "opacity: 0.5"}, // text with style }, - Paths: []Path{ - Path{Desc: "M100,200 C100,100 250,100 250,200 S400,300 400,200"}, // simple path - Path{}, // empty path + Paths: []*Path{ + &Path{Desc: "M100,200 C100,100 250,100 250,200 S400,300 400,200"}, // simple path + &Path{}, // empty path }, }} From 3af25f5588a02693c06ab3905613a081003b40fe Mon Sep 17 00:00:00 2001 From: Drahoslav Date: Tue, 26 Dec 2017 19:52:06 +0100 Subject: [PATCH 14/21] Use font-size in texts of svg context --- draw2dsvg/gc.go | 5 +++-- draw2dsvg/svg.go | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/draw2dsvg/gc.go b/draw2dsvg/gc.go index 94d9abe..884828b 100644 --- a/draw2dsvg/gc.go +++ b/draw2dsvg/gc.go @@ -126,12 +126,13 @@ func (gc *GraphicContext) drawString(text string, drawType drawType, x, y float6 // set attrs to text element svgText.Text = text + svgText.FontSize = gc.Current.FontSize svgText.X = x svgText.Y = y - // TODO set font + svgText.FontFamily = "" // TODO set font // link to group - (*group).Texts = []*Text{&svgText} + group.Texts = []*Text{&svgText} return 0 } diff --git a/draw2dsvg/svg.go b/draw2dsvg/svg.go index 4a22cc6..0867036 100644 --- a/draw2dsvg/svg.go +++ b/draw2dsvg/svg.go @@ -32,8 +32,10 @@ type Path struct { type Text struct { FillStroke Position - Text string `xml:",innerxml"` - Style string `xml:"style,attr,omitempty"` + FontSize float64 `xml:"font-size,attr,omitempty"` + FontFamily string `xml:"font-family,attr,omitempty"` + Text string `xml:",innerxml"` + Style string `xml:"style,attr,omitempty"` } /* shared attrs */ From 6f03f106f6af2ba15539888efd423e6dd510a38e Mon Sep 17 00:00:00 2001 From: Drahoslav Date: Wed, 27 Dec 2017 12:32:23 +0100 Subject: [PATCH 15/21] Minor changes --- draw2dsvg/doc.go | 4 ++-- draw2dsvg/gc.go | 15 +-------------- draw2dsvg/svg.go | 7 +++++++ 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/draw2dsvg/doc.go b/draw2dsvg/doc.go index f07708f..033aaa2 100644 --- a/draw2dsvg/doc.go +++ b/draw2dsvg/doc.go @@ -2,10 +2,10 @@ // created: 16/12/2017 by Drahoslav Bednář // Package draw2svg provides a graphic context that can draw -// vector graphics and text on svg file using the svgo package. +// vector graphics and text on svg file. // // Quick Start // The following Go code geneartes a simple drawing and saves it // to a svg document: -// // TODO +// TODO package draw2dsvg diff --git a/draw2dsvg/gc.go b/draw2dsvg/gc.go index 884828b..ac1f98c 100644 --- a/draw2dsvg/gc.go +++ b/draw2dsvg/gc.go @@ -11,10 +11,6 @@ import ( "strings" ) -const () - -var () - type drawType int const ( @@ -22,15 +18,6 @@ const ( stroked ) -type SVG bytes.Buffer - -func NewSvg() *Svg { - return &Svg{ - Xmlns: "http://www.w3.org/2000/svg", - FillStroke: FillStroke{Fill: "none", Stroke: "none"}, - } -} - // GraphicContext implements the draw2d.GraphicContext interface // It provides draw2d with a svg backend type GraphicContext struct { @@ -210,5 +197,5 @@ func (gc *GraphicContext) GetStringBounds(s string) (left, top, right, bottom fl // CreateStringPath creates a path from the string s at x, y func (gc *GraphicContext) CreateStringPath(text string, x, y float64) (cursor float64) { - return 0 + return 0 // TODO use glyphCache for creating string path } diff --git a/draw2dsvg/svg.go b/draw2dsvg/svg.go index 0867036..baad46e 100644 --- a/draw2dsvg/svg.go +++ b/draw2dsvg/svg.go @@ -16,6 +16,13 @@ type Svg struct { FillStroke } +func NewSvg() *Svg { + return &Svg{ + Xmlns: "http://www.w3.org/2000/svg", + FillStroke: FillStroke{Fill: "none", Stroke: "none"}, + } +} + type Group struct { FillStroke Transform string `xml:"transform,attr,omitempty"` From c1e5edea417a024419193941e5b8f2aeabf136eb Mon Sep 17 00:00:00 2001 From: Drahoslav Date: Sun, 7 Jan 2018 17:47:54 +0100 Subject: [PATCH 16/21] Implement CreateStringPath and GetStringBounds in svg context --- draw2dgl/gc.go | 2 +- draw2dsvg/gc.go | 185 ++++++++++++++++++++++++++++++----- draw2dsvg/text.go | 83 ++++++++++++++++ samples/geometry/geometry.go | 3 +- 4 files changed, 244 insertions(+), 29 deletions(-) create mode 100644 draw2dsvg/text.go diff --git a/draw2dgl/gc.go b/draw2dgl/gc.go index 7253170..c744ee6 100644 --- a/draw2dgl/gc.go +++ b/draw2dgl/gc.go @@ -124,7 +124,7 @@ type GraphicContext struct { painter *Painter fillRasterizer *raster.Rasterizer strokeRasterizer *raster.Rasterizer - FontCache draw2d.FontCache + FontCache draw2d.FontCache glyphCache draw2dbase.GlyphCache glyphBuf *truetype.GlyphBuf DPI int diff --git a/draw2dsvg/gc.go b/draw2dsvg/gc.go index ac1f98c..6e83aa4 100644 --- a/draw2dsvg/gc.go +++ b/draw2dsvg/gc.go @@ -4,11 +4,17 @@ package draw2dsvg import ( - "bytes" "github.com/llgcode/draw2d" "github.com/llgcode/draw2d/draw2dbase" "image" + "log" "strings" + "math" + "github.com/golang/freetype/truetype" + "golang.org/x/image/math/fixed" + "golang.org/x/image/font" + "strconv" + "fmt" ) type drawType int @@ -22,11 +28,22 @@ const ( // It provides draw2d with a svg backend type GraphicContext struct { *draw2dbase.StackGraphicContext - svg *Svg + FontCache draw2d.FontCache + glyphCache draw2dbase.GlyphCache + glyphBuf *truetype.GlyphBuf + svg *Svg + DPI int } func NewGraphicContext(svg *Svg) *GraphicContext { - gc := &GraphicContext{draw2dbase.NewStackGraphicContext(), svg} + gc := &GraphicContext{ + draw2dbase.NewStackGraphicContext(), + draw2d.GetGlobalFontCache(), + draw2dbase.NewGlyphCache(), + &truetype.GlyphBuf{}, + svg, + 92, + } return gc } @@ -120,7 +137,8 @@ func (gc *GraphicContext) drawString(text string, drawType drawType, x, y float6 // link to group group.Texts = []*Text{&svgText} - return 0 + left, _, right, _ := gc.GetStringBounds(text) + return right-left } // Creates new group from current context @@ -165,37 +183,150 @@ func (gc *GraphicContext) GetFontData() draw2d.FontData { return draw2d.FontData{} } + +// NOTE following functions copied from dwra2d{img|gl} +// TODO move them all to common draw2dbase? + +// CreateStringPath creates a path from the string s at x, y, and returns the string width. +// The text is placed so that the left edge of the em square of the first character of s +// and the baseline intersect at x, y. The majority of the affected pixels will be +// above and to the right of the point, but some may be below or to the left. +// For example, drawing a string that starts with a 'J' in an italic font may +// affect pixels below and left of the point. +func (gc *GraphicContext) CreateStringPath(s string, x, y float64) (cursor float64) { + f, err := gc.loadCurrentFont() + if err != nil { + log.Println(err) + return 0.0 + } + startx := x + prev, hasPrev := truetype.Index(0), false + for _, rune := range s { + index := f.Index(rune) + if hasPrev { + x += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index)) + } + err := gc.drawGlyph(index, x, y) + if err != nil { + log.Println(err) + return startx - x + } + x += fUnitsToFloat64(f.HMetric(fixed.Int26_6(gc.Current.Scale), index).AdvanceWidth) + prev, hasPrev = index, true + } + + return x - startx +} + + +// GetStringBounds returns the approximate pixel bounds of the string s at x, y. +// The the left edge of the em square of the first character of s +// and the baseline intersect at 0, 0 in the returned coordinates. +// Therefore the top and left coordinates may well be negative. +func (gc *GraphicContext) GetStringBounds(s string) (left, top, right, bottom float64) { + f, err := gc.loadCurrentFont() + if err != nil { + log.Println(err) + return 0, 0, 0, 0 + } + if gc.Current.Scale == 0 { + panic("zero scale") + } + top, left, bottom, right = 10e6, 10e6, -10e6, -10e6 + cursor := 0.0 + prev, hasPrev := truetype.Index(0), false + for _, rune := range s { + index := f.Index(rune) + if hasPrev { + cursor += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index)) + } + if err := gc.glyphBuf.Load(gc.Current.Font, fixed.Int26_6(gc.Current.Scale), index, font.HintingNone); err != nil { + log.Println(err) + return 0, 0, 0, 0 + } + e0 := 0 + for _, e1 := range gc.glyphBuf.Ends { + ps := gc.glyphBuf.Points[e0:e1] + for _, p := range ps { + x, y := pointToF64Point(p) + top = math.Min(top, y) + bottom = math.Max(bottom, y) + left = math.Min(left, x+cursor) + right = math.Max(right, x+cursor) + } + } + cursor += fUnitsToFloat64(f.HMetric(fixed.Int26_6(gc.Current.Scale), index).AdvanceWidth) + prev, hasPrev = index, true + } + return left, top, right, bottom +} + +func (gc *GraphicContext) loadCurrentFont() (*truetype.Font, error) { + font, err := gc.FontCache.Load(gc.Current.FontData) + if err != nil { + font, err = gc.FontCache.Load(draw2dbase.DefaultFontData) + } + if font != nil { + gc.SetFont(font) + gc.SetFontSize(gc.Current.FontSize) + } + return font, err +} + +func (gc *GraphicContext) drawGlyph(glyph truetype.Index, dx, dy float64) error { + if err := gc.glyphBuf.Load(gc.Current.Font, fixed.Int26_6(gc.Current.Scale), glyph, font.HintingNone); err != nil { + return err + } + e0 := 0 + for _, e1 := range gc.glyphBuf.Ends { + DrawContour(gc, gc.glyphBuf.Points[e0:e1], dx, dy) + e0 = e1 + } + return nil +} + +// recalc recalculates scale and bounds values from the font size, screen +// resolution and font metrics, and invalidates the glyph cache. +func (gc *GraphicContext) recalc() { + gc.Current.Scale = gc.Current.FontSize * float64(gc.DPI) * (64.0 / 72.0) +} + +func (gc *GraphicContext) SetDPI(dpi int) { + gc.DPI = dpi + gc.recalc() +} + +func (gc *GraphicContext) GetDPI() int { + return gc.DPI +} + +// SetFont sets the font used to draw text. +func (gc *GraphicContext) SetFont(font *truetype.Font) { + gc.Current.Font = font +} + +// SetFontSize sets the font size in points (as in “a 12 point font”). +func (gc *GraphicContext) SetFontSize(fontSize float64) { + gc.Current.FontSize = fontSize + gc.recalc() +} + + +/////////////////////////////////////// +// TODO implement following methods (or remove if not neccesary) + // GetFontName gets the current FontData as a string func (gc *GraphicContext) GetFontName() string { - return "" + fontData := gc.Current.FontData + return fmt.Sprintf("%s:%d:%d:%d", fontData.Name, fontData.Family, fontData.Style, gc.Current.FontSize) } // DrawImage draws the raster image in the current canvas func (gc *GraphicContext) DrawImage(image image.Image) { - + // panic("not implemented") } // ClearRect fills the specified rectangle with a default transparent color func (gc *GraphicContext) ClearRect(x1, y1, x2, y2 int) { - -} - -// SetDPI sets the current DPI -func (gc *GraphicContext) SetDPI(dpi int) { - -} - -// GetDPI gets the current DPI -func (gc *GraphicContext) GetDPI() int { - return 0 -} - -// GetStringBounds gets pixel bounds(dimensions) of given string -func (gc *GraphicContext) GetStringBounds(s string) (left, top, right, bottom float64) { - return 0, 0, 0, 0 -} - -// CreateStringPath creates a path from the string s at x, y -func (gc *GraphicContext) CreateStringPath(text string, x, y float64) (cursor float64) { - return 0 // TODO use glyphCache for creating string path + // panic("not implemented") } diff --git a/draw2dsvg/text.go b/draw2dsvg/text.go new file mode 100644 index 0000000..30959ae --- /dev/null +++ b/draw2dsvg/text.go @@ -0,0 +1,83 @@ +// NOTE that this is identical copy of draw2dgl/text.go and draw2dimg/text.go +package draw2dsvg + +import ( + "github.com/golang/freetype/truetype" + "github.com/llgcode/draw2d" + "golang.org/x/image/math/fixed" +) + +// DrawContour draws the given closed contour at the given sub-pixel offset. +func DrawContour(path draw2d.PathBuilder, ps []truetype.Point, dx, dy float64) { + if len(ps) == 0 { + return + } + startX, startY := pointToF64Point(ps[0]) + + path.MoveTo(startX+dx, startY+dy) + q0X, q0Y, on0 := startX, startY, true + for _, p := range ps[1:] { + qX, qY := pointToF64Point(p) + on := p.Flags&0x01 != 0 + if on { + if on0 { + path.LineTo(qX+dx, qY+dy) + } else { + path.QuadCurveTo(q0X+dx, q0Y+dy, qX+dx, qY+dy) + } + } else { + if on0 { + // No-op. + } else { + midX := (q0X + qX) / 2 + midY := (q0Y + qY) / 2 + path.QuadCurveTo(q0X+dx, q0Y+dy, midX+dx, midY+dy) + } + } + q0X, q0Y, on0 = qX, qY, on + } + // Close the curve. + if on0 { + path.LineTo(startX+dx, startY+dy) + } else { + path.QuadCurveTo(q0X+dx, q0Y+dy, startX+dx, startY+dy) + } +} + +func pointToF64Point(p truetype.Point) (x, y float64) { + return fUnitsToFloat64(p.X), -fUnitsToFloat64(p.Y) +} + +func fUnitsToFloat64(x fixed.Int26_6) float64 { + scaled := x << 2 + return float64(scaled/256) + float64(scaled%256)/256.0 +} + +// FontExtents contains font metric information. +type FontExtents struct { + // Ascent is the distance that the text + // extends above the baseline. + Ascent float64 + + // Descent is the distance that the text + // extends below the baseline. The descent + // is given as a negative value. + Descent float64 + + // Height is the distance from the lowest + // descending point to the highest ascending + // point. + Height float64 +} + +// Extents returns the FontExtents for a font. +// TODO needs to read this https://developer.apple.com/fonts/TrueType-Reference-Manual/RM02/Chap2.html#intro +func Extents(font *truetype.Font, size float64) FontExtents { + bounds := font.Bounds(fixed.Int26_6(font.FUnitsPerEm())) + scale := size / float64(font.FUnitsPerEm()) + return FontExtents{ + Ascent: float64(bounds.Max.Y) * scale, + Descent: float64(bounds.Min.Y) * scale, + Height: float64(bounds.Max.Y-bounds.Min.Y) * scale, + } +} diff --git a/samples/geometry/geometry.go b/samples/geometry/geometry.go index 99f1731..90892f9 100644 --- a/samples/geometry/geometry.go +++ b/samples/geometry/geometry.go @@ -202,7 +202,8 @@ func FillString(gc draw2d.GraphicContext, x, y, width, height float64) { gc.SetFontData(draw2d.FontData{ Name: "luxi", Family: draw2d.FontFamilyMono, - Style: draw2d.FontStyleBold | draw2d.FontStyleItalic}) + Style: draw2d.FontStyleBold | draw2d.FontStyleItalic, + }) w := gc.FillString("Hug") gc.Translate(w+sx, 0) left, top, right, bottom := gc.GetStringBounds("cou") From 1b49270d080a9073a84d485e9d0fad72e0010654 Mon Sep 17 00:00:00 2001 From: Drahoslav Date: Sun, 7 Jan 2018 17:57:38 +0100 Subject: [PATCH 17/21] Experimental embed svg font functionality --- draw2dsvg/fileutil.go | 4 +- draw2dsvg/gc.go | 79 ++++++++++++++++++++++++++--------- draw2dsvg/svg.go | 52 +++++++++++++++++++++-- draw2dsvg/xml_test.go | 1 + output/samples/geometry.png | Bin 23028 -> 24322 bytes path.go | 31 ++++++++++++++ samples/geometry/geometry.go | 9 ++++ 7 files changed, 152 insertions(+), 24 deletions(-) diff --git a/draw2dsvg/fileutil.go b/draw2dsvg/fileutil.go index ed28f03..2ade34b 100644 --- a/draw2dsvg/fileutil.go +++ b/draw2dsvg/fileutil.go @@ -1,9 +1,9 @@ package draw2dsvg import ( - "os" "encoding/xml" _ "errors" + "os" ) func SaveToSvgFile(filePath string, svg *Svg) error { @@ -19,4 +19,4 @@ func SaveToSvgFile(filePath string, svg *Svg) error { err = encoder.Encode(svg) return err -} \ No newline at end of file +} diff --git a/draw2dsvg/gc.go b/draw2dsvg/gc.go index 6e83aa4..b4a6dca 100644 --- a/draw2dsvg/gc.go +++ b/draw2dsvg/gc.go @@ -4,17 +4,17 @@ package draw2dsvg import ( + "fmt" + "github.com/golang/freetype/truetype" "github.com/llgcode/draw2d" "github.com/llgcode/draw2d/draw2dbase" + "golang.org/x/image/font" + "golang.org/x/image/math/fixed" "image" "log" - "strings" "math" - "github.com/golang/freetype/truetype" - "golang.org/x/image/math/fixed" - "golang.org/x/image/font" "strconv" - "fmt" + "strings" ) type drawType int @@ -133,12 +133,16 @@ func (gc *GraphicContext) drawString(text string, drawType drawType, x, y float6 svgText.FontSize = gc.Current.FontSize svgText.X = x svgText.Y = y - svgText.FontFamily = "" // TODO set font + svgText.FontFamily = gc.Current.FontData.Name + + if gc.svg.fontMode == SvgFontMode { + gc.embedSvgFont(text) + } // link to group group.Texts = []*Text{&svgText} left, _, right, _ := gc.GetStringBounds(text) - return right-left + return right - left } // Creates new group from current context @@ -170,20 +174,59 @@ func (gc *GraphicContext) newGroup(drawType drawType) *Group { return &group } -/////////////////////////////////////// -// TODO implement following methods (or remove if not neccesary) +// Embed svg font definition to svg tree itself +func (gc *GraphicContext) embedSvgFont(text string) { + fontName := gc.Current.FontData.Name + gc.loadCurrentFont() -// SetFontData sets the current FontData -func (gc *GraphicContext) SetFontData(fontData draw2d.FontData) { + // find or create font Element + svgFont := (*Font)(nil) + for _, font := range gc.svg.Fonts { + if font.Name == fontName { + svgFont = font + break + } + } + if svgFont == nil { + // create new + svgFont = &Font{} + // and link + gc.svg.Fonts = append(gc.svg.Fonts, svgFont) + } + // fill with glyphs + + gc.Save() + defer gc.Restore() + gc.SetFontSize(2048) + defer gc.SetDPI(gc.GetDPI()) + gc.SetDPI(92) +filling: + for _, rune := range text { + for _, g := range svgFont.Glyphs { + if g.Rune == Rune(rune) { + continue filling + } + } + glyph := gc.glyphCache.Fetch(gc, gc.GetFontName(), rune) + // glyphCache.Load indirectly calls CreateStringPath for single rune string + + glypPath := glyph.Path.VerticalFlip() // svg font glyphs have oposite y axe + svgFont.Glyphs = append(svgFont.Glyphs, &Glyph{ + Rune: Rune(rune), + Desc: toSvgPathDesc(glypPath), + HorizAdvX: glyph.Width, + }) + } + + // set attrs + svgFont.Id = "font-" + strconv.Itoa(len(gc.svg.Fonts)) + svgFont.Name = fontName + + // TODO use css @font-face with id instead of this + svgFont.Face = &Face{Family: fontName, Units: 2048, HorizAdvX: 2048} } -// GetFontData gets the current FontData -func (gc *GraphicContext) GetFontData() draw2d.FontData { - return draw2d.FontData{} -} - - // NOTE following functions copied from dwra2d{img|gl} // TODO move them all to common draw2dbase? @@ -218,7 +261,6 @@ func (gc *GraphicContext) CreateStringPath(s string, x, y float64) (cursor float return x - startx } - // GetStringBounds returns the approximate pixel bounds of the string s at x, y. // The the left edge of the em square of the first character of s // and the baseline intersect at 0, 0 in the returned coordinates. @@ -311,7 +353,6 @@ func (gc *GraphicContext) SetFontSize(fontSize float64) { gc.recalc() } - /////////////////////////////////////// // TODO implement following methods (or remove if not neccesary) diff --git a/draw2dsvg/svg.go b/draw2dsvg/svg.go index baad46e..819832e 100644 --- a/draw2dsvg/svg.go +++ b/draw2dsvg/svg.go @@ -9,10 +9,22 @@ import ( /* svg elements */ +type FontMode int + +const ( + SysFontMode FontMode = 1 << iota + LinkFontMode + SvgFontMode + CssFontMode + PathFontMode +) + type Svg struct { - XMLName xml.Name `xml:"svg"` - Xmlns string `xml:"xmlns,attr"` - Groups []*Group `xml:"g"` + XMLName xml.Name `xml:"svg"` + Xmlns string `xml:"xmlns,attr"` + Fonts []*Font `xml:"defs>font"` + Groups []*Group `xml:"g"` + fontMode FontMode FillStroke } @@ -20,6 +32,7 @@ func NewSvg() *Svg { return &Svg{ Xmlns: "http://www.w3.org/2000/svg", FillStroke: FillStroke{Fill: "none", Stroke: "none"}, + fontMode: SvgFontMode, } } @@ -45,8 +58,41 @@ type Text struct { Style string `xml:"style,attr,omitempty"` } +type Font struct { + Identity + Face *Face `xml:"font-face"` + Glyphs []*Glyph `xml:"glyph"` +} + +type Face struct { + Family string `xml:"font-family,attr"` + Units int `xml:"units-per-em,attr"` + HorizAdvX float64 `xml:"horiz-adv-x,attr"` + // TODO add other attrs, like style, variant, weight... +} + +type Glyph struct { + Rune Rune `xml:"unicode,attr"` + Desc string `xml:"d,attr"` + HorizAdvX float64 `xml:"horiz-adv-x,attr"` +} + +type Rune rune + +func (r Rune) MarshalXMLAttr(name xml.Name) (xml.Attr, error) { + return xml.Attr{ + Name: name, + Value: string(rune(r)), + }, nil +} + /* shared attrs */ +type Identity struct { + Id string `xml:"id,attr"` + Name string `xml:"name,attr"` +} + type Position struct { X float64 `xml:"x,attr,omitempty"` Y float64 `xml:"y,attr,omitempty"` diff --git a/draw2dsvg/xml_test.go b/draw2dsvg/xml_test.go index b0760e7..8a7d11e 100644 --- a/draw2dsvg/xml_test.go +++ b/draw2dsvg/xml_test.go @@ -31,6 +31,7 @@ func TestXml(t *testing.T) { }} expectedOut := ` + diff --git a/output/samples/geometry.png b/output/samples/geometry.png index b68bb3275a511c1bcab41fafd0bdfad1ecd17d33..b46ec8953d3b6fba9d9040e35a36aa13b9776d43 100644 GIT binary patch literal 24322 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 Hu0mjf_k>wS diff --git a/path.go b/path.go index 04a6572..bd24c9b 100644 --- a/path.go +++ b/path.go @@ -190,3 +190,34 @@ func (p *Path) String() string { } return s } + +// Returns new Path with flipped y axes +func (path *Path) VerticalFlip() *Path { + p := path.Copy() + j := 0 + for _, cmd := range p.Components { + switch cmd { + case MoveToCmp, LineToCmp: + p.Points[j+1] = -p.Points[j+1] + j = j + 2 + case QuadCurveToCmp: + p.Points[j+1] = -p.Points[j+1] + p.Points[j+3] = -p.Points[j+3] + j = j + 4 + case CubicCurveToCmp: + p.Points[j+1] = -p.Points[j+1] + p.Points[j+3] = -p.Points[j+3] + p.Points[j+5] = -p.Points[j+5] + j = j + 6 + case ArcToCmp: + p.Points[j+1] = -p.Points[j+1] + p.Points[j+3] = -p.Points[j+3] + p.Points[j+4] = -p.Points[j+4] // start angle + p.Points[j+5] = -p.Points[j+5] // angle + j = j + 6 + case CloseCmp: + } + } + p.y = -p.y + return p +} diff --git a/samples/geometry/geometry.go b/samples/geometry/geometry.go index 90892f9..05d9bdb 100644 --- a/samples/geometry/geometry.go +++ b/samples/geometry/geometry.go @@ -189,6 +189,7 @@ func CubicCurve(gc draw2d.GraphicContext, x, y, width, height float64) { } // FillString draws a filled and stroked string. +// And filles/stroked path created from string. Which may have different - unselectable - output in non raster gc implementations. func FillString(gc draw2d.GraphicContext, x, y, width, height float64) { sx, sy := width/100, height/100 gc.Save() @@ -215,6 +216,14 @@ func FillString(gc draw2d.GraphicContext, x, y, width, height float64) { gc.SetStrokeColor(color.NRGBA{0x33, 0x33, 0xff, 0xff}) gc.SetLineWidth(height / 100) gc.StrokeString("Hug") + + gc.Translate(-(w + sx), sy*24) + w = gc.CreateStringPath("Hug", 0, 0) + gc.Fill() + gc.Translate(w+sx, 0) + gc.CreateStringPath("Hug", 0, 0) + path := gc.GetPath() + gc.Stroke((&path).VerticalFlip()) gc.Restore() } From 99cc16d0ac8b3c21184f17a4e4b43e90103f0a8e Mon Sep 17 00:00:00 2001 From: Drahoslav Date: Mon, 8 Jan 2018 00:25:25 +0100 Subject: [PATCH 18/21] Implement DrawImage in svg context --- draw2dsvg/converters.go | 12 ++++++++++++ draw2dsvg/gc.go | 18 +++++++++++++----- draw2dsvg/svg.go | 11 +++++++++++ 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/draw2dsvg/converters.go b/draw2dsvg/converters.go index 59d5064..70c6ace 100644 --- a/draw2dsvg/converters.go +++ b/draw2dsvg/converters.go @@ -4,9 +4,13 @@ package draw2dsvg import ( + "bytes" + "encoding/base64" "fmt" "github.com/llgcode/draw2d" + "image" "image/color" + "image/png" "math" "strings" ) @@ -110,3 +114,11 @@ func toSvgTransform(mat draw2d.Matrix) string { mat[0], mat[1], mat[2], mat[3], mat[4], mat[5], ) } + +func imageToSvgHref(image image.Image) string { + out := "data:image/png;base64," + pngBuf := &bytes.Buffer{} + png.Encode(pngBuf, image) + out += base64.RawStdEncoding.EncodeToString(pngBuf.Bytes()) + return out +} diff --git a/draw2dsvg/gc.go b/draw2dsvg/gc.go index b4a6dca..50cc7bf 100644 --- a/draw2dsvg/gc.go +++ b/draw2dsvg/gc.go @@ -353,6 +353,19 @@ func (gc *GraphicContext) SetFontSize(fontSize float64) { gc.recalc() } +// DrawImage draws the raster image in the current canvas +func (gc *GraphicContext) DrawImage(image image.Image) { + bounds := image.Bounds() + + gc.newGroup(0).Image = &Image{ + Href: imageToSvgHref(image), + X: bounds.Min.X, + Y: bounds.Min.Y, + Width: bounds.Max.X - bounds.Min.X, + Height: bounds.Max.Y - bounds.Min.Y, + } +} + /////////////////////////////////////// // TODO implement following methods (or remove if not neccesary) @@ -362,11 +375,6 @@ func (gc *GraphicContext) GetFontName() string { return fmt.Sprintf("%s:%d:%d:%d", fontData.Name, fontData.Family, fontData.Style, gc.Current.FontSize) } -// DrawImage draws the raster image in the current canvas -func (gc *GraphicContext) DrawImage(image image.Image) { - // panic("not implemented") -} - // ClearRect fills the specified rectangle with a default transparent color func (gc *GraphicContext) ClearRect(x1, y1, x2, y2 int) { // panic("not implemented") diff --git a/draw2dsvg/svg.go b/draw2dsvg/svg.go index 819832e..f703052 100644 --- a/draw2dsvg/svg.go +++ b/draw2dsvg/svg.go @@ -42,6 +42,7 @@ type Group struct { Groups []*Group `xml:"g"` Paths []*Path `xml:"path"` Texts []*Text `xml:"text"` + Image *Image `xml:"image"` } type Path struct { @@ -58,6 +59,16 @@ type Text struct { Style string `xml:"style,attr,omitempty"` } +type Image struct { + Href string `xml:"href,attr"` + X int `xml:"x,attr,omitempty"` + Y int `xml:"y,attr,omitempty"` + Width int `xml:"width,attr"` + Height int `xml:"height,attr"` +} + +/* font related elements */ + type Font struct { Identity Face *Face `xml:"font-face"` From 1588b49f0d3d121334470c0a7ed56fb39ebd966a Mon Sep 17 00:00:00 2001 From: Drahoslav Date: Wed, 10 Jan 2018 20:21:07 +0100 Subject: [PATCH 19/21] Optimize svg output sligtly --- draw2dsvg/converters.go | 77 +++++++++++++++++++++++++++++++++-------- draw2dsvg/gc.go | 2 +- 2 files changed, 63 insertions(+), 16 deletions(-) diff --git a/draw2dsvg/converters.go b/draw2dsvg/converters.go index 70c6ace..2b6d462 100644 --- a/draw2dsvg/converters.go +++ b/draw2dsvg/converters.go @@ -12,6 +12,7 @@ import ( "image/color" "image/png" "math" + "strconv" "strings" ) @@ -19,19 +20,19 @@ func toSvgRGBA(c color.Color) string { r, g, b, a := c.RGBA() r, g, b, a = r>>8, g>>8, b>>8, a>>8 if a == 255 { - return fmt.Sprintf("#%02X%02X%02X", r, g, b) + return optiSprintf("#%02X%02X%02X", r, g, b) } - return fmt.Sprintf("rgba(%v,%v,%v,%.3f)", r, g, b, float64(a)/255) + return optiSprintf("rgba(%v,%v,%v,%f)", r, g, b, float64(a)/255) } func toSvgLength(l float64) string { - return fmt.Sprintf("%.4f", l) + return optiSprintf("%f", l) } func toSvgArray(nums []float64) string { arr := make([]string, len(nums)) for i, num := range nums { - arr[i] = fmt.Sprintf("%.4f", num) + arr[i] = optiSprintf("%f", num) } return strings.Join(arr, ",") } @@ -49,16 +50,16 @@ func toSvgPathDesc(p *draw2d.Path) string { for i, cmp := range p.Components { switch cmp { case draw2d.MoveToCmp: - parts[i] = fmt.Sprintf("M %.4f,%.4f", ps[0], ps[1]) + parts[i] = optiSprintf("M %f,%f", ps[0], ps[1]) ps = ps[2:] case draw2d.LineToCmp: - parts[i] = fmt.Sprintf("L %.4f,%.4f", ps[0], ps[1]) + parts[i] = optiSprintf("L %f,%f", ps[0], ps[1]) ps = ps[2:] case draw2d.QuadCurveToCmp: - parts[i] = fmt.Sprintf("Q %.4f,%.4f %.4f,%.4f", ps[0], ps[1], ps[2], ps[3]) + parts[i] = optiSprintf("Q %f,%f %f,%f", ps[0], ps[1], ps[2], ps[3]) ps = ps[4:] case draw2d.CubicCurveToCmp: - parts[i] = fmt.Sprintf("C %.4f,%.4f %.4f,%.4f %.4f,%.4f", ps[0], ps[1], ps[2], ps[3], ps[4], ps[5]) + parts[i] = optiSprintf("C %f,%f %f,%f %f,%f", ps[0], ps[1], ps[2], ps[3], ps[4], ps[5]) ps = ps[6:] case draw2d.ArcToCmp: cx, cy := ps[0], ps[1] // center @@ -83,15 +84,15 @@ func toSvgPathDesc(p *draw2d.Path) string { // dirty hack to ensure whole arc is drawn // if start point equals end point if sweep == 1 { - x += 0.001 * sinfi - y += 0.001 * -cosfi + x += 0.0001 * sinfi + y += 0.0001 * -cosfi } else { - x += 0.001 * sinfi - y += 0.001 * cosfi + x += 0.0001 * sinfi + y += 0.0001 * cosfi } // rx ry x-axis-rotation large-arc-flag sweep-flag x y - parts[i] = fmt.Sprintf("A %.4f %.4f %v %v %v %.4f %.4f", + parts[i] = optiSprintf("A %f %f %v %v %v %F %F", rx, ry, 0, large, sweep, x, y, ) ps = ps[6:] @@ -108,9 +109,9 @@ func toSvgTransform(mat draw2d.Matrix) string { } if mat.IsTranslation() { x, y := mat.GetTranslation() - return fmt.Sprintf("translate(%f,%f)", x, y) + return optiSprintf("translate(%f,%f)", x, y) } - return fmt.Sprintf("matrix(%f,%f,%f,%f,%f,%f)", + return optiSprintf("matrix(%f,%f,%f,%f,%f,%f)", mat[0], mat[1], mat[2], mat[3], mat[4], mat[5], ) } @@ -122,3 +123,49 @@ func imageToSvgHref(image image.Image) string { out += base64.RawStdEncoding.EncodeToString(pngBuf.Bytes()) return out } + +// Do the same thing as fmt.Sprintf +// except it uses the optimal precition for floats: (0-3) for f and (0-6) for F +// eg.: +// optiSprintf("%f", 3.0) => fmt.Sprintf("%.0f", 3.0) +// optiSprintf("%f", 3.33) => fmt.Sprintf("%.2f", 3.33) +// optiSprintf("%f", 3.3001) => fmt.Sprintf("%.1f", 3.3001) +// optiSprintf("%f", 3.333333333333333) => fmt.Sprintf("%.3f", 3.333333333333333) +// optiSprintf("%F", 3.333333333333333) => fmt.Sprintf("%.6f", 3.333333333333333) +func optiSprintf(format string, a ...interface{}) string { + chunks := strings.Split(format, "%") + newChunks := make([]string, len(chunks)) + for i, chunk := range chunks { + if i != 0 { + verb := chunk[0] + if verb == 'f' || verb == 'F' { + num := a[i-1].(float64) + p := strconv.Itoa(getPrec(num, verb == 'F')) + chunk = strings.Replace(chunk, string(verb), "."+p+"f", 1) + } + } + newChunks[i] = chunk + } + format = strings.Join(newChunks, "%") + return fmt.Sprintf(format, a...) +} + +func getPrec(num float64, better bool) int { + max := 3 + eps := 0.0005 + if better { + max = 6 + eps = 0.0000005 + } + prec := 0 + for math.Mod(num, 1) > eps { + num *= 10 + eps *= 10 + prec++ + } + + if max < prec { + return max + } + return prec +} diff --git a/draw2dsvg/gc.go b/draw2dsvg/gc.go index 50cc7bf..2af5ad7 100644 --- a/draw2dsvg/gc.go +++ b/draw2dsvg/gc.go @@ -369,7 +369,7 @@ func (gc *GraphicContext) DrawImage(image image.Image) { /////////////////////////////////////// // TODO implement following methods (or remove if not neccesary) -// GetFontName gets the current FontData as a string +// GetFontName gets the current FontData with fontSize as a string func (gc *GraphicContext) GetFontName() string { fontData := gc.Current.FontData return fmt.Sprintf("%s:%d:%d:%d", fontData.Name, fontData.Family, fontData.Style, gc.Current.FontSize) From 7419075cb6506016cde72b1d14c34c57b24028c0 Mon Sep 17 00:00:00 2001 From: Drahoslav Date: Wed, 10 Jan 2018 22:13:38 +0100 Subject: [PATCH 20/21] Implement ClearRect in svg context --- draw2dsvg/converters.go | 3 + draw2dsvg/gc.go | 337 +++++++++++++++++++++------------------- draw2dsvg/svg.go | 51 +++++- 3 files changed, 228 insertions(+), 163 deletions(-) diff --git a/draw2dsvg/converters.go b/draw2dsvg/converters.go index 2b6d462..7fc187b 100644 --- a/draw2dsvg/converters.go +++ b/draw2dsvg/converters.go @@ -26,6 +26,9 @@ func toSvgRGBA(c color.Color) string { } func toSvgLength(l float64) string { + if math.IsInf(l, 1) { + return "100%" + } return optiSprintf("%f", l) } diff --git a/draw2dsvg/gc.go b/draw2dsvg/gc.go index 2af5ad7..b39a1cd 100644 --- a/draw2dsvg/gc.go +++ b/draw2dsvg/gc.go @@ -102,132 +102,52 @@ func (gc *GraphicContext) Restore() { // TODO use common transformation group for multiple elements } -// private funcitons - -func (gc *GraphicContext) drawPaths(drawType drawType, paths ...*draw2d.Path) { - // create elements - svgPath := Path{} - group := gc.newGroup(drawType) - - // set attrs to path element - paths = append(paths, gc.Current.Path) - svgPathsDesc := make([]string, len(paths)) - // multiple pathes has to be joined to single svg path description - // because fill-rule wont work for whole group as excepted - for i, path := range paths { - svgPathsDesc[i] = toSvgPathDesc(path) - } - svgPath.Desc = strings.Join(svgPathsDesc, " ") - - // link to group - group.Paths = []*Path{&svgPath} +func (gc *GraphicContext) SetDPI(dpi int) { + gc.DPI = dpi + gc.recalc() } -func (gc *GraphicContext) drawString(text string, drawType drawType, x, y float64) float64 { - // create elements - svgText := Text{} - group := gc.newGroup(drawType) - - // set attrs to text element - svgText.Text = text - svgText.FontSize = gc.Current.FontSize - svgText.X = x - svgText.Y = y - svgText.FontFamily = gc.Current.FontData.Name - - if gc.svg.fontMode == SvgFontMode { - gc.embedSvgFont(text) - } - - // link to group - group.Texts = []*Text{&svgText} - left, _, right, _ := gc.GetStringBounds(text) - return right - left +func (gc *GraphicContext) GetDPI() int { + return gc.DPI } -// Creates new group from current context -// append it to svg and return -func (gc *GraphicContext) newGroup(drawType drawType) *Group { - group := Group{} - // set attrs to group - if drawType&stroked == stroked { - group.Stroke = toSvgRGBA(gc.Current.StrokeColor) - group.StrokeWidth = toSvgLength(gc.Current.LineWidth) - group.StrokeLinecap = gc.Current.Cap.String() - group.StrokeLinejoin = gc.Current.Join.String() - if len(gc.Current.Dash) > 0 { - group.StrokeDasharray = toSvgArray(gc.Current.Dash) - group.StrokeDashoffset = toSvgLength(gc.Current.DashOffset) - } - } - - if drawType&filled == filled { - group.Fill = toSvgRGBA(gc.Current.FillColor) - group.FillRule = toSvgFillRule(gc.Current.FillRule) - } - - group.Transform = toSvgTransform(gc.Current.Tr) - - // link - gc.svg.Groups = append(gc.svg.Groups, &group) - - return &group +// SetFont sets the font used to draw text. +func (gc *GraphicContext) SetFont(font *truetype.Font) { + gc.Current.Font = font } -// Embed svg font definition to svg tree itself -func (gc *GraphicContext) embedSvgFont(text string) { - fontName := gc.Current.FontData.Name - gc.loadCurrentFont() - - // find or create font Element - svgFont := (*Font)(nil) - for _, font := range gc.svg.Fonts { - if font.Name == fontName { - svgFont = font - break - } - } - if svgFont == nil { - // create new - svgFont = &Font{} - // and link - gc.svg.Fonts = append(gc.svg.Fonts, svgFont) - } - - // fill with glyphs - - gc.Save() - defer gc.Restore() - gc.SetFontSize(2048) - defer gc.SetDPI(gc.GetDPI()) - gc.SetDPI(92) -filling: - for _, rune := range text { - for _, g := range svgFont.Glyphs { - if g.Rune == Rune(rune) { - continue filling - } - } - glyph := gc.glyphCache.Fetch(gc, gc.GetFontName(), rune) - // glyphCache.Load indirectly calls CreateStringPath for single rune string - - glypPath := glyph.Path.VerticalFlip() // svg font glyphs have oposite y axe - svgFont.Glyphs = append(svgFont.Glyphs, &Glyph{ - Rune: Rune(rune), - Desc: toSvgPathDesc(glypPath), - HorizAdvX: glyph.Width, - }) - } - - // set attrs - svgFont.Id = "font-" + strconv.Itoa(len(gc.svg.Fonts)) - svgFont.Name = fontName - - // TODO use css @font-face with id instead of this - svgFont.Face = &Face{Family: fontName, Units: 2048, HorizAdvX: 2048} +// SetFontSize sets the font size in points (as in “a 12 point font”). +func (gc *GraphicContext) SetFontSize(fontSize float64) { + gc.Current.FontSize = fontSize + gc.recalc() } -// NOTE following functions copied from dwra2d{img|gl} +// DrawImage draws the raster image in the current canvas +func (gc *GraphicContext) DrawImage(image image.Image) { + bounds := image.Bounds() + + svgImage := &Image{Href: imageToSvgHref(image)} + svgImage.X = float64(bounds.Min.X) + svgImage.Y = float64(bounds.Min.Y) + svgImage.Width = toSvgLength(float64(bounds.Max.X - bounds.Min.X)) + svgImage.Height = toSvgLength(float64(bounds.Max.Y - bounds.Min.Y)) + gc.newGroup(0).Image = svgImage +} + +// ClearRect fills the specified rectangle with a default transparent color +func (gc *GraphicContext) ClearRect(x1, y1, x2, y2 int) { + mask := gc.newMask(x1, y1, x2-x1, y2-y1) + + newGroup := &Group{ + Groups: gc.svg.Groups, + Mask: "url(#" + mask.Id + ")", + } + + // replace groups with new masked group + gc.svg.Groups = []*Group{newGroup} +} + +// NOTE following two functions and soe other further below copied from dwra2d{img|gl} // TODO move them all to common draw2dbase? // CreateStringPath creates a path from the string s at x, y, and returns the string width. @@ -303,6 +223,149 @@ func (gc *GraphicContext) GetStringBounds(s string) (left, top, right, bottom fl return left, top, right, bottom } +//////////////////// +// private funcitons + +func (gc *GraphicContext) drawPaths(drawType drawType, paths ...*draw2d.Path) { + // create elements + svgPath := Path{} + group := gc.newGroup(drawType) + + // set attrs to path element + paths = append(paths, gc.Current.Path) + svgPathsDesc := make([]string, len(paths)) + // multiple pathes has to be joined to single svg path description + // because fill-rule wont work for whole group as excepted + for i, path := range paths { + svgPathsDesc[i] = toSvgPathDesc(path) + } + svgPath.Desc = strings.Join(svgPathsDesc, " ") + + // attach to group + group.Paths = []*Path{&svgPath} +} + +// Add text element to svg and returns its expected width +func (gc *GraphicContext) drawString(text string, drawType drawType, x, y float64) float64 { + // create elements + svgText := Text{} + group := gc.newGroup(drawType) + + // set attrs to text element + svgText.Text = text + svgText.FontSize = gc.Current.FontSize + svgText.X = x + svgText.Y = y + svgText.FontFamily = gc.Current.FontData.Name + + if gc.svg.FontMode == SvgFontMode { + gc.embedSvgFont(text) + } + + // attach to group + group.Texts = []*Text{&svgText} + left, _, right, _ := gc.GetStringBounds(text) + return right - left +} + +// Creates new group from current context +// attach it to svg and return +func (gc *GraphicContext) newGroup(drawType drawType) *Group { + group := Group{} + // set attrs to group + if drawType&stroked == stroked { + group.Stroke = toSvgRGBA(gc.Current.StrokeColor) + group.StrokeWidth = toSvgLength(gc.Current.LineWidth) + group.StrokeLinecap = gc.Current.Cap.String() + group.StrokeLinejoin = gc.Current.Join.String() + if len(gc.Current.Dash) > 0 { + group.StrokeDasharray = toSvgArray(gc.Current.Dash) + group.StrokeDashoffset = toSvgLength(gc.Current.DashOffset) + } + } + + if drawType&filled == filled { + group.Fill = toSvgRGBA(gc.Current.FillColor) + group.FillRule = toSvgFillRule(gc.Current.FillRule) + } + + group.Transform = toSvgTransform(gc.Current.Tr) + + // attach + gc.svg.Groups = append(gc.svg.Groups, &group) + + return &group +} + +// creates new mask attached to svg +func (gc *GraphicContext) newMask(x, y, width, height int) *Mask { + mask := &Mask{} + mask.X = float64(x) + mask.Y = float64(y) + mask.Width = toSvgLength(float64(width)) + mask.Height = toSvgLength(float64(height)) + + // attach mask + gc.svg.Masks = append(gc.svg.Masks, mask) + mask.Id = "mask-" + strconv.Itoa(len(gc.svg.Masks)) + return mask +} + +// Embed svg font definition to svg tree itself +// Or update existing if already exists for curent font data +func (gc *GraphicContext) embedSvgFont(text string) *Font { + fontName := gc.Current.FontData.Name + gc.loadCurrentFont() + + // find or create font Element + svgFont := (*Font)(nil) + for _, font := range gc.svg.Fonts { + if font.Name == fontName { + svgFont = font + break + } + } + if svgFont == nil { + // create new + svgFont = &Font{} + // and attach + gc.svg.Fonts = append(gc.svg.Fonts, svgFont) + } + + // fill with glyphs + + gc.Save() + defer gc.Restore() + gc.SetFontSize(2048) + defer gc.SetDPI(gc.GetDPI()) + gc.SetDPI(92) +filling: + for _, rune := range text { + for _, g := range svgFont.Glyphs { + if g.Rune == Rune(rune) { + continue filling + } + } + glyph := gc.glyphCache.Fetch(gc, gc.GetFontName(), rune) + // glyphCache.Load indirectly calls CreateStringPath for single rune string + + glypPath := glyph.Path.VerticalFlip() // svg font glyphs have oposite y axe + svgFont.Glyphs = append(svgFont.Glyphs, &Glyph{ + Rune: Rune(rune), + Desc: toSvgPathDesc(glypPath), + HorizAdvX: glyph.Width, + }) + } + + // set attrs + svgFont.Id = "font-" + strconv.Itoa(len(gc.svg.Fonts)) + svgFont.Name = fontName + + // TODO use css @font-face with id instead of this + svgFont.Face = &Face{Family: fontName, Units: 2048, HorizAdvX: 2048} + return svgFont +} + func (gc *GraphicContext) loadCurrentFont() (*truetype.Font, error) { font, err := gc.FontCache.Load(gc.Current.FontData) if err != nil { @@ -333,39 +396,6 @@ func (gc *GraphicContext) recalc() { gc.Current.Scale = gc.Current.FontSize * float64(gc.DPI) * (64.0 / 72.0) } -func (gc *GraphicContext) SetDPI(dpi int) { - gc.DPI = dpi - gc.recalc() -} - -func (gc *GraphicContext) GetDPI() int { - return gc.DPI -} - -// SetFont sets the font used to draw text. -func (gc *GraphicContext) SetFont(font *truetype.Font) { - gc.Current.Font = font -} - -// SetFontSize sets the font size in points (as in “a 12 point font”). -func (gc *GraphicContext) SetFontSize(fontSize float64) { - gc.Current.FontSize = fontSize - gc.recalc() -} - -// DrawImage draws the raster image in the current canvas -func (gc *GraphicContext) DrawImage(image image.Image) { - bounds := image.Bounds() - - gc.newGroup(0).Image = &Image{ - Href: imageToSvgHref(image), - X: bounds.Min.X, - Y: bounds.Min.Y, - Width: bounds.Max.X - bounds.Min.X, - Height: bounds.Max.Y - bounds.Min.Y, - } -} - /////////////////////////////////////// // TODO implement following methods (or remove if not neccesary) @@ -374,8 +404,3 @@ func (gc *GraphicContext) GetFontName() string { fontData := gc.Current.FontData return fmt.Sprintf("%s:%d:%d:%d", fontData.Name, fontData.Family, fontData.Style, gc.Current.FontSize) } - -// ClearRect fills the specified rectangle with a default transparent color -func (gc *GraphicContext) ClearRect(x1, y1, x2, y2 int) { - // panic("not implemented") -} diff --git a/draw2dsvg/svg.go b/draw2dsvg/svg.go index f703052..e1cccbe 100644 --- a/draw2dsvg/svg.go +++ b/draw2dsvg/svg.go @@ -23,8 +23,9 @@ type Svg struct { XMLName xml.Name `xml:"svg"` Xmlns string `xml:"xmlns,attr"` Fonts []*Font `xml:"defs>font"` + Masks []*Mask `xml:"defs>mask"` Groups []*Group `xml:"g"` - fontMode FontMode + FontMode FontMode `xml:"-"` FillStroke } @@ -32,7 +33,7 @@ func NewSvg() *Svg { return &Svg{ Xmlns: "http://www.w3.org/2000/svg", FillStroke: FillStroke{Fill: "none", Stroke: "none"}, - fontMode: SvgFontMode, + FontMode: SvgFontMode, } } @@ -43,6 +44,7 @@ type Group struct { Paths []*Path `xml:"path"` Texts []*Text `xml:"text"` Image *Image `xml:"image"` + Mask string `xml:"mask,attr,omitempty"` } type Path struct { @@ -60,11 +62,41 @@ type Text struct { } type Image struct { - Href string `xml:"href,attr"` - X int `xml:"x,attr,omitempty"` - Y int `xml:"y,attr,omitempty"` - Width int `xml:"width,attr"` - Height int `xml:"height,attr"` + Position + Dimension + Href string `xml:"href,attr"` +} + +type Mask struct { + Identity + Position + Dimension +} + +type Rect struct { + Position + Dimension + FillStroke +} + +func (m Mask) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + bigRect := Rect{} + bigRect.X, bigRect.Y = 0, 0 + bigRect.Width, bigRect.Height = "100%", "100%" + bigRect.Fill = "#fff" + rect := Rect{} + rect.X, rect.Y = m.X, m.Y + rect.Width, rect.Height = m.Width, m.Height + rect.Fill = "#000" + + return e.EncodeElement(struct { + XMLName xml.Name `xml:"mask"` + Rects [2]Rect `xml:"rect"` + Id string `xml:"id,attr"` + }{ + Rects: [2]Rect{bigRect, rect}, + Id: m.Id, + }, start) } /* font related elements */ @@ -109,6 +141,11 @@ type Position struct { Y float64 `xml:"y,attr,omitempty"` } +type Dimension struct { + Width string `xml:"width,attr"` + Height string `xml:"height,attr"` +} + type FillStroke struct { Fill string `xml:"fill,attr,omitempty"` FillRule string `xml:"fill-rule,attr,omitempty"` From 0b72959009ba637eeb3709053e6cb494638dc106 Mon Sep 17 00:00:00 2001 From: Drahoslav Date: Wed, 10 Jan 2018 23:00:32 +0100 Subject: [PATCH 21/21] Implement PathFontMode and make it default --- draw2dsvg/converters.go | 9 +++++---- draw2dsvg/gc.go | 14 ++++++++++---- draw2dsvg/svg.go | 19 ++++++++++++++++--- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/draw2dsvg/converters.go b/draw2dsvg/converters.go index 7fc187b..b4440a1 100644 --- a/draw2dsvg/converters.go +++ b/draw2dsvg/converters.go @@ -87,11 +87,11 @@ func toSvgPathDesc(p *draw2d.Path) string { // dirty hack to ensure whole arc is drawn // if start point equals end point if sweep == 1 { - x += 0.0001 * sinfi - y += 0.0001 * -cosfi + x += 0.01 * sinfi + y += 0.01 * -cosfi } else { - x += 0.0001 * sinfi - y += 0.0001 * cosfi + x += 0.01 * sinfi + y += 0.01 * cosfi } // rx ry x-axis-rotation large-arc-flag sweep-flag x y @@ -153,6 +153,7 @@ func optiSprintf(format string, a ...interface{}) string { return fmt.Sprintf(format, a...) } +// TODO needs test, since it is not quiet right func getPrec(num float64, better bool) int { max := 3 eps := 0.0005 diff --git a/draw2dsvg/gc.go b/draw2dsvg/gc.go index b39a1cd..30fba26 100644 --- a/draw2dsvg/gc.go +++ b/draw2dsvg/gc.go @@ -247,6 +247,16 @@ func (gc *GraphicContext) drawPaths(drawType drawType, paths ...*draw2d.Path) { // Add text element to svg and returns its expected width func (gc *GraphicContext) drawString(text string, drawType drawType, x, y float64) float64 { + switch gc.svg.FontMode { + case PathFontMode: + w := gc.CreateStringPath(text, x, y) + gc.drawPaths(drawType) + gc.Current.Path.Clear() + return w + case SvgFontMode: + gc.embedSvgFont(text) + } + // create elements svgText := Text{} group := gc.newGroup(drawType) @@ -258,10 +268,6 @@ func (gc *GraphicContext) drawString(text string, drawType drawType, x, y float6 svgText.Y = y svgText.FontFamily = gc.Current.FontData.Name - if gc.svg.FontMode == SvgFontMode { - gc.embedSvgFont(text) - } - // attach to group group.Texts = []*Text{&svgText} left, _, right, _ := gc.GetStringBounds(text) diff --git a/draw2dsvg/svg.go b/draw2dsvg/svg.go index e1cccbe..2f15bd6 100644 --- a/draw2dsvg/svg.go +++ b/draw2dsvg/svg.go @@ -11,11 +11,24 @@ import ( type FontMode int +// Modes of font handling in svg const ( + // Does nothing special + // Makes sense only for common system fonts SysFontMode FontMode = 1 << iota - LinkFontMode + + // Links font files in css def + // Requires distribution of font files with outputed svg + LinkFontMode // TODO implement + + // Embeds glyphs definition in svg file itself in svg font format + // Has poor browser support SvgFontMode - CssFontMode + + // Embeds font definiton in svg file itself in woff format as part of css def + CssFontMode // TODO implement + + // Converts texts to paths PathFontMode ) @@ -33,7 +46,7 @@ func NewSvg() *Svg { return &Svg{ Xmlns: "http://www.w3.org/2000/svg", FillStroke: FillStroke{Fill: "none", Stroke: "none"}, - FontMode: SvgFontMode, + FontMode: PathFontMode, } }