Compare commits
77 commits
Author | SHA1 | Date | |
---|---|---|---|
675e82cb64 | |||
ff82ab9451 | |||
a7539fd29b | |||
41b8d7304e | |||
9dbc14edd6 | |||
9941d77460 | |||
e3566f7fc4 | |||
219501b99b | |||
|
f52c8a71af | ||
|
bdf3a69827 | ||
|
587a55234c | ||
|
94de6e33b6 | ||
|
cd0433711b | ||
|
274031cf2a | ||
|
bc151d5e2c | ||
|
72e6a3c750 | ||
|
0b72959009 | ||
|
7419075cb6 | ||
|
1588b49f0d | ||
|
50aafedab4 | ||
|
99cc16d0ac | ||
|
1b49270d08 | ||
|
c1e5edea41 | ||
|
6f03f106f6 | ||
|
3af25f5588 | ||
|
90f962641f | ||
|
215a761ccb | ||
|
6d31bfac59 | ||
|
d297a025cd | ||
|
484fe1caef | ||
|
0b3b26d85f | ||
|
41d8a21ba2 | ||
|
6c0a15c624 | ||
|
ca83e24222 | ||
|
bd7567e331 | ||
|
cdf301b7be | ||
|
295a8365b3 | ||
|
96883adea4 | ||
|
c41aa97d30 | ||
|
647da9ceaa | ||
|
f3e35015aa | ||
|
b81f74eb39 | ||
|
a5f7ac8ebe | ||
|
8167230c09 | ||
|
3e4c36c4c9 | ||
|
4cdcb11e52 | ||
|
e4816c5375 | ||
|
dd69e0c822 | ||
|
dcbfbe505d | ||
|
1f71aa3f15 | ||
|
0cf6b8d61f | ||
|
7c57ea38bb | ||
|
eca7b76ebc | ||
|
dfbef878aa | ||
|
1286d3b203 | ||
|
c12070824c | ||
|
0d961cd299 | ||
|
7cc6abeee3 | ||
|
401ee667f2 | ||
|
c2920005d6 | ||
|
c2851a6eb6 | ||
|
3a5a1d8830 | ||
|
b9005c988d | ||
|
4a3322e29e | ||
|
8380dd9458 | ||
|
51ba099819 | ||
|
a6ceba03c8 | ||
|
475a830567 | ||
|
105a963210 | ||
|
13548be874 | ||
|
e0e534f3a5 | ||
|
155ff5c755 | ||
|
3f01cfe277 | ||
|
5e675a3055 | ||
|
3bb234e85b | ||
|
6c047429f6 | ||
|
598513aa60 |
47 changed files with 2118 additions and 155 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -21,3 +21,5 @@ _test*
|
||||||
**/core*[0-9]
|
**/core*[0-9]
|
||||||
.private
|
.private
|
||||||
|
|
||||||
|
go.sum
|
||||||
|
|
||||||
|
|
2
AUTHORS
2
AUTHORS
|
@ -1,2 +1,4 @@
|
||||||
Laurent Le Goff
|
Laurent Le Goff
|
||||||
Stani Michiels, gmail:stani.be
|
Stani Michiels, gmail:stani.be
|
||||||
|
Drahoslav Bednář
|
||||||
|
Sebastien Binet
|
||||||
|
|
|
@ -57,7 +57,8 @@ func main() {
|
||||||
gc.SetLineWidth(5)
|
gc.SetLineWidth(5)
|
||||||
|
|
||||||
// Draw a closed shape
|
// 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.LineTo(100, 50)
|
||||||
gc.QuadCurveTo(100, 10, 10, 10)
|
gc.QuadCurveTo(100, 10, 10, 10)
|
||||||
gc.Close()
|
gc.Close()
|
||||||
|
|
16
draw2d.go
16
draw2d.go
|
@ -128,6 +128,14 @@ const (
|
||||||
SquareCap
|
SquareCap
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (cap LineCap) String() string {
|
||||||
|
return map[LineCap]string{
|
||||||
|
RoundCap: "round",
|
||||||
|
ButtCap: "cap",
|
||||||
|
SquareCap: "square",
|
||||||
|
}[cap]
|
||||||
|
}
|
||||||
|
|
||||||
// LineJoin is the style of segments joint
|
// LineJoin is the style of segments joint
|
||||||
type LineJoin int
|
type LineJoin int
|
||||||
|
|
||||||
|
@ -140,6 +148,14 @@ const (
|
||||||
MiterJoin
|
MiterJoin
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (join LineJoin) String() string {
|
||||||
|
return map[LineJoin]string{
|
||||||
|
RoundJoin: "round",
|
||||||
|
BevelJoin: "bevel",
|
||||||
|
MiterJoin: "miter",
|
||||||
|
}[join]
|
||||||
|
}
|
||||||
|
|
||||||
// StrokeStyle keeps stroke style attributes
|
// StrokeStyle keeps stroke style attributes
|
||||||
// that is used by the Stroke method of a Drawer
|
// that is used by the Stroke method of a Drawer
|
||||||
type StrokeStyle struct {
|
type StrokeStyle struct {
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
package draw2dbase
|
package draw2dbase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"math"
|
"math"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,6 +18,7 @@ const (
|
||||||
|
|
||||||
// SubdivideCubic a Bezier cubic curve in 2 equivalents Bezier cubic curves.
|
// SubdivideCubic a Bezier cubic curve in 2 equivalents Bezier cubic curves.
|
||||||
// c1 and c2 parameters are the resulting 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) {
|
func SubdivideCubic(c, c1, c2 []float64) {
|
||||||
// First point of c is the first point of c1
|
// First point of c is the first point of c1
|
||||||
c1[0], c1[1] = c[0], c[1]
|
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
|
// TraceCubic generate lines subdividing the cubic curve using a Liner
|
||||||
// flattening_threshold helps determines the flattening expectation of the curve
|
// 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
|
// Allocation curves
|
||||||
var curves [CurveRecursionLimit * 8]float64
|
var curves [CurveRecursionLimit * 8]float64
|
||||||
copy(curves[0:8], cubic[0:8])
|
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
|
var dx, dy, d2, d3 float64
|
||||||
|
|
||||||
for i >= 0 {
|
for i >= 0 {
|
||||||
c = curves[i*8:]
|
c = curves[i:]
|
||||||
dx = c[6] - c[0]
|
dx = c[6] - c[0]
|
||||||
dy = c[7] - c[1]
|
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)
|
d3 = math.Abs((c[4]-c[6])*dy - (c[5]-c[7])*dx)
|
||||||
|
|
||||||
// if it's flat then trace a line
|
// 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])
|
t.LineTo(c[6], c[7])
|
||||||
i--
|
i -= 8
|
||||||
} else {
|
} else {
|
||||||
// second half of bezier go lower onto the stack
|
// second half of bezier go lower onto the stack
|
||||||
SubdivideCubic(c, curves[(i+1)*8:], curves[i*8:])
|
SubdivideCubic(c, curves[i+8:], curves[i:])
|
||||||
i++
|
i += 8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quad
|
// Quad
|
||||||
|
@ -84,6 +90,7 @@ func TraceCubic(t Liner, cubic []float64, flatteningThreshold float64) {
|
||||||
|
|
||||||
// SubdivideQuad a Bezier quad curve in 2 equivalents Bezier quad curves.
|
// SubdivideQuad a Bezier quad curve in 2 equivalents Bezier quad curves.
|
||||||
// c1 and c2 parameters are the resulting 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) {
|
func SubdivideQuad(c, c1, c2 []float64) {
|
||||||
// First point of c is the first point of c1
|
// First point of c is the first point of c1
|
||||||
c1[0], c1[1] = c[0], c[1]
|
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
|
// TraceQuad generate lines subdividing the curve using a Liner
|
||||||
// flattening_threshold helps determines the flattening expectation of the curve
|
// 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
|
// Allocates curves stack
|
||||||
var curves [CurveRecursionLimit * 6]float64
|
var curves [CurveRecursionLimit * 6]float64
|
||||||
copy(curves[0:6], quad[0:6])
|
copy(curves[0:6], quad[0:6])
|
||||||
|
@ -113,22 +123,23 @@ func TraceQuad(t Liner, quad []float64, flatteningThreshold float64) {
|
||||||
var dx, dy, d float64
|
var dx, dy, d float64
|
||||||
|
|
||||||
for i >= 0 {
|
for i >= 0 {
|
||||||
c = curves[i*6:]
|
c = curves[i:]
|
||||||
dx = c[4] - c[0]
|
dx = c[4] - c[0]
|
||||||
dy = c[5] - c[1]
|
dy = c[5] - c[1]
|
||||||
|
|
||||||
d = math.Abs(((c[2]-c[4])*dy - (c[3]-c[5])*dx))
|
d = math.Abs(((c[2]-c[4])*dy - (c[3]-c[5])*dx))
|
||||||
|
|
||||||
// if it's flat then trace a line
|
// 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])
|
t.LineTo(c[4], c[5])
|
||||||
i--
|
i -= 6
|
||||||
} else {
|
} else {
|
||||||
// second half of bezier go lower onto the stack
|
// second half of bezier go lower onto the stack
|
||||||
SubdivideQuad(c, curves[(i+1)*6:], curves[i*6:])
|
SubdivideQuad(c, curves[i+6:], curves[i:])
|
||||||
i++
|
i += 6
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TraceArc trace an arc using a Liner
|
// TraceArc trace an arc using a Liner
|
||||||
|
|
|
@ -101,6 +101,32 @@ func TestQuadCurve(t *testing.T) {
|
||||||
fmt.Println()
|
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) {
|
func BenchmarkCubicCurve(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
for i := 0; i < len(testsCubicFloat64); i += 8 {
|
for i := 0; i < len(testsCubicFloat64); i += 8 {
|
||||||
|
@ -132,3 +158,19 @@ func SaveToPngFile(filePath string, m image.Image) error {
|
||||||
}
|
}
|
||||||
return nil
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
package draw2dbase
|
package draw2dbase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/llgcode/draw2d"
|
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Liner receive segment definition
|
// Liner receive segment definition
|
||||||
|
@ -19,7 +19,7 @@ type Flattener interface {
|
||||||
MoveTo(x, y float64)
|
MoveTo(x, y float64)
|
||||||
// LineTo Draw a line from the current position to the point (x, y)
|
// LineTo Draw a line from the current position to the point (x, y)
|
||||||
LineTo(x, y float64)
|
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()
|
LineJoin()
|
||||||
// Close add the most recent starting point to close the path to create a polygon
|
// Close add the most recent starting point to close the path to create a polygon
|
||||||
Close()
|
Close()
|
||||||
|
|
|
@ -4,12 +4,13 @@
|
||||||
package draw2dbase
|
package draw2dbase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"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}
|
var DefaultFontData = draw2d.FontData{Name: "luxi", Family: draw2d.FontFamilySans, Style: draw2d.FontStyleNormal}
|
||||||
|
@ -40,6 +41,12 @@ type ContextStack struct {
|
||||||
Previous *ContextStack
|
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
|
* Create a new Graphic context from an image
|
||||||
*/
|
*/
|
||||||
|
@ -132,6 +139,10 @@ func (gc *StackGraphicContext) BeginPath() {
|
||||||
gc.Current.Path.Clear()
|
gc.Current.Path.Clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gc *StackGraphicContext) GetPath() draw2d.Path {
|
||||||
|
return *gc.Current.Path.Copy()
|
||||||
|
}
|
||||||
|
|
||||||
func (gc *StackGraphicContext) IsEmpty() bool {
|
func (gc *StackGraphicContext) IsEmpty() bool {
|
||||||
return gc.Current.Path.IsEmpty()
|
return gc.Current.Path.IsEmpty()
|
||||||
}
|
}
|
||||||
|
@ -191,3 +202,7 @@ func (gc *StackGraphicContext) Restore() {
|
||||||
oldContext.Previous = nil
|
oldContext.Previous = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gc *StackGraphicContext) GetFontName() string {
|
||||||
|
return gc.Current.GetFontName()
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ package draw2dbase
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"github.com/llgcode/draw2d"
|
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LineStroker struct {
|
type LineStroker struct {
|
||||||
|
|
82
draw2dbase/text.go
Normal file
82
draw2dbase/text.go
Normal 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
|
||||||
|
}
|
196
draw2dgl/gc.go
196
draw2dgl/gc.go
|
@ -4,13 +4,19 @@ import (
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"image/draw"
|
"image/draw"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
"runtime"
|
"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/go-gl/gl/v2.1/gl"
|
||||||
"github.com/golang/freetype/raster"
|
"github.com/golang/freetype/raster"
|
||||||
"github.com/llgcode/draw2d"
|
"github.com/golang/freetype/truetype"
|
||||||
"github.com/llgcode/draw2d/draw2dbase"
|
|
||||||
"github.com/llgcode/draw2d/draw2dimg"
|
"golang.org/x/image/font"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -118,6 +124,10 @@ type GraphicContext struct {
|
||||||
painter *Painter
|
painter *Painter
|
||||||
fillRasterizer *raster.Rasterizer
|
fillRasterizer *raster.Rasterizer
|
||||||
strokeRasterizer *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.
|
// NewGraphicContext creates a new Graphic context from an image.
|
||||||
|
@ -127,54 +137,200 @@ func NewGraphicContext(width, height int) *GraphicContext {
|
||||||
NewPainter(),
|
NewPainter(),
|
||||||
raster.NewRasterizer(width, height),
|
raster.NewRasterizer(width, height),
|
||||||
raster.NewRasterizer(width, height),
|
raster.NewRasterizer(width, height),
|
||||||
|
draw2d.GetGlobalFontCache(),
|
||||||
|
draw2dbase.NewGlyphCache(),
|
||||||
|
&truetype.GlyphBuf{},
|
||||||
|
92,
|
||||||
}
|
}
|
||||||
return gc
|
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 {
|
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) {
|
// FillString draws the text at point (0, 0)
|
||||||
panic("not implemented")
|
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) {
|
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)
|
return gc.StrokeStringAt(text, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64) {
|
// StrokeStringAt draws the contour of the text at point (x, y)
|
||||||
width := gc.CreateStringPath(text, x, y)
|
func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (width float64) {
|
||||||
gc.Stroke()
|
f, err := gc.loadCurrentFont()
|
||||||
return width
|
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) {
|
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 {
|
func (gc *GraphicContext) GetDPI() int {
|
||||||
return -1
|
return gc.DPI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO
|
||||||
func (gc *GraphicContext) Clear() {
|
func (gc *GraphicContext) Clear() {
|
||||||
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO
|
||||||
func (gc *GraphicContext) ClearRect(x1, y1, x2, y2 int) {
|
func (gc *GraphicContext) ClearRect(x1, y1, x2, y2 int) {
|
||||||
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO
|
||||||
func (gc *GraphicContext) DrawImage(img image.Image) {
|
func (gc *GraphicContext) DrawImage(img image.Image) {
|
||||||
|
panic("not implemented")
|
||||||
}
|
|
||||||
|
|
||||||
func (gc *GraphicContext) FillString(text string) (cursor float64) {
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gc *GraphicContext) paint(rasterizer *raster.Rasterizer, color color.Color) {
|
func (gc *GraphicContext) paint(rasterizer *raster.Rasterizer, color color.Color) {
|
||||||
|
|
82
draw2dgl/text.go
Normal file
82
draw2dgl/text.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
190
draw2dimg/curve_limit_test.go
Normal file
190
draw2dimg/curve_limit_test.go
Normal 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)
|
||||||
|
}
|
|
@ -4,17 +4,17 @@
|
||||||
package draw2dimg
|
package draw2dimg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"github.com/llgcode/draw2d"
|
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||||
"github.com/llgcode/draw2d/draw2dbase"
|
"git.fromouter.space/crunchy-rocks/draw2d/draw2dbase"
|
||||||
|
"git.fromouter.space/crunchy-rocks/emoji"
|
||||||
|
|
||||||
"github.com/golang/freetype/raster"
|
"git.fromouter.space/crunchy-rocks/freetype/raster"
|
||||||
"github.com/golang/freetype/truetype"
|
"git.fromouter.space/crunchy-rocks/freetype/truetype"
|
||||||
|
|
||||||
"golang.org/x/image/draw"
|
"golang.org/x/image/draw"
|
||||||
"golang.org/x/image/font"
|
"golang.org/x/image/font"
|
||||||
|
@ -35,8 +35,11 @@ type GraphicContext struct {
|
||||||
painter Painter
|
painter Painter
|
||||||
fillRasterizer *raster.Rasterizer
|
fillRasterizer *raster.Rasterizer
|
||||||
strokeRasterizer *raster.Rasterizer
|
strokeRasterizer *raster.Rasterizer
|
||||||
|
FontCache draw2d.FontCache
|
||||||
|
glyphCache draw2dbase.GlyphCache
|
||||||
glyphBuf *truetype.GlyphBuf
|
glyphBuf *truetype.GlyphBuf
|
||||||
DPI int
|
DPI int
|
||||||
|
Emojis emoji.Table
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImageFilter defines the type of filter to use
|
// ImageFilter defines the type of filter to use
|
||||||
|
@ -53,7 +56,6 @@ const (
|
||||||
|
|
||||||
// NewGraphicContext creates a new Graphic context from an image.
|
// NewGraphicContext creates a new Graphic context from an image.
|
||||||
func NewGraphicContext(img draw.Image) *GraphicContext {
|
func NewGraphicContext(img draw.Image) *GraphicContext {
|
||||||
|
|
||||||
var painter Painter
|
var painter Painter
|
||||||
switch selectImage := img.(type) {
|
switch selectImage := img.(type) {
|
||||||
case *image.RGBA:
|
case *image.RGBA:
|
||||||
|
@ -74,8 +76,11 @@ func NewGraphicContextWithPainter(img draw.Image, painter Painter) *GraphicConte
|
||||||
painter,
|
painter,
|
||||||
raster.NewRasterizer(width, height),
|
raster.NewRasterizer(width, height),
|
||||||
raster.NewRasterizer(width, height),
|
raster.NewRasterizer(width, height),
|
||||||
|
draw2d.GetGlobalFontCache(),
|
||||||
|
draw2dbase.NewGlyphCache(),
|
||||||
&truetype.GlyphBuf{},
|
&truetype.GlyphBuf{},
|
||||||
dpi,
|
dpi,
|
||||||
|
make(emoji.Table),
|
||||||
}
|
}
|
||||||
return gc
|
return gc
|
||||||
}
|
}
|
||||||
|
@ -108,7 +113,7 @@ func DrawImage(src image.Image, dest draw.Image, tr draw2d.Matrix, op draw.Op, f
|
||||||
case BicubicFilter:
|
case BicubicFilter:
|
||||||
transformer = draw.CatmullRom
|
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
|
// 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)
|
// 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)
|
return gc.FillStringAt(text, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emojiSpacing = 10
|
||||||
|
const emojiScale = 110
|
||||||
|
|
||||||
// FillStringAt draws the text at the specified point (x, y)
|
// FillStringAt draws the text at the specified point (x, y)
|
||||||
func (gc *GraphicContext) FillStringAt(text string, x, y float64) (cursor float64) {
|
func (gc *GraphicContext) FillStringAt(text string, x, y float64) (width float64) {
|
||||||
width := gc.CreateStringPath(text, x, y)
|
f, err := gc.loadCurrentFont()
|
||||||
gc.Fill()
|
if err != nil {
|
||||||
return width
|
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)
|
// 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)
|
return gc.StrokeStringAt(text, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StrokeStringAt draws the contour of the text at point (x, y)
|
// StrokeStringAt draws the contour of the text at point (x, y)
|
||||||
func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64) {
|
func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (width float64) {
|
||||||
width := gc.CreateStringPath(text, x, y)
|
f, err := gc.loadCurrentFont()
|
||||||
gc.Stroke()
|
if err != nil {
|
||||||
return width
|
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) {
|
func (gc *GraphicContext) loadCurrentFont() (*truetype.Font, error) {
|
||||||
font := draw2d.GetFont(gc.Current.FontData)
|
font, err := gc.FontCache.Load(gc.Current.FontData)
|
||||||
if font == nil {
|
if err != nil {
|
||||||
font = draw2d.GetFont(draw2dbase.DefaultFontData)
|
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.SetFont(font)
|
||||||
gc.SetFontSize(gc.Current.FontSize)
|
gc.SetFontSize(gc.Current.FontSize)
|
||||||
return font, nil
|
}
|
||||||
|
return font, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// p is a truetype.Point measured in FUnits and positive Y going upwards.
|
// 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
|
top, left, bottom, right = 10e6, 10e6, -10e6, -10e6
|
||||||
cursor := 0.0
|
cursor := 0.0
|
||||||
prev, hasPrev := truetype.Index(0), false
|
prev, hasPrev := truetype.Index(0), false
|
||||||
for _, rune := range s {
|
// Get sample letter for approximated emoji calculations
|
||||||
index := f.Index(rune)
|
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 {
|
if hasPrev {
|
||||||
cursor += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index))
|
cursor += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index))
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
package draw2dimg
|
package draw2dimg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/golang/freetype/raster"
|
"git.fromouter.space/crunchy-rocks/freetype/raster"
|
||||||
"golang.org/x/image/math/fixed"
|
"golang.org/x/image/math/fixed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package draw2dimg
|
package draw2dimg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/golang/freetype/truetype"
|
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||||
"github.com/llgcode/draw2d"
|
"git.fromouter.space/crunchy-rocks/freetype/truetype"
|
||||||
|
|
||||||
"golang.org/x/image/math/fixed"
|
"golang.org/x/image/math/fixed"
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,7 +14,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/golang/freetype/truetype"
|
"git.fromouter.space/crunchy-rocks/freetype/truetype"
|
||||||
|
|
||||||
"github.com/jung-kurt/gofpdf"
|
"github.com/jung-kurt/gofpdf"
|
||||||
"github.com/llgcode/draw2d"
|
"github.com/llgcode/draw2d"
|
||||||
|
|
176
draw2dsvg/converters.go
Normal file
176
draw2dsvg/converters.go
Normal 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
11
draw2dsvg/doc.go
Normal 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
22
draw2dsvg/fileutil.go
Normal 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
403
draw2dsvg/gc.go
Normal 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
65
draw2dsvg/samples_test.go
Normal 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
172
draw2dsvg/svg.go
Normal 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
32
draw2dsvg/test_test.go
Normal 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
83
draw2dsvg/text.go
Normal 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
58
draw2dsvg/xml_test.go
Normal 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
181
font.go
|
@ -6,16 +6,11 @@ package draw2d
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/golang/freetype/truetype"
|
"sync"
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
"git.fromouter.space/crunchy-rocks/freetype/truetype"
|
||||||
fontFolder = "../resource/font/"
|
|
||||||
fonts = make(map[string]*truetype.Font)
|
|
||||||
fontNamer FontFileNamer = FontFileName
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// FontStyle defines bold and italic styles for the font
|
// 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) {
|
func RegisterFont(fontData FontData, font *truetype.Font) {
|
||||||
fonts[fontNamer(fontData)] = font
|
fontCache.Store(fontData, font)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetFont(fontData FontData) *truetype.Font {
|
func GetFont(fontData FontData) (font *truetype.Font) {
|
||||||
fontFileName := fontNamer(fontData)
|
var err error
|
||||||
font := fonts[fontFileName]
|
|
||||||
if font != nil {
|
if font, err = fontCache.Load(fontData); err != nil {
|
||||||
return font
|
log.Println(err)
|
||||||
}
|
}
|
||||||
fonts[fontFileName] = loadFont(fontFileName)
|
|
||||||
return fonts[fontFileName]
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetFontFolder() string {
|
func GetFontFolder() string {
|
||||||
return fontFolder
|
return defaultFonts.folder
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetFontFolder(folder string) {
|
func SetFontFolder(folder string) {
|
||||||
fontFolder = filepath.Clean(folder)
|
defaultFonts.setFolder(filepath.Clean(folder))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetGlobalFontCache() FontCache {
|
||||||
|
return fontCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetFontNamer(fn FontFileNamer) {
|
func SetFontNamer(fn FontFileNamer) {
|
||||||
fontNamer = fn
|
defaultFonts.setNamer(fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadFont(fontFileName string) *truetype.Font {
|
// Types implementing this interface can be passed to SetFontCache to change the
|
||||||
fontBytes, err := ioutil.ReadFile(path.Join(fontFolder, fontFileName))
|
// way fonts are being stored and retrieved.
|
||||||
if err != nil {
|
type FontCache interface {
|
||||||
log.Println(err)
|
// Loads a truetype font represented by the FontData object passed as
|
||||||
return nil
|
// argument.
|
||||||
}
|
// The method returns an error if the font could not be loaded, either
|
||||||
font, err := truetype.Parse(fontBytes)
|
// because it didn't exist or the resource it was loaded from was corrupted.
|
||||||
if err != nil {
|
Load(FontData) (*truetype.Font, error)
|
||||||
log.Println(err)
|
|
||||||
return nil
|
// Sets the truetype font that will be returned by Load when given the font
|
||||||
}
|
// data passed as first argument.
|
||||||
return font
|
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
30
gc.go
|
@ -10,9 +10,12 @@ import (
|
||||||
|
|
||||||
// GraphicContext describes the interface for the various backends (images, pdf, opengl, ...)
|
// GraphicContext describes the interface for the various backends (images, pdf, opengl, ...)
|
||||||
type GraphicContext interface {
|
type GraphicContext interface {
|
||||||
|
// PathBuilder describes the interface for path drawing
|
||||||
PathBuilder
|
PathBuilder
|
||||||
// BeginPath creates a new path
|
// BeginPath creates a new path
|
||||||
BeginPath()
|
BeginPath()
|
||||||
|
// GetPath copies the current path, then returns it
|
||||||
|
GetPath() Path
|
||||||
// GetMatrixTransform returns the current transformation matrix
|
// GetMatrixTransform returns the current transformation matrix
|
||||||
GetMatrixTransform() Matrix
|
GetMatrixTransform() Matrix
|
||||||
// SetMatrixTransform sets the current transformation matrix
|
// SetMatrixTransform sets the current transformation matrix
|
||||||
|
@ -27,7 +30,7 @@ type GraphicContext interface {
|
||||||
Scale(sx, sy float64)
|
Scale(sx, sy float64)
|
||||||
// SetStrokeColor sets the current stroke color
|
// SetStrokeColor sets the current stroke color
|
||||||
SetStrokeColor(c color.Color)
|
SetStrokeColor(c color.Color)
|
||||||
// SetStrokeColor sets the current fill color
|
// SetFillColor sets the current fill color
|
||||||
SetFillColor(c color.Color)
|
SetFillColor(c color.Color)
|
||||||
// SetFillRule sets the current fill rule
|
// SetFillRule sets the current fill rule
|
||||||
SetFillRule(f FillRule)
|
SetFillRule(f FillRule)
|
||||||
|
@ -37,27 +40,48 @@ type GraphicContext interface {
|
||||||
SetLineCap(cap LineCap)
|
SetLineCap(cap LineCap)
|
||||||
// SetLineJoin sets the current line join
|
// SetLineJoin sets the current line join
|
||||||
SetLineJoin(join LineJoin)
|
SetLineJoin(join LineJoin)
|
||||||
// SetLineJoin sets the current dash
|
// SetLineDash sets the current dash
|
||||||
SetLineDash(dash []float64, dashOffset float64)
|
SetLineDash(dash []float64, dashOffset float64)
|
||||||
// SetFontSize
|
// SetFontSize sets the current font size
|
||||||
SetFontSize(fontSize float64)
|
SetFontSize(fontSize float64)
|
||||||
|
// GetFontSize gets the current font size
|
||||||
GetFontSize() float64
|
GetFontSize() float64
|
||||||
|
// SetFontData sets the current FontData
|
||||||
SetFontData(fontData FontData)
|
SetFontData(fontData FontData)
|
||||||
|
// GetFontData gets the current FontData
|
||||||
GetFontData() 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)
|
DrawImage(image image.Image)
|
||||||
|
// Save the context and push it to the context stack
|
||||||
Save()
|
Save()
|
||||||
|
// Restore remove the current context and restore the last one
|
||||||
Restore()
|
Restore()
|
||||||
|
// Clear fills the current canvas with a default transparent color
|
||||||
Clear()
|
Clear()
|
||||||
|
// ClearRect fills the specified rectangle with a default transparent color
|
||||||
ClearRect(x1, y1, x2, y2 int)
|
ClearRect(x1, y1, x2, y2 int)
|
||||||
|
// SetDPI sets the current DPI
|
||||||
SetDPI(dpi int)
|
SetDPI(dpi int)
|
||||||
|
// GetDPI gets the current DPI
|
||||||
GetDPI() int
|
GetDPI() int
|
||||||
|
// GetStringBounds gets pixel bounds(dimensions) of given string
|
||||||
GetStringBounds(s string) (left, top, right, bottom float64)
|
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)
|
CreateStringPath(text string, x, y float64) (cursor float64)
|
||||||
|
// FillString draws the text at point (0, 0)
|
||||||
FillString(text string) (cursor float64)
|
FillString(text string) (cursor float64)
|
||||||
|
// FillStringAt draws the text at the specified point (x, y)
|
||||||
FillStringAt(text string, x, y float64) (cursor float64)
|
FillStringAt(text string, x, y float64) (cursor float64)
|
||||||
|
// StrokeString draws the contour of the text at point (0, 0)
|
||||||
StrokeString(text string) (cursor float64)
|
StrokeString(text string) (cursor float64)
|
||||||
|
// StrokeStringAt draws the contour of the text at point (x, y)
|
||||||
StrokeStringAt(text string, x, y float64) (cursor float64)
|
StrokeStringAt(text string, x, y float64) (cursor float64)
|
||||||
|
// Stroke strokes the paths with the color specified by SetStrokeColor
|
||||||
Stroke(paths ...*Path)
|
Stroke(paths ...*Path)
|
||||||
|
// Fill fills the paths with the color specified by SetFillColor
|
||||||
Fill(paths ...*Path)
|
Fill(paths ...*Path)
|
||||||
|
// FillStroke first fills the paths and than strokes them
|
||||||
FillStroke(paths ...*Path)
|
FillStroke(paths ...*Path)
|
||||||
}
|
}
|
||||||
|
|
13
go.mod
Normal file
13
go.mod
Normal 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
46
path.go
|
@ -76,9 +76,10 @@ func (p *Path) MoveTo(x, y float64) {
|
||||||
// LineTo adds a line to the current path
|
// LineTo adds a line to the current path
|
||||||
func (p *Path) LineTo(x, y float64) {
|
func (p *Path) LineTo(x, y float64) {
|
||||||
if len(p.Components) == 0 { //special case when no move has been done
|
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.x = x
|
||||||
p.y = y
|
p.y = y
|
||||||
}
|
}
|
||||||
|
@ -86,9 +87,10 @@ func (p *Path) LineTo(x, y float64) {
|
||||||
// QuadCurveTo adds a quadratic bezier curve to the current path
|
// QuadCurveTo adds a quadratic bezier curve to the current path
|
||||||
func (p *Path) QuadCurveTo(cx, cy, x, y float64) {
|
func (p *Path) QuadCurveTo(cx, cy, x, y float64) {
|
||||||
if len(p.Components) == 0 { //special case when no move has been done
|
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.x = x
|
||||||
p.y = y
|
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
|
// CubicCurveTo adds a cubic bezier curve to the current path
|
||||||
func (p *Path) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) {
|
func (p *Path) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) {
|
||||||
if len(p.Components) == 0 { //special case when no move has been done
|
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.x = x
|
||||||
p.y = y
|
p.y = y
|
||||||
}
|
}
|
||||||
|
@ -187,3 +190,34 @@ func (p *Path) String() string {
|
||||||
}
|
}
|
||||||
return s
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -8,9 +8,9 @@ import (
|
||||||
"image/color"
|
"image/color"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"github.com/llgcode/draw2d"
|
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||||
"github.com/llgcode/draw2d/draw2dkit"
|
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
|
||||||
"github.com/llgcode/draw2d/samples"
|
"git.fromouter.space/crunchy-rocks/draw2d/samples"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Main draws a droid and returns the filename. This should only be
|
// Main draws a droid and returns the filename. This should only be
|
||||||
|
|
|
@ -9,9 +9,9 @@ import (
|
||||||
"image/png"
|
"image/png"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/llgcode/draw2d/draw2dimg"
|
"git.fromouter.space/crunchy-rocks/draw2d/draw2dimg"
|
||||||
"github.com/llgcode/draw2d/draw2dpdf"
|
"git.fromouter.space/crunchy-rocks/draw2d/draw2dpdf"
|
||||||
"github.com/llgcode/draw2d/samples/android"
|
"git.fromouter.space/crunchy-rocks/draw2d/samples/android"
|
||||||
|
|
||||||
"appengine"
|
"appengine"
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,10 +7,10 @@ package frameimage
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"github.com/llgcode/draw2d"
|
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||||
"github.com/llgcode/draw2d/draw2dimg"
|
"git.fromouter.space/crunchy-rocks/draw2d/draw2dimg"
|
||||||
"github.com/llgcode/draw2d/draw2dkit"
|
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
|
||||||
"github.com/llgcode/draw2d/samples"
|
"git.fromouter.space/crunchy-rocks/draw2d/samples"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Main draws the image frame and returns the filename.
|
// Main draws the image frame and returns the filename.
|
||||||
|
|
|
@ -9,10 +9,10 @@ import (
|
||||||
"image/color"
|
"image/color"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"github.com/llgcode/draw2d"
|
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||||
"github.com/llgcode/draw2d/draw2dkit"
|
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
|
||||||
"github.com/llgcode/draw2d/samples"
|
"git.fromouter.space/crunchy-rocks/draw2d/samples"
|
||||||
"github.com/llgcode/draw2d/samples/gopher2"
|
"git.fromouter.space/crunchy-rocks/draw2d/samples/gopher2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Main draws geometry and returns the filename. This should only be
|
// 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.
|
// 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) {
|
func FillString(gc draw2d.GraphicContext, x, y, width, height float64) {
|
||||||
sx, sy := width/100, height/100
|
sx, sy := width/100, height/100
|
||||||
gc.Save()
|
gc.Save()
|
||||||
|
@ -202,7 +203,8 @@ func FillString(gc draw2d.GraphicContext, x, y, width, height float64) {
|
||||||
gc.SetFontData(draw2d.FontData{
|
gc.SetFontData(draw2d.FontData{
|
||||||
Name: "luxi",
|
Name: "luxi",
|
||||||
Family: draw2d.FontFamilyMono,
|
Family: draw2d.FontFamilyMono,
|
||||||
Style: draw2d.FontStyleBold | draw2d.FontStyleItalic})
|
Style: draw2d.FontStyleBold | draw2d.FontStyleItalic,
|
||||||
|
})
|
||||||
w := gc.FillString("Hug")
|
w := gc.FillString("Hug")
|
||||||
gc.Translate(w+sx, 0)
|
gc.Translate(w+sx, 0)
|
||||||
left, top, right, bottom := gc.GetStringBounds("cou")
|
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.SetStrokeColor(color.NRGBA{0x33, 0x33, 0xff, 0xff})
|
||||||
gc.SetLineWidth(height / 100)
|
gc.SetLineWidth(height / 100)
|
||||||
gc.StrokeString("Hug")
|
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()
|
gc.Restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,8 @@ package gopher
|
||||||
import (
|
import (
|
||||||
"image/color"
|
"image/color"
|
||||||
|
|
||||||
"github.com/llgcode/draw2d"
|
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||||
"github.com/llgcode/draw2d/samples"
|
"git.fromouter.space/crunchy-rocks/draw2d/samples"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Main draws a left hand and ear of a gopher. Afterwards it returns
|
// Main draws a left hand and ear of a gopher. Afterwards it returns
|
||||||
|
|
|
@ -10,9 +10,9 @@ import (
|
||||||
"image/color"
|
"image/color"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"github.com/llgcode/draw2d"
|
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||||
"github.com/llgcode/draw2d/draw2dkit"
|
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
|
||||||
"github.com/llgcode/draw2d/samples"
|
"git.fromouter.space/crunchy-rocks/draw2d/samples"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Main draws a rotated face of the gopher. Afterwards it returns
|
// Main draws a rotated face of the gopher. Afterwards it returns
|
||||||
|
|
|
@ -9,9 +9,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
|
|
||||||
"github.com/llgcode/draw2d"
|
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||||
"github.com/llgcode/draw2d/draw2dkit"
|
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
|
||||||
"github.com/llgcode/draw2d/samples"
|
"git.fromouter.space/crunchy-rocks/draw2d/samples"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Main draws "Hello World" and returns the filename. This should only be
|
// Main draws "Hello World" and returns the filename. This should only be
|
||||||
|
|
|
@ -6,11 +6,11 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"runtime"
|
"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/gl/v2.1/gl"
|
||||||
"github.com/go-gl/glfw/v3.1/glfw"
|
"github.com/go-gl/glfw/v3.1/glfw"
|
||||||
"github.com/llgcode/draw2d"
|
|
||||||
"github.com/llgcode/draw2d/draw2dgl"
|
|
||||||
"github.com/llgcode/draw2d/draw2dkit"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -7,9 +7,9 @@ package line
|
||||||
import (
|
import (
|
||||||
"image/color"
|
"image/color"
|
||||||
|
|
||||||
"github.com/llgcode/draw2d"
|
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||||
"github.com/llgcode/draw2d/draw2dkit"
|
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
|
||||||
"github.com/llgcode/draw2d/samples"
|
"git.fromouter.space/crunchy-rocks/draw2d/samples"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Main draws vertically spaced lines and returns the filename.
|
// Main draws vertically spaced lines and returns the filename.
|
||||||
|
|
|
@ -7,8 +7,8 @@ package linecapjoin
|
||||||
import (
|
import (
|
||||||
"image/color"
|
"image/color"
|
||||||
|
|
||||||
"github.com/llgcode/draw2d"
|
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||||
"github.com/llgcode/draw2d/samples"
|
"git.fromouter.space/crunchy-rocks/draw2d/samples"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Main draws the different line caps and joins.
|
// Main draws the different line caps and joins.
|
||||||
|
|
|
@ -8,8 +8,8 @@ import (
|
||||||
|
|
||||||
"github.com/llgcode/ps"
|
"github.com/llgcode/ps"
|
||||||
|
|
||||||
"github.com/llgcode/draw2d"
|
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||||
"github.com/llgcode/draw2d/samples"
|
"git.fromouter.space/crunchy-rocks/draw2d/samples"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Main draws the tiger
|
// Main draws the tiger
|
||||||
|
|
|
@ -10,9 +10,9 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.fromouter.space/crunchy-rocks/draw2d/draw2dgl"
|
||||||
"github.com/go-gl/gl/v2.1/gl"
|
"github.com/go-gl/gl/v2.1/gl"
|
||||||
"github.com/go-gl/glfw/v3.1/glfw"
|
"github.com/go-gl/glfw/v3.1/glfw"
|
||||||
"github.com/llgcode/draw2d/draw2dgl"
|
|
||||||
"github.com/llgcode/ps"
|
"github.com/llgcode/ps"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import "fmt"
|
||||||
// Resource returns a resource filename for testing.
|
// Resource returns a resource filename for testing.
|
||||||
func Resource(folder, filename, ext string) string {
|
func Resource(folder, filename, ext string) string {
|
||||||
var root string
|
var root string
|
||||||
if ext == "pdf" {
|
if ext == "pdf" || ext == "svg" {
|
||||||
root = "../"
|
root = "../"
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%sresource/%s/%s", root, folder, filename)
|
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.
|
// Output returns the output filename for testing.
|
||||||
func Output(name, ext string) string {
|
func Output(name, ext string) string {
|
||||||
var root string
|
var root string
|
||||||
if ext == "pdf" {
|
if ext == "pdf" || ext == "svg" {
|
||||||
root = "../"
|
root = "../"
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%soutput/samples/%s.%s", root, name, ext)
|
return fmt.Sprintf("%soutput/samples/%s.%s", root, name, ext)
|
||||||
|
|
|
@ -5,16 +5,16 @@ package draw2d_test
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/llgcode/draw2d"
|
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||||
"github.com/llgcode/draw2d/samples/android"
|
"git.fromouter.space/crunchy-rocks/draw2d/samples/android"
|
||||||
"github.com/llgcode/draw2d/samples/frameimage"
|
"git.fromouter.space/crunchy-rocks/draw2d/samples/frameimage"
|
||||||
"github.com/llgcode/draw2d/samples/geometry"
|
"git.fromouter.space/crunchy-rocks/draw2d/samples/geometry"
|
||||||
"github.com/llgcode/draw2d/samples/gopher"
|
"git.fromouter.space/crunchy-rocks/draw2d/samples/gopher"
|
||||||
"github.com/llgcode/draw2d/samples/gopher2"
|
"git.fromouter.space/crunchy-rocks/draw2d/samples/gopher2"
|
||||||
"github.com/llgcode/draw2d/samples/helloworld"
|
"git.fromouter.space/crunchy-rocks/draw2d/samples/helloworld"
|
||||||
"github.com/llgcode/draw2d/samples/line"
|
"git.fromouter.space/crunchy-rocks/draw2d/samples/line"
|
||||||
"github.com/llgcode/draw2d/samples/linecapjoin"
|
"git.fromouter.space/crunchy-rocks/draw2d/samples/linecapjoin"
|
||||||
"github.com/llgcode/draw2d/samples/postscript"
|
"git.fromouter.space/crunchy-rocks/draw2d/samples/postscript"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSampleAndroid(t *testing.T) {
|
func TestSampleAndroid(t *testing.T) {
|
||||||
|
|
46
sync_test.go
Normal file
46
sync_test.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in a new issue