Compare commits

...

77 Commits

Author SHA1 Message Date
Hamcha 675e82cb64
Update go.mod (for real) 2019-02-08 11:15:35 +01:00
Hamcha ff82ab9451 Update 'go.mod' 2019-02-08 11:13:35 +01:00
Hamcha a7539fd29b
Fix bound calculation with just emojis (again, super hacky) 2018-11-16 17:36:38 +01:00
Hamcha 41b8d7304e
Should fix centering issues with trailing emojis 2018-11-16 17:13:49 +01:00
Hamcha 9dbc14edd6
Better approximation of emoji's bounds 2018-11-16 17:00:00 +01:00
Hamcha 9941d77460
Add emojis to bound calculation 2018-11-16 15:19:48 +01:00
Hamcha e3566f7fc4
Add emoji support 2018-11-16 12:21:26 +01:00
Hamcha 219501b99b
Change import paths 2018-11-16 12:21:14 +01:00
llgcode f52c8a71af
Merge pull request #146 from piotrkowalczuk/get-font-name-proper-font-size-serialization
GetFontName serialize font size properly
thanks @piotrkowalczuk
2018-08-25 15:34:48 +02:00
Piotr Kowalczuk bdf3a69827 single GetFontName implementation that serialize font size properly 2018-08-20 12:17:57 +02:00
llgcode 587a55234c
Merge pull request #144 from sbinet/go-module-support
Go module support
2018-08-17 15:29:18 +02:00
Sebastien Binet 94de6e33b6 draw2d: add support for Go modules 2018-07-24 12:59:37 +02:00
Sebastien Binet cd0433711b draw2d: add Sebastien Binet to AUTHORS 2018-07-24 12:59:37 +02:00
llgcode 274031cf2a
Merge pull request #140 from Drahoslav7/patch-1
Update AUTHORS, Thanks @Drahoslav7
2018-01-24 14:33:39 +01:00
llgcode bc151d5e2c
Merge pull request #137 from Drahoslav7/feature/svg-context
Feature/svg context
2018-01-24 14:27:22 +01:00
Drahoslav Bednář 72e6a3c750
Update AUTHORS 2018-01-22 16:10:39 +01:00
Drahoslav 0b72959009 Implement PathFontMode and make it default 2018-01-10 23:00:49 +01:00
Drahoslav 7419075cb6 Implement ClearRect in svg context 2018-01-10 22:13:38 +01:00
Drahoslav 1588b49f0d Optimize svg output sligtly 2018-01-10 20:21:07 +01:00
llgcode 50aafedab4
Good Practice: always use BeginPath 2018-01-08 17:55:31 +01:00
Drahoslav 99cc16d0ac Implement DrawImage in svg context 2018-01-08 00:25:25 +01:00
Drahoslav 1b49270d08 Experimental embed svg font functionality 2018-01-07 23:05:53 +01:00
Drahoslav c1e5edea41 Implement CreateStringPath and GetStringBounds in svg context 2018-01-07 17:47:54 +01:00
Drahoslav 6f03f106f6 Minor changes 2017-12-27 12:32:23 +01:00
Drahoslav 3af25f5588 Use font-size in texts of svg context 2017-12-26 19:52:06 +01:00
Drahoslav 90f962641f Implement basic text drawing in svg context
fonts, bounds and string paths not yet implemented
2017-12-26 19:38:19 +01:00
Drahoslav 215a761ccb Use transformations in svg context
also few minor changes in to svg transformation functions
2017-12-26 14:25:28 +01:00
Drahoslav 6d31bfac59 Use fill-rule attribute in svg context 2017-12-24 16:05:15 +01:00
Drahoslav d297a025cd Do minor refactoring of svg context 2017-12-24 15:31:41 +01:00
Drahoslav 484fe1caef Use dasharray and dashoffset attributes in svg context 2017-12-24 14:46:58 +01:00
Drahoslav 0b3b26d85f Gofmt 2017-12-24 13:45:56 +01:00
Drahoslav 41d8a21ba2 Improve toSvgPathDesc
some toArcs paths still does not work correctly
2017-12-24 13:27:44 +01:00
Drahoslav 6c0a15c624 Use line-{width,cap,join} attributes in svg context 2017-12-24 12:34:36 +01:00
Drahoslav ca83e24222 Basic path implementation 2017-12-22 22:40:56 +01:00
Drahoslav bd7567e331 Add samples test - no outputs yet 2017-12-22 09:59:31 +01:00
Drahoslav cdf301b7be Use encoding/xml to encode svg 2017-12-21 18:18:29 +01:00
Drahoslav 295a8365b3 Remove svgo dependency 2017-12-21 15:52:26 +01:00
Drahoslav 96883adea4 Initial commit for svg context
Satisfy draw2dc.GrapghicContext using empty methods
2017-12-16 19:26:34 +01:00
llgcode c41aa97d30 fix #136 2017-12-11 17:32:57 +01:00
llgcode 647da9ceaa fix draw2dgl compil' error 2017-12-11 11:02:06 +01:00
llgcode f3e35015aa synchronize global font cache fixes #131 2017-12-04 18:06:33 +01:00
llgcode b81f74eb39 Refactor Api using an interface 2017-12-04 16:49:08 +01:00
llgcode a5f7ac8ebe fix #131 2017-12-04 16:29:40 +01:00
llgcode 8167230c09 go fmt 2017-12-04 09:46:40 +01:00
llgcode 3e4c36c4c9
Merge pull request #126 from gerald1248/sync
test for concurrent text drawing
2017-12-04 09:43:19 +01:00
llgcode 4cdcb11e52 run all tests 2017-11-28 11:12:29 +01:00
llgcode e4816c5375 fix issue 135
SubdivideQuad and TraceQuad index out of range errors
2017-11-28 10:52:49 +01:00
llgcode dd69e0c822 Test curve range 2017-10-11 10:25:10 +02:00
llgcode dcbfbe505d Merge pull request #134 from AndreKR/convert-lineto-to-moveto
Convert LineTo to MoveTo for the first point of a path
2017-05-13 11:26:31 +02:00
André Hänsel 1f71aa3f15 Convert LineTo to MoveTo for the first point of a path
Same for QuadCurveTo and CubicCurveTo.

Closes #133
2017-04-25 07:59:14 +02:00
gerald1248 0cf6b8d61f fixed typo 2017-01-06 22:24:37 +01:00
gerald1248 7c57ea38bb removed all trickery, left only plain draw2d calls 2017-01-06 22:22:34 +01:00
gerald1248 eca7b76ebc fmt 2016-12-13 22:23:50 +01:00
gerald1248 dfbef878aa added test for concurrent text drawing 2016-12-13 22:22:19 +01:00
llgcode 1286d3b203 Merge pull request #120 from redstarcoder/kerning_fix
Factor in kerning when using cached glyphs. fix #119
2016-11-04 09:10:29 +01:00
redstarcoder c12070824c Factor in kerning when using cached glyphs 2016-10-30 12:41:31 -04:00
llgcode 0d961cd299 Merge pull request #118 from redstarcoder/text_cache_upstream
Seamless Glyph Cache
2016-10-26 10:35:50 +02:00
redstarcoder 7cc6abeee3 Made FetchGlyph and Glyph public, made FillGlyph and StrokeGlyph methods of Glyph, made Glyph.Path private 2016-10-25 21:24:40 -04:00
llgcode 401ee667f2 Merge pull request #113 from redstarcoder/pathbuilder_copypath
Add GetPath to the GraphicContext interface.
2016-10-24 10:08:26 +02:00
redstarcoder c2920005d6 Moved cache code to draw2dbase 2016-10-22 22:50:11 -04:00
redstarcoder c2851a6eb6 Improved speed via new gc.GetFontName method 2016-10-22 22:36:52 -04:00
redstarcoder 3a5a1d8830 Implement seamless glyph cache 2016-10-22 22:31:55 -04:00
redstarcoder b9005c988d Fixed comment 2016-10-19 15:01:45 -04:00
redstarcoder 4a3322e29e CopyPath -> GetPath. Also return Path instead of *Path. 2016-10-19 14:53:36 -04:00
redstarcoder 8380dd9458 Added CopyPath to the GraphicContext interface. 2016-10-17 19:36:31 -04:00
llgcode 51ba099819 fix comment 2016-10-17 10:46:10 +02:00
llgcode a6ceba03c8 Merge pull request #112 from redstarcoder/draw2dgl_fonts
Draw2dgl Font Support
2016-10-16 20:47:51 +02:00
redstarcoder 475a830567 Cleaned up extra FIXMEs 2016-10-15 16:15:10 -04:00
redstarcoder 105a963210 Copied font functions from draw2dimg to draw2dgl
cursor -> width for certain functions for clarity
Some panics added, some TODOs added
2016-10-15 16:15:10 -04:00
llgcode 13548be874 Merge pull request #106 from kortschak/fix-font-cache
Fix unnecessary filesystem access
2016-07-12 10:12:54 +02:00
kortschak e0e534f3a5 Fix unnecessary filesystem access
Fixes #105.
2016-07-12 16:24:44 +09:30
llgcode 155ff5c755 Merge pull request #94 from achille-roussel/master
draw2d.FontCache
2016-07-08 10:26:28 +02:00
Laurent Le Goff 3f01cfe277 use parameter op fix #103 2016-06-27 21:46:37 +02:00
llgcode 5e675a3055 Merge pull request #102 from zstyblik/fix-typos
Fix typos and add more comments/documentation
2016-04-28 20:44:47 +02:00
Zdenek Styblik 3bb234e85b Fix typos and add more comments/documentation
Commit fixes couple typos and adds more documentation to
GraphicContext interface in `gc.go`.

Fixes #101
2016-04-27 15:25:21 +02:00
Achille Roussel 6c047429f6 remove unused global variables 2015-11-18 15:00:27 -08:00
Achille Roussel 598513aa60 add FontCache 2015-11-18 14:59:19 -08:00
47 changed files with 2118 additions and 155 deletions

2
.gitignore vendored
View File

@ -21,3 +21,5 @@ _test*
**/core*[0-9]
.private
go.sum

View File

@ -1,2 +1,4 @@
Laurent Le Goff
Stani Michiels, gmail:stani.be
Stani Michiels, gmail:stani.be
Drahoslav Bednář
Sebastien Binet

View File

@ -57,7 +57,8 @@ func main() {
gc.SetLineWidth(5)
// Draw a closed shape
gc.MoveTo(10, 10) // should always be called first for a new path
gc.BeginPath() // Initialize a new path
gc.MoveTo(10, 10) // Move to a position to start the new path
gc.LineTo(100, 50)
gc.QuadCurveTo(100, 10, 10, 10)
gc.Close()

View File

@ -128,6 +128,14 @@ const (
SquareCap
)
func (cap LineCap) String() string {
return map[LineCap]string{
RoundCap: "round",
ButtCap: "cap",
SquareCap: "square",
}[cap]
}
// LineJoin is the style of segments joint
type LineJoin int
@ -140,6 +148,14 @@ const (
MiterJoin
)
func (join LineJoin) String() string {
return map[LineJoin]string{
RoundJoin: "round",
BevelJoin: "bevel",
MiterJoin: "miter",
}[join]
}
// StrokeStyle keeps stroke style attributes
// that is used by the Stroke method of a Drawer
type StrokeStyle struct {

View File

@ -4,6 +4,7 @@
package draw2dbase
import (
"errors"
"math"
)
@ -17,6 +18,7 @@ const (
// SubdivideCubic a Bezier cubic curve in 2 equivalents Bezier cubic curves.
// c1 and c2 parameters are the resulting curves
// length of c, c1 and c2 must be 8 otherwise it panics.
func SubdivideCubic(c, c1, c2 []float64) {
// First point of c is the first point of c1
c1[0], c1[1] = c[0], c[1]
@ -48,7 +50,10 @@ func SubdivideCubic(c, c1, c2 []float64) {
// TraceCubic generate lines subdividing the cubic curve using a Liner
// flattening_threshold helps determines the flattening expectation of the curve
func TraceCubic(t Liner, cubic []float64, flatteningThreshold float64) {
func TraceCubic(t Liner, cubic []float64, flatteningThreshold float64) error {
if len(cubic) < 8 {
return errors.New("cubic length must be >= 8")
}
// Allocation curves
var curves [CurveRecursionLimit * 8]float64
copy(curves[0:8], cubic[0:8])
@ -60,7 +65,7 @@ func TraceCubic(t Liner, cubic []float64, flatteningThreshold float64) {
var dx, dy, d2, d3 float64
for i >= 0 {
c = curves[i*8:]
c = curves[i:]
dx = c[6] - c[0]
dy = c[7] - c[1]
@ -68,15 +73,16 @@ func TraceCubic(t Liner, cubic []float64, flatteningThreshold float64) {
d3 = math.Abs((c[4]-c[6])*dy - (c[5]-c[7])*dx)
// if it's flat then trace a line
if (d2+d3)*(d2+d3) < flatteningThreshold*(dx*dx+dy*dy) || i == len(curves)-1 {
if (d2+d3)*(d2+d3) <= flatteningThreshold*(dx*dx+dy*dy) || i == len(curves)-8 {
t.LineTo(c[6], c[7])
i--
i -= 8
} else {
// second half of bezier go lower onto the stack
SubdivideCubic(c, curves[(i+1)*8:], curves[i*8:])
i++
SubdivideCubic(c, curves[i+8:], curves[i:])
i += 8
}
}
return nil
}
// Quad
@ -84,6 +90,7 @@ func TraceCubic(t Liner, cubic []float64, flatteningThreshold float64) {
// SubdivideQuad a Bezier quad curve in 2 equivalents Bezier quad curves.
// c1 and c2 parameters are the resulting curves
// length of c, c1 and c2 must be 6 otherwise it panics.
func SubdivideQuad(c, c1, c2 []float64) {
// First point of c is the first point of c1
c1[0], c1[1] = c[0], c[1]
@ -103,7 +110,10 @@ func SubdivideQuad(c, c1, c2 []float64) {
// TraceQuad generate lines subdividing the curve using a Liner
// flattening_threshold helps determines the flattening expectation of the curve
func TraceQuad(t Liner, quad []float64, flatteningThreshold float64) {
func TraceQuad(t Liner, quad []float64, flatteningThreshold float64) error {
if len(quad) < 6 {
return errors.New("quad length must be >= 6")
}
// Allocates curves stack
var curves [CurveRecursionLimit * 6]float64
copy(curves[0:6], quad[0:6])
@ -113,22 +123,23 @@ func TraceQuad(t Liner, quad []float64, flatteningThreshold float64) {
var dx, dy, d float64
for i >= 0 {
c = curves[i*6:]
c = curves[i:]
dx = c[4] - c[0]
dy = c[5] - c[1]
d = math.Abs(((c[2]-c[4])*dy - (c[3]-c[5])*dx))
// if it's flat then trace a line
if (d*d) < flatteningThreshold*(dx*dx+dy*dy) || i == len(curves)-1 {
if (d*d) <= flatteningThreshold*(dx*dx+dy*dy) || i == len(curves)-6 {
t.LineTo(c[4], c[5])
i--
i -= 6
} else {
// second half of bezier go lower onto the stack
SubdivideQuad(c, curves[(i+1)*6:], curves[i*6:])
i++
SubdivideQuad(c, curves[i+6:], curves[i:])
i += 6
}
}
return nil
}
// TraceArc trace an arc using a Liner

View File

@ -101,6 +101,32 @@ func TestQuadCurve(t *testing.T) {
fmt.Println()
}
func TestQuadCurveCombinedPoint(t *testing.T) {
var p1 SegmentedPath
TraceQuad(&p1, []float64{0, 0, 0, 0, 0, 0}, flatteningThreshold)
if len(p1.Points) != 2 {
t.Error("It must have one point for this curve", len(p1.Points))
}
var p2 SegmentedPath
TraceQuad(&p2, []float64{0, 0, 100, 100, 0, 0}, flatteningThreshold)
if len(p2.Points) != 2 {
t.Error("It must have one point for this curve", len(p2.Points))
}
}
func TestCubicCurveCombinedPoint(t *testing.T) {
var p1 SegmentedPath
TraceCubic(&p1, []float64{0, 0, 0, 0, 0, 0, 0, 0}, flatteningThreshold)
if len(p1.Points) != 2 {
t.Error("It must have one point for this curve", len(p1.Points))
}
var p2 SegmentedPath
TraceCubic(&p2, []float64{0, 0, 100, 100, 200, 200, 0, 0}, flatteningThreshold)
if len(p2.Points) != 2 {
t.Error("It must have one point for this curve", len(p2.Points))
}
}
func BenchmarkCubicCurve(b *testing.B) {
for i := 0; i < b.N; i++ {
for i := 0; i < len(testsCubicFloat64); i += 8 {
@ -132,3 +158,19 @@ func SaveToPngFile(filePath string, m image.Image) error {
}
return nil
}
func TestOutOfRangeTraceCurve(t *testing.T) {
c := []float64{
100, 100, 200, 100, 100, 200,
}
var p SegmentedPath
TraceCubic(&p, c, flatteningThreshold)
}
func TestOutOfRangeTraceQuad(t *testing.T) {
c := []float64{
100, 100, 200, 100,
}
var p SegmentedPath
TraceQuad(&p, c, flatteningThreshold)
}

View File

@ -4,7 +4,7 @@
package draw2dbase
import (
"github.com/llgcode/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d"
)
// Liner receive segment definition
@ -19,7 +19,7 @@ type Flattener interface {
MoveTo(x, y float64)
// LineTo Draw a line from the current position to the point (x, y)
LineTo(x, y float64)
// LineJoin add the most recent starting point to close the path to create a polygon
// LineJoin use Round, Bevel or miter to join points
LineJoin()
// Close add the most recent starting point to close the path to create a polygon
Close()

View File

@ -4,12 +4,13 @@
package draw2dbase
import (
"fmt"
"image"
"image/color"
"github.com/llgcode/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d"
"github.com/golang/freetype/truetype"
"git.fromouter.space/crunchy-rocks/freetype/truetype"
)
var DefaultFontData = draw2d.FontData{Name: "luxi", Family: draw2d.FontFamilySans, Style: draw2d.FontStyleNormal}
@ -40,6 +41,12 @@ type ContextStack struct {
Previous *ContextStack
}
// GetFontName gets the current FontData with fontSize as a string
func (cs *ContextStack) GetFontName() string {
fontData := cs.FontData
return fmt.Sprintf("%s:%d:%d:%9.2f", fontData.Name, fontData.Family, fontData.Style, cs.FontSize)
}
/**
* Create a new Graphic context from an image
*/
@ -132,6 +139,10 @@ func (gc *StackGraphicContext) BeginPath() {
gc.Current.Path.Clear()
}
func (gc *StackGraphicContext) GetPath() draw2d.Path {
return *gc.Current.Path.Copy()
}
func (gc *StackGraphicContext) IsEmpty() bool {
return gc.Current.Path.IsEmpty()
}
@ -191,3 +202,7 @@ func (gc *StackGraphicContext) Restore() {
oldContext.Previous = nil
}
}
func (gc *StackGraphicContext) GetFontName() string {
return gc.Current.GetFontName()
}

View File

@ -6,7 +6,7 @@ package draw2dbase
import (
"math"
"github.com/llgcode/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d"
)
type LineStroker struct {

82
draw2dbase/text.go Normal file
View File

@ -0,0 +1,82 @@
package draw2dbase
import "git.fromouter.space/crunchy-rocks/draw2d"
// GlyphCache manage a cache of glyphs
type GlyphCache interface {
// Fetch fetches a glyph from the cache, storing with Render first if it doesn't already exist
Fetch(gc draw2d.GraphicContext, fontName string, chr rune) *Glyph
}
// GlyphCacheImp manage a map of glyphs without sync mecanism, not thread safe
type GlyphCacheImp struct {
glyphs map[string]map[rune]*Glyph
}
// NewGlyphCache initializes a GlyphCache
func NewGlyphCache() *GlyphCacheImp {
glyphs := make(map[string]map[rune]*Glyph)
return &GlyphCacheImp{
glyphs: glyphs,
}
}
// Fetch fetches a glyph from the cache, calling renderGlyph first if it doesn't already exist
func (glyphCache *GlyphCacheImp) Fetch(gc draw2d.GraphicContext, fontName string, chr rune) *Glyph {
if glyphCache.glyphs[fontName] == nil {
glyphCache.glyphs[fontName] = make(map[rune]*Glyph, 60)
}
if glyphCache.glyphs[fontName][chr] == nil {
glyphCache.glyphs[fontName][chr] = renderGlyph(gc, fontName, chr)
}
return glyphCache.glyphs[fontName][chr].Copy()
}
// renderGlyph renders a glyph then caches and returns it
func renderGlyph(gc draw2d.GraphicContext, fontName string, chr rune) *Glyph {
gc.Save()
defer gc.Restore()
gc.BeginPath()
width := gc.CreateStringPath(string(chr), 0, 0)
path := gc.GetPath()
return &Glyph{
Path: &path,
Width: width,
}
}
// Glyph represents a rune which has been converted to a Path and width
type Glyph struct {
// path represents a glyph, it is always at (0, 0)
Path *draw2d.Path
// Width of the glyph
Width float64
}
// Copy Returns a copy of a Glyph
func (g *Glyph) Copy() *Glyph {
return &Glyph{
Path: g.Path.Copy(),
Width: g.Width,
}
}
// Fill copies a glyph from the cache, and fills it
func (g *Glyph) Fill(gc draw2d.GraphicContext, x, y float64) float64 {
gc.Save()
gc.BeginPath()
gc.Translate(x, y)
gc.Fill(g.Path)
gc.Restore()
return g.Width
}
// Stroke fetches a glyph from the cache, and strokes it
func (g *Glyph) Stroke(gc draw2d.GraphicContext, x, y float64) float64 {
gc.Save()
gc.BeginPath()
gc.Translate(x, y)
gc.Stroke(g.Path)
gc.Restore()
return g.Width
}

View File

@ -4,13 +4,19 @@ import (
"image"
"image/color"
"image/draw"
"log"
"math"
"runtime"
"git.fromouter.space/crunchy-rocks/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d/draw2dbase"
"git.fromouter.space/crunchy-rocks/draw2d/draw2dimg"
"github.com/go-gl/gl/v2.1/gl"
"github.com/golang/freetype/raster"
"github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/draw2dbase"
"github.com/llgcode/draw2d/draw2dimg"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
)
func init() {
@ -118,6 +124,10 @@ type GraphicContext struct {
painter *Painter
fillRasterizer *raster.Rasterizer
strokeRasterizer *raster.Rasterizer
FontCache draw2d.FontCache
glyphCache draw2dbase.GlyphCache
glyphBuf *truetype.GlyphBuf
DPI int
}
// NewGraphicContext creates a new Graphic context from an image.
@ -127,54 +137,200 @@ func NewGraphicContext(width, height int) *GraphicContext {
NewPainter(),
raster.NewRasterizer(width, height),
raster.NewRasterizer(width, height),
draw2d.GetGlobalFontCache(),
draw2dbase.NewGlyphCache(),
&truetype.GlyphBuf{},
92,
}
return gc
}
func (gc *GraphicContext) loadCurrentFont() (*truetype.Font, error) {
font, err := gc.FontCache.Load(gc.Current.FontData)
if err != nil {
font, err = gc.FontCache.Load(draw2dbase.DefaultFontData)
}
if font != nil {
gc.SetFont(font)
gc.SetFontSize(gc.Current.FontSize)
}
return font, err
}
func (gc *GraphicContext) drawGlyph(glyph truetype.Index, dx, dy float64) error {
if err := gc.glyphBuf.Load(gc.Current.Font, fixed.Int26_6(gc.Current.Scale), glyph, font.HintingNone); err != nil {
return err
}
e0 := 0
for _, e1 := range gc.glyphBuf.Ends {
DrawContour(gc, gc.glyphBuf.Points[e0:e1], dx, dy)
e0 = e1
}
return nil
}
// CreateStringPath creates a path from the string s at x, y, and returns the string width.
// The text is placed so that the left edge of the em square of the first character of s
// and the baseline intersect at x, y. The majority of the affected pixels will be
// above and to the right of the point, but some may be below or to the left.
// For example, drawing a string that starts with a 'J' in an italic font may
// affect pixels below and left of the point.
func (gc *GraphicContext) CreateStringPath(s string, x, y float64) float64 {
panic("not implemented")
f, err := gc.loadCurrentFont()
if err != nil {
log.Println(err)
return 0.0
}
startx := x
prev, hasPrev := truetype.Index(0), false
for _, rune := range s {
index := f.Index(rune)
if hasPrev {
x += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index))
}
err := gc.drawGlyph(index, x, y)
if err != nil {
log.Println(err)
return startx - x
}
x += fUnitsToFloat64(f.HMetric(fixed.Int26_6(gc.Current.Scale), index).AdvanceWidth)
prev, hasPrev = index, true
}
return x - startx
}
func (gc *GraphicContext) FillStringAt(text string, x, y float64) (cursor float64) {
panic("not implemented")
// FillString draws the text at point (0, 0)
func (gc *GraphicContext) FillString(text string) (width float64) {
return gc.FillStringAt(text, 0, 0)
}
// FillStringAt draws the text at the specified point (x, y)
func (gc *GraphicContext) FillStringAt(text string, x, y float64) (width float64) {
f, err := gc.loadCurrentFont()
if err != nil {
log.Println(err)
return 0.0
}
startx := x
prev, hasPrev := truetype.Index(0), false
fontName := gc.GetFontName()
for _, r := range text {
index := f.Index(r)
if hasPrev {
x += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index))
}
glyph := gc.glyphCache.Fetch(gc, fontName, r)
x += glyph.Fill(gc, x, y)
prev, hasPrev = index, true
}
return x - startx
}
// GetStringBounds returns the approximate pixel bounds of the string s at x, y.
// The the left edge of the em square of the first character of s
// and the baseline intersect at 0, 0 in the returned coordinates.
// Therefore the top and left coordinates may well be negative.
func (gc *GraphicContext) GetStringBounds(s string) (left, top, right, bottom float64) {
panic("not implemented")
f, err := gc.loadCurrentFont()
if err != nil {
log.Println(err)
return 0, 0, 0, 0
}
top, left, bottom, right = 10e6, 10e6, -10e6, -10e6
cursor := 0.0
prev, hasPrev := truetype.Index(0), false
for _, rune := range s {
index := f.Index(rune)
if hasPrev {
cursor += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index))
}
if err := gc.glyphBuf.Load(gc.Current.Font, fixed.Int26_6(gc.Current.Scale), index, font.HintingNone); err != nil {
log.Println(err)
return 0, 0, 0, 0
}
e0 := 0
for _, e1 := range gc.glyphBuf.Ends {
ps := gc.glyphBuf.Points[e0:e1]
for _, p := range ps {
x, y := pointToF64Point(p)
top = math.Min(top, y)
bottom = math.Max(bottom, y)
left = math.Min(left, x+cursor)
right = math.Max(right, x+cursor)
}
}
cursor += fUnitsToFloat64(f.HMetric(fixed.Int26_6(gc.Current.Scale), index).AdvanceWidth)
prev, hasPrev = index, true
}
return left, top, right, bottom
}
func (gc *GraphicContext) StrokeString(text string) (cursor float64) {
// StrokeString draws the contour of the text at point (0, 0)
func (gc *GraphicContext) StrokeString(text string) (width float64) {
return gc.StrokeStringAt(text, 0, 0)
}
func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64) {
width := gc.CreateStringPath(text, x, y)
gc.Stroke()
return width
// StrokeStringAt draws the contour of the text at point (x, y)
func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (width float64) {
f, err := gc.loadCurrentFont()
if err != nil {
log.Println(err)
return 0.0
}
startx := x
prev, hasPrev := truetype.Index(0), false
fontName := gc.GetFontName()
for _, r := range text {
index := f.Index(r)
if hasPrev {
x += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index))
}
glyph := gc.glyphCache.Fetch(gc, fontName, r)
x += glyph.Stroke(gc, x, y)
prev, hasPrev = index, true
}
return x - startx
}
// recalc recalculates scale and bounds values from the font size, screen
// resolution and font metrics, and invalidates the glyph cache.
func (gc *GraphicContext) recalc() {
gc.Current.Scale = gc.Current.FontSize * float64(gc.DPI) * (64.0 / 72.0)
}
func (gc *GraphicContext) SetDPI(dpi int) {
gc.DPI = dpi
gc.recalc()
}
// SetFont sets the font used to draw text.
func (gc *GraphicContext) SetFont(font *truetype.Font) {
gc.Current.Font = font
}
// SetFontSize sets the font size in points (as in ``a 12 point font'').
func (gc *GraphicContext) SetFontSize(fontSize float64) {
gc.Current.FontSize = fontSize
gc.recalc()
}
func (gc *GraphicContext) GetDPI() int {
return -1
return gc.DPI
}
//TODO
func (gc *GraphicContext) Clear() {
panic("not implemented")
}
//TODO
func (gc *GraphicContext) ClearRect(x1, y1, x2, y2 int) {
panic("not implemented")
}
//TODO
func (gc *GraphicContext) DrawImage(img image.Image) {
}
func (gc *GraphicContext) FillString(text string) (cursor float64) {
return 0
panic("not implemented")
}
func (gc *GraphicContext) paint(rasterizer *raster.Rasterizer, color color.Color) {

82
draw2dgl/text.go Normal file
View File

@ -0,0 +1,82 @@
package draw2dgl
import (
"git.fromouter.space/crunchy-rocks/draw2d"
"git.fromouter.space/crunchy-rocks/freetype/truetype"
"golang.org/x/image/math/fixed"
)
// DrawContour draws the given closed contour at the given sub-pixel offset.
func DrawContour(path draw2d.PathBuilder, ps []truetype.Point, dx, dy float64) {
if len(ps) == 0 {
return
}
startX, startY := pointToF64Point(ps[0])
path.MoveTo(startX+dx, startY+dy)
q0X, q0Y, on0 := startX, startY, true
for _, p := range ps[1:] {
qX, qY := pointToF64Point(p)
on := p.Flags&0x01 != 0
if on {
if on0 {
path.LineTo(qX+dx, qY+dy)
} else {
path.QuadCurveTo(q0X+dx, q0Y+dy, qX+dx, qY+dy)
}
} else {
if on0 {
// No-op.
} else {
midX := (q0X + qX) / 2
midY := (q0Y + qY) / 2
path.QuadCurveTo(q0X+dx, q0Y+dy, midX+dx, midY+dy)
}
}
q0X, q0Y, on0 = qX, qY, on
}
// Close the curve.
if on0 {
path.LineTo(startX+dx, startY+dy)
} else {
path.QuadCurveTo(q0X+dx, q0Y+dy, startX+dx, startY+dy)
}
}
func pointToF64Point(p truetype.Point) (x, y float64) {
return fUnitsToFloat64(p.X), -fUnitsToFloat64(p.Y)
}
func fUnitsToFloat64(x fixed.Int26_6) float64 {
scaled := x << 2
return float64(scaled/256) + float64(scaled%256)/256.0
}
// FontExtents contains font metric information.
type FontExtents struct {
// Ascent is the distance that the text
// extends above the baseline.
Ascent float64
// Descent is the distance that the text
// extends below the baseline. The descent
// is given as a negative value.
Descent float64
// Height is the distance from the lowest
// descending point to the highest ascending
// point.
Height float64
}
// Extents returns the FontExtents for a font.
// TODO needs to read this https://developer.apple.com/fonts/TrueType-Reference-Manual/RM02/Chap2.html#intro
func Extents(font *truetype.Font, size float64) FontExtents {
bounds := font.Bounds(fixed.Int26_6(font.FUnitsPerEm()))
scale := size / float64(font.FUnitsPerEm())
return FontExtents{
Ascent: float64(bounds.Max.Y) * scale,
Descent: float64(bounds.Min.Y) * scale,
Height: float64(bounds.Max.Y-bounds.Min.Y) * scale,
}
}

View File

@ -0,0 +1,190 @@
package draw2dimg
import (
"fmt"
"image"
"image/color"
"testing"
"git.fromouter.space/crunchy-rocks/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
"git.fromouter.space/crunchy-rocks/freetype/truetype"
"golang.org/x/image/font/gofont/goregular"
)
// font generated from icomoon.io and converted to go byte slice
// contains only two glyphs
// \u2716 - which should look like a cross
// \u25cb - which should look like an empty circle
var icoTTF = []byte{
0, 1, 0, 0, 0, 12, 0, 128, 0, 3, 0, 64, 71, 83, 85, 66, 219, 7, 221, 185,
0, 0, 0, 204, 0, 0, 0, 188, 79, 83, 47, 50, 175, 17, 51, 150, 0, 0, 1, 136,
0, 0, 0, 96, 99, 109, 97, 112, 37, 204, 43, 67, 0, 0, 1, 232, 0, 0, 0, 148,
103, 97, 115, 112, 0, 0, 0, 16, 0, 0, 2, 124, 0, 0, 0, 8, 103, 108, 121, 102,
163, 112, 233, 32, 0, 0, 2, 132, 0, 0, 3, 64, 104, 101, 97, 100, 15, 49, 194, 135,
0, 0, 5, 196, 0, 0, 0, 54, 104, 104, 101, 97, 7, 194, 3, 217, 0, 0, 5, 252,
0, 0, 0, 36, 104, 109, 116, 120, 14, 0, 0, 2, 0, 0, 6, 32, 0, 0, 0, 96,
108, 111, 99, 97, 6, 168, 5, 226, 0, 0, 6, 128, 0, 0, 0, 50, 109, 97, 120, 112,
0, 27, 0, 86, 0, 0, 6, 180, 0, 0, 0, 32, 110, 97, 109, 101, 108, 36, 213, 69,
0, 0, 6, 212, 0, 0, 1, 170, 112, 111, 115, 116, 0, 3, 0, 0, 0, 0, 8, 128,
0, 0, 0, 32, 0, 1, 0, 0, 0, 10, 0, 30, 0, 44, 0, 1, 108, 97, 116, 110,
0, 8, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 108, 105, 103, 97,
0, 8, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4, 0, 4, 0, 0, 0, 1, 0, 10,
0, 0, 0, 1, 0, 12, 0, 3, 0, 22, 0, 54, 0, 120, 0, 1, 0, 3, 0, 8,
0, 17, 0, 23, 0, 2, 0, 6, 0, 18, 0, 22, 0, 5, 0, 17, 0, 16, 0, 18,
0, 18, 0, 22, 0, 6, 0, 6, 0, 15, 0, 8, 0, 10, 0, 14, 0, 2, 0, 6,
0, 38, 0, 21, 0, 15, 0, 6, 0, 9, 0, 12, 0, 16, 0, 4, 0, 20, 0, 15,
0, 8, 0, 11, 0, 10, 0, 8, 0, 13, 0, 10, 0, 9, 0, 21, 0, 13, 0, 6,
0, 9, 0, 12, 0, 16, 0, 4, 0, 7, 0, 20, 0, 19, 0, 19, 0, 16, 0, 15,
0, 5, 0, 1, 0, 4, 0, 22, 0, 2, 0, 23, 0, 3, 3, 85, 1, 144, 0, 5,
0, 0, 2, 153, 2, 204, 0, 0, 0, 143, 2, 153, 2, 204, 0, 0, 1, 235, 0, 51,
1, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 39, 23,
3, 192, 255, 192, 0, 64, 3, 192, 0, 64, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 28,
0, 1, 0, 3, 0, 0, 0, 28, 0, 3, 0, 1, 0, 0, 0, 28, 0, 4, 0, 120,
0, 0, 0, 26, 0, 16, 0, 3, 0, 10, 0, 1, 0, 32, 0, 45, 0, 51, 0, 101,
0, 105, 0, 108, 0, 111, 0, 117, 37, 203, 39, 23, 255, 253, 255, 255, 0, 0, 0, 0,
0, 32, 0, 45, 0, 51, 0, 97, 0, 104, 0, 107, 0, 110, 0, 114, 37, 203, 39, 22,
255, 253, 255, 255, 0, 1, 255, 227, 255, 215, 255, 210, 255, 165, 255, 163, 255, 162, 255, 161,
255, 159, 218, 74, 217, 0, 0, 3, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1,
255, 255, 0, 15, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57,
1, 0, 0, 0, 0, 2, 0, 0, 255, 192, 4, 0, 3, 192, 0, 27, 0, 55, 0, 0,
1, 34, 7, 14, 1, 7, 6, 21, 20, 23, 30, 1, 23, 22, 51, 50, 55, 62, 1, 55,
54, 53, 52, 39, 46, 1, 39, 38, 3, 34, 39, 46, 1, 39, 38, 53, 52, 55, 62, 1,
55, 54, 51, 50, 23, 30, 1, 23, 22, 21, 20, 7, 14, 1, 7, 6, 2, 0, 106, 93,
94, 139, 40, 40, 40, 40, 139, 94, 93, 106, 106, 93, 94, 139, 40, 40, 40, 40, 139, 94,
93, 106, 80, 69, 70, 105, 30, 30, 30, 30, 105, 70, 69, 80, 80, 69, 70, 105, 30, 30,
30, 30, 105, 70, 69, 3, 192, 40, 40, 139, 94, 93, 106, 106, 93, 94, 139, 40, 40, 40,
40, 139, 94, 93, 106, 106, 93, 94, 139, 40, 40, 252, 128, 30, 30, 105, 70, 69, 80, 80,
69, 70, 105, 30, 30, 30, 30, 105, 70, 69, 80, 80, 69, 70, 105, 30, 30, 0, 0, 0,
0, 1, 0, 2, 255, 194, 3, 254, 3, 190, 0, 83, 0, 0, 37, 56, 1, 49, 9, 1,
56, 1, 49, 62, 1, 55, 54, 38, 47, 1, 46, 1, 7, 14, 1, 7, 56, 1, 49, 9,
1, 56, 1, 49, 46, 1, 39, 38, 6, 15, 1, 14, 1, 23, 30, 1, 23, 56, 1, 49,
9, 1, 56, 1, 49, 14, 1, 7, 6, 22, 31, 1, 30, 1, 55, 62, 1, 55, 56, 1,
49, 9, 1, 56, 1, 49, 30, 1, 23, 22, 54, 63, 1, 62, 1, 39, 46, 1, 3, 247,
254, 201, 1, 55, 2, 4, 1, 3, 3, 7, 147, 7, 18, 9, 3, 6, 2, 254, 201, 254,
201, 2, 6, 3, 9, 18, 7, 147, 7, 3, 3, 1, 4, 2, 1, 55, 254, 201, 2, 4,
1, 3, 3, 7, 147, 7, 18, 9, 3, 6, 2, 1, 55, 1, 55, 2, 6, 3, 9, 18,
7, 147, 7, 3, 3, 1, 4, 137, 1, 55, 1, 55, 2, 6, 3, 9, 18, 7, 147, 7,
3, 3, 1, 4, 2, 254, 201, 1, 55, 2, 4, 1, 3, 3, 7, 147, 7, 18, 9, 3,
6, 2, 254, 201, 254, 201, 2, 6, 3, 9, 18, 7, 147, 7, 3, 3, 1, 4, 2, 1,
55, 254, 201, 2, 4, 1, 3, 3, 7, 147, 7, 18, 9, 3, 6, 0, 0, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 55, 57, 1, 0, 0, 0, 0, 1, 0, 0,
0, 1, 0, 0, 32, 120, 21, 165, 95, 15, 60, 245, 0, 11, 4, 0, 0, 0, 0, 0,
214, 9, 63, 5, 0, 0, 0, 0, 214, 9, 63, 5, 0, 0, 255, 192, 4, 0, 3, 192,
0, 0, 0, 8, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 3, 192, 255, 192,
0, 0, 4, 0, 0, 0, 0, 0, 4, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 24, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 2,
0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 20, 0, 30, 0, 40, 0, 50, 0, 60,
0, 70, 0, 80, 0, 90, 0, 100, 0, 110, 0, 120, 0, 130, 0, 140, 0, 150, 0, 160,
0, 170, 0, 180, 0, 190, 0, 200, 1, 32, 1, 150, 1, 160, 0, 0, 0, 1, 0, 0,
0, 24, 0, 84, 0, 2, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 174, 0, 1, 0, 0, 0, 0,
0, 1, 0, 10, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2, 0, 7, 0, 123, 0, 1,
0, 0, 0, 0, 0, 3, 0, 10, 0, 63, 0, 1, 0, 0, 0, 0, 0, 4, 0, 10,
0, 144, 0, 1, 0, 0, 0, 0, 0, 5, 0, 11, 0, 30, 0, 1, 0, 0, 0, 0,
0, 6, 0, 10, 0, 93, 0, 1, 0, 0, 0, 0, 0, 10, 0, 26, 0, 174, 0, 3,
0, 1, 4, 9, 0, 1, 0, 20, 0, 10, 0, 3, 0, 1, 4, 9, 0, 2, 0, 14,
0, 130, 0, 3, 0, 1, 4, 9, 0, 3, 0, 20, 0, 73, 0, 3, 0, 1, 4, 9,
0, 4, 0, 20, 0, 154, 0, 3, 0, 1, 4, 9, 0, 5, 0, 22, 0, 41, 0, 3,
0, 1, 4, 9, 0, 6, 0, 20, 0, 103, 0, 3, 0, 1, 4, 9, 0, 10, 0, 52,
0, 200, 105, 99, 111, 45, 112, 101, 110, 101, 103, 111, 0, 105, 0, 99, 0, 111, 0, 45,
0, 112, 0, 101, 0, 110, 0, 101, 0, 103, 0, 111, 86, 101, 114, 115, 105, 111, 110, 32,
49, 46, 48, 0, 86, 0, 101, 0, 114, 0, 115, 0, 105, 0, 111, 0, 110, 0, 32, 0,
49, 0, 46, 0, 48, 105, 99, 111, 45, 112, 101, 110, 101, 103, 111, 0, 105, 0, 99, 0,
111, 0, 45, 0, 112, 0, 101, 0, 110, 0, 101, 0, 103, 0, 111, 105, 99, 111, 45, 112,
101, 110, 101, 103, 111, 0, 105, 0, 99, 0, 111, 0, 45, 0, 112, 0, 101, 0, 110, 0,
101, 0, 103, 0, 111, 82, 101, 103, 117, 108, 97, 114, 0, 82, 0, 101, 0, 103, 0, 117,
0, 108, 0, 97, 0, 114, 105, 99, 111, 45, 112, 101, 110, 101, 103, 111, 0, 105, 0, 99,
0, 111, 0, 45, 0, 112, 0, 101, 0, 110, 0, 101, 0, 103, 0, 111, 70, 111, 110, 116,
32, 103, 101, 110, 101, 114, 97, 116, 101, 100, 32, 98, 121, 32, 73, 99, 111, 77, 111, 111,
110, 46, 0, 70, 0, 111, 0, 110, 0, 116, 0, 32, 0, 103, 0, 101, 0, 110, 0, 101,
0, 114, 0, 97, 0, 116, 0, 101, 0, 100, 0, 32, 0, 98, 0, 121, 0, 32, 0, 73,
0, 99, 0, 111, 0, 77, 0, 111, 0, 111, 0, 110, 0, 46, 0, 0, 0, 3, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
}
type customFontCache map[string]*truetype.Font
func (fc customFontCache) Store(fd draw2d.FontData, font *truetype.Font) {
fc[fd.Name] = font
}
func (fc customFontCache) Load(fd draw2d.FontData) (*truetype.Font, error) {
font, stored := fc[fd.Name]
if !stored {
return nil, fmt.Errorf("font %s is not stored in font cache", fd.Name)
}
return font, nil
}
func initFontCache() { // init font cache
fontCache := customFontCache{}
// add gofont to cache
gofont, err := truetype.Parse(goregular.TTF)
if err != nil {
panic(err)
}
fontCache.Store(draw2d.FontData{Name: "goregular"}, gofont)
// add icofont to cache
icofont, err := truetype.Parse(icoTTF)
if err != nil {
panic(err)
}
fontCache.Store(draw2d.FontData{Name: "ico"}, icofont)
draw2d.SetFontCache(fontCache)
}
func TestCurveIndexOutOfRange(t *testing.T) {
initFontCache()
// Initialize the graphic context on an RGBA image
dest := image.NewRGBA(image.Rect(0, 0, 512, 512))
gc := NewGraphicContext(dest)
// background
gc.SetFillColor(color.RGBA{0xef, 0xef, 0xef, 0xff})
draw2dkit.Rectangle(gc, 0, 0, 512, 512)
gc.Fill()
// text
gc.SetFontSize(20)
gc.SetFillColor(color.RGBA{0x10, 0x10, 0x10, 0xff})
gc.SetFontData(draw2d.FontData{Name: "goregular"})
// gc.FillStringAt("Hello", 128, 120) // this works well
gc.SetFontData(draw2d.FontData{Name: "ico"})
gc.FillStringAt("\u25cb", 128, 150) // this also works
gc.FillStringAt("\u2716", 128, 170) // Works now
SaveToPngFile("_test_hello.png", dest)
}

View File

@ -4,17 +4,17 @@
package draw2dimg
import (
"errors"
"image"
"image/color"
"log"
"math"
"github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/draw2dbase"
"git.fromouter.space/crunchy-rocks/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d/draw2dbase"
"git.fromouter.space/crunchy-rocks/emoji"
"github.com/golang/freetype/raster"
"github.com/golang/freetype/truetype"
"git.fromouter.space/crunchy-rocks/freetype/raster"
"git.fromouter.space/crunchy-rocks/freetype/truetype"
"golang.org/x/image/draw"
"golang.org/x/image/font"
@ -35,8 +35,11 @@ type GraphicContext struct {
painter Painter
fillRasterizer *raster.Rasterizer
strokeRasterizer *raster.Rasterizer
FontCache draw2d.FontCache
glyphCache draw2dbase.GlyphCache
glyphBuf *truetype.GlyphBuf
DPI int
Emojis emoji.Table
}
// ImageFilter defines the type of filter to use
@ -53,7 +56,6 @@ const (
// NewGraphicContext creates a new Graphic context from an image.
func NewGraphicContext(img draw.Image) *GraphicContext {
var painter Painter
switch selectImage := img.(type) {
case *image.RGBA:
@ -74,8 +76,11 @@ func NewGraphicContextWithPainter(img draw.Image, painter Painter) *GraphicConte
painter,
raster.NewRasterizer(width, height),
raster.NewRasterizer(width, height),
draw2d.GetGlobalFontCache(),
draw2dbase.NewGlyphCache(),
&truetype.GlyphBuf{},
dpi,
make(emoji.Table),
}
return gc
}
@ -108,7 +113,7 @@ func DrawImage(src image.Image, dest draw.Image, tr draw2d.Matrix, op draw.Op, f
case BicubicFilter:
transformer = draw.CatmullRom
}
transformer.Transform(dest, f64.Aff3{tr[0], tr[1], tr[4], tr[2], tr[3], tr[5]}, src, src.Bounds(), draw.Over, nil)
transformer.Transform(dest, f64.Aff3{tr[0], tr[1], tr[4], tr[2], tr[3], tr[5]}, src, src.Bounds(), op, nil)
}
// DrawImage draws the raster image in the current canvas
@ -117,40 +122,98 @@ func (gc *GraphicContext) DrawImage(img image.Image) {
}
// FillString draws the text at point (0, 0)
func (gc *GraphicContext) FillString(text string) (cursor float64) {
func (gc *GraphicContext) FillString(text string) (width float64) {
return gc.FillStringAt(text, 0, 0)
}
const emojiSpacing = 10
const emojiScale = 110
// FillStringAt draws the text at the specified point (x, y)
func (gc *GraphicContext) FillStringAt(text string, x, y float64) (cursor float64) {
width := gc.CreateStringPath(text, x, y)
gc.Fill()
return width
func (gc *GraphicContext) FillStringAt(text string, x, y float64) (width float64) {
f, err := gc.loadCurrentFont()
if err != nil {
log.Println(err)
return 0.0
}
startx := x
prev, hasPrev := truetype.Index(0), false
fontName := gc.GetFontName()
for fragment := range gc.Emojis.Iterate(text) {
if fragment.IsEmoji {
img, err := LoadFromPngFile(fragment.Emoji.Path)
if err == nil {
gc.Save()
scale := gc.GetFontSize() / 100
gc.Translate(x+scale*emojiSpacing, y-scale*emojiScale)
gc.Scale(scale, scale)
gc.DrawImage(img)
gc.Restore()
x += scale*float64(img.Bounds().Size().X) + scale*emojiSpacing*2
}
continue
}
index := f.Index(fragment.Rune)
if hasPrev {
x += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index))
}
glyph := gc.glyphCache.Fetch(gc, fontName, fragment.Rune)
x += glyph.Fill(gc, x, y)
prev, hasPrev = index, true
}
return x - startx
}
// StrokeString draws the contour of the text at point (0, 0)
func (gc *GraphicContext) StrokeString(text string) (cursor float64) {
func (gc *GraphicContext) StrokeString(text string) (width float64) {
return gc.StrokeStringAt(text, 0, 0)
}
// StrokeStringAt draws the contour of the text at point (x, y)
func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64) {
width := gc.CreateStringPath(text, x, y)
gc.Stroke()
return width
func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (width float64) {
f, err := gc.loadCurrentFont()
if err != nil {
log.Println(err)
return 0.0
}
startx := x
prev, hasPrev := truetype.Index(0), false
fontName := gc.GetFontName()
for fragment := range gc.Emojis.Iterate(text) {
if fragment.IsEmoji {
img, err := LoadFromPngFile(fragment.Emoji.Path)
if err == nil {
gc.Save()
scale := gc.GetFontSize() / 100
gc.Translate(x+scale*emojiSpacing, y-scale*emojiScale)
gc.Scale(scale, scale)
gc.DrawImage(img)
gc.Restore()
x += scale*float64(img.Bounds().Size().X) + scale*emojiSpacing*2
}
continue
}
index := f.Index(fragment.Rune)
if hasPrev {
x += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index))
}
glyph := gc.glyphCache.Fetch(gc, fontName, fragment.Rune)
x += glyph.Stroke(gc, x, y)
prev, hasPrev = index, true
}
return x - startx
}
func (gc *GraphicContext) loadCurrentFont() (*truetype.Font, error) {
font := draw2d.GetFont(gc.Current.FontData)
if font == nil {
font = draw2d.GetFont(draw2dbase.DefaultFontData)
font, err := gc.FontCache.Load(gc.Current.FontData)
if err != nil {
font, err = gc.FontCache.Load(draw2dbase.DefaultFontData)
}
if font == nil {
return nil, errors.New("No font set, and no default font available.")
if font != nil {
gc.SetFont(font)
gc.SetFontSize(gc.Current.FontSize)
}
gc.SetFont(font)
gc.SetFontSize(gc.Current.FontSize)
return font, nil
return font, err
}
// p is a truetype.Point measured in FUnits and positive Y going upwards.
@ -212,8 +275,39 @@ func (gc *GraphicContext) GetStringBounds(s string) (left, top, right, bottom fl
top, left, bottom, right = 10e6, 10e6, -10e6, -10e6
cursor := 0.0
prev, hasPrev := truetype.Index(0), false
for _, rune := range s {
index := f.Index(rune)
// Get sample letter for approximated emoji calculations
const letter = 'M'
mindex := f.Index(letter)
mtop, mleft, mheight, mwidth := 10e6, 10e6, -10e6, -10e6
if err := gc.glyphBuf.Load(gc.Current.Font, fixed.Int26_6(gc.Current.Scale), mindex, font.HintingNone); err != nil {
log.Println(err)
return 0, 0, 0, 0
}
e0 := 0
for _, e1 := range gc.glyphBuf.Ends {
ps := gc.glyphBuf.Points[e0:e1]
for _, p := range ps {
x, y := pointToF64Point(p)
mtop = math.Min(mtop, y)
mheight = math.Max(mheight, y)
mleft = math.Min(mleft, x)
mwidth = math.Max(mwidth, x)
}
}
mtop *= 1.2
mwidth *= 1.55
mheight += math.Abs(mtop-mheight) * 0.2
// Actually iterate through the string
for fragment := range gc.Emojis.Iterate(s) {
if fragment.IsEmoji {
cursor += mwidth
top = math.Min(top, mtop)
bottom = math.Max(bottom, mheight)
left = math.Min(left, mleft)
right = math.Max(right, cursor)
continue
}
index := f.Index(fragment.Rune)
if hasPrev {
cursor += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index))
}

View File

@ -4,7 +4,7 @@
package draw2dimg
import (
"github.com/golang/freetype/raster"
"git.fromouter.space/crunchy-rocks/freetype/raster"
"golang.org/x/image/math/fixed"
)

View File

@ -1,8 +1,8 @@
package draw2dimg
import (
"github.com/golang/freetype/truetype"
"github.com/llgcode/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d"
"git.fromouter.space/crunchy-rocks/freetype/truetype"
"golang.org/x/image/math/fixed"
)

View File

@ -14,7 +14,7 @@ import (
"os"
"strconv"
"github.com/golang/freetype/truetype"
"git.fromouter.space/crunchy-rocks/freetype/truetype"
"github.com/jung-kurt/gofpdf"
"github.com/llgcode/draw2d"

176
draw2dsvg/converters.go Normal file
View File

@ -0,0 +1,176 @@
// Copyright 2015 The draw2d Authors. All rights reserved.
// created: 16/12/2017 by Drahoslav Bednářpackage draw2dsvg
package draw2dsvg
import (
"bytes"
"encoding/base64"
"fmt"
"image"
"image/color"
"image/png"
"math"
"strconv"
"strings"
"git.fromouter.space/crunchy-rocks/draw2d"
)
func toSvgRGBA(c color.Color) string {
r, g, b, a := c.RGBA()
r, g, b, a = r>>8, g>>8, b>>8, a>>8
if a == 255 {
return optiSprintf("#%02X%02X%02X", r, g, b)
}
return optiSprintf("rgba(%v,%v,%v,%f)", r, g, b, float64(a)/255)
}
func toSvgLength(l float64) string {
if math.IsInf(l, 1) {
return "100%"
}
return optiSprintf("%f", l)
}
func toSvgArray(nums []float64) string {
arr := make([]string, len(nums))
for i, num := range nums {
arr[i] = optiSprintf("%f", num)
}
return strings.Join(arr, ",")
}
func toSvgFillRule(rule draw2d.FillRule) string {
return map[draw2d.FillRule]string{
draw2d.FillRuleEvenOdd: "evenodd",
draw2d.FillRuleWinding: "nonzero",
}[rule]
}
func toSvgPathDesc(p *draw2d.Path) string {
parts := make([]string, len(p.Components))
ps := p.Points
for i, cmp := range p.Components {
switch cmp {
case draw2d.MoveToCmp:
parts[i] = optiSprintf("M %f,%f", ps[0], ps[1])
ps = ps[2:]
case draw2d.LineToCmp:
parts[i] = optiSprintf("L %f,%f", ps[0], ps[1])
ps = ps[2:]
case draw2d.QuadCurveToCmp:
parts[i] = optiSprintf("Q %f,%f %f,%f", ps[0], ps[1], ps[2], ps[3])
ps = ps[4:]
case draw2d.CubicCurveToCmp:
parts[i] = optiSprintf("C %f,%f %f,%f %f,%f", ps[0], ps[1], ps[2], ps[3], ps[4], ps[5])
ps = ps[6:]
case draw2d.ArcToCmp:
cx, cy := ps[0], ps[1] // center
rx, ry := ps[2], ps[3] // radii
fi := ps[4] + ps[5] // startAngle + angle
// compute endpoint
sinfi, cosfi := math.Sincos(fi)
nom := math.Hypot(ry*cosfi, rx*sinfi)
x := cx + (rx*ry*cosfi)/nom
y := cy + (rx*ry*sinfi)/nom
// compute large and sweep flags
large := 0
sweep := 0
if math.Abs(ps[5]) > math.Pi {
large = 1
}
if !math.Signbit(ps[5]) {
sweep = 1
}
// dirty hack to ensure whole arc is drawn
// if start point equals end point
if sweep == 1 {
x += 0.01 * sinfi
y += 0.01 * -cosfi
} else {
x += 0.01 * sinfi
y += 0.01 * cosfi
}
// rx ry x-axis-rotation large-arc-flag sweep-flag x y
parts[i] = optiSprintf("A %f %f %v %v %v %F %F",
rx, ry, 0, large, sweep, x, y,
)
ps = ps[6:]
case draw2d.CloseCmp:
parts[i] = "Z"
}
}
return strings.Join(parts, " ")
}
func toSvgTransform(mat draw2d.Matrix) string {
if mat.IsIdentity() {
return ""
}
if mat.IsTranslation() {
x, y := mat.GetTranslation()
return optiSprintf("translate(%f,%f)", x, y)
}
return optiSprintf("matrix(%f,%f,%f,%f,%f,%f)",
mat[0], mat[1], mat[2], mat[3], mat[4], mat[5],
)
}
func imageToSvgHref(image image.Image) string {
out := "data:image/png;base64,"
pngBuf := &bytes.Buffer{}
png.Encode(pngBuf, image)
out += base64.RawStdEncoding.EncodeToString(pngBuf.Bytes())
return out
}
// Do the same thing as fmt.Sprintf
// except it uses the optimal precition for floats: (0-3) for f and (0-6) for F
// eg.:
// optiSprintf("%f", 3.0) => fmt.Sprintf("%.0f", 3.0)
// optiSprintf("%f", 3.33) => fmt.Sprintf("%.2f", 3.33)
// optiSprintf("%f", 3.3001) => fmt.Sprintf("%.1f", 3.3001)
// optiSprintf("%f", 3.333333333333333) => fmt.Sprintf("%.3f", 3.333333333333333)
// optiSprintf("%F", 3.333333333333333) => fmt.Sprintf("%.6f", 3.333333333333333)
func optiSprintf(format string, a ...interface{}) string {
chunks := strings.Split(format, "%")
newChunks := make([]string, len(chunks))
for i, chunk := range chunks {
if i != 0 {
verb := chunk[0]
if verb == 'f' || verb == 'F' {
num := a[i-1].(float64)
p := strconv.Itoa(getPrec(num, verb == 'F'))
chunk = strings.Replace(chunk, string(verb), "."+p+"f", 1)
}
}
newChunks[i] = chunk
}
format = strings.Join(newChunks, "%")
return fmt.Sprintf(format, a...)
}
// TODO needs test, since it is not quiet right
func getPrec(num float64, better bool) int {
max := 3
eps := 0.0005
if better {
max = 6
eps = 0.0000005
}
prec := 0
for math.Mod(num, 1) > eps {
num *= 10
eps *= 10
prec++
}
if max < prec {
return max
}
return prec
}

11
draw2dsvg/doc.go Normal file
View File

@ -0,0 +1,11 @@
// Copyright 2015 The draw2d Authors. All rights reserved.
// created: 16/12/2017 by Drahoslav Bednář
// Package draw2svg provides a graphic context that can draw
// vector graphics and text on svg file.
//
// Quick Start
// The following Go code geneartes a simple drawing and saves it
// to a svg document:
// TODO
package draw2dsvg

22
draw2dsvg/fileutil.go Normal file
View File

@ -0,0 +1,22 @@
package draw2dsvg
import (
"encoding/xml"
_ "errors"
"os"
)
func SaveToSvgFile(filePath string, svg *Svg) error {
f, err := os.Create(filePath)
if err != nil {
return err
}
defer f.Close()
f.Write([]byte(xml.Header))
encoder := xml.NewEncoder(f)
encoder.Indent("", "\t")
err = encoder.Encode(svg)
return err
}

403
draw2dsvg/gc.go Normal file
View File

@ -0,0 +1,403 @@
// Copyright 2015 The draw2d Authors. All rights reserved.
// created: 16/12/2017 by Drahoslav Bednář
package draw2dsvg
import (
"image"
"log"
"math"
"strconv"
"strings"
"git.fromouter.space/crunchy-rocks/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d/draw2dbase"
"git.fromouter.space/crunchy-rocks/freetype/truetype"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
)
type drawType int
const (
filled drawType = 1 << iota
stroked
)
// GraphicContext implements the draw2d.GraphicContext interface
// It provides draw2d with a svg backend
type GraphicContext struct {
*draw2dbase.StackGraphicContext
FontCache draw2d.FontCache
glyphCache draw2dbase.GlyphCache
glyphBuf *truetype.GlyphBuf
svg *Svg
DPI int
}
func NewGraphicContext(svg *Svg) *GraphicContext {
gc := &GraphicContext{
draw2dbase.NewStackGraphicContext(),
draw2d.GetGlobalFontCache(),
draw2dbase.NewGlyphCache(),
&truetype.GlyphBuf{},
svg,
92,
}
return gc
}
// Clear fills the current canvas with a default transparent color
func (gc *GraphicContext) Clear() {
gc.svg.Groups = nil
}
// Stroke strokes the paths with the color specified by SetStrokeColor
func (gc *GraphicContext) Stroke(paths ...*draw2d.Path) {
gc.drawPaths(stroked, paths...)
gc.Current.Path.Clear()
}
// Fill fills the paths with the color specified by SetFillColor
func (gc *GraphicContext) Fill(paths ...*draw2d.Path) {
gc.drawPaths(filled, paths...)
gc.Current.Path.Clear()
}
// FillStroke first fills the paths and than strokes them
func (gc *GraphicContext) FillStroke(paths ...*draw2d.Path) {
gc.drawPaths(filled|stroked, paths...)
gc.Current.Path.Clear()
}
// FillString draws the text at point (0, 0)
func (gc *GraphicContext) FillString(text string) (cursor float64) {
return gc.FillStringAt(text, 0, 0)
}
// FillStringAt draws the text at the specified point (x, y)
func (gc *GraphicContext) FillStringAt(text string, x, y float64) (cursor float64) {
return gc.drawString(text, filled, x, y)
}
// StrokeString draws the contour of the text at point (0, 0)
func (gc *GraphicContext) StrokeString(text string) (cursor float64) {
return gc.StrokeStringAt(text, 0, 0)
}
// StrokeStringAt draws the contour of the text at point (x, y)
func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64) {
return gc.drawString(text, stroked, x, y)
}
// Save the context and push it to the context stack
func (gc *GraphicContext) Save() {
gc.StackGraphicContext.Save()
// TODO use common transformation group for multiple elements
}
// Restore remove the current context and restore the last one
func (gc *GraphicContext) Restore() {
gc.StackGraphicContext.Restore()
// TODO use common transformation group for multiple elements
}
func (gc *GraphicContext) SetDPI(dpi int) {
gc.DPI = dpi
gc.recalc()
}
func (gc *GraphicContext) GetDPI() int {
return gc.DPI
}
// SetFont sets the font used to draw text.
func (gc *GraphicContext) SetFont(font *truetype.Font) {
gc.Current.Font = font
}
// SetFontSize sets the font size in points (as in “a 12 point font”).
func (gc *GraphicContext) SetFontSize(fontSize float64) {
gc.Current.FontSize = fontSize
gc.recalc()
}
// DrawImage draws the raster image in the current canvas
func (gc *GraphicContext) DrawImage(image image.Image) {
bounds := image.Bounds()
svgImage := &Image{Href: imageToSvgHref(image)}
svgImage.X = float64(bounds.Min.X)
svgImage.Y = float64(bounds.Min.Y)
svgImage.Width = toSvgLength(float64(bounds.Max.X - bounds.Min.X))
svgImage.Height = toSvgLength(float64(bounds.Max.Y - bounds.Min.Y))
gc.newGroup(0).Image = svgImage
}
// ClearRect fills the specified rectangle with a default transparent color
func (gc *GraphicContext) ClearRect(x1, y1, x2, y2 int) {
mask := gc.newMask(x1, y1, x2-x1, y2-y1)
newGroup := &Group{
Groups: gc.svg.Groups,
Mask: "url(#" + mask.Id + ")",
}
// replace groups with new masked group
gc.svg.Groups = []*Group{newGroup}
}
// NOTE following two functions and soe other further below copied from dwra2d{img|gl}
// TODO move them all to common draw2dbase?
// CreateStringPath creates a path from the string s at x, y, and returns the string width.
// The text is placed so that the left edge of the em square of the first character of s
// and the baseline intersect at x, y. The majority of the affected pixels will be
// above and to the right of the point, but some may be below or to the left.
// For example, drawing a string that starts with a 'J' in an italic font may
// affect pixels below and left of the point.
func (gc *GraphicContext) CreateStringPath(s string, x, y float64) (cursor float64) {
f, err := gc.loadCurrentFont()
if err != nil {
log.Println(err)
return 0.0
}
startx := x
prev, hasPrev := truetype.Index(0), false
for _, rune := range s {
index := f.Index(rune)
if hasPrev {
x += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index))
}
err := gc.drawGlyph(index, x, y)
if err != nil {
log.Println(err)
return startx - x
}
x += fUnitsToFloat64(f.HMetric(fixed.Int26_6(gc.Current.Scale), index).AdvanceWidth)
prev, hasPrev = index, true
}
return x - startx
}
// GetStringBounds returns the approximate pixel bounds of the string s at x, y.
// The the left edge of the em square of the first character of s
// and the baseline intersect at 0, 0 in the returned coordinates.
// Therefore the top and left coordinates may well be negative.
func (gc *GraphicContext) GetStringBounds(s string) (left, top, right, bottom float64) {
f, err := gc.loadCurrentFont()
if err != nil {
log.Println(err)
return 0, 0, 0, 0
}
if gc.Current.Scale == 0 {
panic("zero scale")
}
top, left, bottom, right = 10e6, 10e6, -10e6, -10e6
cursor := 0.0
prev, hasPrev := truetype.Index(0), false
for _, rune := range s {
index := f.Index(rune)
if hasPrev {
cursor += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index))
}
if err := gc.glyphBuf.Load(gc.Current.Font, fixed.Int26_6(gc.Current.Scale), index, font.HintingNone); err != nil {
log.Println(err)
return 0, 0, 0, 0
}
e0 := 0
for _, e1 := range gc.glyphBuf.Ends {
ps := gc.glyphBuf.Points[e0:e1]
for _, p := range ps {
x, y := pointToF64Point(p)
top = math.Min(top, y)
bottom = math.Max(bottom, y)
left = math.Min(left, x+cursor)
right = math.Max(right, x+cursor)
}
}
cursor += fUnitsToFloat64(f.HMetric(fixed.Int26_6(gc.Current.Scale), index).AdvanceWidth)
prev, hasPrev = index, true
}
return left, top, right, bottom
}
////////////////////
// private funcitons
func (gc *GraphicContext) drawPaths(drawType drawType, paths ...*draw2d.Path) {
// create elements
svgPath := Path{}
group := gc.newGroup(drawType)
// set attrs to path element
paths = append(paths, gc.Current.Path)
svgPathsDesc := make([]string, len(paths))
// multiple pathes has to be joined to single svg path description
// because fill-rule wont work for whole group as excepted
for i, path := range paths {
svgPathsDesc[i] = toSvgPathDesc(path)
}
svgPath.Desc = strings.Join(svgPathsDesc, " ")
// attach to group
group.Paths = []*Path{&svgPath}
}
// Add text element to svg and returns its expected width
func (gc *GraphicContext) drawString(text string, drawType drawType, x, y float64) float64 {
switch gc.svg.FontMode {
case PathFontMode:
w := gc.CreateStringPath(text, x, y)
gc.drawPaths(drawType)
gc.Current.Path.Clear()
return w
case SvgFontMode:
gc.embedSvgFont(text)
}
// create elements
svgText := Text{}
group := gc.newGroup(drawType)
// set attrs to text element
svgText.Text = text
svgText.FontSize = gc.Current.FontSize
svgText.X = x
svgText.Y = y
svgText.FontFamily = gc.Current.FontData.Name
// attach to group
group.Texts = []*Text{&svgText}
left, _, right, _ := gc.GetStringBounds(text)
return right - left
}
// Creates new group from current context
// attach it to svg and return
func (gc *GraphicContext) newGroup(drawType drawType) *Group {
group := Group{}
// set attrs to group
if drawType&stroked == stroked {
group.Stroke = toSvgRGBA(gc.Current.StrokeColor)
group.StrokeWidth = toSvgLength(gc.Current.LineWidth)
group.StrokeLinecap = gc.Current.Cap.String()
group.StrokeLinejoin = gc.Current.Join.String()
if len(gc.Current.Dash) > 0 {
group.StrokeDasharray = toSvgArray(gc.Current.Dash)
group.StrokeDashoffset = toSvgLength(gc.Current.DashOffset)
}
}
if drawType&filled == filled {
group.Fill = toSvgRGBA(gc.Current.FillColor)
group.FillRule = toSvgFillRule(gc.Current.FillRule)
}
group.Transform = toSvgTransform(gc.Current.Tr)
// attach
gc.svg.Groups = append(gc.svg.Groups, &group)
return &group
}
// creates new mask attached to svg
func (gc *GraphicContext) newMask(x, y, width, height int) *Mask {
mask := &Mask{}
mask.X = float64(x)
mask.Y = float64(y)
mask.Width = toSvgLength(float64(width))
mask.Height = toSvgLength(float64(height))
// attach mask
gc.svg.Masks = append(gc.svg.Masks, mask)
mask.Id = "mask-" + strconv.Itoa(len(gc.svg.Masks))
return mask
}
// Embed svg font definition to svg tree itself
// Or update existing if already exists for curent font data
func (gc *GraphicContext) embedSvgFont(text string) *Font {
fontName := gc.Current.FontData.Name
gc.loadCurrentFont()
// find or create font Element
svgFont := (*Font)(nil)
for _, font := range gc.svg.Fonts {
if font.Name == fontName {
svgFont = font
break
}
}
if svgFont == nil {
// create new
svgFont = &Font{}
// and attach
gc.svg.Fonts = append(gc.svg.Fonts, svgFont)
}
// fill with glyphs
gc.Save()
defer gc.Restore()
gc.SetFontSize(2048)
defer gc.SetDPI(gc.GetDPI())
gc.SetDPI(92)
filling:
for _, rune := range text {
for _, g := range svgFont.Glyphs {
if g.Rune == Rune(rune) {
continue filling
}
}
glyph := gc.glyphCache.Fetch(gc, gc.GetFontName(), rune)
// glyphCache.Load indirectly calls CreateStringPath for single rune string
glypPath := glyph.Path.VerticalFlip() // svg font glyphs have oposite y axe
svgFont.Glyphs = append(svgFont.Glyphs, &Glyph{
Rune: Rune(rune),
Desc: toSvgPathDesc(glypPath),
HorizAdvX: glyph.Width,
})
}
// set attrs
svgFont.Id = "font-" + strconv.Itoa(len(gc.svg.Fonts))
svgFont.Name = fontName
// TODO use css @font-face with id instead of this
svgFont.Face = &Face{Family: fontName, Units: 2048, HorizAdvX: 2048}
return svgFont
}
func (gc *GraphicContext) loadCurrentFont() (*truetype.Font, error) {
font, err := gc.FontCache.Load(gc.Current.FontData)
if err != nil {
font, err = gc.FontCache.Load(draw2dbase.DefaultFontData)
}
if font != nil {
gc.SetFont(font)
gc.SetFontSize(gc.Current.FontSize)
}
return font, err
}
func (gc *GraphicContext) drawGlyph(glyph truetype.Index, dx, dy float64) error {
if err := gc.glyphBuf.Load(gc.Current.Font, fixed.Int26_6(gc.Current.Scale), glyph, font.HintingNone); err != nil {
return err
}
e0 := 0
for _, e1 := range gc.glyphBuf.Ends {
DrawContour(gc, gc.glyphBuf.Points[e0:e1], dx, dy)
e0 = e1
}
return nil
}
// recalc recalculates scale and bounds values from the font size, screen
// resolution and font metrics, and invalidates the glyph cache.
func (gc *GraphicContext) recalc() {
gc.Current.Scale = gc.Current.FontSize * float64(gc.DPI) * (64.0 / 72.0)
}

65
draw2dsvg/samples_test.go Normal file
View File

@ -0,0 +1,65 @@
// Copyright 2015 The draw2d Authors. All rights reserved.
// created: 26/06/2015 by Stani Michiels
// See also test_test.go
package draw2dsvg_test
import (
"testing"
"git.fromouter.space/crunchy-rocks/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d/samples/android"
"git.fromouter.space/crunchy-rocks/draw2d/samples/frameimage"
"git.fromouter.space/crunchy-rocks/draw2d/samples/geometry"
"git.fromouter.space/crunchy-rocks/draw2d/samples/gopher"
"git.fromouter.space/crunchy-rocks/draw2d/samples/gopher2"
"git.fromouter.space/crunchy-rocks/draw2d/samples/helloworld"
"git.fromouter.space/crunchy-rocks/draw2d/samples/line"
"git.fromouter.space/crunchy-rocks/draw2d/samples/linecapjoin"
"git.fromouter.space/crunchy-rocks/draw2d/samples/postscript"
)
func TestSampleAndroid(t *testing.T) {
test(t, android.Main)
}
// TODO: FillString: w (width) is incorrect
func TestSampleGeometry(t *testing.T) {
// Set the global folder for searching fonts
// The pdf backend needs for every ttf file its corresponding
// json/.z file which is generated by gofpdf/makefont.
draw2d.SetFontFolder("../resource/font")
test(t, geometry.Main)
}
func TestSampleGopher(t *testing.T) {
test(t, gopher.Main)
}
func TestSampleGopher2(t *testing.T) {
test(t, gopher2.Main)
}
func TestSampleHelloWorld(t *testing.T) {
// Set the global folder for searching fonts
// The pdf backend needs for every ttf file its corresponding
// json/.z file which is generated by gofpdf/makefont.
draw2d.SetFontFolder("../resource/font")
test(t, helloworld.Main)
}
func TestSampleFrameImage(t *testing.T) {
test(t, frameimage.Main)
}
func TestSampleLine(t *testing.T) {
test(t, line.Main)
}
func TestSampleLineCap(t *testing.T) {
test(t, linecapjoin.Main)
}
func TestSamplePostscript(t *testing.T) {
test(t, postscript.Main)
}

172
draw2dsvg/svg.go Normal file
View File

@ -0,0 +1,172 @@
// Copyright 2015 The draw2d Authors. All rights reserved.
// created: 16/12/2017 by Drahoslav Bednář
package draw2dsvg
import (
"encoding/xml"
)
/* svg elements */
type FontMode int
// Modes of font handling in svg
const (
// Does nothing special
// Makes sense only for common system fonts
SysFontMode FontMode = 1 << iota
// Links font files in css def
// Requires distribution of font files with outputed svg
LinkFontMode // TODO implement
// Embeds glyphs definition in svg file itself in svg font format
// Has poor browser support
SvgFontMode
// Embeds font definiton in svg file itself in woff format as part of css def
CssFontMode // TODO implement
// Converts texts to paths
PathFontMode
)
type Svg struct {
XMLName xml.Name `xml:"svg"`
Xmlns string `xml:"xmlns,attr"`
Fonts []*Font `xml:"defs>font"`
Masks []*Mask `xml:"defs>mask"`
Groups []*Group `xml:"g"`
FontMode FontMode `xml:"-"`
FillStroke
}
func NewSvg() *Svg {
return &Svg{
Xmlns: "http://www.w3.org/2000/svg",
FillStroke: FillStroke{Fill: "none", Stroke: "none"},
FontMode: PathFontMode,
}
}
type Group struct {
FillStroke
Transform string `xml:"transform,attr,omitempty"`
Groups []*Group `xml:"g"`
Paths []*Path `xml:"path"`
Texts []*Text `xml:"text"`
Image *Image `xml:"image"`
Mask string `xml:"mask,attr,omitempty"`
}
type Path struct {
FillStroke
Desc string `xml:"d,attr"`
}
type Text struct {
FillStroke
Position
FontSize float64 `xml:"font-size,attr,omitempty"`
FontFamily string `xml:"font-family,attr,omitempty"`
Text string `xml:",innerxml"`
Style string `xml:"style,attr,omitempty"`
}
type Image struct {
Position
Dimension
Href string `xml:"href,attr"`
}
type Mask struct {
Identity
Position
Dimension
}
type Rect struct {
Position
Dimension
FillStroke
}
func (m Mask) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
bigRect := Rect{}
bigRect.X, bigRect.Y = 0, 0
bigRect.Width, bigRect.Height = "100%", "100%"
bigRect.Fill = "#fff"
rect := Rect{}
rect.X, rect.Y = m.X, m.Y
rect.Width, rect.Height = m.Width, m.Height
rect.Fill = "#000"
return e.EncodeElement(struct {
XMLName xml.Name `xml:"mask"`
Rects [2]Rect `xml:"rect"`
Id string `xml:"id,attr"`
}{
Rects: [2]Rect{bigRect, rect},
Id: m.Id,
}, start)
}
/* font related elements */
type Font struct {
Identity
Face *Face `xml:"font-face"`
Glyphs []*Glyph `xml:"glyph"`
}
type Face struct {
Family string `xml:"font-family,attr"`
Units int `xml:"units-per-em,attr"`
HorizAdvX float64 `xml:"horiz-adv-x,attr"`
// TODO add other attrs, like style, variant, weight...
}
type Glyph struct {
Rune Rune `xml:"unicode,attr"`
Desc string `xml:"d,attr"`
HorizAdvX float64 `xml:"horiz-adv-x,attr"`
}
type Rune rune
func (r Rune) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
return xml.Attr{
Name: name,
Value: string(rune(r)),
}, nil
}
/* shared attrs */
type Identity struct {
Id string `xml:"id,attr"`
Name string `xml:"name,attr"`
}
type Position struct {
X float64 `xml:"x,attr,omitempty"`
Y float64 `xml:"y,attr,omitempty"`
}
type Dimension struct {
Width string `xml:"width,attr"`
Height string `xml:"height,attr"`
}
type FillStroke struct {
Fill string `xml:"fill,attr,omitempty"`
FillRule string `xml:"fill-rule,attr,omitempty"`
Stroke string `xml:"stroke,attr,omitempty"`
StrokeWidth string `xml:"stroke-width,attr,omitempty"`
StrokeLinecap string `xml:"stroke-linecap,attr,omitempty"`
StrokeLinejoin string `xml:"stroke-linejoin,attr,omitempty"`
StrokeDasharray string `xml:"stroke-dasharray,attr,omitempty"`
StrokeDashoffset string `xml:"stroke-dashoffset,attr,omitempty"`
}

32
draw2dsvg/test_test.go Normal file
View File

@ -0,0 +1,32 @@
// Copyright 2015 The draw2d Authors. All rights reserved.
// created: 16/12/2017 by Drahoslav Bednář
// Package draw2dsvg_test gives test coverage with the command:
// go test -cover ./... | grep -v "no test"
// (It should be run from its parent draw2d directory.)
package draw2dsvg_test
import (
"testing"
"git.fromouter.space/crunchy-rocks/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d/draw2dsvg"
)
type sample func(gc draw2d.GraphicContext, ext string) (string, error)
func test(t *testing.T, draw sample) {
// Initialize the graphic context on an pdf document
dest := draw2dsvg.NewSvg()
gc := draw2dsvg.NewGraphicContext(dest)
// Draw sample
output, err := draw(gc, "svg")
if err != nil {
t.Errorf("Drawing %q failed: %v", output, err)
return
}
err = draw2dsvg.SaveToSvgFile(output, dest)
if err != nil {
t.Errorf("Saving %q failed: %v", output, err)
}
}

83
draw2dsvg/text.go Normal file
View File

@ -0,0 +1,83 @@
// NOTE that this is identical copy of draw2dgl/text.go and draw2dimg/text.go
package draw2dsvg
import (
"git.fromouter.space/crunchy-rocks/draw2d"
"git.fromouter.space/crunchy-rocks/freetype/truetype"
"golang.org/x/image/math/fixed"
)
// DrawContour draws the given closed contour at the given sub-pixel offset.
func DrawContour(path draw2d.PathBuilder, ps []truetype.Point, dx, dy float64) {
if len(ps) == 0 {
return
}
startX, startY := pointToF64Point(ps[0])
path.MoveTo(startX+dx, startY+dy)
q0X, q0Y, on0 := startX, startY, true
for _, p := range ps[1:] {
qX, qY := pointToF64Point(p)
on := p.Flags&0x01 != 0
if on {
if on0 {
path.LineTo(qX+dx, qY+dy)
} else {
path.QuadCurveTo(q0X+dx, q0Y+dy, qX+dx, qY+dy)
}
} else {
if on0 {
// No-op.
} else {
midX := (q0X + qX) / 2
midY := (q0Y + qY) / 2
path.QuadCurveTo(q0X+dx, q0Y+dy, midX+dx, midY+dy)
}
}
q0X, q0Y, on0 = qX, qY, on
}
// Close the curve.
if on0 {
path.LineTo(startX+dx, startY+dy)
} else {
path.QuadCurveTo(q0X+dx, q0Y+dy, startX+dx, startY+dy)
}
}
func pointToF64Point(p truetype.Point) (x, y float64) {
return fUnitsToFloat64(p.X), -fUnitsToFloat64(p.Y)
}
func fUnitsToFloat64(x fixed.Int26_6) float64 {
scaled := x << 2
return float64(scaled/256) + float64(scaled%256)/256.0
}
// FontExtents contains font metric information.
type FontExtents struct {
// Ascent is the distance that the text
// extends above the baseline.
Ascent float64
// Descent is the distance that the text
// extends below the baseline. The descent
// is given as a negative value.
Descent float64
// Height is the distance from the lowest
// descending point to the highest ascending
// point.
Height float64
}
// Extents returns the FontExtents for a font.
// TODO needs to read this https://developer.apple.com/fonts/TrueType-Reference-Manual/RM02/Chap2.html#intro
func Extents(font *truetype.Font, size float64) FontExtents {
bounds := font.Bounds(fixed.Int26_6(font.FUnitsPerEm()))
scale := size / float64(font.FUnitsPerEm())
return FontExtents{
Ascent: float64(bounds.Max.Y) * scale,
Descent: float64(bounds.Min.Y) * scale,
Height: float64(bounds.Max.Y-bounds.Min.Y) * scale,
}
}

58
draw2dsvg/xml_test.go Normal file
View File

@ -0,0 +1,58 @@
// Copyright 2015 The draw2d Authors. All rights reserved.
// created: 16/12/2017 by Drahoslav Bednář
// Package draw2dsvg_test gives test coverage with the command:
// go test -cover ./... | grep -v "no test"
// (It should be run from its parent draw2d directory.)
package draw2dsvg
import (
"encoding/xml"
"testing"
)
// Test basic encoding of svg/xml elements
func TestXml(t *testing.T) {
svg := NewSvg()
svg.Groups = []*Group{&Group{
Groups: []*Group{
&Group{}, // nested groups
&Group{},
},
Texts: []*Text{
&Text{Text: "Hello"}, // text
&Text{Text: "world", Style: "opacity: 0.5"}, // text with style
},
Paths: []*Path{
&Path{Desc: "M100,200 C100,100 250,100 250,200 S400,300 400,200"}, // simple path
&Path{}, // empty path
},
}}
expectedOut := `<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="none">
<defs></defs>
<g>
<g></g>
<g></g>
<path d="M100,200 C100,100 250,100 250,200 S400,300 400,200"></path>
<path d=""></path>
<text>Hello</text>
<text style="opacity: 0.5">world</text>
</g>
</svg>`
out, err := xml.MarshalIndent(svg, "", " ")
if err != nil {
t.Error(err)
}
if string(out) != expectedOut {
t.Errorf("svg output is not as expected\n"+
"got:\n%s\n\n"+
"want:\n%s\n",
string(out),
expectedOut,
)
}
}

181
font.go
View File

@ -6,16 +6,11 @@ package draw2d
import (
"io/ioutil"
"log"
"path"
"path/filepath"
"github.com/golang/freetype/truetype"
)
"sync"
var (
fontFolder = "../resource/font/"
fonts = make(map[string]*truetype.Font)
fontNamer FontFileNamer = FontFileName
"git.fromouter.space/crunchy-rocks/freetype/truetype"
)
// FontStyle defines bold and italic styles for the font
@ -69,41 +64,167 @@ func FontFileName(fontData FontData) string {
}
func RegisterFont(fontData FontData, font *truetype.Font) {
fonts[fontNamer(fontData)] = font
fontCache.Store(fontData, font)
}
func GetFont(fontData FontData) *truetype.Font {
fontFileName := fontNamer(fontData)
font := fonts[fontFileName]
if font != nil {
return font
func GetFont(fontData FontData) (font *truetype.Font) {
var err error
if font, err = fontCache.Load(fontData); err != nil {
log.Println(err)
}
fonts[fontFileName] = loadFont(fontFileName)
return fonts[fontFileName]
return
}
func GetFontFolder() string {
return fontFolder
return defaultFonts.folder
}
func SetFontFolder(folder string) {
fontFolder = filepath.Clean(folder)
defaultFonts.setFolder(filepath.Clean(folder))
}
func GetGlobalFontCache() FontCache {
return fontCache
}
func SetFontNamer(fn FontFileNamer) {
fontNamer = fn
defaultFonts.setNamer(fn)
}
func loadFont(fontFileName string) *truetype.Font {
fontBytes, err := ioutil.ReadFile(path.Join(fontFolder, fontFileName))
if err != nil {
log.Println(err)
return nil
}
font, err := truetype.Parse(fontBytes)
if err != nil {
log.Println(err)
return nil
}
return font
// Types implementing this interface can be passed to SetFontCache to change the
// way fonts are being stored and retrieved.
type FontCache interface {
// Loads a truetype font represented by the FontData object passed as
// argument.
// The method returns an error if the font could not be loaded, either
// because it didn't exist or the resource it was loaded from was corrupted.
Load(FontData) (*truetype.Font, error)
// Sets the truetype font that will be returned by Load when given the font
// data passed as first argument.
Store(FontData, *truetype.Font)
}
// Changes the font cache backend used by the package. After calling this
// functionSetFontFolder and SetFontNamer will not affect anymore how fonts are
// loaded.
// To restore the default font cache, call this function passing nil as argument.
func SetFontCache(cache FontCache) {
if cache == nil {
fontCache = defaultFonts
} else {
fontCache = cache
}
}
// FolderFontCache can Load font from folder
type FolderFontCache struct {
fonts map[string]*truetype.Font
folder string
namer FontFileNamer
}
// NewFolderFontCache creates FolderFontCache
func NewFolderFontCache(folder string) *FolderFontCache {
return &FolderFontCache{
fonts: make(map[string]*truetype.Font),
folder: folder,
namer: FontFileName,
}
}
// Load a font from cache if exists otherwise it will load the font from file
func (cache *FolderFontCache) Load(fontData FontData) (font *truetype.Font, err error) {
if font = cache.fonts[cache.namer(fontData)]; font != nil {
return font, nil
}
var data []byte
var file = cache.namer(fontData)
if data, err = ioutil.ReadFile(filepath.Join(cache.folder, file)); err != nil {
return
}
if font, err = truetype.Parse(data); err != nil {
return
}
cache.fonts[file] = font
return
}
// Store a font to this cache
func (cache *FolderFontCache) Store(fontData FontData, font *truetype.Font) {
cache.fonts[cache.namer(fontData)] = font
}
// SyncFolderFontCache can Load font from folder
type SyncFolderFontCache struct {
sync.RWMutex
fonts map[string]*truetype.Font
folder string
namer FontFileNamer
}
// NewSyncFolderFontCache creates SyncFolderFontCache
func NewSyncFolderFontCache(folder string) *SyncFolderFontCache {
return &SyncFolderFontCache{
fonts: make(map[string]*truetype.Font),
folder: folder,
namer: FontFileName,
}
}
func (cache *SyncFolderFontCache) setFolder(folder string) {
cache.Lock()
cache.folder = folder
cache.Unlock()
}
func (cache *SyncFolderFontCache) setNamer(namer FontFileNamer) {
cache.Lock()
cache.namer = namer
cache.Unlock()
}
// Load a font from cache if exists otherwise it will load the font from file
func (cache *SyncFolderFontCache) Load(fontData FontData) (font *truetype.Font, err error) {
cache.RLock()
font = cache.fonts[cache.namer(fontData)]
cache.RUnlock()
if font != nil {
return font, nil
}
var data []byte
var file = cache.namer(fontData)
if data, err = ioutil.ReadFile(filepath.Join(cache.folder, file)); err != nil {
return
}
if font, err = truetype.Parse(data); err != nil {
return
}
cache.Lock()
cache.fonts[file] = font
cache.Unlock()
return
}
// Store a font to this cache
func (cache *SyncFolderFontCache) Store(fontData FontData, font *truetype.Font) {
cache.Lock()
cache.fonts[cache.namer(fontData)] = font
cache.Unlock()
}
var (
defaultFonts = NewSyncFolderFontCache("../resource/font")
fontCache FontCache = defaultFonts
)

30
gc.go
View File

@ -10,9 +10,12 @@ import (
// GraphicContext describes the interface for the various backends (images, pdf, opengl, ...)
type GraphicContext interface {
// PathBuilder describes the interface for path drawing
PathBuilder
// BeginPath creates a new path
BeginPath()
// GetPath copies the current path, then returns it
GetPath() Path
// GetMatrixTransform returns the current transformation matrix
GetMatrixTransform() Matrix
// SetMatrixTransform sets the current transformation matrix
@ -27,7 +30,7 @@ type GraphicContext interface {
Scale(sx, sy float64)
// SetStrokeColor sets the current stroke color
SetStrokeColor(c color.Color)
// SetStrokeColor sets the current fill color
// SetFillColor sets the current fill color
SetFillColor(c color.Color)
// SetFillRule sets the current fill rule
SetFillRule(f FillRule)
@ -37,27 +40,48 @@ type GraphicContext interface {
SetLineCap(cap LineCap)
// SetLineJoin sets the current line join
SetLineJoin(join LineJoin)
// SetLineJoin sets the current dash
// SetLineDash sets the current dash
SetLineDash(dash []float64, dashOffset float64)
// SetFontSize
// SetFontSize sets the current font size
SetFontSize(fontSize float64)
// GetFontSize gets the current font size
GetFontSize() float64
// SetFontData sets the current FontData
SetFontData(fontData FontData)
// GetFontData gets the current FontData
GetFontData() FontData
// GetFontName gets the current FontData as a string
GetFontName() string
// DrawImage draws the raster image in the current canvas
DrawImage(image image.Image)
// Save the context and push it to the context stack
Save()
// Restore remove the current context and restore the last one
Restore()
// Clear fills the current canvas with a default transparent color
Clear()
// ClearRect fills the specified rectangle with a default transparent color
ClearRect(x1, y1, x2, y2 int)
// SetDPI sets the current DPI
SetDPI(dpi int)
// GetDPI gets the current DPI
GetDPI() int
// GetStringBounds gets pixel bounds(dimensions) of given string
GetStringBounds(s string) (left, top, right, bottom float64)
// CreateStringPath creates a path from the string s at x, y
CreateStringPath(text string, x, y float64) (cursor float64)
// FillString draws the text at point (0, 0)
FillString(text string) (cursor float64)
// FillStringAt draws the text at the specified point (x, y)
FillStringAt(text string, x, y float64) (cursor float64)
// StrokeString draws the contour of the text at point (0, 0)
StrokeString(text string) (cursor float64)
// StrokeStringAt draws the contour of the text at point (x, y)
StrokeStringAt(text string, x, y float64) (cursor float64)
// Stroke strokes the paths with the color specified by SetStrokeColor
Stroke(paths ...*Path)
// Fill fills the paths with the color specified by SetFillColor
Fill(paths ...*Path)
// FillStroke first fills the paths and than strokes them
FillStroke(paths ...*Path)
}

13
go.mod Normal file
View File

@ -0,0 +1,13 @@
module git.fromouter.space/crunchy-rocks/draw2d
require (
git.fromouter.space/crunchy-rocks/emoji v0.0.0-20181116142102-2188aadaf093
git.fromouter.space/crunchy-rocks/freetype v0.0.0-20181116104610-3115318f2577
github.com/go-gl/gl v0.0.0-20180407155706-68e253793080
github.com/go-gl/glfw v0.0.0-20180426074136-46a8d530c326
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/jung-kurt/gofpdf v1.0.0
github.com/llgcode/draw2d v0.0.0-20180825133448-f52c8a71aff0
github.com/llgcode/ps v0.0.0-20150911083025-f1443b32eedb
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 42 KiB

46
path.go
View File

@ -76,9 +76,10 @@ func (p *Path) MoveTo(x, y float64) {
// LineTo adds a line to the current path
func (p *Path) LineTo(x, y float64) {
if len(p.Components) == 0 { //special case when no move has been done
p.MoveTo(0, 0)
p.MoveTo(x, y)
} else {
p.appendToPath(LineToCmp, x, y)
}
p.appendToPath(LineToCmp, x, y)
p.x = x
p.y = y
}
@ -86,9 +87,10 @@ func (p *Path) LineTo(x, y float64) {
// QuadCurveTo adds a quadratic bezier curve to the current path
func (p *Path) QuadCurveTo(cx, cy, x, y float64) {
if len(p.Components) == 0 { //special case when no move has been done
p.MoveTo(0, 0)
p.MoveTo(x, y)
} else {
p.appendToPath(QuadCurveToCmp, cx, cy, x, y)
}
p.appendToPath(QuadCurveToCmp, cx, cy, x, y)
p.x = x
p.y = y
}
@ -96,9 +98,10 @@ func (p *Path) QuadCurveTo(cx, cy, x, y float64) {
// CubicCurveTo adds a cubic bezier curve to the current path
func (p *Path) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) {
if len(p.Components) == 0 { //special case when no move has been done
p.MoveTo(0, 0)
p.MoveTo(x, y)
} else {
p.appendToPath(CubicCurveToCmp, cx1, cy1, cx2, cy2, x, y)
}
p.appendToPath(CubicCurveToCmp, cx1, cy1, cx2, cy2, x, y)
p.x = x
p.y = y
}
@ -187,3 +190,34 @@ func (p *Path) String() string {
}
return s
}
// Returns new Path with flipped y axes
func (path *Path) VerticalFlip() *Path {
p := path.Copy()
j := 0
for _, cmd := range p.Components {
switch cmd {
case MoveToCmp, LineToCmp:
p.Points[j+1] = -p.Points[j+1]
j = j + 2
case QuadCurveToCmp:
p.Points[j+1] = -p.Points[j+1]
p.Points[j+3] = -p.Points[j+3]
j = j + 4
case CubicCurveToCmp:
p.Points[j+1] = -p.Points[j+1]
p.Points[j+3] = -p.Points[j+3]
p.Points[j+5] = -p.Points[j+5]
j = j + 6
case ArcToCmp:
p.Points[j+1] = -p.Points[j+1]
p.Points[j+3] = -p.Points[j+3]
p.Points[j+4] = -p.Points[j+4] // start angle
p.Points[j+5] = -p.Points[j+5] // angle
j = j + 6
case CloseCmp:
}
}
p.y = -p.y
return p
}

View File

@ -8,9 +8,9 @@ import (
"image/color"
"math"
"github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/draw2dkit"
"github.com/llgcode/draw2d/samples"
"git.fromouter.space/crunchy-rocks/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
"git.fromouter.space/crunchy-rocks/draw2d/samples"
)
// Main draws a droid and returns the filename. This should only be

View File

@ -9,9 +9,9 @@ import (
"image/png"
"net/http"
"github.com/llgcode/draw2d/draw2dimg"
"github.com/llgcode/draw2d/draw2dpdf"
"github.com/llgcode/draw2d/samples/android"
"git.fromouter.space/crunchy-rocks/draw2d/draw2dimg"
"git.fromouter.space/crunchy-rocks/draw2d/draw2dpdf"
"git.fromouter.space/crunchy-rocks/draw2d/samples/android"
"appengine"
)

View File

@ -7,10 +7,10 @@ package frameimage
import (
"math"
"github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/draw2dimg"
"github.com/llgcode/draw2d/draw2dkit"
"github.com/llgcode/draw2d/samples"
"git.fromouter.space/crunchy-rocks/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d/draw2dimg"
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
"git.fromouter.space/crunchy-rocks/draw2d/samples"
)
// Main draws the image frame and returns the filename.

View File

@ -9,10 +9,10 @@ import (
"image/color"
"math"
"github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/draw2dkit"
"github.com/llgcode/draw2d/samples"
"github.com/llgcode/draw2d/samples/gopher2"
"git.fromouter.space/crunchy-rocks/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
"git.fromouter.space/crunchy-rocks/draw2d/samples"
"git.fromouter.space/crunchy-rocks/draw2d/samples/gopher2"
)
// Main draws geometry and returns the filename. This should only be
@ -189,6 +189,7 @@ func CubicCurve(gc draw2d.GraphicContext, x, y, width, height float64) {
}
// FillString draws a filled and stroked string.
// And filles/stroked path created from string. Which may have different - unselectable - output in non raster gc implementations.
func FillString(gc draw2d.GraphicContext, x, y, width, height float64) {
sx, sy := width/100, height/100
gc.Save()
@ -202,7 +203,8 @@ func FillString(gc draw2d.GraphicContext, x, y, width, height float64) {
gc.SetFontData(draw2d.FontData{
Name: "luxi",
Family: draw2d.FontFamilyMono,
Style: draw2d.FontStyleBold | draw2d.FontStyleItalic})
Style: draw2d.FontStyleBold | draw2d.FontStyleItalic,
})
w := gc.FillString("Hug")
gc.Translate(w+sx, 0)
left, top, right, bottom := gc.GetStringBounds("cou")
@ -214,6 +216,14 @@ func FillString(gc draw2d.GraphicContext, x, y, width, height float64) {
gc.SetStrokeColor(color.NRGBA{0x33, 0x33, 0xff, 0xff})
gc.SetLineWidth(height / 100)
gc.StrokeString("Hug")
gc.Translate(-(w + sx), sy*24)
w = gc.CreateStringPath("Hug", 0, 0)
gc.Fill()
gc.Translate(w+sx, 0)
gc.CreateStringPath("Hug", 0, 0)
path := gc.GetPath()
gc.Stroke((&path).VerticalFlip())
gc.Restore()
}

View File

@ -8,8 +8,8 @@ package gopher
import (
"image/color"
"github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/samples"
"git.fromouter.space/crunchy-rocks/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d/samples"
)
// Main draws a left hand and ear of a gopher. Afterwards it returns

View File

@ -10,9 +10,9 @@ import (
"image/color"
"math"
"github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/draw2dkit"
"github.com/llgcode/draw2d/samples"
"git.fromouter.space/crunchy-rocks/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
"git.fromouter.space/crunchy-rocks/draw2d/samples"
)
// Main draws a rotated face of the gopher. Afterwards it returns

View File

@ -9,9 +9,9 @@ import (
"fmt"
"image"
"github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/draw2dkit"
"github.com/llgcode/draw2d/samples"
"git.fromouter.space/crunchy-rocks/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
"git.fromouter.space/crunchy-rocks/draw2d/samples"
)
// Main draws "Hello World" and returns the filename. This should only be

View File

@ -6,11 +6,11 @@ import (
"log"
"runtime"
"git.fromouter.space/crunchy-rocks/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d/draw2dgl"
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.1/glfw"
"github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/draw2dgl"
"github.com/llgcode/draw2d/draw2dkit"
)
var (

View File

@ -7,9 +7,9 @@ package line
import (
"image/color"
"github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/draw2dkit"
"github.com/llgcode/draw2d/samples"
"git.fromouter.space/crunchy-rocks/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
"git.fromouter.space/crunchy-rocks/draw2d/samples"
)
// Main draws vertically spaced lines and returns the filename.

View File

@ -7,8 +7,8 @@ package linecapjoin
import (
"image/color"
"github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/samples"
"git.fromouter.space/crunchy-rocks/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d/samples"
)
// Main draws the different line caps and joins.

View File

@ -8,8 +8,8 @@ import (
"github.com/llgcode/ps"
"github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/samples"
"git.fromouter.space/crunchy-rocks/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d/samples"
)
// Main draws the tiger

View File

@ -10,9 +10,9 @@ import (
"strings"
"time"
"git.fromouter.space/crunchy-rocks/draw2d/draw2dgl"
"github.com/go-gl/gl/v2.1/gl"
"github.com/go-gl/glfw/v3.1/glfw"
"github.com/llgcode/draw2d/draw2dgl"
"github.com/llgcode/ps"
)

View File

@ -8,7 +8,7 @@ import "fmt"
// Resource returns a resource filename for testing.
func Resource(folder, filename, ext string) string {
var root string
if ext == "pdf" {
if ext == "pdf" || ext == "svg" {
root = "../"
}
return fmt.Sprintf("%sresource/%s/%s", root, folder, filename)
@ -17,7 +17,7 @@ func Resource(folder, filename, ext string) string {
// Output returns the output filename for testing.
func Output(name, ext string) string {
var root string
if ext == "pdf" {
if ext == "pdf" || ext == "svg" {
root = "../"
}
return fmt.Sprintf("%soutput/samples/%s.%s", root, name, ext)

View File

@ -5,16 +5,16 @@ package draw2d_test
import (
"testing"
"github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/samples/android"
"github.com/llgcode/draw2d/samples/frameimage"
"github.com/llgcode/draw2d/samples/geometry"
"github.com/llgcode/draw2d/samples/gopher"
"github.com/llgcode/draw2d/samples/gopher2"
"github.com/llgcode/draw2d/samples/helloworld"
"github.com/llgcode/draw2d/samples/line"
"github.com/llgcode/draw2d/samples/linecapjoin"
"github.com/llgcode/draw2d/samples/postscript"
"git.fromouter.space/crunchy-rocks/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d/samples/android"
"git.fromouter.space/crunchy-rocks/draw2d/samples/frameimage"
"git.fromouter.space/crunchy-rocks/draw2d/samples/geometry"
"git.fromouter.space/crunchy-rocks/draw2d/samples/gopher"
"git.fromouter.space/crunchy-rocks/draw2d/samples/gopher2"
"git.fromouter.space/crunchy-rocks/draw2d/samples/helloworld"
"git.fromouter.space/crunchy-rocks/draw2d/samples/line"
"git.fromouter.space/crunchy-rocks/draw2d/samples/linecapjoin"
"git.fromouter.space/crunchy-rocks/draw2d/samples/postscript"
)
func TestSampleAndroid(t *testing.T) {

46
sync_test.go Normal file
View File

@ -0,0 +1,46 @@
// go test -race -test.v sync_test.go
package draw2d_test
import (
"fmt"
"image"
"testing"
"git.fromouter.space/crunchy-rocks/draw2d"
"git.fromouter.space/crunchy-rocks/draw2d/draw2dimg"
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
)
func TestSync(t *testing.T) {
ch := make(chan int)
limit := 2
for i := 0; i < limit; i++ {
go Draw(i, ch)
}
for i := 0; i < limit; i++ {
counter := <-ch
t.Logf("Goroutine %d returned\n", counter)
}
}
func Draw(i int, ch chan<- int) {
draw2d.SetFontFolder("./resource/font")
// Draw a rounded rectangle using default colors
dest := image.NewRGBA(image.Rect(0, 0, 297, 210.0))
gc := draw2dimg.NewGraphicContext(dest)
draw2dkit.RoundedRectangle(gc, 5, 5, 135, 95, 10, 10)
gc.FillStroke()
// Set the fill text color to black
gc.SetFillColor(image.Black)
gc.SetFontSize(14)
// Display Hello World dimensions
x1, y1, x2, y2 := gc.GetStringBounds("Hello world")
gc.FillStringAt(fmt.Sprintf("%.2f %.2f %.2f %.2f", x1, y1, x2, y2), 0, 0)
ch <- i
}