├── .github └── workflows │ └── go.yml ├── .gitignore ├── AUTHORS ├── LICENSE ├── README.md ├── draw2d.go ├── draw2dbase ├── README.md ├── curve.go ├── curve_test.go ├── dasher.go ├── demux_flattener.go ├── flattener.go ├── line.go ├── stack_gc.go ├── stroker.go └── text.go ├── draw2dgl ├── doc.go ├── gc.go ├── notes.md └── text.go ├── draw2dimg ├── README.md ├── curve_limit_test.go ├── fileutil.go ├── ftgc.go ├── ftpath.go └── text.go ├── draw2dkit ├── README.md ├── draw2dkit.go └── draw2dkit_test.go ├── draw2dpdf ├── README.md ├── doc.go ├── fileutil.go ├── gc.go ├── path_converter.go ├── samples_test.go ├── test_test.go └── vectorizer.go ├── draw2dsvg ├── converters.go ├── doc.go ├── fileutil.go ├── gc.go ├── samples_test.go ├── svg.go ├── test_test.go ├── text.go └── xml_test.go ├── font.go ├── gc.go ├── go.mod ├── go.sum ├── matrix.go ├── output ├── README.md ├── curve │ └── .gitignore ├── draw2dkit │ └── .gitignore ├── raster │ └── .gitignore └── samples │ ├── .gitignore │ ├── geometry.png │ └── postscript.png ├── path.go ├── resource ├── font │ ├── COPYING │ ├── README │ ├── luximb.ttf │ ├── luximbi.json │ ├── luximbi.ttf │ ├── luximbi.z │ ├── luximr.ttf │ ├── luximri.ttf │ ├── luxirb.ttf │ ├── luxirbi.ttf │ ├── luxirr.ttf │ ├── luxirri.ttf │ ├── luxisb.ttf │ ├── luxisbi.ttf │ ├── luxisr.ttf │ └── luxisri.ttf ├── image │ ├── geometry.pdf │ ├── gopher.png │ ├── postscript.pdf │ └── tiger.ps └── result │ ├── TestAndroid.png │ ├── TestBigPicture.png │ ├── TestBubble.png │ ├── TestCurveRectangle.png │ ├── TestDash.png │ ├── TestDrawArc.png │ ├── TestDrawArcNegative.png │ ├── TestDrawCubicCurve.png │ ├── TestDrawImage.png │ ├── TestFillString.png │ ├── TestFillStroke.png │ ├── TestFillStyle.png │ ├── TestGopher.png │ ├── TestLineCap.png │ ├── TestLineJoin.png │ ├── TestMultiSegmentCaps.png │ ├── TestPath.png │ ├── TestPathTransform.png │ ├── TestRoundRectangle.png │ ├── TestStar.png │ └── TestTransform.png ├── samples ├── README.md ├── android │ └── android.go ├── appengine │ ├── app.yaml │ └── server.go ├── frameimage │ └── frameimage.go ├── geometry │ └── geometry.go ├── gopher │ └── gopher.go ├── gopher2 │ └── gopher2.go ├── helloworld │ └── helloworld.go ├── helloworldgl │ └── helloworldgl.go ├── line │ └── line.go ├── linecapjoin │ └── linecapjoin.go ├── postscript │ └── postscript.go ├── postscriptgl │ ├── postscriptgl.go │ └── tiger.ps └── samples.go ├── samples_test.go ├── sync_test.go ├── test └── test_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version: '1.20' 23 | 24 | - name: Get Dependencies 25 | run: go mod tidy 26 | 27 | - name: Build 28 | run: go build -v . ./draw2dbase ./draw2dimg ./draw2dkit ./draw2dpdf ./draw2dsvg 29 | 30 | - name: Test 31 | run: go test -v . ./draw2dbase ./draw2dimg ./draw2dkit ./draw2dpdf ./draw2dsvg 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | **/*.[568ao] 3 | **/*.ao 4 | **/*.so 5 | **/*.pyc 6 | **/._* 7 | **/.nfs.* 8 | **/[568a].out 9 | **/*.exe 10 | **/*~ 11 | **/*.orig 12 | **/*.out 13 | **/*.test 14 | core 15 | _obj 16 | _test 17 | out.png 18 | _test* 19 | 20 | **/*.dll 21 | **/core*[0-9] 22 | .private 23 | 24 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Laurent Le Goff 2 | Stani Michiels, gmail:stani.be 3 | Drahoslav Bednář 4 | Sebastien Binet 5 | sdkawata 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Laurent Le Goff 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 13 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 14 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 15 | OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 16 | OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 17 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 18 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | draw2d 2 | ====== 3 | [![Coverage](http://gocover.io/_badge/github.com/llgcode/draw2d?0)](http://gocover.io/github.com/llgcode/draw2d) 4 | [![GoDoc](https://godoc.org/github.com/llgcode/draw2d?status.svg)](https://godoc.org/github.com/llgcode/draw2d) 5 | [![BuyMeaBeer](https://img.shields.io/badge/buy_me-a_beer-orange)](https://www.buymeacoffee.com/llgcoffee) 6 | 7 | Package draw2d is a [go](http://golang.org) 2D vector graphics library with support for multiple outputs such as [images](http://golang.org/pkg/image) (draw2d), pdf documents (draw2dpdf), opengl (draw2dgl) and svg (draw2dsvg). 8 | There's also a [Postscript reader](https://github.com/llgcode/ps) that uses draw2d. 9 | draw2d is released under the BSD license. 10 | See the [documentation](http://godoc.org/github.com/llgcode/draw2d) for more details. 11 | 12 | [![geometry](https://raw.githubusercontent.com/llgcode/draw2d/master/output/samples/geometry.png)](https://raw.githubusercontent.com/llgcode/draw2d/master/resource/image/geometry.pdf)[![postscript](https://raw.githubusercontent.com/llgcode/draw2d/master/output/samples/postscript.png)](https://raw.githubusercontent.com/llgcode/draw2d/master/resource/image/postscript.pdf) 13 | 14 | Click on an image above to get the pdf, generated with exactly the same draw2d code. The first image is the output of `samples/geometry`. The second image is the result of `samples/postcript`, which demonstrates that draw2d can draw postscript files into images or pdf documents with the [ps](https://github.com/llgcode/ps) package. 15 | 16 | Features 17 | -------- 18 | 19 | Operations in draw2d include stroking and filling polygons, arcs, Bézier curves, drawing images and text rendering with truetype fonts. All drawing operations can be transformed by affine transformations (scale, rotation, translation). 20 | 21 | Package draw2d follows the conventions of the [HTML Canvas 2D Context](http://www.w3.org/TR/2dcontext/) for coordinate system, angles, etc... 22 | 23 | Installation 24 | ------------ 25 | 26 | Install [golang](http://golang.org/doc/install). To install or update the package draw2d on your system, run: 27 | 28 | Stable release 29 | ``` 30 | go get -u gopkg.in/llgcode/draw2d.v1 31 | ``` 32 | 33 | or Current release 34 | ``` 35 | go get -u github.com/llgcode/draw2d 36 | ``` 37 | 38 | 39 | Quick Start 40 | ----------- 41 | 42 | The following Go code generates a simple drawing and saves it to an image file with package draw2d: 43 | 44 | ```go 45 | package main 46 | 47 | import ( 48 | "github.com/llgcode/draw2d/draw2dimg" 49 | "image" 50 | "image/color" 51 | ) 52 | 53 | func main() { 54 | // Initialize the graphic context on an RGBA image 55 | dest := image.NewRGBA(image.Rect(0, 0, 297, 210.0)) 56 | gc := draw2dimg.NewGraphicContext(dest) 57 | 58 | // Set some properties 59 | gc.SetFillColor(color.RGBA{0x44, 0xff, 0x44, 0xff}) 60 | gc.SetStrokeColor(color.RGBA{0x44, 0x44, 0x44, 0xff}) 61 | gc.SetLineWidth(5) 62 | 63 | // Draw a closed shape 64 | gc.BeginPath() // Initialize a new path 65 | gc.MoveTo(10, 10) // Move to a position to start the new path 66 | gc.LineTo(100, 50) 67 | gc.QuadCurveTo(100, 10, 10, 10) 68 | gc.Close() 69 | gc.FillStroke() 70 | 71 | // Save to file 72 | draw2dimg.SaveToPngFile("hello.png", dest) 73 | } 74 | ``` 75 | 76 | The same Go code can also generate a pdf document with package draw2dpdf: 77 | 78 | ```go 79 | package main 80 | 81 | import ( 82 | "github.com/llgcode/draw2d/draw2dpdf" 83 | "image/color" 84 | ) 85 | 86 | func main() { 87 | // Initialize the graphic context on an RGBA image 88 | dest := draw2dpdf.NewPdf("L", "mm", "A4") 89 | gc := draw2dpdf.NewGraphicContext(dest) 90 | 91 | // Set some properties 92 | gc.SetFillColor(color.RGBA{0x44, 0xff, 0x44, 0xff}) 93 | gc.SetStrokeColor(color.RGBA{0x44, 0x44, 0x44, 0xff}) 94 | gc.SetLineWidth(5) 95 | 96 | // Draw a closed shape 97 | gc.MoveTo(10, 10) // should always be called first for a new path 98 | gc.LineTo(100, 50) 99 | gc.QuadCurveTo(100, 10, 10, 10) 100 | gc.Close() 101 | gc.FillStroke() 102 | 103 | // Save to file 104 | draw2dpdf.SaveToPdfFile("hello.pdf", dest) 105 | } 106 | ``` 107 | 108 | There are more examples here: https://github.com/llgcode/draw2d/tree/master/samples 109 | 110 | Drawing on opengl is provided by the draw2dgl package. 111 | 112 | Testing 113 | ------- 114 | 115 | The samples are run as tests from the root package folder `draw2d` by: 116 | ``` 117 | go test ./... 118 | ``` 119 | Or if you want to run with test coverage: 120 | ``` 121 | go test -cover ./... | grep -v "no test" 122 | ``` 123 | This will generate output by the different backends in the output folder. 124 | 125 | Acknowledgments 126 | --------------- 127 | 128 | [Laurent Le Goff](https://github.com/llgcode) wrote this library, inspired by [Postscript](http://www.tailrecursive.org/postscript) and [HTML5 canvas](http://www.w3.org/TR/2dcontext/). He implemented the image and opengl backend with the [freetype-go](https://code.google.com/p/freetype-go/) package. Also he created a pure go [Postscript interpreter](https://github.com/llgcode/ps), which can read postscript images and draw to a draw2d graphic context. [Stani Michiels](https://github.com/stanim) implemented the pdf backend with the [gofpdf](https://github.com/jung-kurt/gofpdf) package. 129 | 130 | 131 | 132 | Packages using draw2d 133 | --------------------- 134 | 135 | - [ps](https://github.com/llgcode/ps): Postscript interpreter written in Go 136 | - [go.uik](https://github.com/skelterjohn/go.uik): a concurrent UI kit written in pure go. 137 | - [smartcrop](https://github.com/muesli/smartcrop): content aware image cropping 138 | - [karta](https://github.com/peterhellberg/karta): drawing Voronoi diagrams 139 | - [chart](https://github.com/vdobler/chart): basic charts in Go 140 | - [hilbert](https://github.com/google/hilbert): package for drawing Hilbert curves 141 | 142 | References 143 | --------- 144 | 145 | - [antigrain.com](http://www.antigrain.com) 146 | - [freetype-go](http://code.google.com/p/freetype-go) 147 | - 148 | -------------------------------------------------------------------------------- /draw2d.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 13/12/2010 by Laurent Le Goff 3 | 4 | // Package draw2d is a pure go 2D vector graphics library with support 5 | // for multiple output devices such as images (draw2d), pdf documents 6 | // (draw2dpdf) and opengl (draw2dgl), which can also be used on the 7 | // google app engine. It can be used as a pure go Cairo alternative. 8 | // draw2d is released under the BSD license. 9 | // 10 | // Features 11 | // 12 | // Operations in draw2d include stroking and filling polygons, arcs, 13 | // Bézier curves, drawing images and text rendering with truetype fonts. 14 | // All drawing operations can be transformed by affine transformations 15 | // (scale, rotation, translation). 16 | // 17 | // Package draw2d follows the conventions of http://www.w3.org/TR/2dcontext for coordinate system, angles, etc... 18 | // 19 | // Installation 20 | // 21 | // To install or update the package draw2d on your system, run: 22 | // go get -u github.com/llgcode/draw2d 23 | // 24 | // Quick Start 25 | // 26 | // Package draw2d itself provides a graphic context that can draw vector 27 | // graphics and text on an image canvas. The following Go code 28 | // generates a simple drawing and saves it to an image file: 29 | // package main 30 | // 31 | // import ( 32 | // "github.com/llgcode/draw2d/draw2dimg" 33 | // "image" 34 | // "image/color" 35 | // ) 36 | // 37 | // func main() { 38 | // // Initialize the graphic context on an RGBA image 39 | // dest := image.NewRGBA(image.Rect(0, 0, 297, 210.0)) 40 | // gc := draw2dimg.NewGraphicContext(dest) 41 | // 42 | // // Set some properties 43 | // gc.SetFillColor(color.RGBA{0x44, 0xff, 0x44, 0xff}) 44 | // gc.SetStrokeColor(color.RGBA{0x44, 0x44, 0x44, 0xff}) 45 | // gc.SetLineWidth(5) 46 | // 47 | // // Draw a closed shape 48 | // gc.MoveTo(10, 10) // should always be called first for a new path 49 | // gc.LineTo(100, 50) 50 | // gc.QuadCurveTo(100, 10, 10, 10) 51 | // gc.Close() 52 | // gc.FillStroke() 53 | // 54 | // // Save to file 55 | // draw2dimg.SaveToPngFile("hello.png", dest) 56 | // } 57 | // 58 | // 59 | // There are more examples here: 60 | // https://github.com/llgcode/draw2d/tree/master/samples 61 | // 62 | // Drawing on pdf documents is provided by the draw2dpdf package. 63 | // Drawing on opengl is provided by the draw2dgl package. 64 | // See subdirectories at the bottom of this page. 65 | // 66 | // Testing 67 | // 68 | // The samples are run as tests from the root package folder `draw2d` by: 69 | // go test ./... 70 | // 71 | // Or if you want to run with test coverage: 72 | // go test -cover ./... | grep -v "no test" 73 | // 74 | // This will generate output by the different backends in the output folder. 75 | // 76 | // Acknowledgments 77 | // 78 | // Laurent Le Goff wrote this library, inspired by Postscript and 79 | // HTML5 canvas. He implemented the image and opengl backend with the 80 | // freetype-go package. Also he created a pure go Postscript 81 | // interpreter, which can read postscript images and draw to a draw2d 82 | // graphic context (https://github.com/llgcode/ps). Stani Michiels 83 | // implemented the pdf backend with the gofpdf package. 84 | // 85 | // Packages using draw2d 86 | // 87 | // - https://github.com/llgcode/ps: Postscript interpreter written in Go 88 | // 89 | // - https://github.com/gonum/plot: drawing plots in Go 90 | // 91 | // - https://github.com/muesli/smartcrop: content aware image cropping 92 | // 93 | // - https://github.com/peterhellberg/karta: drawing Voronoi diagrams 94 | // 95 | // - https://github.com/vdobler/chart: basic charts in Go 96 | package draw2d 97 | 98 | import "image/color" 99 | 100 | // FillRule defines the type for fill rules 101 | type FillRule int 102 | 103 | const ( 104 | // FillRuleEvenOdd determines the "insideness" of a point in the shape 105 | // by drawing a ray from that point to infinity in any direction 106 | // and counting the number of path segments from the given shape that the ray crosses. 107 | // If this number is odd, the point is inside; if even, the point is outside. 108 | FillRuleEvenOdd FillRule = iota 109 | // FillRuleWinding determines the "insideness" of a point in the shape 110 | // by drawing a ray from that point to infinity in any direction 111 | // and then examining the places where a segment of the shape crosses the ray. 112 | // Starting with a count of zero, add one each time a path segment crosses 113 | // the ray from left to right and subtract one each time 114 | // a path segment crosses the ray from right to left. After counting the crossings, 115 | // if the result is zero then the point is outside the path. Otherwise, it is inside. 116 | FillRuleWinding 117 | ) 118 | 119 | // LineCap is the style of line extremities 120 | type LineCap int 121 | 122 | const ( 123 | // RoundCap defines a rounded shape at the end of the line 124 | RoundCap LineCap = iota 125 | // ButtCap defines a squared shape exactly at the end of the line 126 | ButtCap 127 | // SquareCap defines a squared shape at the end of the line 128 | SquareCap 129 | ) 130 | 131 | func (cap LineCap) String() string { 132 | return map[LineCap]string{ 133 | RoundCap: "round", 134 | ButtCap: "cap", 135 | SquareCap: "square", 136 | }[cap] 137 | } 138 | 139 | // LineJoin is the style of segments joint 140 | type LineJoin int 141 | 142 | const ( 143 | // BevelJoin represents cut segments joint 144 | BevelJoin LineJoin = iota 145 | // RoundJoin represents rounded segments joint 146 | RoundJoin 147 | // MiterJoin represents peaker segments joint 148 | MiterJoin 149 | ) 150 | 151 | func (join LineJoin) String() string { 152 | return map[LineJoin]string{ 153 | RoundJoin: "round", 154 | BevelJoin: "bevel", 155 | MiterJoin: "miter", 156 | }[join] 157 | } 158 | 159 | // StrokeStyle keeps stroke style attributes 160 | // that is used by the Stroke method of a Drawer 161 | type StrokeStyle struct { 162 | // Color defines the color of stroke 163 | Color color.Color 164 | // Line width 165 | Width float64 166 | // Line cap style rounded, butt or square 167 | LineCap LineCap 168 | // Line join style bevel, round or miter 169 | LineJoin LineJoin 170 | // offset of the first dash 171 | DashOffset float64 172 | // array represented dash length pair values are plain dash and impair are space between dash 173 | // if empty display plain line 174 | Dash []float64 175 | } 176 | 177 | // SolidFillStyle define style attributes for a solid fill style 178 | type SolidFillStyle struct { 179 | // Color defines the line color 180 | Color color.Color 181 | // FillRule defines the file rule to used 182 | FillRule FillRule 183 | } 184 | 185 | // Valign Vertical Alignment of the text 186 | type Valign int 187 | 188 | const ( 189 | // ValignTop top align text 190 | ValignTop Valign = iota 191 | // ValignCenter centered text 192 | ValignCenter 193 | // ValignBottom bottom aligned text 194 | ValignBottom 195 | // ValignBaseline align text with the baseline of the font 196 | ValignBaseline 197 | ) 198 | 199 | // Halign Horizontal Alignment of the text 200 | type Halign int 201 | 202 | const ( 203 | // HalignLeft Horizontally align to left 204 | HalignLeft = iota 205 | // HalignCenter Horizontally align to center 206 | HalignCenter 207 | // HalignRight Horizontally align to right 208 | HalignRight 209 | ) 210 | 211 | // TextStyle describe text property 212 | type TextStyle struct { 213 | // Color defines the color of text 214 | Color color.Color 215 | // Size font size 216 | Size float64 217 | // The font to use 218 | Font FontData 219 | // Horizontal Alignment of the text 220 | Halign Halign 221 | // Vertical Alignment of the text 222 | Valign Valign 223 | } 224 | 225 | // ScalingPolicy is a constant to define how to scale an image 226 | type ScalingPolicy int 227 | 228 | const ( 229 | // ScalingNone no scaling applied 230 | ScalingNone ScalingPolicy = iota 231 | // ScalingStretch the image is stretched so that its width and height are exactly the given width and height 232 | ScalingStretch 233 | // ScalingWidth the image is scaled so that its width is exactly the given width 234 | ScalingWidth 235 | // ScalingHeight the image is scaled so that its height is exactly the given height 236 | ScalingHeight 237 | // ScalingFit the image is scaled to the largest scale that allow the image to fit within a rectangle width x height 238 | ScalingFit 239 | // ScalingSameArea the image is scaled so that its area is exactly the area of the given rectangle width x height 240 | ScalingSameArea 241 | // ScalingFill the image is scaled to the smallest scale that allow the image to fully cover a rectangle width x height 242 | ScalingFill 243 | ) 244 | 245 | // ImageScaling style attributes used to display the image 246 | type ImageScaling struct { 247 | // Horizontal Alignment of the image 248 | Halign Halign 249 | // Vertical Alignment of the image 250 | Valign Valign 251 | // Width Height used by scaling policy 252 | Width, Height float64 253 | // ScalingPolicy defines the scaling policy to applied to the image 254 | ScalingPolicy ScalingPolicy 255 | } 256 | -------------------------------------------------------------------------------- /draw2dbase/README.md: -------------------------------------------------------------------------------- 1 | draw2d/draw2dbase 2 | ================= 3 | 4 | [![Coverage](http://gocover.io/_badge/github.com/llgcode/draw2d/draw2dbase?0)](http://gocover.io/github.com/llgcode/draw2d/draw2dbase) 5 | [![GoDoc](https://godoc.org/github.com/llgcode/draw2d/draw2dbase?status.svg)](https://godoc.org/github.com/llgcode/draw2d/draw2dbase) 6 | 7 | Base package implementation that is used by pdf, svg, img, gl implementations. 8 | -------------------------------------------------------------------------------- /draw2dbase/curve.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 17/05/2011 by Laurent Le Goff 3 | 4 | package draw2dbase 5 | 6 | import ( 7 | "errors" 8 | "math" 9 | ) 10 | 11 | const ( 12 | // CurveRecursionLimit represents the maximum recursion that is really necessary to subsivide a curve into straight lines 13 | CurveRecursionLimit = 32 14 | ) 15 | 16 | // Cubic 17 | // x1, y1, cpx1, cpy1, cpx2, cpy2, x2, y2 float64 18 | 19 | // SubdivideCubic a Bezier cubic curve in 2 equivalents Bezier cubic curves. 20 | // c1 and c2 parameters are the resulting curves 21 | // length of c, c1 and c2 must be 8 otherwise it panics. 22 | func SubdivideCubic(c, c1, c2 []float64) { 23 | // First point of c is the first point of c1 24 | c1[0], c1[1] = c[0], c[1] 25 | // Last point of c is the last point of c2 26 | c2[6], c2[7] = c[6], c[7] 27 | 28 | // Subdivide segment using midpoints 29 | c1[2] = (c[0] + c[2]) / 2 30 | c1[3] = (c[1] + c[3]) / 2 31 | 32 | midX := (c[2] + c[4]) / 2 33 | midY := (c[3] + c[5]) / 2 34 | 35 | c2[4] = (c[4] + c[6]) / 2 36 | c2[5] = (c[5] + c[7]) / 2 37 | 38 | c1[4] = (c1[2] + midX) / 2 39 | c1[5] = (c1[3] + midY) / 2 40 | 41 | c2[2] = (midX + c2[4]) / 2 42 | c2[3] = (midY + c2[5]) / 2 43 | 44 | c1[6] = (c1[4] + c2[2]) / 2 45 | c1[7] = (c1[5] + c2[3]) / 2 46 | 47 | // Last Point of c1 is equal to the first point of c2 48 | c2[0], c2[1] = c1[6], c1[7] 49 | } 50 | 51 | // TraceCubic generate lines subdividing the cubic curve using a Liner 52 | // flattening_threshold helps determines the flattening expectation of the curve 53 | func TraceCubic(t Liner, cubic []float64, flatteningThreshold float64) error { 54 | if len(cubic) < 8 { 55 | return errors.New("cubic length must be >= 8") 56 | } 57 | // Allocation curves 58 | var curves [CurveRecursionLimit * 8]float64 59 | copy(curves[0:8], cubic[0:8]) 60 | i := 0 61 | 62 | // current curve 63 | var c []float64 64 | 65 | var dx, dy, d2, d3 float64 66 | 67 | for i >= 0 { 68 | c = curves[i:] 69 | dx = c[6] - c[0] 70 | dy = c[7] - c[1] 71 | 72 | d2 = math.Abs((c[2]-c[6])*dy - (c[3]-c[7])*dx) 73 | d3 = math.Abs((c[4]-c[6])*dy - (c[5]-c[7])*dx) 74 | 75 | // if it's flat then trace a line 76 | if (d2+d3)*(d2+d3) <= flatteningThreshold*(dx*dx+dy*dy) || i == len(curves)-8 { 77 | t.LineTo(c[6], c[7]) 78 | i -= 8 79 | } else { 80 | // second half of bezier go lower onto the stack 81 | SubdivideCubic(c, curves[i+8:], curves[i:]) 82 | i += 8 83 | } 84 | } 85 | return nil 86 | } 87 | 88 | // Quad 89 | // x1, y1, cpx1, cpy2, x2, y2 float64 90 | 91 | // SubdivideQuad a Bezier quad curve in 2 equivalents Bezier quad curves. 92 | // c1 and c2 parameters are the resulting curves 93 | // length of c, c1 and c2 must be 6 otherwise it panics. 94 | func SubdivideQuad(c, c1, c2 []float64) { 95 | // First point of c is the first point of c1 96 | c1[0], c1[1] = c[0], c[1] 97 | // Last point of c is the last point of c2 98 | c2[4], c2[5] = c[4], c[5] 99 | 100 | // Subdivide segment using midpoints 101 | c1[2] = (c[0] + c[2]) / 2 102 | c1[3] = (c[1] + c[3]) / 2 103 | c2[2] = (c[2] + c[4]) / 2 104 | c2[3] = (c[3] + c[5]) / 2 105 | c1[4] = (c1[2] + c2[2]) / 2 106 | c1[5] = (c1[3] + c2[3]) / 2 107 | c2[0], c2[1] = c1[4], c1[5] 108 | return 109 | } 110 | 111 | // TraceQuad generate lines subdividing the curve using a Liner 112 | // flattening_threshold helps determines the flattening expectation of the curve 113 | func TraceQuad(t Liner, quad []float64, flatteningThreshold float64) error { 114 | if len(quad) < 6 { 115 | return errors.New("quad length must be >= 6") 116 | } 117 | // Allocates curves stack 118 | var curves [CurveRecursionLimit * 6]float64 119 | copy(curves[0:6], quad[0:6]) 120 | i := 0 121 | // current curve 122 | var c []float64 123 | var dx, dy, d float64 124 | 125 | for i >= 0 { 126 | c = curves[i:] 127 | dx = c[4] - c[0] 128 | dy = c[5] - c[1] 129 | 130 | d = math.Abs(((c[2]-c[4])*dy - (c[3]-c[5])*dx)) 131 | 132 | // if it's flat then trace a line 133 | if (d*d) <= flatteningThreshold*(dx*dx+dy*dy) || i == len(curves)-6 { 134 | t.LineTo(c[4], c[5]) 135 | i -= 6 136 | } else { 137 | // second half of bezier go lower onto the stack 138 | SubdivideQuad(c, curves[i+6:], curves[i:]) 139 | i += 6 140 | } 141 | } 142 | return nil 143 | } 144 | 145 | // TraceArc trace an arc using a Liner 146 | func TraceArc(t Liner, x, y, rx, ry, start, angle, scale float64) (lastX, lastY float64) { 147 | end := start + angle 148 | clockWise := true 149 | if angle < 0 { 150 | clockWise = false 151 | } 152 | ra := (math.Abs(rx) + math.Abs(ry)) / 2 153 | da := math.Acos(ra/(ra+0.125/scale)) * 2 154 | //normalize 155 | if !clockWise { 156 | da = -da 157 | } 158 | angle = start + da 159 | var curX, curY float64 160 | for { 161 | if (angle < end-da/4) != clockWise { 162 | curX = x + math.Cos(end)*rx 163 | curY = y + math.Sin(end)*ry 164 | return curX, curY 165 | } 166 | curX = x + math.Cos(angle)*rx 167 | curY = y + math.Sin(angle)*ry 168 | 169 | angle += da 170 | t.LineTo(curX, curY) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /draw2dbase/curve_test.go: -------------------------------------------------------------------------------- 1 | package draw2dbase 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "image" 7 | "image/color" 8 | "image/draw" 9 | "image/png" 10 | "log" 11 | "os" 12 | "testing" 13 | ) 14 | 15 | var ( 16 | flatteningThreshold = 0.5 17 | testsCubicFloat64 = []float64{ 18 | 100, 100, 200, 100, 100, 200, 200, 200, 19 | 100, 100, 300, 200, 200, 200, 300, 100, 20 | 100, 100, 0, 300, 200, 0, 300, 300, 21 | 150, 290, 10, 10, 290, 10, 150, 290, 22 | 10, 290, 10, 10, 290, 10, 290, 290, 23 | 100, 290, 290, 10, 10, 10, 200, 290, 24 | } 25 | testsQuadFloat64 = []float64{ 26 | 100, 100, 200, 100, 200, 200, 27 | 100, 100, 290, 200, 290, 100, 28 | 100, 100, 0, 290, 200, 290, 29 | 150, 290, 10, 10, 290, 290, 30 | 10, 290, 10, 10, 290, 290, 31 | 100, 290, 290, 10, 120, 290, 32 | } 33 | ) 34 | 35 | func init() { 36 | os.Mkdir("test_results", 0666) 37 | f, err := os.Create("../output/curve/_test.html") 38 | if err != nil { 39 | log.Println(err) 40 | os.Exit(1) 41 | } 42 | defer f.Close() 43 | log.Printf("Create html viewer") 44 | f.Write([]byte("")) 45 | for i := 0; i < len(testsCubicFloat64)/8; i++ { 46 | f.Write([]byte(fmt.Sprintf("
\n", i))) 47 | } 48 | for i := 0; i < len(testsQuadFloat64); i++ { 49 | f.Write([]byte(fmt.Sprintf("
\n
\n", i))) 50 | } 51 | f.Write([]byte("")) 52 | 53 | } 54 | 55 | func drawPoints(img draw.Image, c color.Color, s ...float64) image.Image { 56 | for i := 0; i < len(s); i += 2 { 57 | x, y := int(s[i]+0.5), int(s[i+1]+0.5) 58 | img.Set(x, y, c) 59 | img.Set(x, y+1, c) 60 | img.Set(x, y-1, c) 61 | img.Set(x+1, y, c) 62 | img.Set(x+1, y+1, c) 63 | img.Set(x+1, y-1, c) 64 | img.Set(x-1, y, c) 65 | img.Set(x-1, y+1, c) 66 | img.Set(x-1, y-1, c) 67 | 68 | } 69 | return img 70 | } 71 | 72 | func TestCubicCurve(t *testing.T) { 73 | for i := 0; i < len(testsCubicFloat64); i += 8 { 74 | var p SegmentedPath 75 | p.MoveTo(testsCubicFloat64[i], testsCubicFloat64[i+1]) 76 | TraceCubic(&p, testsCubicFloat64[i:], flatteningThreshold) 77 | img := image.NewNRGBA(image.Rect(0, 0, 300, 300)) 78 | PolylineBresenham(img, color.NRGBA{0xff, 0, 0, 0xff}, testsCubicFloat64[i:i+8]...) 79 | PolylineBresenham(img, image.Black, p.Points...) 80 | //drawPoints(img, image.NRGBAColor{0, 0, 0, 0xff}, curve[:]...) 81 | drawPoints(img, color.NRGBA{0, 0, 0, 0xff}, p.Points...) 82 | SaveToPngFile(fmt.Sprintf("../output/curve/_test%d.png", i/8), img) 83 | log.Printf("Num of points: %d\n", len(p.Points)) 84 | } 85 | fmt.Println() 86 | } 87 | 88 | func TestQuadCurve(t *testing.T) { 89 | for i := 0; i < len(testsQuadFloat64); i += 6 { 90 | var p SegmentedPath 91 | p.MoveTo(testsQuadFloat64[i], testsQuadFloat64[i+1]) 92 | TraceQuad(&p, testsQuadFloat64[i:], flatteningThreshold) 93 | img := image.NewNRGBA(image.Rect(0, 0, 300, 300)) 94 | PolylineBresenham(img, color.NRGBA{0xff, 0, 0, 0xff}, testsQuadFloat64[i:i+6]...) 95 | PolylineBresenham(img, image.Black, p.Points...) 96 | //drawPoints(img, image.NRGBAColor{0, 0, 0, 0xff}, curve[:]...) 97 | drawPoints(img, color.NRGBA{0, 0, 0, 0xff}, p.Points...) 98 | SaveToPngFile(fmt.Sprintf("../output/curve/_testQuad%d.png", i), img) 99 | log.Printf("Num of points: %d\n", len(p.Points)) 100 | } 101 | fmt.Println() 102 | } 103 | 104 | func TestQuadCurveCombinedPoint(t *testing.T) { 105 | var p1 SegmentedPath 106 | TraceQuad(&p1, []float64{0, 0, 0, 0, 0, 0}, flatteningThreshold) 107 | if len(p1.Points) != 2 { 108 | t.Error("It must have one point for this curve", len(p1.Points)) 109 | } 110 | var p2 SegmentedPath 111 | TraceQuad(&p2, []float64{0, 0, 100, 100, 0, 0}, flatteningThreshold) 112 | if len(p2.Points) != 2 { 113 | t.Error("It must have one point for this curve", len(p2.Points)) 114 | } 115 | } 116 | 117 | func TestCubicCurveCombinedPoint(t *testing.T) { 118 | var p1 SegmentedPath 119 | TraceCubic(&p1, []float64{0, 0, 0, 0, 0, 0, 0, 0}, flatteningThreshold) 120 | if len(p1.Points) != 2 { 121 | t.Error("It must have one point for this curve", len(p1.Points)) 122 | } 123 | var p2 SegmentedPath 124 | TraceCubic(&p2, []float64{0, 0, 100, 100, 200, 200, 0, 0}, flatteningThreshold) 125 | if len(p2.Points) != 2 { 126 | t.Error("It must have one point for this curve", len(p2.Points)) 127 | } 128 | } 129 | 130 | func BenchmarkCubicCurve(b *testing.B) { 131 | for i := 0; i < b.N; i++ { 132 | for i := 0; i < len(testsCubicFloat64); i += 8 { 133 | var p SegmentedPath 134 | p.MoveTo(testsCubicFloat64[i], testsCubicFloat64[i+1]) 135 | TraceCubic(&p, testsCubicFloat64[i:], flatteningThreshold) 136 | } 137 | } 138 | } 139 | 140 | // SaveToPngFile create and save an image to a file using PNG format 141 | func SaveToPngFile(filePath string, m image.Image) error { 142 | // Create the file 143 | f, err := os.Create(filePath) 144 | if err != nil { 145 | return err 146 | } 147 | defer f.Close() 148 | // Create Writer from file 149 | b := bufio.NewWriter(f) 150 | // Write the image into the buffer 151 | err = png.Encode(b, m) 152 | if err != nil { 153 | return err 154 | } 155 | err = b.Flush() 156 | if err != nil { 157 | return err 158 | } 159 | return nil 160 | } 161 | 162 | func TestOutOfRangeTraceCurve(t *testing.T) { 163 | c := []float64{ 164 | 100, 100, 200, 100, 100, 200, 165 | } 166 | var p SegmentedPath 167 | TraceCubic(&p, c, flatteningThreshold) 168 | } 169 | 170 | func TestOutOfRangeTraceQuad(t *testing.T) { 171 | c := []float64{ 172 | 100, 100, 200, 100, 173 | } 174 | var p SegmentedPath 175 | TraceQuad(&p, c, flatteningThreshold) 176 | } 177 | -------------------------------------------------------------------------------- /draw2dbase/dasher.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 13/12/2010 by Laurent Le Goff 3 | 4 | package draw2dbase 5 | 6 | type DashVertexConverter struct { 7 | next Flattener 8 | x, y, distance float64 9 | dash []float64 10 | currentDash int 11 | dashOffset float64 12 | } 13 | 14 | func NewDashConverter(dash []float64, dashOffset float64, flattener Flattener) *DashVertexConverter { 15 | var dasher DashVertexConverter 16 | dasher.dash = dash 17 | dasher.currentDash = 0 18 | dasher.dashOffset = dashOffset 19 | dasher.next = flattener 20 | return &dasher 21 | } 22 | 23 | func (dasher *DashVertexConverter) LineTo(x, y float64) { 24 | dasher.lineTo(x, y) 25 | } 26 | 27 | func (dasher *DashVertexConverter) MoveTo(x, y float64) { 28 | dasher.next.MoveTo(x, y) 29 | dasher.x, dasher.y = x, y 30 | dasher.distance = dasher.dashOffset 31 | dasher.currentDash = 0 32 | } 33 | 34 | func (dasher *DashVertexConverter) LineJoin() { 35 | dasher.next.LineJoin() 36 | } 37 | 38 | func (dasher *DashVertexConverter) Close() { 39 | dasher.next.Close() 40 | } 41 | 42 | func (dasher *DashVertexConverter) End() { 43 | dasher.next.End() 44 | } 45 | 46 | func (dasher *DashVertexConverter) lineTo(x, y float64) { 47 | rest := dasher.dash[dasher.currentDash] - dasher.distance 48 | for rest < 0 { 49 | dasher.distance = dasher.distance - dasher.dash[dasher.currentDash] 50 | dasher.currentDash = (dasher.currentDash + 1) % len(dasher.dash) 51 | rest = dasher.dash[dasher.currentDash] - dasher.distance 52 | } 53 | d := distance(dasher.x, dasher.y, x, y) 54 | for d >= rest { 55 | k := rest / d 56 | lx := dasher.x + k*(x-dasher.x) 57 | ly := dasher.y + k*(y-dasher.y) 58 | if dasher.currentDash%2 == 0 { 59 | // line 60 | dasher.next.LineTo(lx, ly) 61 | } else { 62 | // gap 63 | dasher.next.End() 64 | dasher.next.MoveTo(lx, ly) 65 | } 66 | d = d - rest 67 | dasher.x, dasher.y = lx, ly 68 | dasher.currentDash = (dasher.currentDash + 1) % len(dasher.dash) 69 | rest = dasher.dash[dasher.currentDash] 70 | } 71 | dasher.distance = d 72 | if dasher.currentDash%2 == 0 { 73 | // line 74 | dasher.next.LineTo(x, y) 75 | } else { 76 | // gap 77 | dasher.next.End() 78 | dasher.next.MoveTo(x, y) 79 | } 80 | if dasher.distance >= dasher.dash[dasher.currentDash] { 81 | dasher.distance = dasher.distance - dasher.dash[dasher.currentDash] 82 | dasher.currentDash = (dasher.currentDash + 1) % len(dasher.dash) 83 | } 84 | dasher.x, dasher.y = x, y 85 | } 86 | 87 | func distance(x1, y1, x2, y2 float64) float64 { 88 | return vectorDistance(x2-x1, y2-y1) 89 | } 90 | -------------------------------------------------------------------------------- /draw2dbase/demux_flattener.go: -------------------------------------------------------------------------------- 1 | package draw2dbase 2 | 3 | type DemuxFlattener struct { 4 | Flatteners []Flattener 5 | } 6 | 7 | func (dc DemuxFlattener) MoveTo(x, y float64) { 8 | for _, flattener := range dc.Flatteners { 9 | flattener.MoveTo(x, y) 10 | } 11 | } 12 | 13 | func (dc DemuxFlattener) LineTo(x, y float64) { 14 | for _, flattener := range dc.Flatteners { 15 | flattener.LineTo(x, y) 16 | } 17 | } 18 | 19 | func (dc DemuxFlattener) LineJoin() { 20 | for _, flattener := range dc.Flatteners { 21 | flattener.LineJoin() 22 | } 23 | } 24 | 25 | func (dc DemuxFlattener) Close() { 26 | for _, flattener := range dc.Flatteners { 27 | flattener.Close() 28 | } 29 | } 30 | 31 | func (dc DemuxFlattener) End() { 32 | for _, flattener := range dc.Flatteners { 33 | flattener.End() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /draw2dbase/flattener.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 06/12/2010 by Laurent Le Goff 3 | 4 | package draw2dbase 5 | 6 | import ( 7 | "github.com/llgcode/draw2d" 8 | ) 9 | 10 | // Liner receive segment definition 11 | type Liner interface { 12 | // LineTo Draw a line from the current position to the point (x, y) 13 | LineTo(x, y float64) 14 | } 15 | 16 | // Flattener receive segment definition 17 | type Flattener interface { 18 | // MoveTo Start a New line from the point (x, y) 19 | MoveTo(x, y float64) 20 | // LineTo Draw a line from the current position to the point (x, y) 21 | LineTo(x, y float64) 22 | // LineJoin use Round, Bevel or miter to join points 23 | LineJoin() 24 | // Close add the most recent starting point to close the path to create a polygon 25 | Close() 26 | // End mark the current line as finished so we can draw caps 27 | End() 28 | } 29 | 30 | // Flatten convert curves into straight segments keeping join segments info 31 | func Flatten(path *draw2d.Path, flattener Flattener, scale float64) { 32 | // First Point 33 | var startX, startY float64 = 0, 0 34 | // Current Point 35 | var x, y float64 = 0, 0 36 | i := 0 37 | for _, cmp := range path.Components { 38 | switch cmp { 39 | case draw2d.MoveToCmp: 40 | x, y = path.Points[i], path.Points[i+1] 41 | startX, startY = x, y 42 | if i != 0 { 43 | flattener.End() 44 | } 45 | flattener.MoveTo(x, y) 46 | i += 2 47 | case draw2d.LineToCmp: 48 | x, y = path.Points[i], path.Points[i+1] 49 | flattener.LineTo(x, y) 50 | flattener.LineJoin() 51 | i += 2 52 | case draw2d.QuadCurveToCmp: 53 | TraceQuad(flattener, path.Points[i-2:], 0.5) 54 | x, y = path.Points[i+2], path.Points[i+3] 55 | flattener.LineTo(x, y) 56 | i += 4 57 | case draw2d.CubicCurveToCmp: 58 | TraceCubic(flattener, path.Points[i-2:], 0.5) 59 | x, y = path.Points[i+4], path.Points[i+5] 60 | flattener.LineTo(x, y) 61 | i += 6 62 | case draw2d.ArcToCmp: 63 | x, y = TraceArc(flattener, path.Points[i], path.Points[i+1], path.Points[i+2], path.Points[i+3], path.Points[i+4], path.Points[i+5], scale) 64 | flattener.LineTo(x, y) 65 | i += 6 66 | case draw2d.CloseCmp: 67 | flattener.LineTo(startX, startY) 68 | flattener.Close() 69 | } 70 | } 71 | flattener.End() 72 | } 73 | 74 | // Transformer apply the Matrix transformation tr 75 | type Transformer struct { 76 | Tr draw2d.Matrix 77 | Flattener Flattener 78 | } 79 | 80 | func (t Transformer) MoveTo(x, y float64) { 81 | u := x*t.Tr[0] + y*t.Tr[2] + t.Tr[4] 82 | v := x*t.Tr[1] + y*t.Tr[3] + t.Tr[5] 83 | t.Flattener.MoveTo(u, v) 84 | } 85 | 86 | func (t Transformer) LineTo(x, y float64) { 87 | u := x*t.Tr[0] + y*t.Tr[2] + t.Tr[4] 88 | v := x*t.Tr[1] + y*t.Tr[3] + t.Tr[5] 89 | t.Flattener.LineTo(u, v) 90 | } 91 | 92 | func (t Transformer) LineJoin() { 93 | t.Flattener.LineJoin() 94 | } 95 | 96 | func (t Transformer) Close() { 97 | t.Flattener.Close() 98 | } 99 | 100 | func (t Transformer) End() { 101 | t.Flattener.End() 102 | } 103 | 104 | type SegmentedPath struct { 105 | Points []float64 106 | } 107 | 108 | func (p *SegmentedPath) MoveTo(x, y float64) { 109 | p.Points = append(p.Points, x, y) 110 | // TODO need to mark this point as moveto 111 | } 112 | 113 | func (p *SegmentedPath) LineTo(x, y float64) { 114 | p.Points = append(p.Points, x, y) 115 | } 116 | 117 | func (p *SegmentedPath) LineJoin() { 118 | // TODO need to mark the current point as linejoin 119 | } 120 | 121 | func (p *SegmentedPath) Close() { 122 | // TODO Close 123 | } 124 | 125 | func (p *SegmentedPath) End() { 126 | // Nothing to do 127 | } 128 | -------------------------------------------------------------------------------- /draw2dbase/line.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The draw2d Authors. All rights reserved. 2 | // created: 27/05/2011 by Laurent Le Goff 3 | 4 | package draw2dbase 5 | 6 | import ( 7 | "image/color" 8 | "image/draw" 9 | ) 10 | 11 | func abs(i int) int { 12 | if i < 0 { 13 | return -i 14 | } 15 | return i 16 | } 17 | 18 | // PolylineBresenham draws a polyline to an image 19 | func PolylineBresenham(img draw.Image, c color.Color, s ...float64) { 20 | for i := 2; i < len(s); i += 2 { 21 | Bresenham(img, c, int(s[i-2]+0.5), int(s[i-1]+0.5), int(s[i]+0.5), int(s[i+1]+0.5)) 22 | } 23 | } 24 | 25 | // Bresenham draws a line between (x0, y0) and (x1, y1) 26 | func Bresenham(img draw.Image, color color.Color, x0, y0, x1, y1 int) { 27 | dx := abs(x1 - x0) 28 | dy := abs(y1 - y0) 29 | var sx, sy int 30 | if x0 < x1 { 31 | sx = 1 32 | } else { 33 | sx = -1 34 | } 35 | if y0 < y1 { 36 | sy = 1 37 | } else { 38 | sy = -1 39 | } 40 | err := dx - dy 41 | 42 | var e2 int 43 | for { 44 | img.Set(x0, y0, color) 45 | if x0 == x1 && y0 == y1 { 46 | return 47 | } 48 | e2 = 2 * err 49 | if e2 > -dy { 50 | err = err - dy 51 | x0 = x0 + sx 52 | } 53 | if e2 < dx { 54 | err = err + dx 55 | y0 = y0 + sy 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /draw2dbase/stack_gc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 21/11/2010 by Laurent Le Goff 3 | 4 | package draw2dbase 5 | 6 | import ( 7 | "fmt" 8 | "image" 9 | "image/color" 10 | 11 | "github.com/llgcode/draw2d" 12 | 13 | "github.com/golang/freetype/truetype" 14 | ) 15 | 16 | var DefaultFontData = draw2d.FontData{Name: "luxi", Family: draw2d.FontFamilySans, Style: draw2d.FontStyleNormal} 17 | 18 | type StackGraphicContext struct { 19 | Current *ContextStack 20 | } 21 | 22 | type ContextStack struct { 23 | Tr draw2d.Matrix 24 | Path *draw2d.Path 25 | LineWidth float64 26 | Dash []float64 27 | DashOffset float64 28 | StrokeColor color.Color 29 | FillColor color.Color 30 | FillRule draw2d.FillRule 31 | Cap draw2d.LineCap 32 | Join draw2d.LineJoin 33 | FontSize float64 34 | FontData draw2d.FontData 35 | 36 | Font *truetype.Font 37 | // fontSize and dpi are used to calculate scale. scale is the number of 38 | // 26.6 fixed point units in 1 em. 39 | Scale float64 40 | 41 | Previous *ContextStack 42 | } 43 | 44 | // GetFontName gets the current FontData with fontSize as a string 45 | func (cs *ContextStack) GetFontName() string { 46 | fontData := cs.FontData 47 | return fmt.Sprintf("%s:%d:%d:%9.2f", fontData.Name, fontData.Family, fontData.Style, cs.FontSize) 48 | } 49 | 50 | 51 | /** 52 | * Create a new Graphic context from an image 53 | */ 54 | func NewStackGraphicContext() *StackGraphicContext { 55 | gc := &StackGraphicContext{} 56 | gc.Current = new(ContextStack) 57 | gc.Current.Tr = draw2d.NewIdentityMatrix() 58 | gc.Current.Path = new(draw2d.Path) 59 | gc.Current.LineWidth = 1.0 60 | gc.Current.StrokeColor = image.Black 61 | gc.Current.FillColor = image.White 62 | gc.Current.Cap = draw2d.RoundCap 63 | gc.Current.FillRule = draw2d.FillRuleEvenOdd 64 | gc.Current.Join = draw2d.RoundJoin 65 | gc.Current.FontSize = 10 66 | gc.Current.FontData = DefaultFontData 67 | return gc 68 | } 69 | 70 | func (gc *StackGraphicContext) GetMatrixTransform() draw2d.Matrix { 71 | return gc.Current.Tr 72 | } 73 | 74 | func (gc *StackGraphicContext) SetMatrixTransform(Tr draw2d.Matrix) { 75 | gc.Current.Tr = Tr 76 | } 77 | 78 | func (gc *StackGraphicContext) ComposeMatrixTransform(Tr draw2d.Matrix) { 79 | gc.Current.Tr.Compose(Tr) 80 | } 81 | 82 | func (gc *StackGraphicContext) Rotate(angle float64) { 83 | gc.Current.Tr.Rotate(angle) 84 | } 85 | 86 | func (gc *StackGraphicContext) Translate(tx, ty float64) { 87 | gc.Current.Tr.Translate(tx, ty) 88 | } 89 | 90 | func (gc *StackGraphicContext) Scale(sx, sy float64) { 91 | gc.Current.Tr.Scale(sx, sy) 92 | } 93 | 94 | func (gc *StackGraphicContext) SetStrokeColor(c color.Color) { 95 | gc.Current.StrokeColor = c 96 | } 97 | 98 | func (gc *StackGraphicContext) SetFillColor(c color.Color) { 99 | gc.Current.FillColor = c 100 | } 101 | 102 | func (gc *StackGraphicContext) SetFillRule(f draw2d.FillRule) { 103 | gc.Current.FillRule = f 104 | } 105 | 106 | func (gc *StackGraphicContext) SetLineWidth(lineWidth float64) { 107 | gc.Current.LineWidth = lineWidth 108 | } 109 | 110 | func (gc *StackGraphicContext) SetLineCap(cap draw2d.LineCap) { 111 | gc.Current.Cap = cap 112 | } 113 | 114 | func (gc *StackGraphicContext) SetLineJoin(join draw2d.LineJoin) { 115 | gc.Current.Join = join 116 | } 117 | 118 | func (gc *StackGraphicContext) SetLineDash(dash []float64, dashOffset float64) { 119 | gc.Current.Dash = dash 120 | gc.Current.DashOffset = dashOffset 121 | } 122 | 123 | func (gc *StackGraphicContext) SetFontSize(fontSize float64) { 124 | gc.Current.FontSize = fontSize 125 | } 126 | 127 | func (gc *StackGraphicContext) GetFontSize() float64 { 128 | return gc.Current.FontSize 129 | } 130 | 131 | func (gc *StackGraphicContext) SetFontData(fontData draw2d.FontData) { 132 | gc.Current.FontData = fontData 133 | } 134 | 135 | func (gc *StackGraphicContext) GetFontData() draw2d.FontData { 136 | return gc.Current.FontData 137 | } 138 | 139 | func (gc *StackGraphicContext) BeginPath() { 140 | gc.Current.Path.Clear() 141 | } 142 | 143 | func (gc *StackGraphicContext) GetPath() draw2d.Path { 144 | return *gc.Current.Path.Copy() 145 | } 146 | 147 | func (gc *StackGraphicContext) IsEmpty() bool { 148 | return gc.Current.Path.IsEmpty() 149 | } 150 | 151 | func (gc *StackGraphicContext) LastPoint() (float64, float64) { 152 | return gc.Current.Path.LastPoint() 153 | } 154 | 155 | func (gc *StackGraphicContext) MoveTo(x, y float64) { 156 | gc.Current.Path.MoveTo(x, y) 157 | } 158 | 159 | func (gc *StackGraphicContext) LineTo(x, y float64) { 160 | gc.Current.Path.LineTo(x, y) 161 | } 162 | 163 | func (gc *StackGraphicContext) QuadCurveTo(cx, cy, x, y float64) { 164 | gc.Current.Path.QuadCurveTo(cx, cy, x, y) 165 | } 166 | 167 | func (gc *StackGraphicContext) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) { 168 | gc.Current.Path.CubicCurveTo(cx1, cy1, cx2, cy2, x, y) 169 | } 170 | 171 | func (gc *StackGraphicContext) ArcTo(cx, cy, rx, ry, startAngle, angle float64) { 172 | gc.Current.Path.ArcTo(cx, cy, rx, ry, startAngle, angle) 173 | } 174 | 175 | func (gc *StackGraphicContext) Close() { 176 | gc.Current.Path.Close() 177 | } 178 | 179 | func (gc *StackGraphicContext) Save() { 180 | context := new(ContextStack) 181 | context.FontSize = gc.Current.FontSize 182 | context.FontData = gc.Current.FontData 183 | context.LineWidth = gc.Current.LineWidth 184 | context.StrokeColor = gc.Current.StrokeColor 185 | context.FillColor = gc.Current.FillColor 186 | context.FillRule = gc.Current.FillRule 187 | context.Dash = gc.Current.Dash 188 | context.DashOffset = gc.Current.DashOffset 189 | context.Cap = gc.Current.Cap 190 | context.Join = gc.Current.Join 191 | context.Path = gc.Current.Path.Copy() 192 | context.Font = gc.Current.Font 193 | context.Scale = gc.Current.Scale 194 | copy(context.Tr[:], gc.Current.Tr[:]) 195 | context.Previous = gc.Current 196 | gc.Current = context 197 | } 198 | 199 | func (gc *StackGraphicContext) Restore() { 200 | if gc.Current.Previous != nil { 201 | oldContext := gc.Current 202 | gc.Current = gc.Current.Previous 203 | oldContext.Previous = nil 204 | } 205 | } 206 | 207 | func (gc *StackGraphicContext) GetFontName() string { 208 | return gc.Current.GetFontName() 209 | } 210 | -------------------------------------------------------------------------------- /draw2dbase/stroker.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 13/12/2010 by Laurent Le Goff 3 | 4 | package draw2dbase 5 | 6 | import ( 7 | "math" 8 | 9 | "github.com/llgcode/draw2d" 10 | ) 11 | 12 | type LineStroker struct { 13 | Flattener Flattener 14 | HalfLineWidth float64 15 | Cap draw2d.LineCap 16 | Join draw2d.LineJoin 17 | vertices []float64 18 | rewind []float64 19 | x, y, nx, ny float64 20 | } 21 | 22 | func NewLineStroker(c draw2d.LineCap, j draw2d.LineJoin, flattener Flattener) *LineStroker { 23 | l := new(LineStroker) 24 | l.Flattener = flattener 25 | l.HalfLineWidth = 0.5 26 | l.Cap = c 27 | l.Join = j 28 | return l 29 | } 30 | 31 | func (l *LineStroker) MoveTo(x, y float64) { 32 | l.x, l.y = x, y 33 | } 34 | 35 | func (l *LineStroker) LineTo(x, y float64) { 36 | l.line(l.x, l.y, x, y) 37 | } 38 | 39 | func (l *LineStroker) LineJoin() { 40 | 41 | } 42 | 43 | func (l *LineStroker) line(x1, y1, x2, y2 float64) { 44 | dx := (x2 - x1) 45 | dy := (y2 - y1) 46 | d := vectorDistance(dx, dy) 47 | if d != 0 { 48 | nx := dy * l.HalfLineWidth / d 49 | ny := -(dx * l.HalfLineWidth / d) 50 | l.appendVertex(x1+nx, y1+ny, x2+nx, y2+ny, x1-nx, y1-ny, x2-nx, y2-ny) 51 | l.x, l.y, l.nx, l.ny = x2, y2, nx, ny 52 | } 53 | } 54 | 55 | func (l *LineStroker) Close() { 56 | if len(l.vertices) > 1 { 57 | l.appendVertex(l.vertices[0], l.vertices[1], l.rewind[0], l.rewind[1]) 58 | } 59 | } 60 | 61 | func (l *LineStroker) End() { 62 | if len(l.vertices) > 1 { 63 | l.Flattener.MoveTo(l.vertices[0], l.vertices[1]) 64 | for i, j := 2, 3; j < len(l.vertices); i, j = i+2, j+2 { 65 | l.Flattener.LineTo(l.vertices[i], l.vertices[j]) 66 | } 67 | } 68 | for i, j := len(l.rewind)-2, len(l.rewind)-1; j > 0; i, j = i-2, j-2 { 69 | l.Flattener.LineTo(l.rewind[i], l.rewind[j]) 70 | } 71 | if len(l.vertices) > 1 { 72 | l.Flattener.LineTo(l.vertices[0], l.vertices[1]) 73 | } 74 | l.Flattener.End() 75 | // reinit vertices 76 | l.vertices = l.vertices[0:0] 77 | l.rewind = l.rewind[0:0] 78 | l.x, l.y, l.nx, l.ny = 0, 0, 0, 0 79 | 80 | } 81 | 82 | func (l *LineStroker) appendVertex(vertices ...float64) { 83 | s := len(vertices) / 2 84 | l.vertices = append(l.vertices, vertices[:s]...) 85 | l.rewind = append(l.rewind, vertices[s:]...) 86 | } 87 | 88 | func vectorDistance(dx, dy float64) float64 { 89 | return float64(math.Sqrt(dx*dx + dy*dy)) 90 | } 91 | -------------------------------------------------------------------------------- /draw2dbase/text.go: -------------------------------------------------------------------------------- 1 | package draw2dbase 2 | 3 | import "github.com/llgcode/draw2d" 4 | 5 | // GlyphCache manage a cache of glyphs 6 | type GlyphCache interface { 7 | // Fetch fetches a glyph from the cache, storing with Render first if it doesn't already exist 8 | Fetch(gc draw2d.GraphicContext, fontName string, chr rune) *Glyph 9 | } 10 | 11 | // GlyphCacheImp manage a map of glyphs without sync mecanism, not thread safe 12 | type GlyphCacheImp struct { 13 | glyphs map[string]map[rune]*Glyph 14 | } 15 | 16 | 17 | // NewGlyphCache initializes a GlyphCache 18 | func NewGlyphCache() *GlyphCacheImp { 19 | glyphs := make(map[string]map[rune]*Glyph) 20 | return &GlyphCacheImp { 21 | glyphs: glyphs, 22 | } 23 | } 24 | 25 | // Fetch fetches a glyph from the cache, calling renderGlyph first if it doesn't already exist 26 | func (glyphCache *GlyphCacheImp) Fetch(gc draw2d.GraphicContext, fontName string, chr rune) *Glyph { 27 | if glyphCache.glyphs[fontName] == nil { 28 | glyphCache.glyphs[fontName] = make(map[rune]*Glyph, 60) 29 | } 30 | if glyphCache.glyphs[fontName][chr] == nil { 31 | glyphCache.glyphs[fontName][chr] = renderGlyph(gc, fontName, chr) 32 | } 33 | return glyphCache.glyphs[fontName][chr].Copy() 34 | } 35 | 36 | // renderGlyph renders a glyph then caches and returns it 37 | func renderGlyph(gc draw2d.GraphicContext, fontName string, chr rune) *Glyph { 38 | gc.Save() 39 | defer gc.Restore() 40 | gc.BeginPath() 41 | width := gc.CreateStringPath(string(chr), 0, 0) 42 | path := gc.GetPath() 43 | return &Glyph{ 44 | Path: &path, 45 | Width: width, 46 | } 47 | } 48 | 49 | // Glyph represents a rune which has been converted to a Path and width 50 | type Glyph struct { 51 | // path represents a glyph, it is always at (0, 0) 52 | Path *draw2d.Path 53 | // Width of the glyph 54 | Width float64 55 | } 56 | 57 | // Copy Returns a copy of a Glyph 58 | func (g *Glyph) Copy() *Glyph { 59 | return &Glyph{ 60 | Path: g.Path.Copy(), 61 | Width: g.Width, 62 | } 63 | } 64 | 65 | // Fill copies a glyph from the cache, and fills it 66 | func (g *Glyph) Fill(gc draw2d.GraphicContext, x, y float64) float64 { 67 | gc.Save() 68 | gc.BeginPath() 69 | gc.Translate(x, y) 70 | gc.Fill(g.Path) 71 | gc.Restore() 72 | return g.Width 73 | } 74 | 75 | // Stroke fetches a glyph from the cache, and strokes it 76 | func (g *Glyph) Stroke(gc draw2d.GraphicContext, x, y float64) float64 { 77 | gc.Save() 78 | gc.BeginPath() 79 | gc.Translate(x, y) 80 | gc.Stroke(g.Path) 81 | gc.Restore() 82 | return g.Width 83 | } 84 | -------------------------------------------------------------------------------- /draw2dgl/doc.go: -------------------------------------------------------------------------------- 1 | // Package draw2dgl provides a graphic context that can draw vector 2 | // graphics and text on OpenGL. 3 | package draw2dgl 4 | -------------------------------------------------------------------------------- /draw2dgl/gc.go: -------------------------------------------------------------------------------- 1 | package draw2dgl 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/draw" 7 | "log" 8 | "math" 9 | "runtime" 10 | 11 | "github.com/go-gl/gl/v2.1/gl" 12 | "github.com/golang/freetype/raster" 13 | "github.com/golang/freetype/truetype" 14 | "github.com/llgcode/draw2d" 15 | "github.com/llgcode/draw2d/draw2dbase" 16 | "github.com/llgcode/draw2d/draw2dimg" 17 | 18 | "golang.org/x/image/font" 19 | "golang.org/x/image/math/fixed" 20 | ) 21 | 22 | func init() { 23 | runtime.LockOSThread() 24 | } 25 | 26 | type Painter struct { 27 | // The Porter-Duff composition operator. 28 | Op draw.Op 29 | // The 16-bit color to paint the spans. 30 | cr, cg, cb uint8 31 | ca uint32 32 | colors []uint8 33 | vertices []int32 34 | } 35 | 36 | const M16 uint32 = 1<<16 - 1 37 | 38 | // Paint satisfies the Painter interface by painting ss onto an image.RGBA. 39 | func (p *Painter) Paint(ss []raster.Span, done bool) { 40 | //gl.Begin(gl.LINES) 41 | sslen := len(ss) 42 | clenrequired := sslen * 8 43 | vlenrequired := sslen * 4 44 | if clenrequired >= (cap(p.colors) - len(p.colors)) { 45 | p.Flush() 46 | 47 | if clenrequired >= cap(p.colors) { 48 | p.vertices = make([]int32, 0, vlenrequired+(vlenrequired/2)) 49 | p.colors = make([]uint8, 0, clenrequired+(clenrequired/2)) 50 | } 51 | } 52 | vi := len(p.vertices) 53 | ci := len(p.colors) 54 | p.vertices = p.vertices[0 : vi+vlenrequired] 55 | p.colors = p.colors[0 : ci+clenrequired] 56 | var ( 57 | colors []uint8 58 | vertices []int32 59 | ) 60 | for _, s := range ss { 61 | a := uint8((s.Alpha * p.ca / M16) >> 8) 62 | 63 | colors = p.colors[ci:] 64 | colors[0] = p.cr 65 | colors[1] = p.cg 66 | colors[2] = p.cb 67 | colors[3] = a 68 | colors[4] = p.cr 69 | colors[5] = p.cg 70 | colors[6] = p.cb 71 | colors[7] = a 72 | ci += 8 73 | vertices = p.vertices[vi:] 74 | vertices[0] = int32(s.X0) 75 | vertices[1] = int32(s.Y) 76 | vertices[2] = int32(s.X1) 77 | vertices[3] = int32(s.Y) 78 | vi += 4 79 | } 80 | } 81 | 82 | func (p *Painter) Flush() { 83 | if len(p.vertices) != 0 { 84 | gl.EnableClientState(gl.COLOR_ARRAY) 85 | gl.EnableClientState(gl.VERTEX_ARRAY) 86 | gl.ColorPointer(4, gl.UNSIGNED_BYTE, 0, gl.Ptr(p.colors)) 87 | gl.VertexPointer(2, gl.INT, 0, gl.Ptr(p.vertices)) 88 | 89 | // draw lines 90 | gl.DrawArrays(gl.LINES, 0, int32(len(p.vertices)/2)) 91 | gl.DisableClientState(gl.VERTEX_ARRAY) 92 | gl.DisableClientState(gl.COLOR_ARRAY) 93 | p.vertices = p.vertices[0:0] 94 | p.colors = p.colors[0:0] 95 | } 96 | } 97 | 98 | // SetColor sets the color to paint the spans. 99 | func (p *Painter) SetColor(c color.Color) { 100 | r, g, b, a := c.RGBA() 101 | if a == 0 { 102 | p.cr = 0 103 | p.cg = 0 104 | p.cb = 0 105 | p.ca = a 106 | } else { 107 | p.cr = uint8((r * M16 / a) >> 8) 108 | p.cg = uint8((g * M16 / a) >> 8) 109 | p.cb = uint8((b * M16 / a) >> 8) 110 | p.ca = a 111 | } 112 | } 113 | 114 | // NewRGBAPainter creates a new RGBAPainter for the given image. 115 | func NewPainter() *Painter { 116 | p := new(Painter) 117 | p.vertices = make([]int32, 0, 1024) 118 | p.colors = make([]uint8, 0, 1024) 119 | return p 120 | } 121 | 122 | type GraphicContext struct { 123 | *draw2dbase.StackGraphicContext 124 | painter *Painter 125 | fillRasterizer *raster.Rasterizer 126 | strokeRasterizer *raster.Rasterizer 127 | FontCache draw2d.FontCache 128 | glyphCache draw2dbase.GlyphCache 129 | glyphBuf *truetype.GlyphBuf 130 | DPI int 131 | } 132 | 133 | // NewGraphicContext creates a new Graphic context from an image. 134 | func NewGraphicContext(width, height int) *GraphicContext { 135 | gc := &GraphicContext{ 136 | draw2dbase.NewStackGraphicContext(), 137 | NewPainter(), 138 | raster.NewRasterizer(width, height), 139 | raster.NewRasterizer(width, height), 140 | draw2d.GetGlobalFontCache(), 141 | draw2dbase.NewGlyphCache(), 142 | &truetype.GlyphBuf{}, 143 | 92, 144 | } 145 | return gc 146 | } 147 | 148 | func (gc *GraphicContext) loadCurrentFont() (*truetype.Font, error) { 149 | font, err := gc.FontCache.Load(gc.Current.FontData) 150 | if err != nil { 151 | font, err = gc.FontCache.Load(draw2dbase.DefaultFontData) 152 | } 153 | if font != nil { 154 | gc.SetFont(font) 155 | gc.SetFontSize(gc.Current.FontSize) 156 | } 157 | return font, err 158 | } 159 | 160 | func (gc *GraphicContext) drawGlyph(glyph truetype.Index, dx, dy float64) error { 161 | if err := gc.glyphBuf.Load(gc.Current.Font, fixed.Int26_6(gc.Current.Scale), glyph, font.HintingNone); err != nil { 162 | return err 163 | } 164 | e0 := 0 165 | for _, e1 := range gc.glyphBuf.Ends { 166 | DrawContour(gc, gc.glyphBuf.Points[e0:e1], dx, dy) 167 | e0 = e1 168 | } 169 | return nil 170 | } 171 | 172 | // CreateStringPath creates a path from the string s at x, y, and returns the string width. 173 | // The text is placed so that the left edge of the em square of the first character of s 174 | // and the baseline intersect at x, y. The majority of the affected pixels will be 175 | // above and to the right of the point, but some may be below or to the left. 176 | // For example, drawing a string that starts with a 'J' in an italic font may 177 | // affect pixels below and left of the point. 178 | func (gc *GraphicContext) CreateStringPath(s string, x, y float64) float64 { 179 | f, err := gc.loadCurrentFont() 180 | if err != nil { 181 | log.Println(err) 182 | return 0.0 183 | } 184 | startx := x 185 | prev, hasPrev := truetype.Index(0), false 186 | for _, rune := range s { 187 | index := f.Index(rune) 188 | if hasPrev { 189 | x += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index)) 190 | } 191 | err := gc.drawGlyph(index, x, y) 192 | if err != nil { 193 | log.Println(err) 194 | return startx - x 195 | } 196 | x += fUnitsToFloat64(f.HMetric(fixed.Int26_6(gc.Current.Scale), index).AdvanceWidth) 197 | prev, hasPrev = index, true 198 | } 199 | return x - startx 200 | } 201 | 202 | // FillString draws the text at point (0, 0) 203 | func (gc *GraphicContext) FillString(text string) (width float64) { 204 | return gc.FillStringAt(text, 0, 0) 205 | } 206 | 207 | // FillStringAt draws the text at the specified point (x, y) 208 | func (gc *GraphicContext) FillStringAt(text string, x, y float64) (width float64) { 209 | f, err := gc.loadCurrentFont() 210 | if err != nil { 211 | log.Println(err) 212 | return 0.0 213 | } 214 | startx := x 215 | prev, hasPrev := truetype.Index(0), false 216 | fontName := gc.GetFontName() 217 | for _, r := range text { 218 | index := f.Index(r) 219 | if hasPrev { 220 | x += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index)) 221 | } 222 | glyph := gc.glyphCache.Fetch(gc, fontName, r) 223 | x += glyph.Fill(gc, x, y) 224 | prev, hasPrev = index, true 225 | } 226 | return x - startx 227 | } 228 | 229 | // GetStringBounds returns the approximate pixel bounds of the string s at x, y. 230 | // The the left edge of the em square of the first character of s 231 | // and the baseline intersect at 0, 0 in the returned coordinates. 232 | // Therefore the top and left coordinates may well be negative. 233 | func (gc *GraphicContext) GetStringBounds(s string) (left, top, right, bottom float64) { 234 | f, err := gc.loadCurrentFont() 235 | if err != nil { 236 | log.Println(err) 237 | return 0, 0, 0, 0 238 | } 239 | top, left, bottom, right = 10e6, 10e6, -10e6, -10e6 240 | cursor := 0.0 241 | prev, hasPrev := truetype.Index(0), false 242 | for _, rune := range s { 243 | index := f.Index(rune) 244 | if hasPrev { 245 | cursor += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index)) 246 | } 247 | if err := gc.glyphBuf.Load(gc.Current.Font, fixed.Int26_6(gc.Current.Scale), index, font.HintingNone); err != nil { 248 | log.Println(err) 249 | return 0, 0, 0, 0 250 | } 251 | e0 := 0 252 | for _, e1 := range gc.glyphBuf.Ends { 253 | ps := gc.glyphBuf.Points[e0:e1] 254 | for _, p := range ps { 255 | x, y := pointToF64Point(p) 256 | top = math.Min(top, y) 257 | bottom = math.Max(bottom, y) 258 | left = math.Min(left, x+cursor) 259 | right = math.Max(right, x+cursor) 260 | } 261 | } 262 | cursor += fUnitsToFloat64(f.HMetric(fixed.Int26_6(gc.Current.Scale), index).AdvanceWidth) 263 | prev, hasPrev = index, true 264 | } 265 | return left, top, right, bottom 266 | } 267 | 268 | // StrokeString draws the contour of the text at point (0, 0) 269 | func (gc *GraphicContext) StrokeString(text string) (width float64) { 270 | return gc.StrokeStringAt(text, 0, 0) 271 | } 272 | 273 | // StrokeStringAt draws the contour of the text at point (x, y) 274 | func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (width float64) { 275 | f, err := gc.loadCurrentFont() 276 | if err != nil { 277 | log.Println(err) 278 | return 0.0 279 | } 280 | startx := x 281 | prev, hasPrev := truetype.Index(0), false 282 | fontName := gc.GetFontName() 283 | for _, r := range text { 284 | index := f.Index(r) 285 | if hasPrev { 286 | x += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index)) 287 | } 288 | glyph := gc.glyphCache.Fetch(gc, fontName, r) 289 | x += glyph.Stroke(gc, x, y) 290 | prev, hasPrev = index, true 291 | } 292 | return x - startx 293 | } 294 | 295 | // recalc recalculates scale and bounds values from the font size, screen 296 | // resolution and font metrics, and invalidates the glyph cache. 297 | func (gc *GraphicContext) recalc() { 298 | gc.Current.Scale = gc.Current.FontSize * float64(gc.DPI) * (64.0 / 72.0) 299 | } 300 | 301 | func (gc *GraphicContext) SetDPI(dpi int) { 302 | gc.DPI = dpi 303 | gc.recalc() 304 | } 305 | 306 | // SetFont sets the font used to draw text. 307 | func (gc *GraphicContext) SetFont(font *truetype.Font) { 308 | gc.Current.Font = font 309 | } 310 | 311 | // SetFontSize sets the font size in points (as in ``a 12 point font''). 312 | func (gc *GraphicContext) SetFontSize(fontSize float64) { 313 | gc.Current.FontSize = fontSize 314 | gc.recalc() 315 | } 316 | 317 | func (gc *GraphicContext) GetDPI() int { 318 | return gc.DPI 319 | } 320 | 321 | //TODO 322 | func (gc *GraphicContext) Clear() { 323 | panic("not implemented") 324 | } 325 | 326 | //TODO 327 | func (gc *GraphicContext) ClearRect(x1, y1, x2, y2 int) { 328 | panic("not implemented") 329 | } 330 | 331 | //TODO 332 | func (gc *GraphicContext) DrawImage(img image.Image) { 333 | panic("not implemented") 334 | } 335 | 336 | func (gc *GraphicContext) paint(rasterizer *raster.Rasterizer, color color.Color) { 337 | gc.painter.SetColor(color) 338 | rasterizer.Rasterize(gc.painter) 339 | rasterizer.Clear() 340 | gc.painter.Flush() 341 | gc.Current.Path.Clear() 342 | } 343 | 344 | func (gc *GraphicContext) Stroke(paths ...*draw2d.Path) { 345 | paths = append(paths, gc.Current.Path) 346 | gc.strokeRasterizer.UseNonZeroWinding = true 347 | 348 | stroker := draw2dbase.NewLineStroker(gc.Current.Cap, gc.Current.Join, draw2dbase.Transformer{Tr: gc.Current.Tr, Flattener: draw2dimg.FtLineBuilder{Adder: gc.strokeRasterizer}}) 349 | stroker.HalfLineWidth = gc.Current.LineWidth / 2 350 | 351 | var liner draw2dbase.Flattener 352 | if gc.Current.Dash != nil && len(gc.Current.Dash) > 0 { 353 | liner = draw2dbase.NewDashConverter(gc.Current.Dash, gc.Current.DashOffset, stroker) 354 | } else { 355 | liner = stroker 356 | } 357 | for _, p := range paths { 358 | draw2dbase.Flatten(p, liner, gc.Current.Tr.GetScale()) 359 | } 360 | 361 | gc.paint(gc.strokeRasterizer, gc.Current.StrokeColor) 362 | } 363 | 364 | func (gc *GraphicContext) Fill(paths ...*draw2d.Path) { 365 | paths = append(paths, gc.Current.Path) 366 | gc.fillRasterizer.UseNonZeroWinding = useNonZeroWinding(gc.Current.FillRule) 367 | 368 | /**** first method ****/ 369 | flattener := draw2dbase.Transformer{Tr: gc.Current.Tr, Flattener: draw2dimg.FtLineBuilder{Adder: gc.fillRasterizer}} 370 | for _, p := range paths { 371 | draw2dbase.Flatten(p, flattener, gc.Current.Tr.GetScale()) 372 | } 373 | 374 | gc.paint(gc.fillRasterizer, gc.Current.FillColor) 375 | } 376 | 377 | func (gc *GraphicContext) FillStroke(paths ...*draw2d.Path) { 378 | paths = append(paths, gc.Current.Path) 379 | gc.fillRasterizer.UseNonZeroWinding = useNonZeroWinding(gc.Current.FillRule) 380 | gc.strokeRasterizer.UseNonZeroWinding = true 381 | 382 | flattener := draw2dbase.Transformer{Tr: gc.Current.Tr, Flattener: draw2dimg.FtLineBuilder{Adder: gc.fillRasterizer}} 383 | 384 | stroker := draw2dbase.NewLineStroker(gc.Current.Cap, gc.Current.Join, draw2dbase.Transformer{Tr: gc.Current.Tr, Flattener: draw2dimg.FtLineBuilder{Adder: gc.strokeRasterizer}}) 385 | stroker.HalfLineWidth = gc.Current.LineWidth / 2 386 | 387 | var liner draw2dbase.Flattener 388 | if gc.Current.Dash != nil && len(gc.Current.Dash) > 0 { 389 | liner = draw2dbase.NewDashConverter(gc.Current.Dash, gc.Current.DashOffset, stroker) 390 | } else { 391 | liner = stroker 392 | } 393 | 394 | demux := draw2dbase.DemuxFlattener{Flatteners: []draw2dbase.Flattener{flattener, liner}} 395 | for _, p := range paths { 396 | draw2dbase.Flatten(p, demux, gc.Current.Tr.GetScale()) 397 | } 398 | 399 | // Fill 400 | gc.paint(gc.fillRasterizer, gc.Current.FillColor) 401 | // Stroke 402 | gc.paint(gc.strokeRasterizer, gc.Current.StrokeColor) 403 | } 404 | 405 | func useNonZeroWinding(f draw2d.FillRule) bool { 406 | switch f { 407 | case draw2d.FillRuleEvenOdd: 408 | return false 409 | case draw2d.FillRuleWinding: 410 | return true 411 | } 412 | return false 413 | } 414 | -------------------------------------------------------------------------------- /draw2dgl/notes.md: -------------------------------------------------------------------------------- 1 | Rendering Vector Art OpenGl 2 | References: 3 | * https://www.youtube.com/watch?v=K5u8ZZBWSdw 4 | * http://http.developer.nvidia.com/GPUGems3/gpugems3_ch25.html 5 | * http://research.microsoft.com/en-us/um/people/cloop/loopblinn05.pdf 6 | * https://github.com/openframeworks/openFrameworks/issues/1190 7 | -------------------------------------------------------------------------------- /draw2dgl/text.go: -------------------------------------------------------------------------------- 1 | package draw2dgl 2 | 3 | import ( 4 | "github.com/golang/freetype/truetype" 5 | "github.com/llgcode/draw2d" 6 | 7 | "golang.org/x/image/math/fixed" 8 | ) 9 | 10 | // DrawContour draws the given closed contour at the given sub-pixel offset. 11 | func DrawContour(path draw2d.PathBuilder, ps []truetype.Point, dx, dy float64) { 12 | if len(ps) == 0 { 13 | return 14 | } 15 | startX, startY := pointToF64Point(ps[0]) 16 | var others []truetype.Point 17 | if ps[0].Flags&0x01 != 0 { 18 | others = ps[1:] 19 | } else { 20 | lastX, lastY := pointToF64Point(ps[len(ps)-1]) 21 | if ps[len(ps)-1].Flags&0x01 != 0 { 22 | startX, startY = lastX, lastY 23 | others = ps[:len(ps)-1] 24 | } else { 25 | startX = (startX + lastX) / 2 26 | startY = (startY + lastY) / 2 27 | others = ps 28 | } 29 | } 30 | path.MoveTo(startX+dx, startY+dy) 31 | q0X, q0Y, on0 := startX, startY, true 32 | for _, p := range others { 33 | qX, qY := pointToF64Point(p) 34 | on := p.Flags&0x01 != 0 35 | if on { 36 | if on0 { 37 | path.LineTo(qX+dx, qY+dy) 38 | } else { 39 | path.QuadCurveTo(q0X+dx, q0Y+dy, qX+dx, qY+dy) 40 | } 41 | } else { 42 | if on0 { 43 | // No-op. 44 | } else { 45 | midX := (q0X + qX) / 2 46 | midY := (q0Y + qY) / 2 47 | path.QuadCurveTo(q0X+dx, q0Y+dy, midX+dx, midY+dy) 48 | } 49 | } 50 | q0X, q0Y, on0 = qX, qY, on 51 | } 52 | // Close the curve. 53 | if on0 { 54 | path.LineTo(startX+dx, startY+dy) 55 | } else { 56 | path.QuadCurveTo(q0X+dx, q0Y+dy, startX+dx, startY+dy) 57 | } 58 | } 59 | 60 | func pointToF64Point(p truetype.Point) (x, y float64) { 61 | return fUnitsToFloat64(p.X), -fUnitsToFloat64(p.Y) 62 | } 63 | 64 | func fUnitsToFloat64(x fixed.Int26_6) float64 { 65 | scaled := x << 2 66 | return float64(scaled/256) + float64(scaled%256)/256.0 67 | } 68 | 69 | // FontExtents contains font metric information. 70 | type FontExtents struct { 71 | // Ascent is the distance that the text 72 | // extends above the baseline. 73 | Ascent float64 74 | 75 | // Descent is the distance that the text 76 | // extends below the baseline. The descent 77 | // is given as a negative value. 78 | Descent float64 79 | 80 | // Height is the distance from the lowest 81 | // descending point to the highest ascending 82 | // point. 83 | Height float64 84 | } 85 | 86 | // Extents returns the FontExtents for a font. 87 | // TODO needs to read this https://developer.apple.com/fonts/TrueType-Reference-Manual/RM02/Chap2.html#intro 88 | func Extents(font *truetype.Font, size float64) FontExtents { 89 | bounds := font.Bounds(fixed.Int26_6(font.FUnitsPerEm())) 90 | scale := size / float64(font.FUnitsPerEm()) 91 | return FontExtents{ 92 | Ascent: float64(bounds.Max.Y) * scale, 93 | Descent: float64(bounds.Min.Y) * scale, 94 | Height: float64(bounds.Max.Y-bounds.Min.Y) * scale, 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /draw2dimg/README.md: -------------------------------------------------------------------------------- 1 | draw2d/draw2dimg 2 | ================= 3 | 4 | [![Coverage](http://gocover.io/_badge/github.com/llgcode/draw2d/draw2dimg?0)](http://gocover.io/github.com/llgcode/draw2d/draw2dimg) 5 | [![GoDoc](https://godoc.org/github.com/llgcode/draw2d/draw2dimg?status.svg)](https://godoc.org/github.com/llgcode/draw2d/draw2dimg) 6 | 7 | 8 | draw2d implementation that generates raster images using https://github.com/golang/freetype package. 9 | -------------------------------------------------------------------------------- /draw2dimg/curve_limit_test.go: -------------------------------------------------------------------------------- 1 | package draw2dimg 2 | 3 | import ( 4 | "fmt" 5 | "github.com/golang/freetype/truetype" 6 | "github.com/llgcode/draw2d" 7 | "github.com/llgcode/draw2d/draw2dkit" 8 | "golang.org/x/image/font/gofont/goregular" 9 | "image" 10 | "image/color" 11 | "testing" 12 | ) 13 | 14 | // font generated from icomoon.io and converted to go byte slice 15 | // contains only two glyphs 16 | // \u2716 - which should look like a cross 17 | // \u25cb - which should look like an empty circle 18 | var icoTTF = []byte{ 19 | 0, 1, 0, 0, 0, 12, 0, 128, 0, 3, 0, 64, 71, 83, 85, 66, 219, 7, 221, 185, 20 | 0, 0, 0, 204, 0, 0, 0, 188, 79, 83, 47, 50, 175, 17, 51, 150, 0, 0, 1, 136, 21 | 0, 0, 0, 96, 99, 109, 97, 112, 37, 204, 43, 67, 0, 0, 1, 232, 0, 0, 0, 148, 22 | 103, 97, 115, 112, 0, 0, 0, 16, 0, 0, 2, 124, 0, 0, 0, 8, 103, 108, 121, 102, 23 | 163, 112, 233, 32, 0, 0, 2, 132, 0, 0, 3, 64, 104, 101, 97, 100, 15, 49, 194, 135, 24 | 0, 0, 5, 196, 0, 0, 0, 54, 104, 104, 101, 97, 7, 194, 3, 217, 0, 0, 5, 252, 25 | 0, 0, 0, 36, 104, 109, 116, 120, 14, 0, 0, 2, 0, 0, 6, 32, 0, 0, 0, 96, 26 | 108, 111, 99, 97, 6, 168, 5, 226, 0, 0, 6, 128, 0, 0, 0, 50, 109, 97, 120, 112, 27 | 0, 27, 0, 86, 0, 0, 6, 180, 0, 0, 0, 32, 110, 97, 109, 101, 108, 36, 213, 69, 28 | 0, 0, 6, 212, 0, 0, 1, 170, 112, 111, 115, 116, 0, 3, 0, 0, 0, 0, 8, 128, 29 | 0, 0, 0, 32, 0, 1, 0, 0, 0, 10, 0, 30, 0, 44, 0, 1, 108, 97, 116, 110, 30 | 0, 8, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 108, 105, 103, 97, 31 | 0, 8, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4, 0, 4, 0, 0, 0, 1, 0, 10, 32 | 0, 0, 0, 1, 0, 12, 0, 3, 0, 22, 0, 54, 0, 120, 0, 1, 0, 3, 0, 8, 33 | 0, 17, 0, 23, 0, 2, 0, 6, 0, 18, 0, 22, 0, 5, 0, 17, 0, 16, 0, 18, 34 | 0, 18, 0, 22, 0, 6, 0, 6, 0, 15, 0, 8, 0, 10, 0, 14, 0, 2, 0, 6, 35 | 0, 38, 0, 21, 0, 15, 0, 6, 0, 9, 0, 12, 0, 16, 0, 4, 0, 20, 0, 15, 36 | 0, 8, 0, 11, 0, 10, 0, 8, 0, 13, 0, 10, 0, 9, 0, 21, 0, 13, 0, 6, 37 | 0, 9, 0, 12, 0, 16, 0, 4, 0, 7, 0, 20, 0, 19, 0, 19, 0, 16, 0, 15, 38 | 0, 5, 0, 1, 0, 4, 0, 22, 0, 2, 0, 23, 0, 3, 3, 85, 1, 144, 0, 5, 39 | 0, 0, 2, 153, 2, 204, 0, 0, 0, 143, 2, 153, 2, 204, 0, 0, 1, 235, 0, 51, 40 | 1, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 41 | 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 39, 23, 42 | 3, 192, 255, 192, 0, 64, 3, 192, 0, 64, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 43 | 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 28, 44 | 0, 1, 0, 3, 0, 0, 0, 28, 0, 3, 0, 1, 0, 0, 0, 28, 0, 4, 0, 120, 45 | 0, 0, 0, 26, 0, 16, 0, 3, 0, 10, 0, 1, 0, 32, 0, 45, 0, 51, 0, 101, 46 | 0, 105, 0, 108, 0, 111, 0, 117, 37, 203, 39, 23, 255, 253, 255, 255, 0, 0, 0, 0, 47 | 0, 32, 0, 45, 0, 51, 0, 97, 0, 104, 0, 107, 0, 110, 0, 114, 37, 203, 39, 22, 48 | 255, 253, 255, 255, 0, 1, 255, 227, 255, 215, 255, 210, 255, 165, 255, 163, 255, 162, 255, 161, 49 | 255, 159, 218, 74, 217, 0, 0, 3, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 51 | 255, 255, 0, 15, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 52 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 53 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 54 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 55 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 56 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 57 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 58 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 59 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 60 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 61 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 62 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 63 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 64 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 65 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 66 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 67 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 68 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 69 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 70 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 71 | 1, 0, 0, 0, 0, 2, 0, 0, 255, 192, 4, 0, 3, 192, 0, 27, 0, 55, 0, 0, 72 | 1, 34, 7, 14, 1, 7, 6, 21, 20, 23, 30, 1, 23, 22, 51, 50, 55, 62, 1, 55, 73 | 54, 53, 52, 39, 46, 1, 39, 38, 3, 34, 39, 46, 1, 39, 38, 53, 52, 55, 62, 1, 74 | 55, 54, 51, 50, 23, 30, 1, 23, 22, 21, 20, 7, 14, 1, 7, 6, 2, 0, 106, 93, 75 | 94, 139, 40, 40, 40, 40, 139, 94, 93, 106, 106, 93, 94, 139, 40, 40, 40, 40, 139, 94, 76 | 93, 106, 80, 69, 70, 105, 30, 30, 30, 30, 105, 70, 69, 80, 80, 69, 70, 105, 30, 30, 77 | 30, 30, 105, 70, 69, 3, 192, 40, 40, 139, 94, 93, 106, 106, 93, 94, 139, 40, 40, 40, 78 | 40, 139, 94, 93, 106, 106, 93, 94, 139, 40, 40, 252, 128, 30, 30, 105, 70, 69, 80, 80, 79 | 69, 70, 105, 30, 30, 30, 30, 105, 70, 69, 80, 80, 69, 70, 105, 30, 30, 0, 0, 0, 80 | 0, 1, 0, 2, 255, 194, 3, 254, 3, 190, 0, 83, 0, 0, 37, 56, 1, 49, 9, 1, 81 | 56, 1, 49, 62, 1, 55, 54, 38, 47, 1, 46, 1, 7, 14, 1, 7, 56, 1, 49, 9, 82 | 1, 56, 1, 49, 46, 1, 39, 38, 6, 15, 1, 14, 1, 23, 30, 1, 23, 56, 1, 49, 83 | 9, 1, 56, 1, 49, 14, 1, 7, 6, 22, 31, 1, 30, 1, 55, 62, 1, 55, 56, 1, 84 | 49, 9, 1, 56, 1, 49, 30, 1, 23, 22, 54, 63, 1, 62, 1, 39, 46, 1, 3, 247, 85 | 254, 201, 1, 55, 2, 4, 1, 3, 3, 7, 147, 7, 18, 9, 3, 6, 2, 254, 201, 254, 86 | 201, 2, 6, 3, 9, 18, 7, 147, 7, 3, 3, 1, 4, 2, 1, 55, 254, 201, 2, 4, 87 | 1, 3, 3, 7, 147, 7, 18, 9, 3, 6, 2, 1, 55, 1, 55, 2, 6, 3, 9, 18, 88 | 7, 147, 7, 3, 3, 1, 4, 137, 1, 55, 1, 55, 2, 6, 3, 9, 18, 7, 147, 7, 89 | 3, 3, 1, 4, 2, 254, 201, 1, 55, 2, 4, 1, 3, 3, 7, 147, 7, 18, 9, 3, 90 | 6, 2, 254, 201, 254, 201, 2, 6, 3, 9, 18, 7, 147, 7, 3, 3, 1, 4, 2, 1, 91 | 55, 254, 201, 2, 4, 1, 3, 3, 7, 147, 7, 18, 9, 3, 6, 0, 0, 1, 0, 0, 92 | 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 1, 0, 0, 0, 0, 1, 0, 0, 93 | 0, 1, 0, 0, 32, 120, 21, 165, 95, 15, 60, 245, 0, 11, 4, 0, 0, 0, 0, 0, 94 | 214, 9, 63, 5, 0, 0, 0, 0, 214, 9, 63, 5, 0, 0, 255, 192, 4, 0, 3, 192, 95 | 0, 0, 0, 8, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 3, 192, 255, 192, 96 | 0, 0, 4, 0, 0, 0, 0, 0, 4, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 97 | 0, 0, 0, 0, 0, 0, 0, 24, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 98 | 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 100 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 2, 102 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 20, 0, 30, 0, 40, 0, 50, 0, 60, 103 | 0, 70, 0, 80, 0, 90, 0, 100, 0, 110, 0, 120, 0, 130, 0, 140, 0, 150, 0, 160, 104 | 0, 170, 0, 180, 0, 190, 0, 200, 1, 32, 1, 150, 1, 160, 0, 0, 0, 1, 0, 0, 105 | 0, 24, 0, 84, 0, 2, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 106 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 174, 0, 1, 0, 0, 0, 0, 107 | 0, 1, 0, 10, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2, 0, 7, 0, 123, 0, 1, 108 | 0, 0, 0, 0, 0, 3, 0, 10, 0, 63, 0, 1, 0, 0, 0, 0, 0, 4, 0, 10, 109 | 0, 144, 0, 1, 0, 0, 0, 0, 0, 5, 0, 11, 0, 30, 0, 1, 0, 0, 0, 0, 110 | 0, 6, 0, 10, 0, 93, 0, 1, 0, 0, 0, 0, 0, 10, 0, 26, 0, 174, 0, 3, 111 | 0, 1, 4, 9, 0, 1, 0, 20, 0, 10, 0, 3, 0, 1, 4, 9, 0, 2, 0, 14, 112 | 0, 130, 0, 3, 0, 1, 4, 9, 0, 3, 0, 20, 0, 73, 0, 3, 0, 1, 4, 9, 113 | 0, 4, 0, 20, 0, 154, 0, 3, 0, 1, 4, 9, 0, 5, 0, 22, 0, 41, 0, 3, 114 | 0, 1, 4, 9, 0, 6, 0, 20, 0, 103, 0, 3, 0, 1, 4, 9, 0, 10, 0, 52, 115 | 0, 200, 105, 99, 111, 45, 112, 101, 110, 101, 103, 111, 0, 105, 0, 99, 0, 111, 0, 45, 116 | 0, 112, 0, 101, 0, 110, 0, 101, 0, 103, 0, 111, 86, 101, 114, 115, 105, 111, 110, 32, 117 | 49, 46, 48, 0, 86, 0, 101, 0, 114, 0, 115, 0, 105, 0, 111, 0, 110, 0, 32, 0, 118 | 49, 0, 46, 0, 48, 105, 99, 111, 45, 112, 101, 110, 101, 103, 111, 0, 105, 0, 99, 0, 119 | 111, 0, 45, 0, 112, 0, 101, 0, 110, 0, 101, 0, 103, 0, 111, 105, 99, 111, 45, 112, 120 | 101, 110, 101, 103, 111, 0, 105, 0, 99, 0, 111, 0, 45, 0, 112, 0, 101, 0, 110, 0, 121 | 101, 0, 103, 0, 111, 82, 101, 103, 117, 108, 97, 114, 0, 82, 0, 101, 0, 103, 0, 117, 122 | 0, 108, 0, 97, 0, 114, 105, 99, 111, 45, 112, 101, 110, 101, 103, 111, 0, 105, 0, 99, 123 | 0, 111, 0, 45, 0, 112, 0, 101, 0, 110, 0, 101, 0, 103, 0, 111, 70, 111, 110, 116, 124 | 32, 103, 101, 110, 101, 114, 97, 116, 101, 100, 32, 98, 121, 32, 73, 99, 111, 77, 111, 111, 125 | 110, 46, 0, 70, 0, 111, 0, 110, 0, 116, 0, 32, 0, 103, 0, 101, 0, 110, 0, 101, 126 | 0, 114, 0, 97, 0, 116, 0, 101, 0, 100, 0, 32, 0, 98, 0, 121, 0, 32, 0, 73, 127 | 0, 99, 0, 111, 0, 77, 0, 111, 0, 111, 0, 110, 0, 46, 0, 0, 0, 3, 0, 0, 128 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129 | 0, 0, 0, 0, 0, 0, 0, 0, 130 | } 131 | 132 | type customFontCache map[string]*truetype.Font 133 | 134 | func (fc customFontCache) Store(fd draw2d.FontData, font *truetype.Font) { 135 | fc[fd.Name] = font 136 | } 137 | 138 | func (fc customFontCache) Load(fd draw2d.FontData) (*truetype.Font, error) { 139 | font, stored := fc[fd.Name] 140 | if !stored { 141 | return nil, fmt.Errorf("font %s is not stored in font cache", fd.Name) 142 | } 143 | return font, nil 144 | } 145 | 146 | func initFontCache() { // init font cache 147 | fontCache := customFontCache{} 148 | // add gofont to cache 149 | gofont, err := truetype.Parse(goregular.TTF) 150 | if err != nil { 151 | panic(err) 152 | } 153 | fontCache.Store(draw2d.FontData{Name: "goregular"}, gofont) 154 | // add icofont to cache 155 | icofont, err := truetype.Parse(icoTTF) 156 | if err != nil { 157 | panic(err) 158 | } 159 | fontCache.Store(draw2d.FontData{Name: "ico"}, icofont) 160 | 161 | draw2d.SetFontCache(fontCache) 162 | } 163 | 164 | func TestCurveIndexOutOfRange(t *testing.T) { 165 | 166 | initFontCache() 167 | 168 | // Initialize the graphic context on an RGBA image 169 | dest := image.NewRGBA(image.Rect(0, 0, 512, 512)) 170 | gc := NewGraphicContext(dest) 171 | 172 | // background 173 | gc.SetFillColor(color.RGBA{0xef, 0xef, 0xef, 0xff}) 174 | draw2dkit.Rectangle(gc, 0, 0, 512, 512) 175 | gc.Fill() 176 | 177 | // text 178 | gc.SetFontSize(20) 179 | gc.SetFillColor(color.RGBA{0x10, 0x10, 0x10, 0xff}) 180 | gc.SetFontData(draw2d.FontData{Name: "goregular"}) 181 | 182 | // gc.FillStringAt("Hello", 128, 120) // this works well 183 | 184 | gc.SetFontData(draw2d.FontData{Name: "ico"}) 185 | gc.FillStringAt("\u25cb", 128, 150) // this also works 186 | gc.FillStringAt("\u2716", 128, 170) // Works now 187 | 188 | SaveToPngFile("_test_hello.png", dest) 189 | } 190 | -------------------------------------------------------------------------------- /draw2dimg/fileutil.go: -------------------------------------------------------------------------------- 1 | package draw2dimg 2 | 3 | import ( 4 | "bufio" 5 | "image" 6 | "image/png" 7 | "os" 8 | ) 9 | 10 | // SaveToPngFile create and save an image to a file using PNG format 11 | func SaveToPngFile(filePath string, m image.Image) error { 12 | // Create the file 13 | f, err := os.Create(filePath) 14 | if err != nil { 15 | return err 16 | } 17 | defer f.Close() 18 | // Create Writer from file 19 | b := bufio.NewWriter(f) 20 | // Write the image into the buffer 21 | err = png.Encode(b, m) 22 | if err != nil { 23 | return err 24 | } 25 | err = b.Flush() 26 | if err != nil { 27 | return err 28 | } 29 | return nil 30 | } 31 | 32 | // LoadFromPngFile Open a png file 33 | func LoadFromPngFile(filePath string) (image.Image, error) { 34 | // Open file 35 | f, err := os.OpenFile(filePath, 0, 0) 36 | if err != nil { 37 | return nil, err 38 | } 39 | defer f.Close() 40 | b := bufio.NewReader(f) 41 | img, err := png.Decode(b) 42 | if err != nil { 43 | return nil, err 44 | } 45 | return img, nil 46 | } 47 | -------------------------------------------------------------------------------- /draw2dimg/ftpath.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 13/12/2010 by Laurent Le Goff 3 | 4 | package draw2dimg 5 | 6 | import ( 7 | "github.com/golang/freetype/raster" 8 | "golang.org/x/image/math/fixed" 9 | ) 10 | 11 | type FtLineBuilder struct { 12 | Adder raster.Adder 13 | } 14 | 15 | func (liner FtLineBuilder) MoveTo(x, y float64) { 16 | liner.Adder.Start(fixed.Point26_6{X: fixed.Int26_6(x * 64), Y: fixed.Int26_6(y * 64)}) 17 | } 18 | 19 | func (liner FtLineBuilder) LineTo(x, y float64) { 20 | liner.Adder.Add1(fixed.Point26_6{X: fixed.Int26_6(x * 64), Y: fixed.Int26_6(y * 64)}) 21 | } 22 | 23 | func (liner FtLineBuilder) LineJoin() { 24 | } 25 | 26 | func (liner FtLineBuilder) Close() { 27 | } 28 | 29 | func (liner FtLineBuilder) End() { 30 | } 31 | -------------------------------------------------------------------------------- /draw2dimg/text.go: -------------------------------------------------------------------------------- 1 | package draw2dimg 2 | 3 | import ( 4 | "github.com/golang/freetype/truetype" 5 | "github.com/llgcode/draw2d" 6 | 7 | "golang.org/x/image/math/fixed" 8 | ) 9 | 10 | // DrawContour draws the given closed contour at the given sub-pixel offset. 11 | func DrawContour(path draw2d.PathBuilder, ps []truetype.Point, dx, dy float64) { 12 | if len(ps) == 0 { 13 | return 14 | } 15 | startX, startY := pointToF64Point(ps[0]) 16 | var others []truetype.Point 17 | if ps[0].Flags&0x01 != 0 { 18 | others = ps[1:] 19 | } else { 20 | lastX, lastY := pointToF64Point(ps[len(ps)-1]) 21 | if ps[len(ps)-1].Flags&0x01 != 0 { 22 | startX, startY = lastX, lastY 23 | others = ps[:len(ps)-1] 24 | } else { 25 | startX = (startX + lastX) / 2 26 | startY = (startY + lastY) / 2 27 | others = ps 28 | } 29 | } 30 | path.MoveTo(startX+dx, startY+dy) 31 | q0X, q0Y, on0 := startX, startY, true 32 | for _, p := range others { 33 | qX, qY := pointToF64Point(p) 34 | on := p.Flags&0x01 != 0 35 | if on { 36 | if on0 { 37 | path.LineTo(qX+dx, qY+dy) 38 | } else { 39 | path.QuadCurveTo(q0X+dx, q0Y+dy, qX+dx, qY+dy) 40 | } 41 | } else { 42 | if on0 { 43 | // No-op. 44 | } else { 45 | midX := (q0X + qX) / 2 46 | midY := (q0Y + qY) / 2 47 | path.QuadCurveTo(q0X+dx, q0Y+dy, midX+dx, midY+dy) 48 | } 49 | } 50 | q0X, q0Y, on0 = qX, qY, on 51 | } 52 | // Close the curve. 53 | if on0 { 54 | path.LineTo(startX+dx, startY+dy) 55 | } else { 56 | path.QuadCurveTo(q0X+dx, q0Y+dy, startX+dx, startY+dy) 57 | } 58 | } 59 | 60 | func pointToF64Point(p truetype.Point) (x, y float64) { 61 | return fUnitsToFloat64(p.X), -fUnitsToFloat64(p.Y) 62 | } 63 | 64 | func fUnitsToFloat64(x fixed.Int26_6) float64 { 65 | scaled := x << 2 66 | return float64(scaled/256) + float64(scaled%256)/256.0 67 | } 68 | 69 | // FontExtents contains font metric information. 70 | type FontExtents struct { 71 | // Ascent is the distance that the text 72 | // extends above the baseline. 73 | Ascent float64 74 | 75 | // Descent is the distance that the text 76 | // extends below the baseline. The descent 77 | // is given as a negative value. 78 | Descent float64 79 | 80 | // Height is the distance from the lowest 81 | // descending point to the highest ascending 82 | // point. 83 | Height float64 84 | } 85 | 86 | // Extents returns the FontExtents for a font. 87 | // TODO needs to read this https://developer.apple.com/fonts/TrueType-Reference-Manual/RM02/Chap2.html#intro 88 | func Extents(font *truetype.Font, size float64) FontExtents { 89 | bounds := font.Bounds(fixed.Int26_6(font.FUnitsPerEm())) 90 | scale := size / float64(font.FUnitsPerEm()) 91 | return FontExtents{ 92 | Ascent: float64(bounds.Max.Y) * scale, 93 | Descent: float64(bounds.Min.Y) * scale, 94 | Height: float64(bounds.Max.Y-bounds.Min.Y) * scale, 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /draw2dkit/README.md: -------------------------------------------------------------------------------- 1 | draw2d/draw2dkit 2 | ================= 3 | 4 | [![Coverage](http://gocover.io/_badge/github.com/llgcode/draw2d/draw2dkit?0)](http://gocover.io/github.com/llgcode/draw2d/draw2dkit) 5 | [![GoDoc](https://godoc.org/github.com/llgcode/draw2d/draw2dkit?status.svg)](https://godoc.org/github.com/llgcode/draw2d/draw2dkit) 6 | 7 | Util package that help drawing common vectorial draw. 8 | -------------------------------------------------------------------------------- /draw2dkit/draw2dkit.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 13/12/2010 by Laurent Le Goff 3 | 4 | // Package draw2dkit provides helpers to draw common figures using a Path 5 | package draw2dkit 6 | 7 | import ( 8 | "math" 9 | 10 | "github.com/llgcode/draw2d" 11 | ) 12 | 13 | // Rectangle draws a rectangle using a path between (x1,y1) and (x2,y2) 14 | func Rectangle(path draw2d.PathBuilder, x1, y1, x2, y2 float64) { 15 | path.MoveTo(x1, y1) 16 | path.LineTo(x2, y1) 17 | path.LineTo(x2, y2) 18 | path.LineTo(x1, y2) 19 | path.Close() 20 | } 21 | 22 | // RoundedRectangle draws a rectangle using a path between (x1,y1) and (x2,y2) 23 | func RoundedRectangle(path draw2d.PathBuilder, x1, y1, x2, y2, arcWidth, arcHeight float64) { 24 | arcWidth = arcWidth / 2 25 | arcHeight = arcHeight / 2 26 | path.MoveTo(x1, y1+arcHeight) 27 | path.QuadCurveTo(x1, y1, x1+arcWidth, y1) 28 | path.LineTo(x2-arcWidth, y1) 29 | path.QuadCurveTo(x2, y1, x2, y1+arcHeight) 30 | path.LineTo(x2, y2-arcHeight) 31 | path.QuadCurveTo(x2, y2, x2-arcWidth, y2) 32 | path.LineTo(x1+arcWidth, y2) 33 | path.QuadCurveTo(x1, y2, x1, y2-arcHeight) 34 | path.Close() 35 | } 36 | 37 | // Ellipse draws an ellipse using a path with center (cx,cy) and radius (rx,ry) 38 | func Ellipse(path draw2d.PathBuilder, cx, cy, rx, ry float64) { 39 | path.ArcTo(cx, cy, rx, ry, 0, -math.Pi*2) 40 | path.Close() 41 | } 42 | 43 | // Circle draws a circle using a path with center (cx,cy) and radius 44 | func Circle(path draw2d.PathBuilder, cx, cy, radius float64) { 45 | path.ArcTo(cx, cy, radius, radius, 0, -math.Pi*2) 46 | path.Close() 47 | } 48 | -------------------------------------------------------------------------------- /draw2dkit/draw2dkit_test.go: -------------------------------------------------------------------------------- 1 | package draw2dkit 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "testing" 7 | 8 | "github.com/llgcode/draw2d/draw2dimg" 9 | ) 10 | 11 | func TestCircle(t *testing.T) { 12 | width := 200 13 | height := 200 14 | img := image.NewRGBA(image.Rect(0, 0, width, height)) 15 | gc := draw2dimg.NewGraphicContext(img) 16 | 17 | gc.SetStrokeColor(color.NRGBA{255, 255, 255, 255}) 18 | gc.SetFillColor(color.NRGBA{255, 255, 255, 255}) 19 | gc.Clear() 20 | 21 | gc.SetStrokeColor(color.NRGBA{255, 0, 0, 255}) 22 | gc.SetLineWidth(1) 23 | 24 | // Draw a circle 25 | Circle(gc, 100, 100, 50) 26 | gc.Stroke() 27 | 28 | draw2dimg.SaveToPngFile("../output/draw2dkit/TestCircle.png", img) 29 | } 30 | -------------------------------------------------------------------------------- /draw2dpdf/README.md: -------------------------------------------------------------------------------- 1 | draw2d pdf 2 | ========== 3 | 4 | Package draw2dpdf provides a graphic context that can draw vector graphics and text on pdf file with the [gofpdf](https://github.com/jung-kurt/gofpdf) package. 5 | 6 | Quick Start 7 | ----------- 8 | 9 | The following Go code generates a simple drawing and saves it to a pdf document: 10 | ```go 11 | // Initialize the graphic context on an RGBA image 12 | dest := draw2dpdf.NewPdf("L", "mm", "A4") 13 | gc := draw2dpdf.NewGraphicContext(dest) 14 | 15 | // Set some properties 16 | gc.SetFillColor(color.RGBA{0x44, 0xff, 0x44, 0xff}) 17 | gc.SetStrokeColor(color.RGBA{0x44, 0x44, 0x44, 0xff}) 18 | gc.SetLineWidth(5) 19 | 20 | // Draw a closed shape 21 | gc.MoveTo(10, 10) // should always be called first for a new path 22 | gc.LineTo(100, 50) 23 | gc.QuadCurveTo(100, 10, 10, 10) 24 | gc.Close() 25 | gc.FillStroke() 26 | 27 | // Save to file 28 | draw2dpdf.SaveToPdfFile("hello.pdf", dest) 29 | ``` 30 | 31 | There are more examples here: https://github.com/llgcode/draw2d/tree/master/samples 32 | 33 | Alternative backends 34 | -------------------- 35 | 36 | - Drawing on images is provided by the draw2d package. 37 | - Drawing on opengl is provided by the draw2dgl package. 38 | 39 | Acknowledgments 40 | --------------- 41 | 42 | The pdf backend uses https://github.com/jung-kurt/gofpdf 43 | -------------------------------------------------------------------------------- /draw2dpdf/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The draw2d Authors. All rights reserved. 2 | // created: 26/06/2015 by Stani Michiels 3 | 4 | // Package draw2dpdf provides a graphic context that can draw vector 5 | // graphics and text on pdf file with the gofpdf package. 6 | // 7 | // Quick Start 8 | // 9 | // The following Go code generates a simple drawing and saves it to a 10 | // pdf document: 11 | // // Initialize the graphic context on an RGBA image 12 | // dest := draw2dpdf.NewPdf("L", "mm", "A4") 13 | // gc := draw2dpdf.NewGraphicContext(dest) 14 | // 15 | // // Set some properties 16 | // gc.SetFillColor(color.RGBA{0x44, 0xff, 0x44, 0xff}) 17 | // gc.SetStrokeColor(color.RGBA{0x44, 0x44, 0x44, 0xff}) 18 | // gc.SetLineWidth(5) 19 | // 20 | // // Draw a closed shape 21 | // gc.MoveTo(10, 10) // should always be called first for a new path 22 | // gc.LineTo(100, 50) 23 | // gc.QuadCurveTo(100, 10, 10, 10) 24 | // gc.Close() 25 | // gc.FillStroke() 26 | // 27 | // // Save to file 28 | // draw2dpdf.SaveToPdfFile("hello.pdf", dest) 29 | // 30 | // There are more examples here: 31 | // https://github.com/llgcode/draw2d/tree/master/samples 32 | // 33 | // Alternative backends 34 | // 35 | // Drawing on images is provided by the draw2d package. 36 | // Drawing on opengl is provided by the draw2dgl package. 37 | // 38 | // Acknowledgments 39 | // 40 | // The pdf backend uses https://github.com/jung-kurt/gofpdf 41 | package draw2dpdf 42 | -------------------------------------------------------------------------------- /draw2dpdf/fileutil.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The draw2d Authors. All rights reserved. 2 | // created: 26/06/2015 by Stani Michiels 3 | 4 | package draw2dpdf 5 | 6 | import "github.com/jung-kurt/gofpdf" 7 | 8 | // SaveToPdfFile creates and saves a pdf document to a file 9 | func SaveToPdfFile(filePath string, pdf *gofpdf.Fpdf) error { 10 | return pdf.OutputFileAndClose(filePath) 11 | } 12 | -------------------------------------------------------------------------------- /draw2dpdf/gc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The draw2d Authors. All rights reserved. 2 | // created: 26/06/2015 by Stani Michiels 3 | // TODO: dashed line 4 | 5 | package draw2dpdf 6 | 7 | import ( 8 | "bytes" 9 | "image" 10 | "image/color" 11 | "image/png" 12 | "log" 13 | "math" 14 | "os" 15 | "strconv" 16 | 17 | "github.com/golang/freetype/truetype" 18 | 19 | "github.com/jung-kurt/gofpdf" 20 | "github.com/llgcode/draw2d" 21 | "github.com/llgcode/draw2d/draw2dbase" 22 | "github.com/llgcode/draw2d/draw2dkit" 23 | ) 24 | 25 | const ( 26 | // DPI of a pdf document is fixed at 72. 27 | DPI = 72 28 | c255 = 255.0 / 65535.0 29 | ) 30 | 31 | var ( 32 | caps = map[draw2d.LineCap]string{ 33 | draw2d.RoundCap: "round", 34 | draw2d.ButtCap: "butt", 35 | draw2d.SquareCap: "square"} 36 | joins = map[draw2d.LineJoin]string{ 37 | draw2d.RoundJoin: "round", 38 | draw2d.BevelJoin: "bevel", 39 | draw2d.MiterJoin: "miter", 40 | } 41 | imageCount uint32 42 | white color.Color = color.RGBA{255, 255, 255, 255} 43 | ) 44 | 45 | // NewPdf creates a new pdf document with the draw2d fontfolder, adds 46 | // a page and set fill color to white. 47 | func NewPdf(orientationStr, unitStr, sizeStr string) *gofpdf.Fpdf { 48 | pdf := gofpdf.New(orientationStr, unitStr, sizeStr, draw2d.GetFontFolder()) 49 | // to be compatible with draw2d 50 | pdf.SetMargins(0, 0, 0) 51 | pdf.SetDrawColor(0, 0, 0) 52 | pdf.SetFillColor(255, 255, 255) 53 | pdf.SetLineCapStyle("round") 54 | pdf.SetLineJoinStyle("round") 55 | pdf.SetLineWidth(1) 56 | pdf.AddPage() 57 | return pdf 58 | } 59 | 60 | // rgb converts a color (used by draw2d) into 3 int (used by gofpdf) 61 | func rgb(c color.Color) (int, int, int) { 62 | r, g, b, _ := c.RGBA() 63 | return int(float64(r) * c255), int(float64(g) * c255), int(float64(b) * c255) 64 | } 65 | 66 | // clearRect draws a white rectangle 67 | func clearRect(gc *GraphicContext, x1, y1, x2, y2 float64) { 68 | // save state 69 | f := gc.Current.FillColor 70 | x, y := gc.pdf.GetXY() 71 | // cover page with white rectangle 72 | gc.SetFillColor(white) 73 | draw2dkit.Rectangle(gc, x1, y1, x2, y2) 74 | gc.Fill() 75 | // restore state 76 | gc.SetFillColor(f) 77 | gc.pdf.MoveTo(x, y) 78 | } 79 | 80 | // GraphicContext implements the draw2d.GraphicContext interface 81 | // It provides draw2d with a pdf backend (based on gofpdf) 82 | type GraphicContext struct { 83 | *draw2dbase.StackGraphicContext 84 | pdf *gofpdf.Fpdf 85 | DPI int 86 | } 87 | 88 | // NewGraphicContext creates a new pdf GraphicContext 89 | func NewGraphicContext(pdf *gofpdf.Fpdf) *GraphicContext { 90 | gc := &GraphicContext{draw2dbase.NewStackGraphicContext(), pdf, DPI} 91 | gc.SetDPI(DPI) 92 | return gc 93 | } 94 | 95 | // DrawImage draws an image as PNG 96 | // TODO: add type (tp) as parameter to argument list? 97 | func (gc *GraphicContext) DrawImage(image image.Image) { 98 | name := strconv.Itoa(int(imageCount)) 99 | imageCount++ 100 | tp := "PNG" // "JPG", "JPEG", "PNG" and "GIF" 101 | b := &bytes.Buffer{} 102 | png.Encode(b, image) 103 | gc.pdf.RegisterImageReader(name, tp, b) 104 | bounds := image.Bounds() 105 | x0, y0 := float64(bounds.Min.X), float64(bounds.Min.Y) 106 | w, h := float64(bounds.Dx()), float64(bounds.Dy()) 107 | gc.pdf.Image(name, x0, y0, w, h, false, tp, 0, "") 108 | } 109 | 110 | // Clear draws a white rectangle over the whole page 111 | func (gc *GraphicContext) Clear() { 112 | width, height := gc.pdf.GetPageSize() 113 | clearRect(gc, 0, 0, width, height) 114 | } 115 | 116 | // ClearRect draws a white rectangle over the specified area. 117 | // Samples: line. 118 | func (gc *GraphicContext) ClearRect(x1, y1, x2, y2 int) { 119 | clearRect(gc, float64(x1), float64(y1), float64(x2), float64(y2)) 120 | } 121 | 122 | // recalc recalculates scale and bounds values from the font size, screen 123 | // resolution and font metrics, and invalidates the glyph cache. 124 | func (gc *GraphicContext) recalc() { 125 | // TODO: resolve properly the font size for pdf and bitmap 126 | gc.Current.Scale = 3 * float64(gc.DPI) / 72 127 | } 128 | 129 | // SetDPI sets the DPI which influences the font size. 130 | func (gc *GraphicContext) SetDPI(dpi int) { 131 | gc.DPI = dpi 132 | gc.recalc() 133 | } 134 | 135 | // GetDPI returns the DPI which influences the font size. 136 | // (Note that gofpdf uses a fixed dpi of 72: 137 | // https://godoc.org/code.google.com/p/gofpdf#Fpdf.PointConvert) 138 | func (gc *GraphicContext) GetDPI() int { 139 | return gc.DPI 140 | } 141 | 142 | // GetStringBounds returns the approximate pixel bounds of the string s at x, y. 143 | // The left edge of the em square of the first character of s 144 | // and the baseline intersect at 0, 0 in the returned coordinates. 145 | // Therefore the top and left coordinates may well be negative. 146 | func (gc *GraphicContext) GetStringBounds(s string) (left, top, right, bottom float64) { 147 | _, h := gc.pdf.GetFontSize() 148 | d := gc.pdf.GetFontDesc("", "") 149 | if d.Ascent == 0 { 150 | // not defined (standard font?), use average of 81% 151 | top = 0.81 * h 152 | } else { 153 | top = -float64(d.Ascent) * h / float64(d.Ascent-d.Descent) 154 | } 155 | return 0, top, gc.pdf.GetStringWidth(s), top + h 156 | } 157 | 158 | // CreateStringPath creates a path from the string s at x, y, and returns the string width. 159 | func (gc *GraphicContext) CreateStringPath(text string, x, y float64) (cursor float64) { 160 | //fpdf uses the top left corner 161 | left, top, right, bottom := gc.GetStringBounds(text) 162 | w := right - left 163 | h := bottom - top 164 | // gc.pdf.SetXY(x, y-h) do not use this as y-h might be negative 165 | margin := gc.pdf.GetCellMargin() 166 | gc.pdf.MoveTo(x-left-margin, y+top) 167 | gc.pdf.CellFormat(w, h, text, "", 0, "BL", false, 0, "") 168 | return w 169 | } 170 | 171 | // FillString draws a string at 0, 0 172 | func (gc *GraphicContext) FillString(text string) (cursor float64) { 173 | return gc.FillStringAt(text, 0, 0) 174 | } 175 | 176 | // FillStringAt draws a string at x, y 177 | func (gc *GraphicContext) FillStringAt(text string, x, y float64) (cursor float64) { 178 | return gc.CreateStringPath(text, x, y) 179 | } 180 | 181 | // StrokeString draws a string at 0, 0 (stroking is unsupported, 182 | // string will be filled) 183 | func (gc *GraphicContext) StrokeString(text string) (cursor float64) { 184 | return gc.StrokeStringAt(text, 0, 0) 185 | } 186 | 187 | // StrokeStringAt draws a string at x, y (stroking is unsupported, 188 | // string will be filled) 189 | func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64) { 190 | return gc.CreateStringPath(text, x, y) 191 | } 192 | 193 | // Stroke strokes the paths with the color specified by SetStrokeColor 194 | func (gc *GraphicContext) Stroke(paths ...*draw2d.Path) { 195 | _, _, _, alphaS := gc.Current.StrokeColor.RGBA() 196 | gc.draw("D", alphaS, paths...) 197 | gc.Current.Path.Clear() 198 | } 199 | 200 | // Fill fills the paths with the color specified by SetFillColor 201 | func (gc *GraphicContext) Fill(paths ...*draw2d.Path) { 202 | style := "F" 203 | if gc.Current.FillRule != draw2d.FillRuleWinding { 204 | style += "*" 205 | } 206 | _, _, _, alphaF := gc.Current.FillColor.RGBA() 207 | gc.draw(style, alphaF, paths...) 208 | gc.Current.Path.Clear() 209 | } 210 | 211 | // FillStroke first fills the paths and than strokes them 212 | func (gc *GraphicContext) FillStroke(paths ...*draw2d.Path) { 213 | var rule string 214 | if gc.Current.FillRule != draw2d.FillRuleWinding { 215 | rule = "*" 216 | } 217 | _, _, _, alphaS := gc.Current.StrokeColor.RGBA() 218 | _, _, _, alphaF := gc.Current.FillColor.RGBA() 219 | if alphaS == alphaF { 220 | gc.draw("FD"+rule, alphaF, paths...) 221 | } else { 222 | gc.draw("F"+rule, alphaF, paths...) 223 | gc.draw("S", alphaS, paths...) 224 | } 225 | gc.Current.Path.Clear() 226 | } 227 | 228 | var logger = log.New(os.Stdout, "", log.Lshortfile) 229 | 230 | const alphaMax = float64(0xFFFF) 231 | 232 | // draw fills and/or strokes paths 233 | func (gc *GraphicContext) draw(style string, alpha uint32, paths ...*draw2d.Path) { 234 | paths = append(paths, gc.Current.Path) 235 | for _, p := range paths { 236 | ConvertPath(p, gc.pdf) 237 | } 238 | a := float64(alpha) / alphaMax 239 | current, blendMode := gc.pdf.GetAlpha() 240 | if a != current { 241 | gc.pdf.SetAlpha(a, blendMode) 242 | } 243 | gc.pdf.DrawPath(style) 244 | } 245 | 246 | // overwrite StackGraphicContext methods 247 | 248 | // SetStrokeColor sets the stroke color 249 | func (gc *GraphicContext) SetStrokeColor(c color.Color) { 250 | gc.StackGraphicContext.SetStrokeColor(c) 251 | gc.pdf.SetDrawColor(rgb(c)) 252 | } 253 | 254 | // SetFillColor sets the fill and text color 255 | func (gc *GraphicContext) SetFillColor(c color.Color) { 256 | gc.StackGraphicContext.SetFillColor(c) 257 | gc.pdf.SetFillColor(rgb(c)) 258 | gc.pdf.SetTextColor(rgb(c)) 259 | } 260 | 261 | // SetFont is unsupported by the pdf graphic context, use SetFontData 262 | // instead. 263 | func (gc *GraphicContext) SetFont(font *truetype.Font) { 264 | // TODO: what to do with this api conflict between draw2d and gofpdf?! 265 | } 266 | 267 | // SetFontData sets the current font used to draw text. Always use 268 | // this method, as SetFont is unsupported by the pdf graphic context. 269 | // It is mandatory to call this method at least once before printing 270 | // text or the resulting document will not be valid. 271 | // It is necessary to generate a font definition file first with the 272 | // makefont utility. It is not necessary to call this function for the 273 | // core PDF fonts (courier, helvetica, times, zapfdingbats). 274 | // go get github.com/jung-kurt/gofpdf/makefont 275 | // http://godoc.org/github.com/jung-kurt/gofpdf#Fpdf.AddFont 276 | func (gc *GraphicContext) SetFontData(fontData draw2d.FontData) { 277 | // TODO: call Makefont embed if json file does not exist yet 278 | gc.StackGraphicContext.SetFontData(fontData) 279 | var style string 280 | if fontData.Style&draw2d.FontStyleBold != 0 { 281 | style += "B" 282 | } 283 | if fontData.Style&draw2d.FontStyleItalic != 0 { 284 | style += "I" 285 | } 286 | fn := draw2d.FontFileName(fontData) 287 | fn = fn[:len(fn)-4] 288 | size, _ := gc.pdf.GetFontSize() 289 | gc.pdf.AddFont(fontData.Name, style, fn+".json") 290 | gc.pdf.SetFont(fontData.Name, style, size) 291 | } 292 | 293 | // SetFontSize sets the font size in points (as in ``a 12 point font''). 294 | // TODO: resolve this with ImgGraphicContext (now done with gc.Current.Scale) 295 | func (gc *GraphicContext) SetFontSize(fontSize float64) { 296 | gc.StackGraphicContext.SetFontSize(fontSize) 297 | gc.recalc() 298 | gc.pdf.SetFontSize(fontSize * gc.Current.Scale) 299 | } 300 | 301 | // SetLineDash sets the line dash pattern 302 | func (gc *GraphicContext) SetLineDash(Dash []float64, DashOffset float64) { 303 | gc.StackGraphicContext.SetLineDash(Dash, DashOffset) 304 | gc.pdf.SetDashPattern(Dash, DashOffset) 305 | } 306 | 307 | // SetLineWidth sets the line width 308 | func (gc *GraphicContext) SetLineWidth(LineWidth float64) { 309 | gc.StackGraphicContext.SetLineWidth(LineWidth) 310 | gc.pdf.SetLineWidth(LineWidth) 311 | } 312 | 313 | // SetLineCap sets the line cap (round, but or square) 314 | func (gc *GraphicContext) SetLineCap(Cap draw2d.LineCap) { 315 | gc.StackGraphicContext.SetLineCap(Cap) 316 | gc.pdf.SetLineCapStyle(caps[Cap]) 317 | } 318 | 319 | // SetLineJoin sets the line cap (round, bevel or miter) 320 | func (gc *GraphicContext) SetLineJoin(Join draw2d.LineJoin) { 321 | gc.StackGraphicContext.SetLineJoin(Join) 322 | gc.pdf.SetLineJoinStyle(joins[Join]) 323 | } 324 | 325 | // Transformations 326 | 327 | // Scale generally scales the following text, drawings and images. 328 | // sx and sy are the scaling factors for width and height. 329 | // This must be placed between gc.Save() and gc.Restore(), otherwise 330 | // the pdf is invalid. 331 | func (gc *GraphicContext) Scale(sx, sy float64) { 332 | gc.StackGraphicContext.Scale(sx, sy) 333 | gc.pdf.TransformScale(sx*100, sy*100, 0, 0) 334 | } 335 | 336 | // Rotate rotates the following text, drawings and images. 337 | // Angle is specified in radians and measured clockwise from the 338 | // 3 o'clock position. 339 | // This must be placed between gc.Save() and gc.Restore(), otherwise 340 | // the pdf is invalid. 341 | func (gc *GraphicContext) Rotate(angle float64) { 342 | gc.StackGraphicContext.Rotate(angle) 343 | gc.pdf.TransformRotate(-angle*180/math.Pi, 0, 0) 344 | } 345 | 346 | // Translate moves the following text, drawings and images 347 | // horizontally and vertically by the amounts specified by tx and ty. 348 | // This must be placed between gc.Save() and gc.Restore(), otherwise 349 | // the pdf is invalid. 350 | func (gc *GraphicContext) Translate(tx, ty float64) { 351 | gc.StackGraphicContext.Translate(tx, ty) 352 | gc.pdf.TransformTranslate(tx, ty) 353 | } 354 | 355 | // Save saves the current context stack 356 | // (transformation, font, color,...). 357 | func (gc *GraphicContext) Save() { 358 | gc.StackGraphicContext.Save() 359 | gc.pdf.TransformBegin() 360 | } 361 | 362 | // Restore restores the current context stack 363 | // (transformation, color,...). Restoring the font is not supported. 364 | func (gc *GraphicContext) Restore() { 365 | gc.pdf.TransformEnd() 366 | gc.StackGraphicContext.Restore() 367 | c := gc.Current 368 | gc.SetFontSize(c.FontSize) 369 | // gc.SetFontData(c.FontData) unsupported, causes bug (do not enable) 370 | gc.SetLineWidth(c.LineWidth) 371 | gc.SetStrokeColor(c.StrokeColor) 372 | gc.SetFillColor(c.FillColor) 373 | gc.SetFillRule(c.FillRule) 374 | // gc.SetLineDash(c.Dash, c.DashOffset) // TODO 375 | gc.SetLineCap(c.Cap) 376 | gc.SetLineJoin(c.Join) 377 | // c.Path unsupported 378 | // c.Font unsupported 379 | } 380 | -------------------------------------------------------------------------------- /draw2dpdf/path_converter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The draw2d Authors. All rights reserved. 2 | // created: 26/06/2015 by Stani Michiels 3 | 4 | package draw2dpdf 5 | 6 | import ( 7 | "math" 8 | 9 | "github.com/llgcode/draw2d" 10 | ) 11 | 12 | const deg = 180 / math.Pi 13 | 14 | // ConvertPath converts a paths to the pdf api 15 | func ConvertPath(path *draw2d.Path, pdf Vectorizer) { 16 | var startX, startY float64 = 0, 0 17 | i := 0 18 | for _, cmp := range path.Components { 19 | switch cmp { 20 | case draw2d.MoveToCmp: 21 | startX, startY = path.Points[i], path.Points[i+1] 22 | pdf.MoveTo(startX, startY) 23 | i += 2 24 | case draw2d.LineToCmp: 25 | pdf.LineTo(path.Points[i], path.Points[i+1]) 26 | i += 2 27 | case draw2d.QuadCurveToCmp: 28 | pdf.CurveTo(path.Points[i], path.Points[i+1], path.Points[i+2], path.Points[i+3]) 29 | i += 4 30 | case draw2d.CubicCurveToCmp: 31 | pdf.CurveBezierCubicTo(path.Points[i], path.Points[i+1], path.Points[i+2], path.Points[i+3], path.Points[i+4], path.Points[i+5]) 32 | i += 6 33 | case draw2d.ArcToCmp: 34 | pdf.ArcTo(path.Points[i], path.Points[i+1], path.Points[i+2], path.Points[i+3], 35 | 0, // degRotate 36 | path.Points[i+4]*deg, // degStart = startAngle 37 | (path.Points[i+4]-path.Points[i+5])*deg) // degEnd = startAngle-angle 38 | i += 6 39 | case draw2d.CloseCmp: 40 | pdf.LineTo(startX, startY) 41 | pdf.ClosePath() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /draw2dpdf/samples_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The draw2d Authors. All rights reserved. 2 | // created: 26/06/2015 by Stani Michiels 3 | // See also test_test.go 4 | 5 | package draw2dpdf_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/llgcode/draw2d" 11 | "github.com/llgcode/draw2d/samples/android" 12 | "github.com/llgcode/draw2d/samples/frameimage" 13 | "github.com/llgcode/draw2d/samples/geometry" 14 | "github.com/llgcode/draw2d/samples/gopher" 15 | "github.com/llgcode/draw2d/samples/gopher2" 16 | "github.com/llgcode/draw2d/samples/helloworld" 17 | "github.com/llgcode/draw2d/samples/line" 18 | "github.com/llgcode/draw2d/samples/linecapjoin" 19 | "github.com/llgcode/draw2d/samples/postscript" 20 | ) 21 | 22 | func TestSampleAndroid(t *testing.T) { 23 | test(t, android.Main) 24 | } 25 | 26 | // TODO: FillString: w (width) is incorrect 27 | func TestSampleGeometry(t *testing.T) { 28 | // Set the global folder for searching fonts 29 | // The pdf backend needs for every ttf file its corresponding 30 | // json/.z file which is generated by gofpdf/makefont. 31 | draw2d.SetFontFolder("../resource/font") 32 | test(t, geometry.Main) 33 | } 34 | 35 | func TestSampleGopher(t *testing.T) { 36 | test(t, gopher.Main) 37 | } 38 | 39 | func TestSampleGopher2(t *testing.T) { 40 | test(t, gopher2.Main) 41 | } 42 | 43 | func TestSampleHelloWorld(t *testing.T) { 44 | // Set the global folder for searching fonts 45 | // The pdf backend needs for every ttf file its corresponding 46 | // json/.z file which is generated by gofpdf/makefont. 47 | draw2d.SetFontFolder("../resource/font") 48 | test(t, helloworld.Main) 49 | } 50 | 51 | func TestSampleFrameImage(t *testing.T) { 52 | test(t, frameimage.Main) 53 | } 54 | 55 | func TestSampleLine(t *testing.T) { 56 | test(t, line.Main) 57 | } 58 | 59 | func TestSampleLineCap(t *testing.T) { 60 | test(t, linecapjoin.Main) 61 | } 62 | 63 | func TestSamplePostscript(t *testing.T) { 64 | test(t, postscript.Main) 65 | } 66 | -------------------------------------------------------------------------------- /draw2dpdf/test_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The draw2d Authors. All rights reserved. 2 | // created: 26/06/2015 by Stani Michiels 3 | 4 | // Package draw2dpdf_test gives test coverage with the command: 5 | // go test -cover ./... | grep -v "no test" 6 | // (It should be run from its parent draw2d directory.) 7 | package draw2dpdf_test 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/llgcode/draw2d" 13 | "github.com/llgcode/draw2d/draw2dpdf" 14 | ) 15 | 16 | type sample func(gc draw2d.GraphicContext, ext string) (string, error) 17 | 18 | func test(t *testing.T, draw sample) { 19 | // Initialize the graphic context on an pdf document 20 | dest := draw2dpdf.NewPdf("L", "mm", "A4") 21 | gc := draw2dpdf.NewGraphicContext(dest) 22 | // Draw sample 23 | output, err := draw(gc, "pdf") 24 | if err != nil { 25 | t.Errorf("Drawing %q failed: %v", output, err) 26 | return 27 | } 28 | /* 29 | // Save to pdf only if it doesn't exist because of git 30 | if _, err = os.Stat(output); err == nil { 31 | t.Skipf("Saving %q skipped, as it exists already. (Git would consider it modified.)", output) 32 | return 33 | } 34 | */ 35 | err = draw2dpdf.SaveToPdfFile(output, dest) 36 | if err != nil { 37 | t.Errorf("Saving %q failed: %v", output, err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /draw2dpdf/vectorizer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The draw2d Authors. All rights reserved. 2 | // created: 26/06/2015 by Stani Michiels 3 | 4 | package draw2dpdf 5 | 6 | // Vectorizer defines the minimal interface for gofpdf.Fpdf 7 | // to be passed to a PathConvertor. 8 | // It is also implemented by for example VertexMatrixTransform 9 | type Vectorizer interface { 10 | // MoveTo creates a new subpath that start at the specified point 11 | MoveTo(x, y float64) 12 | // LineTo adds a line to the current subpath 13 | LineTo(x, y float64) 14 | // CurveTo adds a quadratic bezier curve to the current subpath 15 | CurveTo(cx, cy, x, y float64) 16 | // CurveTo adds a cubic bezier curve to the current subpath 17 | CurveBezierCubicTo(cx1, cy1, cx2, cy2, x, y float64) 18 | // ArcTo adds an arc to the current subpath 19 | ArcTo(x, y, rx, ry, degRotate, degStart, degEnd float64) 20 | // ClosePath closes the subpath 21 | ClosePath() 22 | } 23 | -------------------------------------------------------------------------------- /draw2dsvg/converters.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The draw2d Authors. All rights reserved. 2 | // created: 16/12/2017 by Drahoslav Bednářpackage draw2dsvg 3 | 4 | package draw2dsvg 5 | 6 | import ( 7 | "bytes" 8 | "encoding/base64" 9 | "fmt" 10 | "github.com/llgcode/draw2d" 11 | "image" 12 | "image/color" 13 | "image/png" 14 | "math" 15 | "strconv" 16 | "strings" 17 | ) 18 | 19 | func toSvgRGBA(c color.Color) string { 20 | r, g, b, a := c.RGBA() 21 | r, g, b, a = r>>8, g>>8, b>>8, a>>8 22 | if a == 255 { 23 | return optiSprintf("#%02X%02X%02X", r, g, b) 24 | } 25 | return optiSprintf("rgba(%v,%v,%v,%f)", r, g, b, float64(a)/255) 26 | } 27 | 28 | func toSvgLength(l float64) string { 29 | if math.IsInf(l, 1) { 30 | return "100%" 31 | } 32 | return optiSprintf("%f", l) 33 | } 34 | 35 | func toSvgArray(nums []float64) string { 36 | arr := make([]string, len(nums)) 37 | for i, num := range nums { 38 | arr[i] = optiSprintf("%f", num) 39 | } 40 | return strings.Join(arr, ",") 41 | } 42 | 43 | func toSvgFillRule(rule draw2d.FillRule) string { 44 | return map[draw2d.FillRule]string{ 45 | draw2d.FillRuleEvenOdd: "evenodd", 46 | draw2d.FillRuleWinding: "nonzero", 47 | }[rule] 48 | } 49 | 50 | func toSvgPathDesc(p *draw2d.Path) string { 51 | parts := make([]string, len(p.Components)) 52 | ps := p.Points 53 | for i, cmp := range p.Components { 54 | switch cmp { 55 | case draw2d.MoveToCmp: 56 | parts[i] = optiSprintf("M %f,%f", ps[0], ps[1]) 57 | ps = ps[2:] 58 | case draw2d.LineToCmp: 59 | parts[i] = optiSprintf("L %f,%f", ps[0], ps[1]) 60 | ps = ps[2:] 61 | case draw2d.QuadCurveToCmp: 62 | parts[i] = optiSprintf("Q %f,%f %f,%f", ps[0], ps[1], ps[2], ps[3]) 63 | ps = ps[4:] 64 | case draw2d.CubicCurveToCmp: 65 | parts[i] = optiSprintf("C %f,%f %f,%f %f,%f", ps[0], ps[1], ps[2], ps[3], ps[4], ps[5]) 66 | ps = ps[6:] 67 | case draw2d.ArcToCmp: 68 | cx, cy := ps[0], ps[1] // center 69 | rx, ry := ps[2], ps[3] // radii 70 | fi := ps[4] + ps[5] // startAngle + angle 71 | 72 | // compute endpoint 73 | sinfi, cosfi := math.Sincos(fi) 74 | nom := math.Hypot(ry*cosfi, rx*sinfi) 75 | x := cx + (rx*ry*cosfi)/nom 76 | y := cy + (rx*ry*sinfi)/nom 77 | 78 | // compute large and sweep flags 79 | large := 0 80 | sweep := 0 81 | if math.Abs(ps[5]) > math.Pi { 82 | large = 1 83 | } 84 | if !math.Signbit(ps[5]) { 85 | sweep = 1 86 | } 87 | // dirty hack to ensure whole arc is drawn 88 | // if start point equals end point 89 | if sweep == 1 { 90 | x += 0.01 * sinfi 91 | y += 0.01 * -cosfi 92 | } else { 93 | x += 0.01 * sinfi 94 | y += 0.01 * cosfi 95 | } 96 | 97 | // rx ry x-axis-rotation large-arc-flag sweep-flag x y 98 | parts[i] = optiSprintf("A %f %f %v %v %v %F %F", 99 | rx, ry, 0, large, sweep, x, y, 100 | ) 101 | ps = ps[6:] 102 | case draw2d.CloseCmp: 103 | parts[i] = "Z" 104 | } 105 | } 106 | return strings.Join(parts, " ") 107 | } 108 | 109 | func toSvgTransform(mat draw2d.Matrix) string { 110 | if mat.IsIdentity() { 111 | return "" 112 | } 113 | if mat.IsTranslation() { 114 | x, y := mat.GetTranslation() 115 | return optiSprintf("translate(%f,%f)", x, y) 116 | } 117 | return optiSprintf("matrix(%f,%f,%f,%f,%f,%f)", 118 | mat[0], mat[1], mat[2], mat[3], mat[4], mat[5], 119 | ) 120 | } 121 | 122 | func imageToSvgHref(image image.Image) string { 123 | out := "data:image/png;base64," 124 | pngBuf := &bytes.Buffer{} 125 | png.Encode(pngBuf, image) 126 | out += base64.RawStdEncoding.EncodeToString(pngBuf.Bytes()) 127 | return out 128 | } 129 | 130 | // Do the same thing as fmt.Sprintf 131 | // except it uses the optimal precition for floats: (0-3) for f and (0-6) for F 132 | // eg.: 133 | // optiSprintf("%f", 3.0) => fmt.Sprintf("%.0f", 3.0) 134 | // optiSprintf("%f", 3.33) => fmt.Sprintf("%.2f", 3.33) 135 | // optiSprintf("%f", 3.3001) => fmt.Sprintf("%.1f", 3.3001) 136 | // optiSprintf("%f", 3.333333333333333) => fmt.Sprintf("%.3f", 3.333333333333333) 137 | // optiSprintf("%F", 3.333333333333333) => fmt.Sprintf("%.6f", 3.333333333333333) 138 | func optiSprintf(format string, a ...interface{}) string { 139 | chunks := strings.Split(format, "%") 140 | newChunks := make([]string, len(chunks)) 141 | for i, chunk := range chunks { 142 | if i != 0 { 143 | verb := chunk[0] 144 | if verb == 'f' || verb == 'F' { 145 | num := a[i-1].(float64) 146 | p := strconv.Itoa(getPrec(num, verb == 'F')) 147 | chunk = strings.Replace(chunk, string(verb), "."+p+"f", 1) 148 | } 149 | } 150 | newChunks[i] = chunk 151 | } 152 | format = strings.Join(newChunks, "%") 153 | return fmt.Sprintf(format, a...) 154 | } 155 | 156 | // TODO needs test, since it is not quiet right 157 | func getPrec(num float64, better bool) int { 158 | num = math.Abs(num) 159 | max := 3 160 | eps := 0.0005 161 | if better { 162 | max = 6 163 | eps = 0.0000005 164 | } 165 | prec := 0 166 | for math.Mod(num, 1) > eps { 167 | num *= 10 168 | eps *= 10 169 | prec++ 170 | } 171 | 172 | if max < prec { 173 | return max 174 | } 175 | return prec 176 | } 177 | -------------------------------------------------------------------------------- /draw2dsvg/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The draw2d Authors. All rights reserved. 2 | // created: 16/12/2017 by Drahoslav Bednář 3 | 4 | // Package draw2svg provides a graphic context that can draw 5 | // vector graphics and text on svg file. 6 | // 7 | // Quick Start 8 | // The following Go code geneartes a simple drawing and saves it 9 | // to a svg document: 10 | // TODO 11 | package draw2dsvg 12 | -------------------------------------------------------------------------------- /draw2dsvg/fileutil.go: -------------------------------------------------------------------------------- 1 | package draw2dsvg 2 | 3 | import ( 4 | "encoding/xml" 5 | _ "errors" 6 | "io" 7 | "os" 8 | ) 9 | 10 | func WriteSvg(w io.Writer, svg *Svg) error { 11 | _, err := w.Write([]byte(xml.Header)) 12 | if err != nil { 13 | return err 14 | } 15 | encoder := xml.NewEncoder(w) 16 | encoder.Indent("", "\t") 17 | return encoder.Encode(svg) 18 | } 19 | 20 | func SaveToSvgFile(filePath string, svg *Svg) error { 21 | f, err := os.Create(filePath) 22 | if err != nil { 23 | return err 24 | } 25 | defer f.Close() 26 | return WriteSvg(f, svg) 27 | } 28 | -------------------------------------------------------------------------------- /draw2dsvg/gc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The draw2d Authors. All rights reserved. 2 | // created: 16/12/2017 by Drahoslav Bednář 3 | 4 | package draw2dsvg 5 | 6 | import ( 7 | "github.com/golang/freetype/truetype" 8 | "github.com/llgcode/draw2d" 9 | "github.com/llgcode/draw2d/draw2dbase" 10 | "golang.org/x/image/font" 11 | "golang.org/x/image/math/fixed" 12 | "image" 13 | "log" 14 | "math" 15 | "strconv" 16 | "strings" 17 | ) 18 | 19 | type drawType int 20 | 21 | const ( 22 | filled drawType = 1 << iota 23 | stroked 24 | ) 25 | 26 | // GraphicContext implements the draw2d.GraphicContext interface 27 | // It provides draw2d with a svg backend 28 | type GraphicContext struct { 29 | *draw2dbase.StackGraphicContext 30 | FontCache draw2d.FontCache 31 | glyphCache draw2dbase.GlyphCache 32 | glyphBuf *truetype.GlyphBuf 33 | svg *Svg 34 | DPI int 35 | } 36 | 37 | func NewGraphicContext(svg *Svg) *GraphicContext { 38 | gc := &GraphicContext{ 39 | draw2dbase.NewStackGraphicContext(), 40 | draw2d.GetGlobalFontCache(), 41 | draw2dbase.NewGlyphCache(), 42 | &truetype.GlyphBuf{}, 43 | svg, 44 | 92, 45 | } 46 | return gc 47 | } 48 | 49 | // Clear fills the current canvas with a default transparent color 50 | func (gc *GraphicContext) Clear() { 51 | gc.svg.Groups = nil 52 | } 53 | 54 | // Stroke strokes the paths with the color specified by SetStrokeColor 55 | func (gc *GraphicContext) Stroke(paths ...*draw2d.Path) { 56 | gc.drawPaths(stroked, paths...) 57 | gc.Current.Path.Clear() 58 | } 59 | 60 | // Fill fills the paths with the color specified by SetFillColor 61 | func (gc *GraphicContext) Fill(paths ...*draw2d.Path) { 62 | gc.drawPaths(filled, paths...) 63 | gc.Current.Path.Clear() 64 | } 65 | 66 | // FillStroke first fills the paths and than strokes them 67 | func (gc *GraphicContext) FillStroke(paths ...*draw2d.Path) { 68 | gc.drawPaths(filled|stroked, paths...) 69 | gc.Current.Path.Clear() 70 | } 71 | 72 | // FillString draws the text at point (0, 0) 73 | func (gc *GraphicContext) FillString(text string) (cursor float64) { 74 | return gc.FillStringAt(text, 0, 0) 75 | } 76 | 77 | // FillStringAt draws the text at the specified point (x, y) 78 | func (gc *GraphicContext) FillStringAt(text string, x, y float64) (cursor float64) { 79 | return gc.drawString(text, filled, x, y) 80 | } 81 | 82 | // StrokeString draws the contour of the text at point (0, 0) 83 | func (gc *GraphicContext) StrokeString(text string) (cursor float64) { 84 | return gc.StrokeStringAt(text, 0, 0) 85 | } 86 | 87 | // StrokeStringAt draws the contour of the text at point (x, y) 88 | func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64) { 89 | return gc.drawString(text, stroked, x, y) 90 | } 91 | 92 | // Save the context and push it to the context stack 93 | func (gc *GraphicContext) Save() { 94 | gc.StackGraphicContext.Save() 95 | // TODO use common transformation group for multiple elements 96 | } 97 | 98 | // Restore remove the current context and restore the last one 99 | func (gc *GraphicContext) Restore() { 100 | gc.StackGraphicContext.Restore() 101 | // TODO use common transformation group for multiple elements 102 | } 103 | 104 | func (gc *GraphicContext) SetDPI(dpi int) { 105 | gc.DPI = dpi 106 | gc.recalc() 107 | } 108 | 109 | func (gc *GraphicContext) GetDPI() int { 110 | return gc.DPI 111 | } 112 | 113 | // SetFont sets the font used to draw text. 114 | func (gc *GraphicContext) SetFont(font *truetype.Font) { 115 | gc.Current.Font = font 116 | } 117 | 118 | // SetFontSize sets the font size in points (as in “a 12 point font”). 119 | func (gc *GraphicContext) SetFontSize(fontSize float64) { 120 | gc.Current.FontSize = fontSize 121 | gc.recalc() 122 | } 123 | 124 | // DrawImage draws the raster image in the current canvas 125 | func (gc *GraphicContext) DrawImage(image image.Image) { 126 | bounds := image.Bounds() 127 | 128 | svgImage := &Image{Href: imageToSvgHref(image)} 129 | svgImage.X = float64(bounds.Min.X) 130 | svgImage.Y = float64(bounds.Min.Y) 131 | svgImage.Width = toSvgLength(float64(bounds.Max.X - bounds.Min.X)) 132 | svgImage.Height = toSvgLength(float64(bounds.Max.Y - bounds.Min.Y)) 133 | gc.newGroup(0).Image = svgImage 134 | } 135 | 136 | // ClearRect fills the specified rectangle with a default transparent color 137 | func (gc *GraphicContext) ClearRect(x1, y1, x2, y2 int) { 138 | mask := gc.newMask(x1, y1, x2-x1, y2-y1) 139 | 140 | newGroup := &Group{ 141 | Groups: gc.svg.Groups, 142 | Mask: "url(#" + mask.Id + ")", 143 | } 144 | 145 | // replace groups with new masked group 146 | gc.svg.Groups = []*Group{newGroup} 147 | } 148 | 149 | // NOTE following two functions and soe other further below copied from dwra2d{img|gl} 150 | // TODO move them all to common draw2dbase? 151 | 152 | // CreateStringPath creates a path from the string s at x, y, and returns the string width. 153 | // The text is placed so that the left edge of the em square of the first character of s 154 | // and the baseline intersect at x, y. The majority of the affected pixels will be 155 | // above and to the right of the point, but some may be below or to the left. 156 | // For example, drawing a string that starts with a 'J' in an italic font may 157 | // affect pixels below and left of the point. 158 | func (gc *GraphicContext) CreateStringPath(s string, x, y float64) (cursor float64) { 159 | f, err := gc.loadCurrentFont() 160 | if err != nil { 161 | log.Println(err) 162 | return 0.0 163 | } 164 | startx := x 165 | prev, hasPrev := truetype.Index(0), false 166 | for _, rune := range s { 167 | index := f.Index(rune) 168 | if hasPrev { 169 | x += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index)) 170 | } 171 | err := gc.drawGlyph(index, x, y) 172 | if err != nil { 173 | log.Println(err) 174 | return startx - x 175 | } 176 | x += fUnitsToFloat64(f.HMetric(fixed.Int26_6(gc.Current.Scale), index).AdvanceWidth) 177 | prev, hasPrev = index, true 178 | } 179 | 180 | return x - startx 181 | } 182 | 183 | // GetStringBounds returns the approximate pixel bounds of the string s at x, y. 184 | // The the left edge of the em square of the first character of s 185 | // and the baseline intersect at 0, 0 in the returned coordinates. 186 | // Therefore the top and left coordinates may well be negative. 187 | func (gc *GraphicContext) GetStringBounds(s string) (left, top, right, bottom float64) { 188 | f, err := gc.loadCurrentFont() 189 | if err != nil { 190 | log.Println(err) 191 | return 0, 0, 0, 0 192 | } 193 | if gc.Current.Scale == 0 { 194 | panic("zero scale") 195 | } 196 | top, left, bottom, right = 10e6, 10e6, -10e6, -10e6 197 | cursor := 0.0 198 | prev, hasPrev := truetype.Index(0), false 199 | for _, rune := range s { 200 | index := f.Index(rune) 201 | if hasPrev { 202 | cursor += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index)) 203 | } 204 | if err := gc.glyphBuf.Load(gc.Current.Font, fixed.Int26_6(gc.Current.Scale), index, font.HintingNone); err != nil { 205 | log.Println(err) 206 | return 0, 0, 0, 0 207 | } 208 | e0 := 0 209 | for _, e1 := range gc.glyphBuf.Ends { 210 | ps := gc.glyphBuf.Points[e0:e1] 211 | for _, p := range ps { 212 | x, y := pointToF64Point(p) 213 | top = math.Min(top, y) 214 | bottom = math.Max(bottom, y) 215 | left = math.Min(left, x+cursor) 216 | right = math.Max(right, x+cursor) 217 | } 218 | } 219 | cursor += fUnitsToFloat64(f.HMetric(fixed.Int26_6(gc.Current.Scale), index).AdvanceWidth) 220 | prev, hasPrev = index, true 221 | } 222 | return left, top, right, bottom 223 | } 224 | 225 | //////////////////// 226 | // private funcitons 227 | 228 | func (gc *GraphicContext) drawPaths(drawType drawType, paths ...*draw2d.Path) { 229 | // create elements 230 | svgPath := Path{} 231 | group := gc.newGroup(drawType) 232 | 233 | // set attrs to path element 234 | paths = append(paths, gc.Current.Path) 235 | svgPathsDesc := make([]string, len(paths)) 236 | // multiple pathes has to be joined to single svg path description 237 | // because fill-rule wont work for whole group as excepted 238 | for i, path := range paths { 239 | svgPathsDesc[i] = toSvgPathDesc(path) 240 | } 241 | svgPath.Desc = strings.Join(svgPathsDesc, " ") 242 | 243 | // attach to group 244 | group.Paths = []*Path{&svgPath} 245 | } 246 | 247 | // Add text element to svg and returns its expected width 248 | func (gc *GraphicContext) drawString(text string, drawType drawType, x, y float64) float64 { 249 | switch gc.svg.FontMode { 250 | case PathFontMode: 251 | w := gc.CreateStringPath(text, x, y) 252 | gc.drawPaths(drawType) 253 | gc.Current.Path.Clear() 254 | return w 255 | case SvgFontMode: 256 | gc.embedSvgFont(text) 257 | } 258 | 259 | // create elements 260 | svgText := Text{} 261 | group := gc.newGroup(drawType) 262 | 263 | // set attrs to text element 264 | svgText.Text = text 265 | svgText.FontSize = gc.Current.FontSize 266 | svgText.X = x 267 | svgText.Y = y 268 | svgText.FontFamily = gc.Current.FontData.Name 269 | 270 | // attach to group 271 | group.Texts = []*Text{&svgText} 272 | left, _, right, _ := gc.GetStringBounds(text) 273 | return right - left 274 | } 275 | 276 | // Creates new group from current context 277 | // attach it to svg and return 278 | func (gc *GraphicContext) newGroup(drawType drawType) *Group { 279 | group := Group{} 280 | // set attrs to group 281 | if drawType&stroked == stroked { 282 | group.Stroke = toSvgRGBA(gc.Current.StrokeColor) 283 | group.StrokeWidth = toSvgLength(gc.Current.LineWidth) 284 | group.StrokeLinecap = gc.Current.Cap.String() 285 | group.StrokeLinejoin = gc.Current.Join.String() 286 | if len(gc.Current.Dash) > 0 { 287 | group.StrokeDasharray = toSvgArray(gc.Current.Dash) 288 | group.StrokeDashoffset = toSvgLength(gc.Current.DashOffset) 289 | } 290 | } 291 | 292 | if drawType&filled == filled { 293 | group.Fill = toSvgRGBA(gc.Current.FillColor) 294 | group.FillRule = toSvgFillRule(gc.Current.FillRule) 295 | } 296 | 297 | group.Transform = toSvgTransform(gc.Current.Tr) 298 | 299 | // attach 300 | gc.svg.Groups = append(gc.svg.Groups, &group) 301 | 302 | return &group 303 | } 304 | 305 | // creates new mask attached to svg 306 | func (gc *GraphicContext) newMask(x, y, width, height int) *Mask { 307 | mask := &Mask{} 308 | mask.X = float64(x) 309 | mask.Y = float64(y) 310 | mask.Width = toSvgLength(float64(width)) 311 | mask.Height = toSvgLength(float64(height)) 312 | 313 | // attach mask 314 | gc.svg.Masks = append(gc.svg.Masks, mask) 315 | mask.Id = "mask-" + strconv.Itoa(len(gc.svg.Masks)) 316 | return mask 317 | } 318 | 319 | // Embed svg font definition to svg tree itself 320 | // Or update existing if already exists for curent font data 321 | func (gc *GraphicContext) embedSvgFont(text string) *Font { 322 | fontName := gc.Current.FontData.Name 323 | gc.loadCurrentFont() 324 | 325 | // find or create font Element 326 | svgFont := (*Font)(nil) 327 | for _, font := range gc.svg.Fonts { 328 | if font.Name == fontName { 329 | svgFont = font 330 | break 331 | } 332 | } 333 | if svgFont == nil { 334 | // create new 335 | svgFont = &Font{} 336 | // and attach 337 | gc.svg.Fonts = append(gc.svg.Fonts, svgFont) 338 | } 339 | 340 | // fill with glyphs 341 | 342 | gc.Save() 343 | defer gc.Restore() 344 | gc.SetFontSize(2048) 345 | defer gc.SetDPI(gc.GetDPI()) 346 | gc.SetDPI(92) 347 | filling: 348 | for _, rune := range text { 349 | for _, g := range svgFont.Glyphs { 350 | if g.Rune == Rune(rune) { 351 | continue filling 352 | } 353 | } 354 | glyph := gc.glyphCache.Fetch(gc, gc.GetFontName(), rune) 355 | // glyphCache.Load indirectly calls CreateStringPath for single rune string 356 | 357 | glypPath := glyph.Path.VerticalFlip() // svg font glyphs have oposite y axe 358 | svgFont.Glyphs = append(svgFont.Glyphs, &Glyph{ 359 | Rune: Rune(rune), 360 | Desc: toSvgPathDesc(glypPath), 361 | HorizAdvX: glyph.Width, 362 | }) 363 | } 364 | 365 | // set attrs 366 | svgFont.Id = "font-" + strconv.Itoa(len(gc.svg.Fonts)) 367 | svgFont.Name = fontName 368 | 369 | // TODO use css @font-face with id instead of this 370 | svgFont.Face = &Face{Family: fontName, Units: 2048, HorizAdvX: 2048} 371 | return svgFont 372 | } 373 | 374 | func (gc *GraphicContext) loadCurrentFont() (*truetype.Font, error) { 375 | font, err := gc.FontCache.Load(gc.Current.FontData) 376 | if err != nil { 377 | font, err = gc.FontCache.Load(draw2dbase.DefaultFontData) 378 | } 379 | if font != nil { 380 | gc.SetFont(font) 381 | gc.SetFontSize(gc.Current.FontSize) 382 | } 383 | return font, err 384 | } 385 | 386 | func (gc *GraphicContext) drawGlyph(glyph truetype.Index, dx, dy float64) error { 387 | if err := gc.glyphBuf.Load(gc.Current.Font, fixed.Int26_6(gc.Current.Scale), glyph, font.HintingNone); err != nil { 388 | return err 389 | } 390 | e0 := 0 391 | for _, e1 := range gc.glyphBuf.Ends { 392 | DrawContour(gc, gc.glyphBuf.Points[e0:e1], dx, dy) 393 | e0 = e1 394 | } 395 | return nil 396 | } 397 | 398 | // recalc recalculates scale and bounds values from the font size, screen 399 | // resolution and font metrics, and invalidates the glyph cache. 400 | func (gc *GraphicContext) recalc() { 401 | gc.Current.Scale = gc.Current.FontSize * float64(gc.DPI) * (64.0 / 72.0) 402 | } 403 | -------------------------------------------------------------------------------- /draw2dsvg/samples_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The draw2d Authors. All rights reserved. 2 | // created: 26/06/2015 by Stani Michiels 3 | // See also test_test.go 4 | 5 | package draw2dsvg_test 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/llgcode/draw2d" 11 | "github.com/llgcode/draw2d/samples/android" 12 | "github.com/llgcode/draw2d/samples/frameimage" 13 | "github.com/llgcode/draw2d/samples/geometry" 14 | "github.com/llgcode/draw2d/samples/gopher" 15 | "github.com/llgcode/draw2d/samples/gopher2" 16 | "github.com/llgcode/draw2d/samples/helloworld" 17 | "github.com/llgcode/draw2d/samples/line" 18 | "github.com/llgcode/draw2d/samples/linecapjoin" 19 | "github.com/llgcode/draw2d/samples/postscript" 20 | ) 21 | 22 | func TestSampleAndroid(t *testing.T) { 23 | test(t, android.Main) 24 | } 25 | 26 | // TODO: FillString: w (width) is incorrect 27 | func TestSampleGeometry(t *testing.T) { 28 | // Set the global folder for searching fonts 29 | // The pdf backend needs for every ttf file its corresponding 30 | // json/.z file which is generated by gofpdf/makefont. 31 | draw2d.SetFontFolder("../resource/font") 32 | test(t, geometry.Main) 33 | } 34 | 35 | func TestSampleGopher(t *testing.T) { 36 | test(t, gopher.Main) 37 | } 38 | 39 | func TestSampleGopher2(t *testing.T) { 40 | test(t, gopher2.Main) 41 | } 42 | 43 | func TestSampleHelloWorld(t *testing.T) { 44 | // Set the global folder for searching fonts 45 | // The pdf backend needs for every ttf file its corresponding 46 | // json/.z file which is generated by gofpdf/makefont. 47 | draw2d.SetFontFolder("../resource/font") 48 | test(t, helloworld.Main) 49 | } 50 | 51 | func TestSampleFrameImage(t *testing.T) { 52 | test(t, frameimage.Main) 53 | } 54 | 55 | func TestSampleLine(t *testing.T) { 56 | test(t, line.Main) 57 | } 58 | 59 | func TestSampleLineCap(t *testing.T) { 60 | test(t, linecapjoin.Main) 61 | } 62 | 63 | func TestSamplePostscript(t *testing.T) { 64 | test(t, postscript.Main) 65 | } 66 | -------------------------------------------------------------------------------- /draw2dsvg/svg.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The draw2d Authors. All rights reserved. 2 | // created: 16/12/2017 by Drahoslav Bednář 3 | 4 | package draw2dsvg 5 | 6 | import ( 7 | "encoding/xml" 8 | ) 9 | 10 | /* svg elements */ 11 | 12 | type FontMode int 13 | 14 | // Modes of font handling in svg 15 | const ( 16 | // Does nothing special 17 | // Makes sense only for common system fonts 18 | SysFontMode FontMode = 1 << iota 19 | 20 | // Links font files in css def 21 | // Requires distribution of font files with outputed svg 22 | LinkFontMode // TODO implement 23 | 24 | // Embeds glyphs definition in svg file itself in svg font format 25 | // Has poor browser support 26 | SvgFontMode 27 | 28 | // Embeds font definiton in svg file itself in woff format as part of css def 29 | CssFontMode // TODO implement 30 | 31 | // Converts texts to paths 32 | PathFontMode 33 | ) 34 | 35 | type Svg struct { 36 | XMLName xml.Name `xml:"svg"` 37 | Xmlns string `xml:"xmlns,attr"` 38 | Width string `xml:"width,attr,omitempty"` 39 | Height string `xml:"height,attr,omitempty"` 40 | ViewBox string `xml:"viewBox,attr,omitempty"` 41 | Fonts []*Font `xml:"defs>font"` 42 | Masks []*Mask `xml:"defs>mask"` 43 | Groups []*Group `xml:"g"` 44 | FontMode FontMode `xml:"-"` 45 | FillStroke 46 | } 47 | 48 | func NewSvg() *Svg { 49 | return &Svg{ 50 | Xmlns: "http://www.w3.org/2000/svg", 51 | FillStroke: FillStroke{Fill: "none", Stroke: "none"}, 52 | FontMode: PathFontMode, 53 | } 54 | } 55 | 56 | type Group struct { 57 | FillStroke 58 | Transform string `xml:"transform,attr,omitempty"` 59 | Groups []*Group `xml:"g"` 60 | Paths []*Path `xml:"path"` 61 | Texts []*Text `xml:"text"` 62 | Image *Image `xml:"image"` 63 | Mask string `xml:"mask,attr,omitempty"` 64 | } 65 | 66 | type Path struct { 67 | FillStroke 68 | Desc string `xml:"d,attr"` 69 | } 70 | 71 | type Text struct { 72 | FillStroke 73 | Position 74 | FontSize float64 `xml:"font-size,attr,omitempty"` 75 | FontFamily string `xml:"font-family,attr,omitempty"` 76 | Text string `xml:",innerxml"` 77 | Style string `xml:"style,attr,omitempty"` 78 | } 79 | 80 | type Image struct { 81 | Position 82 | Dimension 83 | Href string `xml:"href,attr"` 84 | } 85 | 86 | type Mask struct { 87 | Identity 88 | Position 89 | Dimension 90 | } 91 | 92 | type Rect struct { 93 | Position 94 | Dimension 95 | FillStroke 96 | } 97 | 98 | func (m Mask) MarshalXML(e *xml.Encoder, start xml.StartElement) error { 99 | bigRect := Rect{} 100 | bigRect.X, bigRect.Y = 0, 0 101 | bigRect.Width, bigRect.Height = "100%", "100%" 102 | bigRect.Fill = "#fff" 103 | rect := Rect{} 104 | rect.X, rect.Y = m.X, m.Y 105 | rect.Width, rect.Height = m.Width, m.Height 106 | rect.Fill = "#000" 107 | 108 | return e.EncodeElement(struct { 109 | XMLName xml.Name `xml:"mask"` 110 | Rects [2]Rect `xml:"rect"` 111 | Id string `xml:"id,attr"` 112 | }{ 113 | Rects: [2]Rect{bigRect, rect}, 114 | Id: m.Id, 115 | }, start) 116 | } 117 | 118 | /* font related elements */ 119 | 120 | type Font struct { 121 | Identity 122 | Face *Face `xml:"font-face"` 123 | Glyphs []*Glyph `xml:"glyph"` 124 | } 125 | 126 | type Face struct { 127 | Family string `xml:"font-family,attr"` 128 | Units int `xml:"units-per-em,attr"` 129 | HorizAdvX float64 `xml:"horiz-adv-x,attr"` 130 | // TODO add other attrs, like style, variant, weight... 131 | } 132 | 133 | type Glyph struct { 134 | Rune Rune `xml:"unicode,attr"` 135 | Desc string `xml:"d,attr"` 136 | HorizAdvX float64 `xml:"horiz-adv-x,attr"` 137 | } 138 | 139 | type Rune rune 140 | 141 | func (r Rune) MarshalXMLAttr(name xml.Name) (xml.Attr, error) { 142 | return xml.Attr{ 143 | Name: name, 144 | Value: string(rune(r)), 145 | }, nil 146 | } 147 | 148 | /* shared attrs */ 149 | 150 | type Identity struct { 151 | Id string `xml:"id,attr"` 152 | Name string `xml:"name,attr"` 153 | } 154 | 155 | type Position struct { 156 | X float64 `xml:"x,attr,omitempty"` 157 | Y float64 `xml:"y,attr,omitempty"` 158 | } 159 | 160 | type Dimension struct { 161 | Width string `xml:"width,attr"` 162 | Height string `xml:"height,attr"` 163 | } 164 | 165 | type FillStroke struct { 166 | Fill string `xml:"fill,attr,omitempty"` 167 | FillRule string `xml:"fill-rule,attr,omitempty"` 168 | 169 | Stroke string `xml:"stroke,attr,omitempty"` 170 | StrokeWidth string `xml:"stroke-width,attr,omitempty"` 171 | StrokeLinecap string `xml:"stroke-linecap,attr,omitempty"` 172 | StrokeLinejoin string `xml:"stroke-linejoin,attr,omitempty"` 173 | StrokeDasharray string `xml:"stroke-dasharray,attr,omitempty"` 174 | StrokeDashoffset string `xml:"stroke-dashoffset,attr,omitempty"` 175 | } 176 | -------------------------------------------------------------------------------- /draw2dsvg/test_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The draw2d Authors. All rights reserved. 2 | // created: 16/12/2017 by Drahoslav Bednář 3 | 4 | // Package draw2dsvg_test gives test coverage with the command: 5 | // go test -cover ./... | grep -v "no test" 6 | // (It should be run from its parent draw2d directory.) 7 | package draw2dsvg_test 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/llgcode/draw2d" 13 | "github.com/llgcode/draw2d/draw2dsvg" 14 | ) 15 | 16 | type sample func(gc draw2d.GraphicContext, ext string) (string, error) 17 | 18 | func test(t *testing.T, draw sample) { 19 | // Initialize the graphic context on an pdf document 20 | dest := draw2dsvg.NewSvg() 21 | gc := draw2dsvg.NewGraphicContext(dest) 22 | // Draw sample 23 | output, err := draw(gc, "svg") 24 | if err != nil { 25 | t.Errorf("Drawing %q failed: %v", output, err) 26 | return 27 | } 28 | err = draw2dsvg.SaveToSvgFile(output, dest) 29 | if err != nil { 30 | t.Errorf("Saving %q failed: %v", output, err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /draw2dsvg/text.go: -------------------------------------------------------------------------------- 1 | // NOTE that this is identical copy of draw2dgl/text.go and draw2dimg/text.go 2 | package draw2dsvg 3 | 4 | import ( 5 | "github.com/golang/freetype/truetype" 6 | "github.com/llgcode/draw2d" 7 | "golang.org/x/image/math/fixed" 8 | ) 9 | 10 | // DrawContour draws the given closed contour at the given sub-pixel offset. 11 | func DrawContour(path draw2d.PathBuilder, ps []truetype.Point, dx, dy float64) { 12 | if len(ps) == 0 { 13 | return 14 | } 15 | startX, startY := pointToF64Point(ps[0]) 16 | var others []truetype.Point 17 | if ps[0].Flags&0x01 != 0 { 18 | others = ps[1:] 19 | } else { 20 | lastX, lastY := pointToF64Point(ps[len(ps)-1]) 21 | if ps[len(ps)-1].Flags&0x01 != 0 { 22 | startX, startY = lastX, lastY 23 | others = ps[:len(ps)-1] 24 | } else { 25 | startX = (startX + lastX) / 2 26 | startY = (startY + lastY) / 2 27 | others = ps 28 | } 29 | } 30 | path.MoveTo(startX+dx, startY+dy) 31 | q0X, q0Y, on0 := startX, startY, true 32 | for _, p := range others { 33 | qX, qY := pointToF64Point(p) 34 | on := p.Flags&0x01 != 0 35 | if on { 36 | if on0 { 37 | path.LineTo(qX+dx, qY+dy) 38 | } else { 39 | path.QuadCurveTo(q0X+dx, q0Y+dy, qX+dx, qY+dy) 40 | } 41 | } else { 42 | if on0 { 43 | // No-op. 44 | } else { 45 | midX := (q0X + qX) / 2 46 | midY := (q0Y + qY) / 2 47 | path.QuadCurveTo(q0X+dx, q0Y+dy, midX+dx, midY+dy) 48 | } 49 | } 50 | q0X, q0Y, on0 = qX, qY, on 51 | } 52 | // Close the curve. 53 | if on0 { 54 | path.LineTo(startX+dx, startY+dy) 55 | } else { 56 | path.QuadCurveTo(q0X+dx, q0Y+dy, startX+dx, startY+dy) 57 | } 58 | } 59 | 60 | func pointToF64Point(p truetype.Point) (x, y float64) { 61 | return fUnitsToFloat64(p.X), -fUnitsToFloat64(p.Y) 62 | } 63 | 64 | func fUnitsToFloat64(x fixed.Int26_6) float64 { 65 | scaled := x << 2 66 | return float64(scaled/256) + float64(scaled%256)/256.0 67 | } 68 | 69 | // FontExtents contains font metric information. 70 | type FontExtents struct { 71 | // Ascent is the distance that the text 72 | // extends above the baseline. 73 | Ascent float64 74 | 75 | // Descent is the distance that the text 76 | // extends below the baseline. The descent 77 | // is given as a negative value. 78 | Descent float64 79 | 80 | // Height is the distance from the lowest 81 | // descending point to the highest ascending 82 | // point. 83 | Height float64 84 | } 85 | 86 | // Extents returns the FontExtents for a font. 87 | // TODO needs to read this https://developer.apple.com/fonts/TrueType-Reference-Manual/RM02/Chap2.html#intro 88 | func Extents(font *truetype.Font, size float64) FontExtents { 89 | bounds := font.Bounds(fixed.Int26_6(font.FUnitsPerEm())) 90 | scale := size / float64(font.FUnitsPerEm()) 91 | return FontExtents{ 92 | Ascent: float64(bounds.Max.Y) * scale, 93 | Descent: float64(bounds.Min.Y) * scale, 94 | Height: float64(bounds.Max.Y-bounds.Min.Y) * scale, 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /draw2dsvg/xml_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The draw2d Authors. All rights reserved. 2 | // created: 16/12/2017 by Drahoslav Bednář 3 | 4 | // Package draw2dsvg_test gives test coverage with the command: 5 | // go test -cover ./... | grep -v "no test" 6 | // (It should be run from its parent draw2d directory.) 7 | package draw2dsvg 8 | 9 | import ( 10 | "encoding/xml" 11 | "testing" 12 | ) 13 | 14 | // Test basic encoding of svg/xml elements 15 | func TestXml(t *testing.T) { 16 | 17 | svg := NewSvg() 18 | svg.Groups = []*Group{&Group{ 19 | Groups: []*Group{ 20 | &Group{}, // nested groups 21 | &Group{}, 22 | }, 23 | Texts: []*Text{ 24 | &Text{Text: "Hello"}, // text 25 | &Text{Text: "world", Style: "opacity: 0.5"}, // text with style 26 | }, 27 | Paths: []*Path{ 28 | &Path{Desc: "M100,200 C100,100 250,100 250,200 S400,300 400,200"}, // simple path 29 | &Path{}, // empty path 30 | }, 31 | }} 32 | 33 | expectedOut := ` 34 | 35 | 36 | 37 | 38 | 39 | 40 | Hello 41 | world 42 | 43 | ` 44 | 45 | out, err := xml.MarshalIndent(svg, "", " ") 46 | 47 | if err != nil { 48 | t.Error(err) 49 | } 50 | if string(out) != expectedOut { 51 | t.Errorf("svg output is not as expected\n"+ 52 | "got:\n%s\n\n"+ 53 | "want:\n%s\n", 54 | string(out), 55 | expectedOut, 56 | ) 57 | } 58 | } 59 | 60 | func TestXml_WidthHeight(t *testing.T) { 61 | expect := ` 62 | 63 | ` 64 | svg := NewSvg() 65 | subtest := func(t *testing.T) { 66 | out, err := xml.MarshalIndent(svg, "", " ") 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | if string(out) != expect { 71 | t.Errorf("svg output is not as expected\n"+ 72 | "got:\n%s\n\n"+ 73 | "want:\n%s\n", 74 | string(out), 75 | expect) 76 | } 77 | } 78 | t.Run("no width, height", subtest) 79 | 80 | expect = ` 81 | 82 | ` 83 | svg.Width, svg.Height = "640px", "480" 84 | t.Run("with width, height", subtest) 85 | } 86 | 87 | func TestXml_ViewBox(t *testing.T) { 88 | expect := ` 89 | 90 | ` 91 | 92 | svg := NewSvg() 93 | subtest := func(t *testing.T) { 94 | out, err := xml.MarshalIndent(svg, "", " ") 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | if string(out) != expect { 99 | t.Errorf("svg output is not as expected\n"+ 100 | "got:\n%s\n\n"+ 101 | "want:\n%s\n", 102 | string(out), 103 | expect) 104 | } 105 | } 106 | svg.ViewBox = "0 100 -10 -100" 107 | t.Run("with viewBox", subtest) 108 | } 109 | -------------------------------------------------------------------------------- /font.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 13/12/2010 by Laurent Le Goff 3 | 4 | package draw2d 5 | 6 | import ( 7 | "log" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/golang/freetype/truetype" 12 | "sync" 13 | ) 14 | 15 | // FontStyle defines bold and italic styles for the font 16 | // It is possible to combine values for mixed styles, eg. 17 | // FontData.Style = FontStyleBold | FontStyleItalic 18 | type FontStyle byte 19 | 20 | const ( 21 | FontStyleNormal FontStyle = iota 22 | FontStyleBold 23 | FontStyleItalic 24 | ) 25 | 26 | type FontFamily byte 27 | 28 | const ( 29 | FontFamilySans FontFamily = iota 30 | FontFamilySerif 31 | FontFamilyMono 32 | ) 33 | 34 | type FontData struct { 35 | Name string 36 | Family FontFamily 37 | Style FontStyle 38 | } 39 | 40 | type FontFileNamer func(fontData FontData) string 41 | 42 | func FontFileName(fontData FontData) string { 43 | fontFileName := fontData.Name 44 | switch fontData.Family { 45 | case FontFamilySans: 46 | fontFileName += "s" 47 | case FontFamilySerif: 48 | fontFileName += "r" 49 | case FontFamilyMono: 50 | fontFileName += "m" 51 | } 52 | if fontData.Style&FontStyleBold != 0 { 53 | fontFileName += "b" 54 | } else { 55 | fontFileName += "r" 56 | } 57 | 58 | if fontData.Style&FontStyleItalic != 0 { 59 | fontFileName += "i" 60 | } 61 | fontFileName += ".ttf" 62 | return fontFileName 63 | } 64 | 65 | func RegisterFont(fontData FontData, font *truetype.Font) { 66 | fontCache.Store(fontData, font) 67 | } 68 | 69 | func GetFont(fontData FontData) (font *truetype.Font) { 70 | var err error 71 | 72 | if font, err = fontCache.Load(fontData); err != nil { 73 | log.Println(err) 74 | } 75 | 76 | return 77 | } 78 | 79 | func GetFontFolder() string { 80 | return defaultFonts.folder 81 | } 82 | 83 | func SetFontFolder(folder string) { 84 | defaultFonts.setFolder(filepath.Clean(folder)) 85 | } 86 | 87 | func GetGlobalFontCache() FontCache { 88 | return fontCache 89 | } 90 | 91 | func SetFontNamer(fn FontFileNamer) { 92 | defaultFonts.setNamer(fn) 93 | } 94 | 95 | // Types implementing this interface can be passed to SetFontCache to change the 96 | // way fonts are being stored and retrieved. 97 | type FontCache interface { 98 | // Loads a truetype font represented by the FontData object passed as 99 | // argument. 100 | // The method returns an error if the font could not be loaded, either 101 | // because it didn't exist or the resource it was loaded from was corrupted. 102 | Load(FontData) (*truetype.Font, error) 103 | 104 | // Sets the truetype font that will be returned by Load when given the font 105 | // data passed as first argument. 106 | Store(FontData, *truetype.Font) 107 | } 108 | 109 | // Changes the font cache backend used by the package. After calling this 110 | // functionSetFontFolder and SetFontNamer will not affect anymore how fonts are 111 | // loaded. 112 | // To restore the default font cache, call this function passing nil as argument. 113 | func SetFontCache(cache FontCache) { 114 | if cache == nil { 115 | fontCache = defaultFonts 116 | } else { 117 | fontCache = cache 118 | } 119 | } 120 | 121 | // FolderFontCache can Load font from folder 122 | type FolderFontCache struct { 123 | fonts map[string]*truetype.Font 124 | folder string 125 | namer FontFileNamer 126 | } 127 | 128 | // NewFolderFontCache creates FolderFontCache 129 | func NewFolderFontCache(folder string) *FolderFontCache { 130 | return &FolderFontCache{ 131 | fonts: make(map[string]*truetype.Font), 132 | folder: folder, 133 | namer: FontFileName, 134 | } 135 | } 136 | 137 | // Load a font from cache if exists otherwise it will load the font from file 138 | func (cache *FolderFontCache) Load(fontData FontData) (font *truetype.Font, err error) { 139 | if font = cache.fonts[cache.namer(fontData)]; font != nil { 140 | return font, nil 141 | } 142 | 143 | var data []byte 144 | var file = cache.namer(fontData) 145 | 146 | if data, err = os.ReadFile(filepath.Join(cache.folder, file)); err != nil { 147 | return 148 | } 149 | 150 | if font, err = truetype.Parse(data); err != nil { 151 | return 152 | } 153 | 154 | cache.fonts[file] = font 155 | return 156 | } 157 | 158 | // Store a font to this cache 159 | func (cache *FolderFontCache) Store(fontData FontData, font *truetype.Font) { 160 | cache.fonts[cache.namer(fontData)] = font 161 | } 162 | 163 | // SyncFolderFontCache can Load font from folder 164 | type SyncFolderFontCache struct { 165 | sync.RWMutex 166 | fonts map[string]*truetype.Font 167 | folder string 168 | namer FontFileNamer 169 | } 170 | 171 | // NewSyncFolderFontCache creates SyncFolderFontCache 172 | func NewSyncFolderFontCache(folder string) *SyncFolderFontCache { 173 | return &SyncFolderFontCache{ 174 | fonts: make(map[string]*truetype.Font), 175 | folder: folder, 176 | namer: FontFileName, 177 | } 178 | } 179 | 180 | func (cache *SyncFolderFontCache) setFolder(folder string) { 181 | cache.Lock() 182 | cache.folder = folder 183 | cache.Unlock() 184 | } 185 | 186 | func (cache *SyncFolderFontCache) setNamer(namer FontFileNamer) { 187 | cache.Lock() 188 | cache.namer = namer 189 | cache.Unlock() 190 | } 191 | 192 | // Load a font from cache if exists otherwise it will load the font from file 193 | func (cache *SyncFolderFontCache) Load(fontData FontData) (font *truetype.Font, err error) { 194 | cache.RLock() 195 | font = cache.fonts[cache.namer(fontData)] 196 | cache.RUnlock() 197 | 198 | if font != nil { 199 | return font, nil 200 | } 201 | 202 | var data []byte 203 | var file = cache.namer(fontData) 204 | 205 | if data, err = os.ReadFile(filepath.Join(cache.folder, file)); err != nil { 206 | return 207 | } 208 | 209 | if font, err = truetype.Parse(data); err != nil { 210 | return 211 | } 212 | cache.Lock() 213 | cache.fonts[file] = font 214 | cache.Unlock() 215 | return 216 | } 217 | 218 | // Store a font to this cache 219 | func (cache *SyncFolderFontCache) Store(fontData FontData, font *truetype.Font) { 220 | cache.Lock() 221 | cache.fonts[cache.namer(fontData)] = font 222 | cache.Unlock() 223 | } 224 | 225 | var ( 226 | defaultFonts = NewSyncFolderFontCache("../resource/font") 227 | 228 | fontCache FontCache = defaultFonts 229 | ) 230 | -------------------------------------------------------------------------------- /gc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 21/11/2010 by Laurent Le Goff 3 | 4 | package draw2d 5 | 6 | import ( 7 | "image" 8 | "image/color" 9 | ) 10 | 11 | // GraphicContext describes the interface for the various backends (images, pdf, opengl, ...) 12 | type GraphicContext interface { 13 | // PathBuilder describes the interface for path drawing 14 | PathBuilder 15 | // BeginPath creates a new path 16 | BeginPath() 17 | // GetPath copies the current path, then returns it 18 | GetPath() Path 19 | // GetMatrixTransform returns the current transformation matrix 20 | GetMatrixTransform() Matrix 21 | // SetMatrixTransform sets the current transformation matrix 22 | SetMatrixTransform(tr Matrix) 23 | // ComposeMatrixTransform composes the current transformation matrix with tr 24 | ComposeMatrixTransform(tr Matrix) 25 | // Rotate applies a rotation to the current transformation matrix. angle is in radian. 26 | Rotate(angle float64) 27 | // Translate applies a translation to the current transformation matrix. 28 | Translate(tx, ty float64) 29 | // Scale applies a scale to the current transformation matrix. 30 | Scale(sx, sy float64) 31 | // SetStrokeColor sets the current stroke color 32 | SetStrokeColor(c color.Color) 33 | // SetFillColor sets the current fill color 34 | SetFillColor(c color.Color) 35 | // SetFillRule sets the current fill rule 36 | SetFillRule(f FillRule) 37 | // SetLineWidth sets the current line width 38 | SetLineWidth(lineWidth float64) 39 | // SetLineCap sets the current line cap 40 | SetLineCap(cap LineCap) 41 | // SetLineJoin sets the current line join 42 | SetLineJoin(join LineJoin) 43 | // SetLineDash sets the current dash 44 | SetLineDash(dash []float64, dashOffset float64) 45 | // SetFontSize sets the current font size 46 | SetFontSize(fontSize float64) 47 | // GetFontSize gets the current font size 48 | GetFontSize() float64 49 | // SetFontData sets the current FontData 50 | SetFontData(fontData FontData) 51 | // GetFontData gets the current FontData 52 | GetFontData() FontData 53 | // GetFontName gets the current FontData as a string 54 | GetFontName() string 55 | // DrawImage draws the raster image in the current canvas 56 | DrawImage(image image.Image) 57 | // Save the context and push it to the context stack 58 | Save() 59 | // Restore remove the current context and restore the last one 60 | Restore() 61 | // Clear fills the current canvas with a default transparent color 62 | Clear() 63 | // ClearRect fills the specified rectangle with a default transparent color 64 | ClearRect(x1, y1, x2, y2 int) 65 | // SetDPI sets the current DPI 66 | SetDPI(dpi int) 67 | // GetDPI gets the current DPI 68 | GetDPI() int 69 | // GetStringBounds gets pixel bounds(dimensions) of given string 70 | GetStringBounds(s string) (left, top, right, bottom float64) 71 | // CreateStringPath creates a path from the string s at x, y 72 | CreateStringPath(text string, x, y float64) (cursor float64) 73 | // FillString draws the text at point (0, 0) 74 | FillString(text string) (cursor float64) 75 | // FillStringAt draws the text at the specified point (x, y) 76 | FillStringAt(text string, x, y float64) (cursor float64) 77 | // StrokeString draws the contour of the text at point (0, 0) 78 | StrokeString(text string) (cursor float64) 79 | // StrokeStringAt draws the contour of the text at point (x, y) 80 | StrokeStringAt(text string, x, y float64) (cursor float64) 81 | // Stroke strokes the paths with the color specified by SetStrokeColor 82 | Stroke(paths ...*Path) 83 | // Fill fills the paths with the color specified by SetFillColor 84 | Fill(paths ...*Path) 85 | // FillStroke first fills the paths and than strokes them 86 | FillStroke(paths ...*Path) 87 | } 88 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/llgcode/draw2d 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 7 | github.com/go-gl/glfw v0.0.0-20231124074035-2de0cf0c80af 8 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 9 | github.com/jung-kurt/gofpdf v1.16.2 10 | github.com/llgcode/ps v0.0.0-20210114104736-f4b0c5d1e02e 11 | golang.org/x/image v0.18.0 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA= 4 | github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= 5 | github.com/go-gl/glfw v0.0.0-20231124074035-2de0cf0c80af h1:V4DLCrN57QoLQYtwnlmGQvAIM6DMU5eMrR9VQhmxPPs= 6 | github.com/go-gl/glfw v0.0.0-20231124074035-2de0cf0c80af/go.mod h1:wyvWpaEu9B/VQiV1jsPs7Mha9I7yto/HqIBw197ZAzk= 7 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 8 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 9 | github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= 10 | github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc= 11 | github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0= 12 | github.com/llgcode/ps v0.0.0-20210114104736-f4b0c5d1e02e h1:ZAvbj5hI/G/EbAYAcj4yCXUNiFKefEhH0qfImDDD0/8= 13 | github.com/llgcode/ps v0.0.0-20210114104736-f4b0c5d1e02e/go.mod h1:1l8ky+Ew27CMX29uG+a2hNOKpeNYEQjjtiALiBlFQbY= 14 | github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= 15 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= 18 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 19 | golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 20 | golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= 21 | golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= 22 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 23 | -------------------------------------------------------------------------------- /matrix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 21/11/2010 by Laurent Le Goff 3 | 4 | package draw2d 5 | 6 | import ( 7 | "math" 8 | ) 9 | 10 | // Matrix represents an affine transformation 11 | type Matrix [6]float64 12 | 13 | const ( 14 | epsilon = 1e-6 15 | ) 16 | 17 | // Determinant compute the determinant of the matrix 18 | func (tr Matrix) Determinant() float64 { 19 | return tr[0]*tr[3] - tr[1]*tr[2] 20 | } 21 | 22 | // Transform applies the transformation matrix to points. It modify the points passed in parameter. 23 | func (tr Matrix) Transform(points []float64) { 24 | for i, j := 0, 1; j < len(points); i, j = i+2, j+2 { 25 | x := points[i] 26 | y := points[j] 27 | points[i] = x*tr[0] + y*tr[2] + tr[4] 28 | points[j] = x*tr[1] + y*tr[3] + tr[5] 29 | } 30 | } 31 | 32 | // TransformPoint applies the transformation matrix to point. It returns the point the transformed point. 33 | func (tr Matrix) TransformPoint(x, y float64) (xres, yres float64) { 34 | xres = x*tr[0] + y*tr[2] + tr[4] 35 | yres = x*tr[1] + y*tr[3] + tr[5] 36 | return xres, yres 37 | } 38 | 39 | func minMax(x, y float64) (min, max float64) { 40 | if x > y { 41 | return y, x 42 | } 43 | return x, y 44 | } 45 | 46 | // Transform applies the transformation matrix to the rectangle represented by the min and the max point of the rectangle 47 | func (tr Matrix) TransformRectangle(x0, y0, x2, y2 float64) (nx0, ny0, nx2, ny2 float64) { 48 | points := []float64{x0, y0, x2, y0, x2, y2, x0, y2} 49 | tr.Transform(points) 50 | points[0], points[2] = minMax(points[0], points[2]) 51 | points[4], points[6] = minMax(points[4], points[6]) 52 | points[1], points[3] = minMax(points[1], points[3]) 53 | points[5], points[7] = minMax(points[5], points[7]) 54 | 55 | nx0 = math.Min(points[0], points[4]) 56 | ny0 = math.Min(points[1], points[5]) 57 | nx2 = math.Max(points[2], points[6]) 58 | ny2 = math.Max(points[3], points[7]) 59 | return nx0, ny0, nx2, ny2 60 | } 61 | 62 | // InverseTransform applies the transformation inverse matrix to the rectangle represented by the min and the max point of the rectangle 63 | func (tr Matrix) InverseTransform(points []float64) { 64 | d := tr.Determinant() // matrix determinant 65 | for i, j := 0, 1; j < len(points); i, j = i+2, j+2 { 66 | x := points[i] 67 | y := points[j] 68 | points[i] = ((x-tr[4])*tr[3] - (y-tr[5])*tr[2]) / d 69 | points[j] = ((y-tr[5])*tr[0] - (x-tr[4])*tr[1]) / d 70 | } 71 | } 72 | 73 | // InverseTransformPoint applies the transformation inverse matrix to point. It returns the point the transformed point. 74 | func (tr Matrix) InverseTransformPoint(x, y float64) (xres, yres float64) { 75 | d := tr.Determinant() // matrix determinant 76 | xres = ((x-tr[4])*tr[3] - (y-tr[5])*tr[2]) / d 77 | yres = ((y-tr[5])*tr[0] - (x-tr[4])*tr[1]) / d 78 | return xres, yres 79 | } 80 | 81 | // VectorTransform applies the transformation matrix to points without using the translation parameter of the affine matrix. 82 | // It modify the points passed in parameter. 83 | func (tr Matrix) VectorTransform(points []float64) { 84 | for i, j := 0, 1; j < len(points); i, j = i+2, j+2 { 85 | x := points[i] 86 | y := points[j] 87 | points[i] = x*tr[0] + y*tr[2] 88 | points[j] = x*tr[1] + y*tr[3] 89 | } 90 | } 91 | 92 | // NewIdentityMatrix creates an identity transformation matrix. 93 | func NewIdentityMatrix() Matrix { 94 | return Matrix{1, 0, 0, 1, 0, 0} 95 | } 96 | 97 | // NewTranslationMatrix creates a transformation matrix with a translation tx and ty translation parameter 98 | func NewTranslationMatrix(tx, ty float64) Matrix { 99 | return Matrix{1, 0, 0, 1, tx, ty} 100 | } 101 | 102 | // NewScaleMatrix creates a transformation matrix with a sx, sy scale factor 103 | func NewScaleMatrix(sx, sy float64) Matrix { 104 | return Matrix{sx, 0, 0, sy, 0, 0} 105 | } 106 | 107 | // NewRotationMatrix creates a rotation transformation matrix. angle is in radian 108 | func NewRotationMatrix(angle float64) Matrix { 109 | c := math.Cos(angle) 110 | s := math.Sin(angle) 111 | return Matrix{c, s, -s, c, 0, 0} 112 | } 113 | 114 | // NewMatrixFromRects creates a transformation matrix, combining a scale and a translation, that transform rectangle1 into rectangle2. 115 | func NewMatrixFromRects(rectangle1, rectangle2 [4]float64) Matrix { 116 | xScale := (rectangle2[2] - rectangle2[0]) / (rectangle1[2] - rectangle1[0]) 117 | yScale := (rectangle2[3] - rectangle2[1]) / (rectangle1[3] - rectangle1[1]) 118 | xOffset := rectangle2[0] - (rectangle1[0] * xScale) 119 | yOffset := rectangle2[1] - (rectangle1[1] * yScale) 120 | return Matrix{xScale, 0, 0, yScale, xOffset, yOffset} 121 | } 122 | 123 | // Inverse computes the inverse matrix 124 | func (tr *Matrix) Inverse() { 125 | d := tr.Determinant() // matrix determinant 126 | tr0, tr1, tr2, tr3, tr4, tr5 := tr[0], tr[1], tr[2], tr[3], tr[4], tr[5] 127 | tr[0] = tr3 / d 128 | tr[1] = -tr1 / d 129 | tr[2] = -tr2 / d 130 | tr[3] = tr0 / d 131 | tr[4] = (tr2*tr5 - tr3*tr4) / d 132 | tr[5] = (tr1*tr4 - tr0*tr5) / d 133 | } 134 | 135 | func (tr Matrix) Copy() Matrix { 136 | var result Matrix 137 | copy(result[:], tr[:]) 138 | return result 139 | } 140 | 141 | // Compose multiplies trToConcat x tr 142 | func (tr *Matrix) Compose(trToCompose Matrix) { 143 | tr0, tr1, tr2, tr3, tr4, tr5 := tr[0], tr[1], tr[2], tr[3], tr[4], tr[5] 144 | tr[0] = trToCompose[0]*tr0 + trToCompose[1]*tr2 145 | tr[1] = trToCompose[1]*tr3 + trToCompose[0]*tr1 146 | tr[2] = trToCompose[2]*tr0 + trToCompose[3]*tr2 147 | tr[3] = trToCompose[3]*tr3 + trToCompose[2]*tr1 148 | tr[4] = trToCompose[4]*tr0 + trToCompose[5]*tr2 + tr4 149 | tr[5] = trToCompose[5]*tr3 + trToCompose[4]*tr1 + tr5 150 | } 151 | 152 | // Scale adds a scale to the matrix 153 | func (tr *Matrix) Scale(sx, sy float64) { 154 | tr[0] = sx * tr[0] 155 | tr[1] = sx * tr[1] 156 | tr[2] = sy * tr[2] 157 | tr[3] = sy * tr[3] 158 | } 159 | 160 | // Translate adds a translation to the matrix 161 | func (tr *Matrix) Translate(tx, ty float64) { 162 | tr[4] = tx*tr[0] + ty*tr[2] + tr[4] 163 | tr[5] = ty*tr[3] + tx*tr[1] + tr[5] 164 | } 165 | 166 | // Rotate adds a rotation to the matrix. angle is in radian 167 | func (tr *Matrix) Rotate(angle float64) { 168 | c := math.Cos(angle) 169 | s := math.Sin(angle) 170 | t0 := c*tr[0] + s*tr[2] 171 | t1 := s*tr[3] + c*tr[1] 172 | t2 := c*tr[2] - s*tr[0] 173 | t3 := c*tr[3] - s*tr[1] 174 | tr[0] = t0 175 | tr[1] = t1 176 | tr[2] = t2 177 | tr[3] = t3 178 | } 179 | 180 | // GetTranslation 181 | func (tr Matrix) GetTranslation() (x, y float64) { 182 | return tr[4], tr[5] 183 | } 184 | 185 | // GetScaling 186 | func (tr Matrix) GetScaling() (x, y float64) { 187 | return tr[0], tr[3] 188 | } 189 | 190 | // GetScale computes a scale for the matrix 191 | func (tr Matrix) GetScale() float64 { 192 | x := 0.707106781*tr[0] + 0.707106781*tr[1] 193 | y := 0.707106781*tr[2] + 0.707106781*tr[3] 194 | return math.Sqrt(x*x + y*y) 195 | } 196 | 197 | // ******************** Testing ******************** 198 | 199 | // Equals tests if a two transformation are equal. A tolerance is applied when comparing matrix elements. 200 | func (tr1 Matrix) Equals(tr2 Matrix) bool { 201 | for i := 0; i < 6; i = i + 1 { 202 | if !fequals(tr1[i], tr2[i]) { 203 | return false 204 | } 205 | } 206 | return true 207 | } 208 | 209 | // IsIdentity tests if a transformation is the identity transformation. A tolerance is applied when comparing matrix elements. 210 | func (tr Matrix) IsIdentity() bool { 211 | return fequals(tr[4], 0) && fequals(tr[5], 0) && tr.IsTranslation() 212 | } 213 | 214 | // IsTranslation tests if a transformation is is a pure translation. A tolerance is applied when comparing matrix elements. 215 | func (tr Matrix) IsTranslation() bool { 216 | return fequals(tr[0], 1) && fequals(tr[1], 0) && fequals(tr[2], 0) && fequals(tr[3], 1) 217 | } 218 | 219 | // fequals compares two floats. return true if the distance between the two floats is less than epsilon, false otherwise 220 | func fequals(float1, float2 float64) bool { 221 | return math.Abs(float1-float2) <= epsilon 222 | } 223 | -------------------------------------------------------------------------------- /output/README.md: -------------------------------------------------------------------------------- 1 | Demo output 2 | =========== 3 | 4 | These folders are empty when you check out the git repository. The output is generated by the tests: 5 | ``` 6 | go test ./... 7 | ``` 8 | or with coverage: 9 | ``` 10 | go test -cover ./... | grep -v "no test" 11 | ``` 12 | -------------------------------------------------------------------------------- /output/curve/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /output/draw2dkit/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /output/raster/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /output/samples/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | !geometry.png 6 | !postscript.png -------------------------------------------------------------------------------- /output/samples/geometry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/output/samples/geometry.png -------------------------------------------------------------------------------- /output/samples/postscript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/output/samples/postscript.png -------------------------------------------------------------------------------- /path.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 21/11/2010 by Laurent Le Goff 3 | 4 | package draw2d 5 | 6 | import ( 7 | "fmt" 8 | "math" 9 | ) 10 | 11 | // PathBuilder describes the interface for path drawing. 12 | type PathBuilder interface { 13 | // LastPoint returns the current point of the current sub path 14 | LastPoint() (x, y float64) 15 | // MoveTo creates a new subpath that start at the specified point 16 | MoveTo(x, y float64) 17 | // LineTo adds a line to the current subpath 18 | LineTo(x, y float64) 19 | // QuadCurveTo adds a quadratic Bézier curve to the current subpath 20 | QuadCurveTo(cx, cy, x, y float64) 21 | // CubicCurveTo adds a cubic Bézier curve to the current subpath 22 | CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) 23 | // ArcTo adds an arc to the current subpath 24 | ArcTo(cx, cy, rx, ry, startAngle, angle float64) 25 | // Close creates a line from the current point to the last MoveTo 26 | // point (if not the same) and mark the path as closed so the 27 | // first and last lines join nicely. 28 | Close() 29 | } 30 | 31 | // PathCmp represents component of a path 32 | type PathCmp int 33 | 34 | const ( 35 | // MoveToCmp is a MoveTo component in a Path 36 | MoveToCmp PathCmp = iota 37 | // LineToCmp is a LineTo component in a Path 38 | LineToCmp 39 | // QuadCurveToCmp is a QuadCurveTo component in a Path 40 | QuadCurveToCmp 41 | // CubicCurveToCmp is a CubicCurveTo component in a Path 42 | CubicCurveToCmp 43 | // ArcToCmp is a ArcTo component in a Path 44 | ArcToCmp 45 | // CloseCmp is a ArcTo component in a Path 46 | CloseCmp 47 | ) 48 | 49 | // Path stores points 50 | type Path struct { 51 | // Components is a slice of PathCmp in a Path and mark the role of each points in the Path 52 | Components []PathCmp 53 | // Points are combined with Components to have a specific role in the path 54 | Points []float64 55 | // Last Point of the Path 56 | x, y float64 57 | } 58 | 59 | func (p *Path) appendToPath(cmd PathCmp, points ...float64) { 60 | p.Components = append(p.Components, cmd) 61 | p.Points = append(p.Points, points...) 62 | } 63 | 64 | // LastPoint returns the current point of the current path 65 | func (p *Path) LastPoint() (x, y float64) { 66 | return p.x, p.y 67 | } 68 | 69 | // MoveTo starts a new path at (x, y) position 70 | func (p *Path) MoveTo(x, y float64) { 71 | p.appendToPath(MoveToCmp, x, y) 72 | p.x = x 73 | p.y = y 74 | } 75 | 76 | // LineTo adds a line to the current path 77 | func (p *Path) LineTo(x, y float64) { 78 | if len(p.Components) == 0 { //special case when no move has been done 79 | p.MoveTo(x, y) 80 | } else { 81 | p.appendToPath(LineToCmp, x, y) 82 | } 83 | p.x = x 84 | p.y = y 85 | } 86 | 87 | // QuadCurveTo adds a quadratic bezier curve to the current path 88 | func (p *Path) QuadCurveTo(cx, cy, x, y float64) { 89 | if len(p.Components) == 0 { //special case when no move has been done 90 | p.MoveTo(x, y) 91 | } else { 92 | p.appendToPath(QuadCurveToCmp, cx, cy, x, y) 93 | } 94 | p.x = x 95 | p.y = y 96 | } 97 | 98 | // CubicCurveTo adds a cubic bezier curve to the current path 99 | func (p *Path) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) { 100 | if len(p.Components) == 0 { //special case when no move has been done 101 | p.MoveTo(x, y) 102 | } else { 103 | p.appendToPath(CubicCurveToCmp, cx1, cy1, cx2, cy2, x, y) 104 | } 105 | p.x = x 106 | p.y = y 107 | } 108 | 109 | // ArcTo adds an arc to the path 110 | func (p *Path) ArcTo(cx, cy, rx, ry, startAngle, angle float64) { 111 | endAngle := startAngle + angle 112 | clockWise := true 113 | if angle < 0 { 114 | clockWise = false 115 | } 116 | // normalize 117 | if clockWise { 118 | for endAngle < startAngle { 119 | endAngle += math.Pi * 2.0 120 | } 121 | } else { 122 | for startAngle < endAngle { 123 | startAngle += math.Pi * 2.0 124 | } 125 | } 126 | startX := cx + math.Cos(startAngle)*rx 127 | startY := cy + math.Sin(startAngle)*ry 128 | if len(p.Components) > 0 { 129 | p.LineTo(startX, startY) 130 | } else { 131 | p.MoveTo(startX, startY) 132 | } 133 | p.appendToPath(ArcToCmp, cx, cy, rx, ry, startAngle, angle) 134 | p.x = cx + math.Cos(endAngle)*rx 135 | p.y = cy + math.Sin(endAngle)*ry 136 | } 137 | 138 | // Close closes the current path 139 | func (p *Path) Close() { 140 | p.appendToPath(CloseCmp) 141 | } 142 | 143 | // Copy make a clone of the current path and return it 144 | func (p *Path) Copy() (dest *Path) { 145 | dest = new(Path) 146 | dest.Components = make([]PathCmp, len(p.Components)) 147 | copy(dest.Components, p.Components) 148 | dest.Points = make([]float64, len(p.Points)) 149 | copy(dest.Points, p.Points) 150 | dest.x, dest.y = p.x, p.y 151 | return dest 152 | } 153 | 154 | // Clear reset the path 155 | func (p *Path) Clear() { 156 | p.Components = p.Components[0:0] 157 | p.Points = p.Points[0:0] 158 | return 159 | } 160 | 161 | // IsEmpty returns true if the path is empty 162 | func (p *Path) IsEmpty() bool { 163 | return len(p.Components) == 0 164 | } 165 | 166 | // String returns a debug text view of the path 167 | func (p *Path) String() string { 168 | s := "" 169 | j := 0 170 | for _, cmd := range p.Components { 171 | switch cmd { 172 | case MoveToCmp: 173 | s += fmt.Sprintf("MoveTo: %f, %f\n", p.Points[j], p.Points[j+1]) 174 | j = j + 2 175 | case LineToCmp: 176 | s += fmt.Sprintf("LineTo: %f, %f\n", p.Points[j], p.Points[j+1]) 177 | j = j + 2 178 | case QuadCurveToCmp: 179 | s += fmt.Sprintf("QuadCurveTo: %f, %f, %f, %f\n", p.Points[j], p.Points[j+1], p.Points[j+2], p.Points[j+3]) 180 | j = j + 4 181 | case CubicCurveToCmp: 182 | s += fmt.Sprintf("CubicCurveTo: %f, %f, %f, %f, %f, %f\n", p.Points[j], p.Points[j+1], p.Points[j+2], p.Points[j+3], p.Points[j+4], p.Points[j+5]) 183 | j = j + 6 184 | case ArcToCmp: 185 | s += fmt.Sprintf("ArcTo: %f, %f, %f, %f, %f, %f\n", p.Points[j], p.Points[j+1], p.Points[j+2], p.Points[j+3], p.Points[j+4], p.Points[j+5]) 186 | j = j + 6 187 | case CloseCmp: 188 | s += "Close\n" 189 | } 190 | } 191 | return s 192 | } 193 | 194 | // Returns new Path with flipped y axes 195 | func (path *Path) VerticalFlip() *Path { 196 | p := path.Copy() 197 | j := 0 198 | for _, cmd := range p.Components { 199 | switch cmd { 200 | case MoveToCmp, LineToCmp: 201 | p.Points[j+1] = -p.Points[j+1] 202 | j = j + 2 203 | case QuadCurveToCmp: 204 | p.Points[j+1] = -p.Points[j+1] 205 | p.Points[j+3] = -p.Points[j+3] 206 | j = j + 4 207 | case CubicCurveToCmp: 208 | p.Points[j+1] = -p.Points[j+1] 209 | p.Points[j+3] = -p.Points[j+3] 210 | p.Points[j+5] = -p.Points[j+5] 211 | j = j + 6 212 | case ArcToCmp: 213 | p.Points[j+1] = -p.Points[j+1] 214 | p.Points[j+3] = -p.Points[j+3] 215 | p.Points[j+4] = -p.Points[j+4] // start angle 216 | p.Points[j+5] = -p.Points[j+5] // angle 217 | j = j + 6 218 | case CloseCmp: 219 | } 220 | } 221 | p.y = -p.y 222 | return p 223 | } 224 | -------------------------------------------------------------------------------- /resource/font/COPYING: -------------------------------------------------------------------------------- 1 | Luxi fonts copyright (c) 2001 by Bigelow & Holmes Inc. Luxi font 2 | instruction code copyright (c) 2001 by URW++ GmbH. All Rights 3 | Reserved. Luxi is a registered trademark of Bigelow & Holmes Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of these Fonts and associated documentation files (the "Font 7 | Software"), to deal in the Font Software, including without 8 | limitation the rights to use, copy, merge, publish, distribute, 9 | sublicense, and/or sell copies of the Font Software, and to permit 10 | persons to whom the Font Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright and trademark notices and this permission notice 14 | shall be included in all copies of one or more of the Font Software. 15 | 16 | The Font Software may not be modified, altered, or added to, and in 17 | particular the designs of glyphs or characters in the Fonts may not 18 | be modified nor may additional glyphs or characters be added to the 19 | Fonts. This License becomes null and void when the Fonts or Font 20 | Software have been modified. 21 | 22 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 23 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 24 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 25 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL 26 | BIGELOW & HOLMES INC. OR URW++ GMBH. BE LIABLE FOR ANY CLAIM, DAMAGES 27 | OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, 28 | INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF 29 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR 30 | INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT 31 | SOFTWARE. 32 | 33 | Except as contained in this notice, the names of Bigelow & Holmes 34 | Inc. and URW++ GmbH. shall not be used in advertising or otherwise to 35 | promote the sale, use or other dealings in this Font Software without 36 | prior written authorization from Bigelow & Holmes Inc. and URW++ GmbH. 37 | 38 | For further information, contact: 39 | 40 | info@urwpp.de 41 | or 42 | design@bigelowandholmes.com 43 | -------------------------------------------------------------------------------- /resource/font/README: -------------------------------------------------------------------------------- 1 | 2 | All questions regarding this software should be directed at the 3 | Xorg mailing list: 4 | 5 | http://lists.freedesktop.org/mailman/listinfo/xorg 6 | 7 | Please submit bug reports to the Xorg bugzilla: 8 | 9 | https://bugs.freedesktop.org/enter_bug.cgi?product=xorg 10 | 11 | The master development code repository can be found at: 12 | 13 | git://anongit.freedesktop.org/git/xorg/font/bh-ttf 14 | 15 | http://cgit.freedesktop.org/xorg/font/bh-ttf 16 | 17 | For patch submission instructions, see: 18 | 19 | http://www.x.org/wiki/Development/Documentation/SubmittingPatches 20 | 21 | For more information on the git code manager, see: 22 | 23 | http://wiki.x.org/wiki/GitPage 24 | 25 | -------------------------------------------------------------------------------- /resource/font/luximb.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/font/luximb.ttf -------------------------------------------------------------------------------- /resource/font/luximbi.json: -------------------------------------------------------------------------------- 1 | {"Tp":"TrueType","Name":"LuxiMono-BoldOblique","Desc":{"Ascent":783,"Descent":-205,"CapHeight":783,"Flags":97,"FontBBox":{"Xmin":-29,"Ymin":-211,"Xmax":764,"Ymax":1012},"ItalicAngle":-8,"StemV":120,"MissingWidth":600},"Up":0,"Ut":0,"Cw":[600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,653,600,653,600,600,653,600,653,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600],"Enc":"cp1252","Diff":"","File":"luximbi.z","Size1":0,"Size2":0,"OriginalSize":69872,"I":0,"N":0,"DiffN":0} -------------------------------------------------------------------------------- /resource/font/luximbi.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/font/luximbi.ttf -------------------------------------------------------------------------------- /resource/font/luximbi.z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/font/luximbi.z -------------------------------------------------------------------------------- /resource/font/luximr.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/font/luximr.ttf -------------------------------------------------------------------------------- /resource/font/luximri.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/font/luximri.ttf -------------------------------------------------------------------------------- /resource/font/luxirb.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/font/luxirb.ttf -------------------------------------------------------------------------------- /resource/font/luxirbi.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/font/luxirbi.ttf -------------------------------------------------------------------------------- /resource/font/luxirr.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/font/luxirr.ttf -------------------------------------------------------------------------------- /resource/font/luxirri.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/font/luxirri.ttf -------------------------------------------------------------------------------- /resource/font/luxisb.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/font/luxisb.ttf -------------------------------------------------------------------------------- /resource/font/luxisbi.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/font/luxisbi.ttf -------------------------------------------------------------------------------- /resource/font/luxisr.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/font/luxisr.ttf -------------------------------------------------------------------------------- /resource/font/luxisri.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/font/luxisri.ttf -------------------------------------------------------------------------------- /resource/image/geometry.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/image/geometry.pdf -------------------------------------------------------------------------------- /resource/image/gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/image/gopher.png -------------------------------------------------------------------------------- /resource/image/postscript.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/image/postscript.pdf -------------------------------------------------------------------------------- /resource/result/TestAndroid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/result/TestAndroid.png -------------------------------------------------------------------------------- /resource/result/TestBigPicture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/result/TestBigPicture.png -------------------------------------------------------------------------------- /resource/result/TestBubble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/result/TestBubble.png -------------------------------------------------------------------------------- /resource/result/TestCurveRectangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/result/TestCurveRectangle.png -------------------------------------------------------------------------------- /resource/result/TestDash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/result/TestDash.png -------------------------------------------------------------------------------- /resource/result/TestDrawArc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/result/TestDrawArc.png -------------------------------------------------------------------------------- /resource/result/TestDrawArcNegative.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/result/TestDrawArcNegative.png -------------------------------------------------------------------------------- /resource/result/TestDrawCubicCurve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/result/TestDrawCubicCurve.png -------------------------------------------------------------------------------- /resource/result/TestDrawImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/result/TestDrawImage.png -------------------------------------------------------------------------------- /resource/result/TestFillString.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/result/TestFillString.png -------------------------------------------------------------------------------- /resource/result/TestFillStroke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/result/TestFillStroke.png -------------------------------------------------------------------------------- /resource/result/TestFillStyle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/result/TestFillStyle.png -------------------------------------------------------------------------------- /resource/result/TestGopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/result/TestGopher.png -------------------------------------------------------------------------------- /resource/result/TestLineCap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/result/TestLineCap.png -------------------------------------------------------------------------------- /resource/result/TestLineJoin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/result/TestLineJoin.png -------------------------------------------------------------------------------- /resource/result/TestMultiSegmentCaps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/result/TestMultiSegmentCaps.png -------------------------------------------------------------------------------- /resource/result/TestPath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/result/TestPath.png -------------------------------------------------------------------------------- /resource/result/TestPathTransform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/result/TestPathTransform.png -------------------------------------------------------------------------------- /resource/result/TestRoundRectangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/result/TestRoundRectangle.png -------------------------------------------------------------------------------- /resource/result/TestStar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/result/TestStar.png -------------------------------------------------------------------------------- /resource/result/TestTransform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llgcode/draw2d/0ed1ff131195ddb59f6c689414724fe25436d2fc/resource/result/TestTransform.png -------------------------------------------------------------------------------- /samples/README.md: -------------------------------------------------------------------------------- 1 | draw2d samples 2 | ============== 3 | 4 | Various samples for using draw2d 5 | 6 | Using the image backend 7 | ----------------------- 8 | 9 | The following Go code draws the android sample on a png image: 10 | 11 | ``` 12 | package main 13 | 14 | import "log" 15 | import "image" 16 | import "github.com/llgcode/draw2d/draw2dimg" 17 | import "github.com/llgcode/draw2d/samples/android" 18 | 19 | func main(){ 20 | // Initialize the graphic context on an RGBA image 21 | dest := image.NewRGBA(image.Rect(0, 0, 297, 210.0)) 22 | gc := draw2dimg.NewGraphicContext(dest) 23 | // Draw Android logo 24 | fn, err := android.Main(gc, "png") 25 | if err != nil { 26 | log.Printf("Drawing %q failed: %v", fn, err) 27 | return 28 | } 29 | // Save to png 30 | err = draw2dimg.SaveToPngFile(fn, dest) 31 | if err != nil { 32 | log.Printf("Saving %q failed: %v", fn, err) 33 | return 34 | } 35 | 36 | log.Printf("Succesfully created %q", fn) 37 | } 38 | ``` 39 | 40 | Using the pdf backend 41 | --------------------- 42 | 43 | The following Go code draws the android sample on a pdf document: 44 | 45 | ``` 46 | import ( 47 | "image" 48 | 49 | "github.com/llgcode/draw2d/draw2dpdf" 50 | "github.com/llgcode/draw2d/samples/android" 51 | ) 52 | 53 | function main(){} 54 | // Initialize the graphic context on a pdf document 55 | dest := draw2dpdf.NewPdf("L", "mm", "A4") 56 | gc := draw2dpdf.NewGraphicContext(dest) 57 | // Draw Android logo 58 | fn, err := android.Main(gc, "png") 59 | if err != nil { 60 | t.Errorf("Drawing %q failed: %v", fn, err) 61 | return 62 | } 63 | // Save to pdf 64 | err = draw2dpdf.SaveToPdfFile(fn, dest) 65 | if err != nil { 66 | t.Errorf("Saving %q failed: %v", fn, err) 67 | } 68 | } 69 | ``` 70 | 71 | Testing 72 | ------- 73 | 74 | These samples are run as tests from the root package folder `draw2d` by: 75 | ``` 76 | go test ./... 77 | ``` 78 | Or if you want to run with test coverage: 79 | ``` 80 | go test -cover ./... | grep -v "no test" 81 | ``` 82 | The following files are responsible to run the image tests: 83 | ``` 84 | draw2d/test_test.go 85 | draw2d/samples_test.go 86 | ``` 87 | The following files are responsible to run the pdf tests: 88 | ``` 89 | draw2d/pdf/test_test.go 90 | draw2dpdf/samples_test.go 91 | ``` 92 | -------------------------------------------------------------------------------- /samples/android/android.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 21/11/2010 by Laurent Le Goff 3 | 4 | // Package android draws an android avatar. 5 | package android 6 | 7 | import ( 8 | "image/color" 9 | "math" 10 | 11 | "github.com/llgcode/draw2d" 12 | "github.com/llgcode/draw2d/draw2dkit" 13 | "github.com/llgcode/draw2d/samples" 14 | ) 15 | 16 | // Main draws a droid and returns the filename. This should only be 17 | // used during testing. 18 | func Main(gc draw2d.GraphicContext, ext string) (string, error) { 19 | // Draw the droid 20 | Draw(gc, 65, 0) 21 | 22 | // Return the output filename 23 | return samples.Output("android", ext), nil 24 | } 25 | 26 | // Draw the droid on a certain position. 27 | func Draw(gc draw2d.GraphicContext, x, y float64) { 28 | // set the fill and stroke color of the droid 29 | gc.SetFillColor(color.RGBA{0x44, 0xff, 0x44, 0xff}) 30 | gc.SetStrokeColor(color.RGBA{0x44, 0x44, 0x44, 0xff}) 31 | 32 | // set line properties 33 | gc.SetLineCap(draw2d.RoundCap) 34 | gc.SetLineWidth(5) 35 | 36 | // head 37 | gc.MoveTo(x+30, y+70) 38 | gc.ArcTo(x+80, y+70, 50, 50, 180*(math.Pi/180), 180*(math.Pi/180)) 39 | gc.Close() 40 | gc.FillStroke() 41 | gc.MoveTo(x+60, y+25) 42 | gc.LineTo(x+50, y+10) 43 | gc.MoveTo(x+100, y+25) 44 | gc.LineTo(x+110, y+10) 45 | gc.Stroke() 46 | 47 | // left eye 48 | draw2dkit.Circle(gc, x+60, y+45, 5) 49 | gc.FillStroke() 50 | 51 | // right eye 52 | draw2dkit.Circle(gc, x+100, y+45, 5) 53 | gc.FillStroke() 54 | 55 | // body 56 | draw2dkit.RoundedRectangle(gc, x+30, y+75, x+30+100, y+75+90, 10, 10) 57 | gc.FillStroke() 58 | draw2dkit.Rectangle(gc, x+30, y+75, x+30+100, y+75+80) 59 | gc.FillStroke() 60 | 61 | // left arm 62 | draw2dkit.RoundedRectangle(gc, x+5, y+80, x+5+20, y+80+70, 10, 10) 63 | gc.FillStroke() 64 | 65 | // right arm 66 | draw2dkit.RoundedRectangle(gc, x+135, y+80, x+135+20, y+80+70, 10, 10) 67 | gc.FillStroke() 68 | 69 | // left leg 70 | draw2dkit.RoundedRectangle(gc, x+50, y+150, x+50+20, y+150+50, 10, 10) 71 | gc.FillStroke() 72 | 73 | // right leg 74 | draw2dkit.RoundedRectangle(gc, x+90, y+150, x+90+20, y+150+50, 10, 10) 75 | gc.FillStroke() 76 | } 77 | -------------------------------------------------------------------------------- /samples/appengine/app.yaml: -------------------------------------------------------------------------------- 1 | application: draw2d-test 2 | version: 1 3 | runtime: go 4 | api_version: go1 5 | 6 | handlers: 7 | - url: /.* 8 | script: _go_app -------------------------------------------------------------------------------- /samples/appengine/server.go: -------------------------------------------------------------------------------- 1 | // +build appengine 2 | 3 | // Package gae demonstrates draw2d on a Google appengine server. 4 | package gae 5 | 6 | import ( 7 | "fmt" 8 | "image" 9 | "image/png" 10 | "net/http" 11 | 12 | "github.com/llgcode/draw2d/draw2dimg" 13 | "github.com/llgcode/draw2d/draw2dpdf" 14 | "github.com/llgcode/draw2d/samples/android" 15 | 16 | "appengine" 17 | ) 18 | 19 | type appError struct { 20 | Error error 21 | Message string 22 | Code int 23 | } 24 | 25 | type appHandler func(http.ResponseWriter, *http.Request) *appError 26 | 27 | func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 28 | if e := fn(w, r); e != nil { // e is *appError, not os.Error. 29 | c := appengine.NewContext(r) 30 | c.Errorf("%v", e.Error) 31 | http.Error(w, e.Message, e.Code) 32 | } 33 | } 34 | 35 | func init() { 36 | http.Handle("/pdf", appHandler(pdf)) 37 | http.Handle("/png", appHandler(imgPng)) 38 | } 39 | 40 | func pdf(w http.ResponseWriter, r *http.Request) *appError { 41 | w.Header().Set("Content-type", "application/pdf") 42 | 43 | // Initialize the graphic context on an pdf document 44 | dest := draw2dpdf.NewPdf("L", "mm", "A4") 45 | gc := draw2dpdf.NewGraphicContext(dest) 46 | 47 | // Draw sample 48 | android.Draw(gc, 65, 0) 49 | 50 | err := dest.Output(w) 51 | if err != nil { 52 | return &appError{err, fmt.Sprintf("Can't write: %s", err), 500} 53 | } 54 | return nil 55 | } 56 | 57 | func imgPng(w http.ResponseWriter, r *http.Request) *appError { 58 | w.Header().Set("Content-type", "image/png") 59 | 60 | // Initialize the graphic context on an RGBA image 61 | dest := image.NewRGBA(image.Rect(0, 0, 297, 210.0)) 62 | gc := draw2dimg.NewGraphicContext(dest) 63 | 64 | // Draw sample 65 | android.Draw(gc, 65, 0) 66 | 67 | err := png.Encode(w, dest) 68 | if err != nil { 69 | return &appError{err, fmt.Sprintf("Can't encode: %s", err), 500} 70 | } 71 | 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /samples/frameimage/frameimage.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 21/11/2010 by Laurent Le Goff, Stani Michiels 3 | 4 | // Package frameimage centers a png image and rotates it. 5 | package frameimage 6 | 7 | import ( 8 | "math" 9 | 10 | "github.com/llgcode/draw2d" 11 | "github.com/llgcode/draw2d/draw2dimg" 12 | "github.com/llgcode/draw2d/draw2dkit" 13 | "github.com/llgcode/draw2d/samples" 14 | ) 15 | 16 | // Main draws the image frame and returns the filename. 17 | // This should only be used during testing. 18 | func Main(gc draw2d.GraphicContext, ext string) (string, error) { 19 | // Margin between the image and the frame 20 | const margin = 30 21 | // Line width od the frame 22 | const lineWidth = 3 23 | 24 | // Gopher image 25 | gopher := samples.Resource("image", "gopher.png", ext) 26 | 27 | // Draw gopher 28 | err := Draw(gc, gopher, 297, 210, margin, lineWidth) 29 | 30 | // Return the output filename 31 | return samples.Output("frameimage", ext), err 32 | } 33 | 34 | // Draw the image frame with certain parameters. 35 | func Draw(gc draw2d.GraphicContext, png string, 36 | dw, dh, margin, lineWidth float64) error { 37 | // Draw frame 38 | draw2dkit.RoundedRectangle(gc, lineWidth, lineWidth, dw-lineWidth, dh-lineWidth, 100, 100) 39 | gc.SetLineWidth(lineWidth) 40 | gc.FillStroke() 41 | 42 | // load the source image 43 | source, err := draw2dimg.LoadFromPngFile(png) 44 | if err != nil { 45 | return err 46 | } 47 | // Size of source image 48 | sw, sh := float64(source.Bounds().Dx()), float64(source.Bounds().Dy()) 49 | // Draw image to fit in the frame 50 | // TODO Seems to have a transform bug here on draw image 51 | scale := math.Min((dw-margin*2)/sw, (dh-margin*2)/sh) 52 | gc.Save() 53 | gc.Translate((dw-sw*scale)/2, (dh-sh*scale)/2) 54 | gc.Scale(scale, scale) 55 | gc.Rotate(0.2) 56 | 57 | gc.DrawImage(source) 58 | gc.Restore() 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /samples/geometry/geometry.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 21/11/2010 by Laurent Le Goff 3 | 4 | // Package geometry draws some geometric tests. 5 | package geometry 6 | 7 | import ( 8 | "image" 9 | "image/color" 10 | "math" 11 | 12 | "github.com/llgcode/draw2d" 13 | "github.com/llgcode/draw2d/draw2dkit" 14 | "github.com/llgcode/draw2d/samples" 15 | "github.com/llgcode/draw2d/samples/gopher2" 16 | ) 17 | 18 | // Main draws geometry and returns the filename. This should only be 19 | // used during testing. 20 | func Main(gc draw2d.GraphicContext, ext string) (string, error) { 21 | // Draw the droid 22 | Draw(gc, 297, 210) 23 | 24 | // Return the output filename 25 | return samples.Output("geometry", ext), nil 26 | } 27 | 28 | // Bubble draws a text balloon. 29 | func Bubble(gc draw2d.GraphicContext, x, y, width, height float64) { 30 | sx, sy := width/100, height/100 31 | gc.MoveTo(x+sx*50, y) 32 | gc.QuadCurveTo(x, y, x, y+sy*37.5) 33 | gc.QuadCurveTo(x, y+sy*75, x+sx*25, y+sy*75) 34 | gc.QuadCurveTo(x+sx*25, y+sy*95, x+sx*5, y+sy*100) 35 | gc.QuadCurveTo(x+sx*35, y+sy*95, x+sx*40, y+sy*75) 36 | gc.QuadCurveTo(x+sx*100, y+sy*75, x+sx*100, y+sy*37.5) 37 | gc.QuadCurveTo(x+sx*100, y, x+sx*50, y) 38 | gc.Stroke() 39 | } 40 | 41 | // CurveRectangle draws a rectangle with bezier curves (not rounded rectangle). 42 | func CurveRectangle(gc draw2d.GraphicContext, x0, y0, 43 | rectWidth, rectHeight float64, stroke, fill color.Color) { 44 | radius := (rectWidth + rectHeight) / 4 45 | 46 | x1 := x0 + rectWidth 47 | y1 := y0 + rectHeight 48 | if rectWidth/2 < radius { 49 | if rectHeight/2 < radius { 50 | gc.MoveTo(x0, (y0+y1)/2) 51 | gc.CubicCurveTo(x0, y0, x0, y0, (x0+x1)/2, y0) 52 | gc.CubicCurveTo(x1, y0, x1, y0, x1, (y0+y1)/2) 53 | gc.CubicCurveTo(x1, y1, x1, y1, (x1+x0)/2, y1) 54 | gc.CubicCurveTo(x0, y1, x0, y1, x0, (y0+y1)/2) 55 | } else { 56 | gc.MoveTo(x0, y0+radius) 57 | gc.CubicCurveTo(x0, y0, x0, y0, (x0+x1)/2, y0) 58 | gc.CubicCurveTo(x1, y0, x1, y0, x1, y0+radius) 59 | gc.LineTo(x1, y1-radius) 60 | gc.CubicCurveTo(x1, y1, x1, y1, (x1+x0)/2, y1) 61 | gc.CubicCurveTo(x0, y1, x0, y1, x0, y1-radius) 62 | } 63 | } else { 64 | if rectHeight/2 < radius { 65 | gc.MoveTo(x0, (y0+y1)/2) 66 | gc.CubicCurveTo(x0, y0, x0, y0, x0+radius, y0) 67 | gc.LineTo(x1-radius, y0) 68 | gc.CubicCurveTo(x1, y0, x1, y0, x1, (y0+y1)/2) 69 | gc.CubicCurveTo(x1, y1, x1, y1, x1-radius, y1) 70 | gc.LineTo(x0+radius, y1) 71 | gc.CubicCurveTo(x0, y1, x0, y1, x0, (y0+y1)/2) 72 | } else { 73 | gc.MoveTo(x0, y0+radius) 74 | gc.CubicCurveTo(x0, y0, x0, y0, x0+radius, y0) 75 | gc.LineTo(x1-radius, y0) 76 | gc.CubicCurveTo(x1, y0, x1, y0, x1, y0+radius) 77 | gc.LineTo(x1, y1-radius) 78 | gc.CubicCurveTo(x1, y1, x1, y1, x1-radius, y1) 79 | gc.LineTo(x0+radius, y1) 80 | gc.CubicCurveTo(x0, y1, x0, y1, x0, y1-radius) 81 | } 82 | } 83 | gc.Close() 84 | gc.SetStrokeColor(stroke) 85 | gc.SetFillColor(fill) 86 | gc.SetLineWidth(10.0) 87 | gc.FillStroke() 88 | } 89 | 90 | // Dash draws a line with a dash pattern 91 | func Dash(gc draw2d.GraphicContext, x, y, width, height float64) { 92 | sx, sy := width/162, height/205 93 | gc.SetStrokeColor(image.Black) 94 | gc.SetLineDash([]float64{height / 10, height / 50, height / 50, height / 50}, -50.0) 95 | gc.SetLineCap(draw2d.ButtCap) 96 | gc.SetLineJoin(draw2d.RoundJoin) 97 | gc.SetLineWidth(height / 50) 98 | 99 | gc.MoveTo(x+sx*60.0, y) 100 | gc.LineTo(x+sx*60.0, y) 101 | gc.LineTo(x+sx*162, y+sy*205) 102 | rLineTo(gc, sx*-102.4, 0) 103 | gc.CubicCurveTo(x+sx*-17, y+sy*205, x+sx*-17, y+sy*103, x+sx*60.0, y+sy*103.0) 104 | gc.Stroke() 105 | gc.SetLineDash(nil, 0.0) 106 | } 107 | 108 | // Arc draws an arc with a positive angle (clockwise) 109 | func Arc(gc draw2d.GraphicContext, xc, yc, width, height float64) { 110 | // draw an arc 111 | xc += width / 2 112 | yc += height / 2 113 | radiusX, radiusY := width/2, height/2 114 | startAngle := 45 * (math.Pi / 180.0) /* angles are specified */ 115 | angle := 135 * (math.Pi / 180.0) /* clockwise in radians */ 116 | gc.SetLineWidth(width / 10) 117 | gc.SetLineCap(draw2d.ButtCap) 118 | gc.SetStrokeColor(image.Black) 119 | gc.MoveTo(xc+math.Cos(startAngle)*radiusX, yc+math.Sin(startAngle)*radiusY) 120 | gc.ArcTo(xc, yc, radiusX, radiusY, startAngle, angle) 121 | gc.Stroke() 122 | 123 | // fill a circle 124 | gc.SetStrokeColor(color.NRGBA{255, 0x33, 0x33, 0x80}) 125 | gc.SetFillColor(color.NRGBA{255, 0x33, 0x33, 0x80}) 126 | gc.SetLineWidth(width / 20) 127 | 128 | gc.MoveTo(xc+math.Cos(startAngle)*radiusX, yc+math.Sin(startAngle)*radiusY) 129 | gc.LineTo(xc, yc) 130 | gc.LineTo(xc-radiusX, yc) 131 | gc.Stroke() 132 | 133 | gc.MoveTo(xc, yc) 134 | gc.ArcTo(xc, yc, width/10.0, height/10.0, 0, 2*math.Pi) 135 | gc.Fill() 136 | } 137 | 138 | // ArcNegative draws an arc with a negative angle (anti clockwise). 139 | func ArcNegative(gc draw2d.GraphicContext, xc, yc, width, height float64) { 140 | xc += width / 2 141 | yc += height / 2 142 | radiusX, radiusY := width/2, height/2 143 | startAngle := 45.0 * (math.Pi / 180.0) /* angles are specified */ 144 | angle := -225 * (math.Pi / 180.0) /* clockwise in radians */ 145 | gc.SetLineWidth(width / 10) 146 | gc.SetLineCap(draw2d.ButtCap) 147 | gc.SetStrokeColor(image.Black) 148 | 149 | gc.ArcTo(xc, yc, radiusX, radiusY, startAngle, angle) 150 | gc.Stroke() 151 | // fill a circle 152 | gc.SetStrokeColor(color.NRGBA{255, 0x33, 0x33, 0x80}) 153 | gc.SetFillColor(color.NRGBA{255, 0x33, 0x33, 0x80}) 154 | gc.SetLineWidth(width / 20) 155 | 156 | gc.MoveTo(xc+math.Cos(startAngle)*radiusX, yc+math.Sin(startAngle)*radiusY) 157 | gc.LineTo(xc, yc) 158 | gc.LineTo(xc-radiusX, yc) 159 | gc.Stroke() 160 | 161 | gc.ArcTo(xc, yc, width/10.0, height/10.0, 0, 2*math.Pi) 162 | gc.Fill() 163 | } 164 | 165 | // CubicCurve draws a cubic curve with its control points. 166 | func CubicCurve(gc draw2d.GraphicContext, x, y, width, height float64) { 167 | sx, sy := width/162, height/205 168 | x0, y0 := x, y+sy*100.0 169 | x1, y1 := x+sx*75, y+sy*205 170 | x2, y2 := x+sx*125, y 171 | x3, y3 := x+sx*205, y+sy*100 172 | 173 | gc.SetStrokeColor(image.Black) 174 | gc.SetFillColor(color.NRGBA{0xAA, 0xAA, 0xAA, 0xFF}) 175 | gc.SetLineWidth(width / 10) 176 | gc.MoveTo(x0, y0) 177 | gc.CubicCurveTo(x1, y1, x2, y2, x3, y3) 178 | gc.Stroke() 179 | 180 | gc.SetStrokeColor(color.NRGBA{0xFF, 0x33, 0x33, 0x88}) 181 | 182 | gc.SetLineWidth(width / 20) 183 | // draw segment of curve 184 | gc.MoveTo(x0, y0) 185 | gc.LineTo(x1, y1) 186 | gc.LineTo(x2, y2) 187 | gc.LineTo(x3, y3) 188 | gc.Stroke() 189 | } 190 | 191 | // FillString draws a filled and stroked string. 192 | // And filles/stroked path created from string. Which may have different - unselectable - output in non raster gc implementations. 193 | func FillString(gc draw2d.GraphicContext, x, y, width, height float64) { 194 | sx, sy := width/100, height/100 195 | gc.Save() 196 | gc.SetStrokeColor(image.Black) 197 | gc.SetLineWidth(1) 198 | draw2dkit.RoundedRectangle(gc, x+sx*5, y+sy*5, x+sx*95, y+sy*95, sx*10, sy*10) 199 | gc.FillStroke() 200 | gc.SetFillColor(image.Black) 201 | gc.SetFontSize(height / 6) 202 | gc.Translate(x+sx*6, y+sy*52) 203 | gc.SetFontData(draw2d.FontData{ 204 | Name: "luxi", 205 | Family: draw2d.FontFamilyMono, 206 | Style: draw2d.FontStyleBold | draw2d.FontStyleItalic, 207 | }) 208 | w := gc.FillString("Hug") 209 | gc.Translate(w+sx, 0) 210 | left, top, right, bottom := gc.GetStringBounds("cou") 211 | gc.SetStrokeColor(color.NRGBA{255, 0x33, 0x33, 0x80}) 212 | draw2dkit.Rectangle(gc, left, top, right, bottom) 213 | gc.SetLineWidth(height / 50) 214 | gc.Stroke() 215 | gc.SetFillColor(color.NRGBA{0x33, 0x33, 0xff, 0xff}) 216 | gc.SetStrokeColor(color.NRGBA{0x33, 0x33, 0xff, 0xff}) 217 | gc.SetLineWidth(height / 100) 218 | gc.StrokeString("Hug") 219 | 220 | gc.Translate(-(w + sx), sy*24) 221 | w = gc.CreateStringPath("Hug", 0, 0) 222 | gc.Fill() 223 | gc.Translate(w+sx, 0) 224 | gc.CreateStringPath("Hug", 0, 0) 225 | path := gc.GetPath() 226 | gc.Stroke((&path).VerticalFlip()) 227 | gc.Restore() 228 | } 229 | 230 | // FillStroke first fills and afterwards strokes a path. 231 | func FillStroke(gc draw2d.GraphicContext, x, y, width, height float64) { 232 | sx, sy := width/210, height/215 233 | gc.MoveTo(x+sx*113.0, y) 234 | gc.LineTo(x+sx*215.0, y+sy*215) 235 | rLineTo(gc, sx*-100, 0) 236 | gc.CubicCurveTo(x+sx*35, y+sy*215, x+sx*35, y+sy*113, x+sx*113.0, y+sy*113) 237 | gc.Close() 238 | 239 | gc.MoveTo(x+sx*50.0, y) 240 | rLineTo(gc, sx*51.2, sy*51.2) 241 | rLineTo(gc, sx*-51.2, sy*51.2) 242 | rLineTo(gc, sx*-51.2, sy*-51.2) 243 | gc.Close() 244 | 245 | gc.SetLineWidth(width / 20.0) 246 | gc.SetFillColor(color.NRGBA{0, 0, 0xFF, 0xFF}) 247 | gc.SetStrokeColor(image.Black) 248 | gc.FillStroke() 249 | } 250 | 251 | func rLineTo(path draw2d.PathBuilder, x, y float64) { 252 | x0, y0 := path.LastPoint() 253 | path.LineTo(x0+x, y0+y) 254 | } 255 | 256 | // FillStyle demonstrates the difference between even odd and non zero winding rule. 257 | func FillStyle(gc draw2d.GraphicContext, x, y, width, height float64) { 258 | sx, sy := width/232, height/220 259 | gc.SetLineWidth(width / 40) 260 | 261 | draw2dkit.Rectangle(gc, x+sx*0, y+sy*12, x+sx*232, y+sy*70) 262 | 263 | var wheel1, wheel2 draw2d.Path 264 | wheel1.ArcTo(x+sx*52, y+sy*70, sx*40, sy*40, 0, 2*math.Pi) 265 | wheel2.ArcTo(x+sx*180, y+sy*70, sx*40, sy*40, 0, -2*math.Pi) 266 | 267 | gc.SetFillRule(draw2d.FillRuleEvenOdd) 268 | gc.SetFillColor(color.NRGBA{0, 0xB2, 0, 0xFF}) 269 | 270 | gc.SetStrokeColor(image.Black) 271 | gc.FillStroke(&wheel1, &wheel2) 272 | 273 | draw2dkit.Rectangle(gc, x, y+sy*140, x+sx*232, y+sy*198) 274 | wheel1.Clear() 275 | wheel1.ArcTo(x+sx*52, y+sy*198, sx*40, sy*40, 0, 2*math.Pi) 276 | wheel2.Clear() 277 | wheel2.ArcTo(x+sx*180, y+sy*198, sx*40, sy*40, 0, -2*math.Pi) 278 | 279 | gc.SetFillRule(draw2d.FillRuleWinding) 280 | gc.SetFillColor(color.NRGBA{0, 0, 0xE5, 0xFF}) 281 | gc.FillStroke(&wheel1, &wheel2) 282 | } 283 | 284 | // PathTransform scales a path differently in horizontal and vertical direction. 285 | func PathTransform(gc draw2d.GraphicContext, x, y, width, height float64) { 286 | gc.Save() 287 | gc.SetLineWidth(width / 10) 288 | gc.Translate(x+width/2, y+height/2) 289 | gc.Scale(1, 4) 290 | gc.ArcTo(0, 0, width/8, height/8, 0, math.Pi*2) 291 | gc.Close() 292 | gc.Stroke() 293 | gc.Restore() 294 | } 295 | 296 | // Star draws many lines from a center. 297 | func Star(gc draw2d.GraphicContext, x, y, width, height float64) { 298 | gc.Save() 299 | gc.Translate(x+width/2, y+height/2) 300 | gc.SetLineWidth(width / 40) 301 | for i := 0.0; i < 360; i = i + 10 { // Go from 0 to 360 degrees in 10 degree steps 302 | gc.Save() // Keep rotations temporary 303 | gc.Rotate(i * (math.Pi / 180.0)) // Rotate by degrees on stack from 'for' 304 | gc.MoveTo(0, 0) 305 | gc.LineTo(width/2, 0) 306 | gc.Stroke() 307 | gc.Restore() 308 | } 309 | gc.Restore() 310 | } 311 | 312 | // Draw all figures in a nice 4x3 grid. 313 | func Draw(gc draw2d.GraphicContext, width, height float64) { 314 | mx, my := width*0.025, height*0.025 // margin 315 | dx, dy := (width-2*mx)/4, (height-2*my)/3 316 | w, h := dx-2*mx, dy-2*my 317 | x0, y := 2*mx, 2*my 318 | x := x0 319 | Bubble(gc, x, y, w, h) 320 | x += dx 321 | CurveRectangle(gc, x, y, w, h, color.NRGBA{0x80, 0, 0, 0x80}, color.NRGBA{0x80, 0x80, 0xFF, 0xFF}) 322 | x += dx 323 | Dash(gc, x, y, w, h) 324 | x += dx 325 | Arc(gc, x, y, w, h) 326 | x = x0 327 | y += dy 328 | ArcNegative(gc, x, y, w, h) 329 | x += dx 330 | CubicCurve(gc, x, y, w, h) 331 | x += dx 332 | FillString(gc, x, y, w, h) 333 | x += dx 334 | FillStroke(gc, x, y, w, h) 335 | x = x0 336 | y += dy 337 | FillStyle(gc, x, y, w, h) 338 | x += dx 339 | PathTransform(gc, x, y, w, h) 340 | x += dx 341 | Star(gc, x, y, w, h) 342 | x += dx 343 | gopher2.Draw(gc, x, y, w, h/2) 344 | } 345 | -------------------------------------------------------------------------------- /samples/gopher/gopher.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 21/11/2010 by Laurent Le Goff 3 | 4 | // Package gopher draws a gopher avatar based on a svg of: 5 | // https://github.com/golang-samples/gopher-vector/ 6 | package gopher 7 | 8 | import ( 9 | "image/color" 10 | 11 | "github.com/llgcode/draw2d" 12 | "github.com/llgcode/draw2d/samples" 13 | ) 14 | 15 | // Main draws a left hand and ear of a gopher. Afterwards it returns 16 | // the filename. This should only be used during testing. 17 | func Main(gc draw2d.GraphicContext, ext string) (string, error) { 18 | gc.Save() 19 | gc.Scale(0.5, 0.5) 20 | // Draw a (partial) gopher 21 | Draw(gc) 22 | gc.Restore() 23 | 24 | // Return the output filename 25 | return samples.Output("gopher", ext), nil 26 | } 27 | 28 | // Draw a left hand and ear of a gopher using a gc thanks to 29 | // https://github.com/golang-samples/gopher-vector/ 30 | func Draw(gc draw2d.GraphicContext) { 31 | // Initialize Stroke Attribute 32 | gc.SetLineWidth(3) 33 | gc.SetLineCap(draw2d.RoundCap) 34 | gc.SetStrokeColor(color.Black) 35 | 36 | // Left hand 37 | // 40 | gc.SetFillColor(color.RGBA{0xF6, 0xD2, 0xA2, 0xff}) 41 | gc.MoveTo(10.634, 300.493) 42 | rCubicCurveTo(gc, 0.764, 15.751, 16.499, 8.463, 23.626, 3.539) 43 | rCubicCurveTo(gc, 6.765, -4.675, 8.743, -0.789, 9.337, -10.015) 44 | rCubicCurveTo(gc, 0.389, -6.064, 1.088, -12.128, 0.744, -18.216) 45 | rCubicCurveTo(gc, -10.23, -0.927, -21.357, 1.509, -29.744, 7.602) 46 | gc.CubicCurveTo(10.277, 286.542, 2.177, 296.561, 10.634, 300.493) 47 | gc.FillStroke() 48 | 49 | // 51 | gc.MoveTo(10.634, 300.493) 52 | rCubicCurveTo(gc, 2.29, -0.852, 4.717, -1.457, 6.271, -3.528) 53 | gc.Stroke() 54 | 55 | // Left Ear 56 | // 58 | gc.MoveTo(46.997, 112.853) 59 | gc.CubicCurveTo(-13.3, 95.897, 31.536, 19.189, 79.956, 50.74) 60 | gc.LineTo(46.997, 112.853) 61 | gc.Close() 62 | gc.Stroke() 63 | } 64 | 65 | func rQuadCurveTo(path draw2d.PathBuilder, dcx, dcy, dx, dy float64) { 66 | x, y := path.LastPoint() 67 | path.QuadCurveTo(x+dcx, y+dcy, x+dx, y+dy) 68 | } 69 | 70 | func rCubicCurveTo(path draw2d.PathBuilder, dcx1, dcy1, dcx2, dcy2, dx, dy float64) { 71 | x, y := path.LastPoint() 72 | path.CubicCurveTo(x+dcx1, y+dcy1, x+dcx2, y+dcy2, x+dx, y+dy) 73 | } 74 | -------------------------------------------------------------------------------- /samples/gopher2/gopher2.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 21/11/2010 by Laurent Le Goff 3 | 4 | // Package gopher2 draws a gopher avatar based on a svg of: 5 | // https://github.com/golang-samples/gopher-vector/ 6 | package gopher2 7 | 8 | import ( 9 | "image" 10 | "image/color" 11 | "math" 12 | 13 | "github.com/llgcode/draw2d" 14 | "github.com/llgcode/draw2d/draw2dkit" 15 | "github.com/llgcode/draw2d/samples" 16 | ) 17 | 18 | // Main draws a rotated face of the gopher. Afterwards it returns 19 | // the filename. This should only be used during testing. 20 | func Main(gc draw2d.GraphicContext, ext string) (string, error) { 21 | gc.SetStrokeColor(image.Black) 22 | gc.SetFillColor(image.White) 23 | gc.Save() 24 | // Draw a (partial) gopher 25 | gc.Translate(-60, 65) 26 | gc.Rotate(-30 * (math.Pi / 180.0)) 27 | Draw(gc, 48, 48, 240, 72) 28 | gc.Restore() 29 | 30 | // Return the output filename 31 | return samples.Output("gopher2", ext), nil 32 | } 33 | 34 | // Draw a gopher head (not rotated) 35 | func Draw(gc draw2d.GraphicContext, x, y, w, h float64) { 36 | h23 := (h * 2) / 3 37 | 38 | blf := color.RGBA{0, 0, 0, 0xff} // black 39 | wf := color.RGBA{0xff, 0xff, 0xff, 0xff} // white 40 | nf := color.RGBA{0x8B, 0x45, 0x13, 0xff} // brown opaque 41 | brf := color.RGBA{0x8B, 0x45, 0x13, 0x99} // brown transparant 42 | brb := color.RGBA{0x8B, 0x45, 0x13, 0xBB} // brown transparant 43 | 44 | // round head top 45 | gc.MoveTo(x, y+h*1.002) 46 | gc.CubicCurveTo(x+w/4, y-h/3, x+3*w/4, y-h/3, x+w, y+h*1.002) 47 | gc.Close() 48 | gc.SetFillColor(brb) 49 | gc.Fill() 50 | 51 | // rectangle head bottom 52 | draw2dkit.RoundedRectangle(gc, x, y+h, x+w, y+h+h, h/5, h/5) 53 | gc.Fill() 54 | 55 | // left ear outside 56 | draw2dkit.Circle(gc, x, y+h, w/12) 57 | gc.SetFillColor(brf) 58 | gc.Fill() 59 | 60 | // left ear inside 61 | draw2dkit.Circle(gc, x, y+h, 0.5*w/12) 62 | gc.SetFillColor(nf) 63 | gc.Fill() 64 | 65 | // right ear outside 66 | draw2dkit.Circle(gc, x+w, y+h, w/12) 67 | gc.SetFillColor(brf) 68 | gc.Fill() 69 | 70 | // right ear inside 71 | draw2dkit.Circle(gc, x+w, y+h, 0.5*w/12) 72 | gc.SetFillColor(nf) 73 | gc.Fill() 74 | 75 | // left eye outside white 76 | draw2dkit.Circle(gc, x+w/3, y+h23, w/9) 77 | gc.SetFillColor(wf) 78 | gc.Fill() 79 | 80 | // left eye black 81 | draw2dkit.Circle(gc, x+w/3+w/24, y+h23, 0.5*w/9) 82 | gc.SetFillColor(blf) 83 | gc.Fill() 84 | 85 | // left eye inside white 86 | draw2dkit.Circle(gc, x+w/3+w/24+w/48, y+h23, 0.2*w/9) 87 | gc.SetFillColor(wf) 88 | gc.Fill() 89 | 90 | // right eye outside white 91 | draw2dkit.Circle(gc, x+w-w/3, y+h23, w/9) 92 | gc.Fill() 93 | 94 | // right eye black 95 | draw2dkit.Circle(gc, x+w-w/3+w/24, y+h23, 0.5*w/9) 96 | gc.SetFillColor(blf) 97 | gc.Fill() 98 | 99 | // right eye inside white 100 | draw2dkit.Circle(gc, x+w-(w/3)+w/24+w/48, y+h23, 0.2*w/9) 101 | gc.SetFillColor(wf) 102 | gc.Fill() 103 | 104 | // left tooth 105 | gc.SetFillColor(wf) 106 | draw2dkit.RoundedRectangle(gc, x+w/2-w/8, y+h+h/2.5, x+w/2-w/8+w/8, y+h+h/2.5+w/6, w/10, w/10) 107 | gc.Fill() 108 | 109 | // right tooth 110 | draw2dkit.RoundedRectangle(gc, x+w/2, y+h+h/2.5, x+w/2+w/8, y+h+h/2.5+w/6, w/10, w/10) 111 | gc.Fill() 112 | 113 | // snout 114 | draw2dkit.Ellipse(gc, x+(w/2), y+h+h/2.5, w/6, w/12) 115 | gc.SetFillColor(nf) 116 | gc.Fill() 117 | 118 | // nose 119 | draw2dkit.Ellipse(gc, x+(w/2), y+h+h/7, w/10, w/12) 120 | gc.SetFillColor(blf) 121 | gc.Fill() 122 | } 123 | -------------------------------------------------------------------------------- /samples/helloworld/helloworld.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 21/11/2010 by Laurent Le Goff, Stani Michiels 3 | 4 | // Package helloworld displays multiple "Hello World" (one rotated) 5 | // in a rounded rectangle. 6 | package helloworld 7 | 8 | import ( 9 | "fmt" 10 | "image" 11 | 12 | "github.com/llgcode/draw2d" 13 | "github.com/llgcode/draw2d/draw2dkit" 14 | "github.com/llgcode/draw2d/samples" 15 | ) 16 | 17 | // Main draws "Hello World" and returns the filename. This should only be 18 | // used during testing. 19 | func Main(gc draw2d.GraphicContext, ext string) (string, error) { 20 | // Draw hello world 21 | Draw(gc, fmt.Sprintf("Hello World %d dpi", gc.GetDPI())) 22 | 23 | // Return the output filename 24 | return samples.Output("helloworld", ext), nil 25 | } 26 | 27 | // Draw "Hello World" 28 | func Draw(gc draw2d.GraphicContext, text string) { 29 | // Draw a rounded rectangle using default colors 30 | draw2dkit.RoundedRectangle(gc, 5, 5, 135, 95, 10, 10) 31 | gc.FillStroke() 32 | 33 | // Set the font luximbi.ttf 34 | gc.SetFontData(draw2d.FontData{Name: "luxi", Family: draw2d.FontFamilyMono, Style: draw2d.FontStyleBold | draw2d.FontStyleItalic}) 35 | // Set the fill text color to black 36 | gc.SetFillColor(image.Black) 37 | gc.SetFontSize(14) 38 | // Display Hello World 39 | gc.FillStringAt("Hello World", 8, 52) 40 | } 41 | -------------------------------------------------------------------------------- /samples/helloworldgl/helloworldgl.go: -------------------------------------------------------------------------------- 1 | // Open an OpenGl window and display a rectangle using a OpenGl GraphicContext 2 | package main 3 | 4 | import ( 5 | "image/color" 6 | "log" 7 | "runtime" 8 | 9 | "github.com/go-gl/gl/v2.1/gl" 10 | "github.com/go-gl/glfw/v3.1/glfw" 11 | "github.com/llgcode/draw2d" 12 | "github.com/llgcode/draw2d/draw2dgl" 13 | "github.com/llgcode/draw2d/draw2dkit" 14 | ) 15 | 16 | var ( 17 | // global rotation 18 | rotate int 19 | width, height int 20 | redraw = true 21 | font draw2d.FontData 22 | ) 23 | 24 | func reshape(window *glfw.Window, w, h int) { 25 | gl.ClearColor(1, 1, 1, 1) 26 | /* Establish viewing area to cover entire window. */ 27 | gl.Viewport(0, 0, int32(w), int32(h)) 28 | /* PROJECTION Matrix mode. */ 29 | gl.MatrixMode(gl.PROJECTION) 30 | /* Reset project matrix. */ 31 | gl.LoadIdentity() 32 | /* Map abstract coords directly to window coords. */ 33 | gl.Ortho(0, float64(w), 0, float64(h), -1, 1) 34 | /* Invert Y axis so increasing Y goes down. */ 35 | gl.Scalef(1, -1, 1) 36 | /* Shift origin up to upper-left corner. */ 37 | gl.Translatef(0, float32(-h), 0) 38 | gl.Enable(gl.BLEND) 39 | gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) 40 | gl.Disable(gl.DEPTH_TEST) 41 | width, height = w, h 42 | redraw = true 43 | } 44 | 45 | // Ask to refresh 46 | func invalidate() { 47 | redraw = true 48 | } 49 | 50 | func display() { 51 | gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) 52 | 53 | gl.LineWidth(1) 54 | gc := draw2dgl.NewGraphicContext(width, height) 55 | gc.SetFontData(draw2d.FontData{ 56 | Name: "luxi", 57 | Family: draw2d.FontFamilyMono, 58 | Style: draw2d.FontStyleBold | draw2d.FontStyleItalic}) 59 | 60 | gc.BeginPath() 61 | draw2dkit.RoundedRectangle(gc, 200, 200, 600, 600, 100, 100) 62 | 63 | gc.SetFillColor(color.RGBA{0, 0, 0, 0xff}) 64 | gc.Fill() 65 | 66 | gl.Flush() /* Single buffered, so needs a flush. */ 67 | } 68 | 69 | func init() { 70 | runtime.LockOSThread() 71 | } 72 | 73 | func main() { 74 | err := glfw.Init() 75 | if err != nil { 76 | panic(err) 77 | } 78 | defer glfw.Terminate() 79 | width, height = 800, 800 80 | window, err := glfw.CreateWindow(width, height, "Show RoundedRect", nil, nil) 81 | if err != nil { 82 | panic(err) 83 | } 84 | 85 | window.MakeContextCurrent() 86 | window.SetSizeCallback(reshape) 87 | window.SetKeyCallback(onKey) 88 | window.SetCharCallback(onChar) 89 | 90 | glfw.SwapInterval(1) 91 | 92 | err = gl.Init() 93 | if err != nil { 94 | panic(err) 95 | } 96 | 97 | reshape(window, width, height) 98 | for !window.ShouldClose() { 99 | if redraw { 100 | display() 101 | window.SwapBuffers() 102 | redraw = false 103 | } 104 | glfw.PollEvents() 105 | // time.Sleep(2 * time.Second) 106 | } 107 | } 108 | 109 | func onChar(w *glfw.Window, char rune) { 110 | log.Println(char) 111 | } 112 | 113 | func onKey(w *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) { 114 | switch { 115 | case key == glfw.KeyEscape && action == glfw.Press, 116 | key == glfw.KeyQ && action == glfw.Press: 117 | w.SetShouldClose(true) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /samples/line/line.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 21/11/2010 by Laurent Le Goff, Stani Michiels 3 | 4 | // Package line draws vertically spaced lines. 5 | package line 6 | 7 | import ( 8 | "image/color" 9 | 10 | "github.com/llgcode/draw2d" 11 | "github.com/llgcode/draw2d/draw2dkit" 12 | "github.com/llgcode/draw2d/samples" 13 | ) 14 | 15 | // Main draws vertically spaced lines and returns the filename. 16 | // This should only be used during testing. 17 | func Main(gc draw2d.GraphicContext, ext string) (string, error) { 18 | gc.SetFillRule(draw2d.FillRuleWinding) 19 | gc.Clear() 20 | // Draw the line 21 | for x := 5.0; x < 297; x += 10 { 22 | Draw(gc, x, 0, x, 210) 23 | } 24 | gc.ClearRect(100, 75, 197, 135) 25 | draw2dkit.Ellipse(gc, 148.5, 105, 35, 25) 26 | gc.SetFillColor(color.RGBA{0xff, 0xff, 0x44, 0xff}) 27 | gc.FillStroke() 28 | 29 | // Return the output filename 30 | return samples.Output("line", ext), nil 31 | } 32 | 33 | // Draw vertically spaced lines 34 | func Draw(gc draw2d.GraphicContext, x0, y0, x1, y1 float64) { 35 | // Draw a line 36 | gc.MoveTo(x0, y0) 37 | gc.LineTo(x1, y1) 38 | gc.Stroke() 39 | } 40 | -------------------------------------------------------------------------------- /samples/linecapjoin/linecapjoin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 21/11/2010 by Laurent Le Goff 3 | 4 | // Package linecapjoin demonstrates the different line caps and joins. 5 | package linecapjoin 6 | 7 | import ( 8 | "image/color" 9 | 10 | "github.com/llgcode/draw2d" 11 | "github.com/llgcode/draw2d/samples" 12 | ) 13 | 14 | // Main draws the different line caps and joins. 15 | // This should only be used during testing. 16 | func Main(gc draw2d.GraphicContext, ext string) (string, error) { 17 | // Draw the line 18 | const offset = 75.0 19 | x := 35.0 20 | caps := []draw2d.LineCap{draw2d.ButtCap, draw2d.SquareCap, draw2d.RoundCap} 21 | joins := []draw2d.LineJoin{draw2d.BevelJoin, draw2d.MiterJoin, draw2d.RoundJoin} 22 | for i := range caps { 23 | Draw(gc, caps[i], joins[i], x, 50, x, 160, offset) 24 | x += offset 25 | } 26 | 27 | // Return the output filename 28 | return samples.Output("linecapjoin", ext), nil 29 | } 30 | 31 | // Draw a line with an angle with specified line cap and join 32 | func Draw(gc draw2d.GraphicContext, cap draw2d.LineCap, join draw2d.LineJoin, 33 | x0, y0, x1, y1, offset float64) { 34 | gc.SetLineCap(cap) 35 | gc.SetLineJoin(join) 36 | 37 | // Draw thick line 38 | gc.SetStrokeColor(color.NRGBA{0x33, 0x33, 0x33, 0xFF}) 39 | gc.SetLineWidth(30.0) 40 | gc.MoveTo(x0, y0) 41 | gc.LineTo((x0+x1)/2+offset, (y0+y1)/2) 42 | gc.LineTo(x1, y1) 43 | gc.Stroke() 44 | 45 | // Draw thin helping line 46 | gc.SetStrokeColor(color.NRGBA{0xFF, 0x33, 0x33, 0xFF}) 47 | gc.SetLineWidth(2.56) 48 | gc.MoveTo(x0, y0) 49 | gc.LineTo((x0+x1)/2+offset, (y0+y1)/2) 50 | gc.LineTo(x1, y1) 51 | gc.Stroke() 52 | } 53 | -------------------------------------------------------------------------------- /samples/postscript/postscript.go: -------------------------------------------------------------------------------- 1 | // Package postscript reads the tiger.ps file and draws it to a backend. 2 | package postscript 3 | 4 | import ( 5 | "io" 6 | "os" 7 | "strings" 8 | 9 | "github.com/llgcode/ps" 10 | 11 | "github.com/llgcode/draw2d" 12 | "github.com/llgcode/draw2d/samples" 13 | ) 14 | 15 | // Main draws the tiger 16 | func Main(gc draw2d.GraphicContext, ext string) (string, error) { 17 | gc.Save() 18 | 19 | // flip the image 20 | gc.Translate(0, 200) 21 | gc.Scale(0.35, -0.35) 22 | gc.Translate(70, -200) 23 | 24 | // Tiger postscript drawing 25 | tiger := samples.Resource("image", "tiger.ps", ext) 26 | 27 | // Draw tiger 28 | Draw(gc, tiger) 29 | gc.Restore() 30 | 31 | // Return the output filename 32 | return samples.Output("postscript", ext), nil 33 | } 34 | 35 | // Draw a tiger 36 | func Draw(gc draw2d.GraphicContext, filename string) { 37 | // Open the postscript 38 | src, err := os.OpenFile(filename, 0, 0) 39 | if err != nil { 40 | panic(err) 41 | } 42 | defer src.Close() 43 | bytes, err := io.ReadAll(src) 44 | reader := strings.NewReader(string(bytes)) 45 | 46 | // Initialize and interpret the postscript 47 | interpreter := ps.NewInterpreter(gc) 48 | interpreter.Execute(reader) 49 | } 50 | -------------------------------------------------------------------------------- /samples/postscriptgl/postscriptgl.go: -------------------------------------------------------------------------------- 1 | // Open a OpenGL window and display a tiger interpreting a postscript file 2 | package main 3 | 4 | import ( 5 | "io" 6 | "log" 7 | "math" 8 | "os" 9 | "runtime" 10 | "strings" 11 | "time" 12 | 13 | "github.com/go-gl/gl/v2.1/gl" 14 | "github.com/go-gl/glfw/v3.1/glfw" 15 | "github.com/llgcode/draw2d/draw2dgl" 16 | "github.com/llgcode/ps" 17 | ) 18 | 19 | var postscriptContent string 20 | 21 | var ( 22 | width, height int 23 | rotate int 24 | window *glfw.Window 25 | ) 26 | 27 | func reshape(window *glfw.Window, w, h int) { 28 | gl.ClearColor(1, 1, 1, 1) 29 | //fmt.Println(gl.GetString(gl.EXTENSIONS)) 30 | gl.Viewport(0, 0, int32(w), int32(h)) /* Establish viewing area to cover entire window. */ 31 | gl.MatrixMode(gl.PROJECTION) /* Start modifying the projection matrix. */ 32 | gl.LoadIdentity() /* Reset project matrix. */ 33 | gl.Ortho(0, float64(w), 0, float64(h), -1, 1) /* Map abstract coords directly to window coords. */ 34 | gl.Scalef(1, -1, 1) /* Invert Y axis so increasing Y goes down. */ 35 | gl.Translatef(0, float32(-h), 0) /* Shift origin up to upper-left corner. */ 36 | gl.Enable(gl.BLEND) 37 | gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) 38 | gl.Disable(gl.DEPTH_TEST) 39 | width, height = w, h 40 | } 41 | 42 | func display() { 43 | 44 | gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) 45 | 46 | lastTime := time.Now() 47 | gl.LineWidth(1) 48 | gc := draw2dgl.NewGraphicContext(width, height) 49 | 50 | gc.Translate(380, 400) 51 | gc.Scale(1, -1) 52 | rotate = (rotate + 1) % 360 53 | gc.Rotate(float64(rotate) * math.Pi / 180) 54 | gc.Translate(-380, -400) 55 | 56 | interpreter := ps.NewInterpreter(gc) 57 | reader := strings.NewReader(postscriptContent) 58 | 59 | interpreter.Execute(reader) 60 | dt := time.Now().Sub(lastTime) 61 | log.Printf("Redraw in : %f ms\n", float64(dt)*1e-6) 62 | gl.Flush() /* Single buffered, so needs a flush. */ 63 | } 64 | 65 | func main() { 66 | src, err := os.OpenFile("tiger.ps", 0, 0) 67 | if err != nil { 68 | log.Println("can't find postscript file.") 69 | return 70 | } 71 | defer src.Close() 72 | bytes, err := io.ReadAll(src) 73 | postscriptContent = string(bytes) 74 | err = glfw.Init() 75 | if err != nil { 76 | panic(err) 77 | } 78 | defer glfw.Terminate() 79 | 80 | window, err = glfw.CreateWindow(800, 800, "Show Tiger in OpenGL", nil, nil) 81 | if err != nil { 82 | panic(err) 83 | } 84 | 85 | window.MakeContextCurrent() 86 | window.SetSizeCallback(reshape) 87 | window.SetKeyCallback(onKey) 88 | 89 | glfw.SwapInterval(1) 90 | 91 | err = gl.Init() 92 | if err != nil { 93 | panic(err) 94 | } 95 | reshape(window, 800, 800) 96 | for !window.ShouldClose() { 97 | display() 98 | window.SwapBuffers() 99 | glfw.PollEvents() 100 | // time.Sleep(2 * time.Second) 101 | } 102 | } 103 | 104 | func onKey(w *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) { 105 | switch { 106 | case key == glfw.KeyEscape && action == glfw.Press, 107 | key == glfw.KeyQ && action == glfw.Press: 108 | w.SetShouldClose(true) 109 | } 110 | } 111 | 112 | func init() { 113 | runtime.LockOSThread() 114 | } 115 | -------------------------------------------------------------------------------- /samples/samples.go: -------------------------------------------------------------------------------- 1 | // Package samples provides examples which can be used with different 2 | // backends. They are also used for testing and coverage of the 3 | // draw2d package. 4 | package samples 5 | 6 | import "fmt" 7 | 8 | // Resource returns a resource filename for testing. 9 | func Resource(folder, filename, ext string) string { 10 | var root string 11 | if ext == "pdf" || ext == "svg" { 12 | root = "../" 13 | } 14 | return fmt.Sprintf("%sresource/%s/%s", root, folder, filename) 15 | } 16 | 17 | // Output returns the output filename for testing. 18 | func Output(name, ext string) string { 19 | var root string 20 | if ext == "pdf" || ext == "svg" { 21 | root = "../" 22 | } 23 | return fmt.Sprintf("%soutput/samples/%s.%s", root, name, ext) 24 | } 25 | -------------------------------------------------------------------------------- /samples_test.go: -------------------------------------------------------------------------------- 1 | // See also test_test.go 2 | 3 | package draw2d_test 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/llgcode/draw2d" 9 | "github.com/llgcode/draw2d/samples/android" 10 | "github.com/llgcode/draw2d/samples/frameimage" 11 | "github.com/llgcode/draw2d/samples/geometry" 12 | "github.com/llgcode/draw2d/samples/gopher" 13 | "github.com/llgcode/draw2d/samples/gopher2" 14 | "github.com/llgcode/draw2d/samples/helloworld" 15 | "github.com/llgcode/draw2d/samples/line" 16 | "github.com/llgcode/draw2d/samples/linecapjoin" 17 | "github.com/llgcode/draw2d/samples/postscript" 18 | ) 19 | 20 | func TestSampleAndroid(t *testing.T) { 21 | test(t, android.Main) 22 | } 23 | 24 | func TestSampleGeometry(t *testing.T) { 25 | // Set the global folder for searching fonts 26 | // The pdf backend needs for every ttf file its corresponding 27 | // json/.z file which is generated by gofpdf/makefont. 28 | draw2d.SetFontFolder("resource/font") 29 | test(t, geometry.Main) 30 | } 31 | 32 | func TestSampleGopher(t *testing.T) { 33 | test(t, gopher.Main) 34 | } 35 | 36 | func TestSampleGopher2(t *testing.T) { 37 | test(t, gopher2.Main) 38 | } 39 | 40 | func TestSampleHelloWorld(t *testing.T) { 41 | // Set the global folder for searching fonts 42 | draw2d.SetFontFolder("resource/font") 43 | test(t, helloworld.Main) 44 | } 45 | 46 | func TestSampleFrameImage(t *testing.T) { 47 | test(t, frameimage.Main) 48 | } 49 | 50 | func TestSampleLine(t *testing.T) { 51 | test(t, line.Main) 52 | } 53 | 54 | func TestSampleLineCapJoin(t *testing.T) { 55 | test(t, linecapjoin.Main) 56 | } 57 | 58 | func TestSamplePostscript(t *testing.T) { 59 | test(t, postscript.Main) 60 | } 61 | -------------------------------------------------------------------------------- /sync_test.go: -------------------------------------------------------------------------------- 1 | // go test -race -test.v sync_test.go 2 | 3 | package draw2d_test 4 | 5 | import ( 6 | "fmt" 7 | "github.com/llgcode/draw2d" 8 | "github.com/llgcode/draw2d/draw2dimg" 9 | "github.com/llgcode/draw2d/draw2dkit" 10 | "image" 11 | "testing" 12 | ) 13 | 14 | func TestSync(t *testing.T) { 15 | ch := make(chan int) 16 | limit := 2 17 | for i := 0; i < limit; i++ { 18 | go Draw(i, ch) 19 | } 20 | 21 | for i := 0; i < limit; i++ { 22 | counter := <-ch 23 | t.Logf("Goroutine %d returned\n", counter) 24 | } 25 | } 26 | 27 | func Draw(i int, ch chan<- int) { 28 | draw2d.SetFontFolder("./resource/font") 29 | // Draw a rounded rectangle using default colors 30 | dest := image.NewRGBA(image.Rect(0, 0, 297, 210.0)) 31 | gc := draw2dimg.NewGraphicContext(dest) 32 | 33 | draw2dkit.RoundedRectangle(gc, 5, 5, 135, 95, 10, 10) 34 | gc.FillStroke() 35 | 36 | // Set the fill text color to black 37 | gc.SetFillColor(image.Black) 38 | gc.SetFontSize(14) 39 | 40 | // Display Hello World dimensions 41 | x1, y1, x2, y2 := gc.GetStringBounds("Hello world") 42 | gc.FillStringAt(fmt.Sprintf("%.2f %.2f %.2f %.2f", x1, y1, x2, y2), 0, 0) 43 | 44 | ch <- i 45 | } 46 | -------------------------------------------------------------------------------- /test: -------------------------------------------------------------------------------- 1 | echo golint 2 | golint ./... | grep "draw2dpdf\|samples\|^advanced_path\|^arc\|draw2d[.]\|fileutil\|^gc\|math\|^path[.]\|rgba_interpolation\|test\|vertex2d" 3 | echo 4 | echo go vet 5 | go vet ./... 6 | echo 7 | echo go test 8 | go test -cover ./... | grep -v "no test" -------------------------------------------------------------------------------- /test_test.go: -------------------------------------------------------------------------------- 1 | // Package draw2d_test gives test coverage with the command: 2 | // go test -cover ./... | grep -v "no test" 3 | package draw2d_test 4 | 5 | import ( 6 | "image" 7 | "testing" 8 | 9 | "github.com/llgcode/draw2d" 10 | "github.com/llgcode/draw2d/draw2dimg" 11 | ) 12 | 13 | type sample func(gc draw2d.GraphicContext, ext string) (string, error) 14 | 15 | func test(t *testing.T, draw sample) { 16 | // Initialize the graphic context on an RGBA image 17 | dest := image.NewRGBA(image.Rect(0, 0, 297, 210.0)) 18 | gc := draw2dimg.NewGraphicContext(dest) 19 | // Draw Android logo 20 | output, err := draw(gc, "png") 21 | if err != nil { 22 | t.Errorf("Drawing %q failed: %v", output, err) 23 | return 24 | } 25 | // Save to png 26 | err = draw2dimg.SaveToPngFile(output, dest) 27 | if err != nil { 28 | t.Errorf("Saving %q failed: %v", output, err) 29 | } 30 | } 31 | --------------------------------------------------------------------------------