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]
|
||||
.private
|
||||
|
||||
go.sum
|
||||
|
||||
|
|
4
AUTHORS
4
AUTHORS
|
@ -1,2 +1,4 @@
|
|||
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)
|
||||
|
||||
// Draw a closed shape
|
||||
gc.MoveTo(10, 10) // should always be called first for a new path
|
||||
gc.BeginPath() // Initialize a new path
|
||||
gc.MoveTo(10, 10) // Move to a position to start the new path
|
||||
gc.LineTo(100, 50)
|
||||
gc.QuadCurveTo(100, 10, 10, 10)
|
||||
gc.Close()
|
||||
|
|
16
draw2d.go
16
draw2d.go
|
@ -128,6 +128,14 @@ const (
|
|||
SquareCap
|
||||
)
|
||||
|
||||
func (cap LineCap) String() string {
|
||||
return map[LineCap]string{
|
||||
RoundCap: "round",
|
||||
ButtCap: "cap",
|
||||
SquareCap: "square",
|
||||
}[cap]
|
||||
}
|
||||
|
||||
// LineJoin is the style of segments joint
|
||||
type LineJoin int
|
||||
|
||||
|
@ -140,6 +148,14 @@ const (
|
|||
MiterJoin
|
||||
)
|
||||
|
||||
func (join LineJoin) String() string {
|
||||
return map[LineJoin]string{
|
||||
RoundJoin: "round",
|
||||
BevelJoin: "bevel",
|
||||
MiterJoin: "miter",
|
||||
}[join]
|
||||
}
|
||||
|
||||
// StrokeStyle keeps stroke style attributes
|
||||
// that is used by the Stroke method of a Drawer
|
||||
type StrokeStyle struct {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package draw2dbase
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
)
|
||||
|
||||
|
@ -17,6 +18,7 @@ const (
|
|||
|
||||
// SubdivideCubic a Bezier cubic curve in 2 equivalents Bezier cubic curves.
|
||||
// c1 and c2 parameters are the resulting curves
|
||||
// length of c, c1 and c2 must be 8 otherwise it panics.
|
||||
func SubdivideCubic(c, c1, c2 []float64) {
|
||||
// First point of c is the first point of c1
|
||||
c1[0], c1[1] = c[0], c[1]
|
||||
|
@ -48,7 +50,10 @@ func SubdivideCubic(c, c1, c2 []float64) {
|
|||
|
||||
// TraceCubic generate lines subdividing the cubic curve using a Liner
|
||||
// flattening_threshold helps determines the flattening expectation of the curve
|
||||
func TraceCubic(t Liner, cubic []float64, flatteningThreshold float64) {
|
||||
func TraceCubic(t Liner, cubic []float64, flatteningThreshold float64) error {
|
||||
if len(cubic) < 8 {
|
||||
return errors.New("cubic length must be >= 8")
|
||||
}
|
||||
// Allocation curves
|
||||
var curves [CurveRecursionLimit * 8]float64
|
||||
copy(curves[0:8], cubic[0:8])
|
||||
|
@ -60,7 +65,7 @@ func TraceCubic(t Liner, cubic []float64, flatteningThreshold float64) {
|
|||
var dx, dy, d2, d3 float64
|
||||
|
||||
for i >= 0 {
|
||||
c = curves[i*8:]
|
||||
c = curves[i:]
|
||||
dx = c[6] - c[0]
|
||||
dy = c[7] - c[1]
|
||||
|
||||
|
@ -68,15 +73,16 @@ func TraceCubic(t Liner, cubic []float64, flatteningThreshold float64) {
|
|||
d3 = math.Abs((c[4]-c[6])*dy - (c[5]-c[7])*dx)
|
||||
|
||||
// if it's flat then trace a line
|
||||
if (d2+d3)*(d2+d3) < flatteningThreshold*(dx*dx+dy*dy) || i == len(curves)-1 {
|
||||
if (d2+d3)*(d2+d3) <= flatteningThreshold*(dx*dx+dy*dy) || i == len(curves)-8 {
|
||||
t.LineTo(c[6], c[7])
|
||||
i--
|
||||
i -= 8
|
||||
} else {
|
||||
// second half of bezier go lower onto the stack
|
||||
SubdivideCubic(c, curves[(i+1)*8:], curves[i*8:])
|
||||
i++
|
||||
SubdivideCubic(c, curves[i+8:], curves[i:])
|
||||
i += 8
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Quad
|
||||
|
@ -84,6 +90,7 @@ func TraceCubic(t Liner, cubic []float64, flatteningThreshold float64) {
|
|||
|
||||
// SubdivideQuad a Bezier quad curve in 2 equivalents Bezier quad curves.
|
||||
// c1 and c2 parameters are the resulting curves
|
||||
// length of c, c1 and c2 must be 6 otherwise it panics.
|
||||
func SubdivideQuad(c, c1, c2 []float64) {
|
||||
// First point of c is the first point of c1
|
||||
c1[0], c1[1] = c[0], c[1]
|
||||
|
@ -103,7 +110,10 @@ func SubdivideQuad(c, c1, c2 []float64) {
|
|||
|
||||
// TraceQuad generate lines subdividing the curve using a Liner
|
||||
// flattening_threshold helps determines the flattening expectation of the curve
|
||||
func TraceQuad(t Liner, quad []float64, flatteningThreshold float64) {
|
||||
func TraceQuad(t Liner, quad []float64, flatteningThreshold float64) error {
|
||||
if len(quad) < 6 {
|
||||
return errors.New("quad length must be >= 6")
|
||||
}
|
||||
// Allocates curves stack
|
||||
var curves [CurveRecursionLimit * 6]float64
|
||||
copy(curves[0:6], quad[0:6])
|
||||
|
@ -113,22 +123,23 @@ func TraceQuad(t Liner, quad []float64, flatteningThreshold float64) {
|
|||
var dx, dy, d float64
|
||||
|
||||
for i >= 0 {
|
||||
c = curves[i*6:]
|
||||
c = curves[i:]
|
||||
dx = c[4] - c[0]
|
||||
dy = c[5] - c[1]
|
||||
|
||||
d = math.Abs(((c[2]-c[4])*dy - (c[3]-c[5])*dx))
|
||||
|
||||
// if it's flat then trace a line
|
||||
if (d*d) < flatteningThreshold*(dx*dx+dy*dy) || i == len(curves)-1 {
|
||||
if (d*d) <= flatteningThreshold*(dx*dx+dy*dy) || i == len(curves)-6 {
|
||||
t.LineTo(c[4], c[5])
|
||||
i--
|
||||
i -= 6
|
||||
} else {
|
||||
// second half of bezier go lower onto the stack
|
||||
SubdivideQuad(c, curves[(i+1)*6:], curves[i*6:])
|
||||
i++
|
||||
SubdivideQuad(c, curves[i+6:], curves[i:])
|
||||
i += 6
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TraceArc trace an arc using a Liner
|
||||
|
|
|
@ -101,6 +101,32 @@ func TestQuadCurve(t *testing.T) {
|
|||
fmt.Println()
|
||||
}
|
||||
|
||||
func TestQuadCurveCombinedPoint(t *testing.T) {
|
||||
var p1 SegmentedPath
|
||||
TraceQuad(&p1, []float64{0, 0, 0, 0, 0, 0}, flatteningThreshold)
|
||||
if len(p1.Points) != 2 {
|
||||
t.Error("It must have one point for this curve", len(p1.Points))
|
||||
}
|
||||
var p2 SegmentedPath
|
||||
TraceQuad(&p2, []float64{0, 0, 100, 100, 0, 0}, flatteningThreshold)
|
||||
if len(p2.Points) != 2 {
|
||||
t.Error("It must have one point for this curve", len(p2.Points))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCubicCurveCombinedPoint(t *testing.T) {
|
||||
var p1 SegmentedPath
|
||||
TraceCubic(&p1, []float64{0, 0, 0, 0, 0, 0, 0, 0}, flatteningThreshold)
|
||||
if len(p1.Points) != 2 {
|
||||
t.Error("It must have one point for this curve", len(p1.Points))
|
||||
}
|
||||
var p2 SegmentedPath
|
||||
TraceCubic(&p2, []float64{0, 0, 100, 100, 200, 200, 0, 0}, flatteningThreshold)
|
||||
if len(p2.Points) != 2 {
|
||||
t.Error("It must have one point for this curve", len(p2.Points))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCubicCurve(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for i := 0; i < len(testsCubicFloat64); i += 8 {
|
||||
|
@ -132,3 +158,19 @@ func SaveToPngFile(filePath string, m image.Image) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestOutOfRangeTraceCurve(t *testing.T) {
|
||||
c := []float64{
|
||||
100, 100, 200, 100, 100, 200,
|
||||
}
|
||||
var p SegmentedPath
|
||||
TraceCubic(&p, c, flatteningThreshold)
|
||||
}
|
||||
|
||||
func TestOutOfRangeTraceQuad(t *testing.T) {
|
||||
c := []float64{
|
||||
100, 100, 200, 100,
|
||||
}
|
||||
var p SegmentedPath
|
||||
TraceQuad(&p, c, flatteningThreshold)
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
package draw2dbase
|
||||
|
||||
import (
|
||||
"github.com/llgcode/draw2d"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||
)
|
||||
|
||||
// Liner receive segment definition
|
||||
|
@ -19,7 +19,7 @@ type Flattener interface {
|
|||
MoveTo(x, y float64)
|
||||
// LineTo Draw a line from the current position to the point (x, y)
|
||||
LineTo(x, y float64)
|
||||
// LineJoin add the most recent starting point to close the path to create a polygon
|
||||
// LineJoin use Round, Bevel or miter to join points
|
||||
LineJoin()
|
||||
// Close add the most recent starting point to close the path to create a polygon
|
||||
Close()
|
||||
|
|
|
@ -4,12 +4,13 @@
|
|||
package draw2dbase
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"github.com/llgcode/draw2d"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||
|
||||
"github.com/golang/freetype/truetype"
|
||||
"git.fromouter.space/crunchy-rocks/freetype/truetype"
|
||||
)
|
||||
|
||||
var DefaultFontData = draw2d.FontData{Name: "luxi", Family: draw2d.FontFamilySans, Style: draw2d.FontStyleNormal}
|
||||
|
@ -40,6 +41,12 @@ type ContextStack struct {
|
|||
Previous *ContextStack
|
||||
}
|
||||
|
||||
// GetFontName gets the current FontData with fontSize as a string
|
||||
func (cs *ContextStack) GetFontName() string {
|
||||
fontData := cs.FontData
|
||||
return fmt.Sprintf("%s:%d:%d:%9.2f", fontData.Name, fontData.Family, fontData.Style, cs.FontSize)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Graphic context from an image
|
||||
*/
|
||||
|
@ -132,6 +139,10 @@ func (gc *StackGraphicContext) BeginPath() {
|
|||
gc.Current.Path.Clear()
|
||||
}
|
||||
|
||||
func (gc *StackGraphicContext) GetPath() draw2d.Path {
|
||||
return *gc.Current.Path.Copy()
|
||||
}
|
||||
|
||||
func (gc *StackGraphicContext) IsEmpty() bool {
|
||||
return gc.Current.Path.IsEmpty()
|
||||
}
|
||||
|
@ -191,3 +202,7 @@ func (gc *StackGraphicContext) Restore() {
|
|||
oldContext.Previous = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (gc *StackGraphicContext) GetFontName() string {
|
||||
return gc.Current.GetFontName()
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ package draw2dbase
|
|||
import (
|
||||
"math"
|
||||
|
||||
"github.com/llgcode/draw2d"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||
)
|
||||
|
||||
type LineStroker struct {
|
||||
|
|
82
draw2dbase/text.go
Normal file
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/color"
|
||||
"image/draw"
|
||||
"log"
|
||||
"math"
|
||||
"runtime"
|
||||
|
||||
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/draw2dbase"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/draw2dimg"
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/golang/freetype/raster"
|
||||
"github.com/llgcode/draw2d"
|
||||
"github.com/llgcode/draw2d/draw2dbase"
|
||||
"github.com/llgcode/draw2d/draw2dimg"
|
||||
"github.com/golang/freetype/truetype"
|
||||
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -118,6 +124,10 @@ type GraphicContext struct {
|
|||
painter *Painter
|
||||
fillRasterizer *raster.Rasterizer
|
||||
strokeRasterizer *raster.Rasterizer
|
||||
FontCache draw2d.FontCache
|
||||
glyphCache draw2dbase.GlyphCache
|
||||
glyphBuf *truetype.GlyphBuf
|
||||
DPI int
|
||||
}
|
||||
|
||||
// NewGraphicContext creates a new Graphic context from an image.
|
||||
|
@ -127,54 +137,200 @@ func NewGraphicContext(width, height int) *GraphicContext {
|
|||
NewPainter(),
|
||||
raster.NewRasterizer(width, height),
|
||||
raster.NewRasterizer(width, height),
|
||||
draw2d.GetGlobalFontCache(),
|
||||
draw2dbase.NewGlyphCache(),
|
||||
&truetype.GlyphBuf{},
|
||||
92,
|
||||
}
|
||||
return gc
|
||||
}
|
||||
|
||||
func (gc *GraphicContext) loadCurrentFont() (*truetype.Font, error) {
|
||||
font, err := gc.FontCache.Load(gc.Current.FontData)
|
||||
if err != nil {
|
||||
font, err = gc.FontCache.Load(draw2dbase.DefaultFontData)
|
||||
}
|
||||
if font != nil {
|
||||
gc.SetFont(font)
|
||||
gc.SetFontSize(gc.Current.FontSize)
|
||||
}
|
||||
return font, err
|
||||
}
|
||||
|
||||
func (gc *GraphicContext) drawGlyph(glyph truetype.Index, dx, dy float64) error {
|
||||
if err := gc.glyphBuf.Load(gc.Current.Font, fixed.Int26_6(gc.Current.Scale), glyph, font.HintingNone); err != nil {
|
||||
return err
|
||||
}
|
||||
e0 := 0
|
||||
for _, e1 := range gc.glyphBuf.Ends {
|
||||
DrawContour(gc, gc.glyphBuf.Points[e0:e1], dx, dy)
|
||||
e0 = e1
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateStringPath creates a path from the string s at x, y, and returns the string width.
|
||||
// The text is placed so that the left edge of the em square of the first character of s
|
||||
// and the baseline intersect at x, y. The majority of the affected pixels will be
|
||||
// above and to the right of the point, but some may be below or to the left.
|
||||
// For example, drawing a string that starts with a 'J' in an italic font may
|
||||
// affect pixels below and left of the point.
|
||||
func (gc *GraphicContext) CreateStringPath(s string, x, y float64) float64 {
|
||||
panic("not implemented")
|
||||
f, err := gc.loadCurrentFont()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return 0.0
|
||||
}
|
||||
startx := x
|
||||
prev, hasPrev := truetype.Index(0), false
|
||||
for _, rune := range s {
|
||||
index := f.Index(rune)
|
||||
if hasPrev {
|
||||
x += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index))
|
||||
}
|
||||
err := gc.drawGlyph(index, x, y)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return startx - x
|
||||
}
|
||||
x += fUnitsToFloat64(f.HMetric(fixed.Int26_6(gc.Current.Scale), index).AdvanceWidth)
|
||||
prev, hasPrev = index, true
|
||||
}
|
||||
return x - startx
|
||||
}
|
||||
|
||||
func (gc *GraphicContext) FillStringAt(text string, x, y float64) (cursor float64) {
|
||||
panic("not implemented")
|
||||
// FillString draws the text at point (0, 0)
|
||||
func (gc *GraphicContext) FillString(text string) (width float64) {
|
||||
return gc.FillStringAt(text, 0, 0)
|
||||
}
|
||||
|
||||
// FillStringAt draws the text at the specified point (x, y)
|
||||
func (gc *GraphicContext) FillStringAt(text string, x, y float64) (width float64) {
|
||||
f, err := gc.loadCurrentFont()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return 0.0
|
||||
}
|
||||
startx := x
|
||||
prev, hasPrev := truetype.Index(0), false
|
||||
fontName := gc.GetFontName()
|
||||
for _, r := range text {
|
||||
index := f.Index(r)
|
||||
if hasPrev {
|
||||
x += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index))
|
||||
}
|
||||
glyph := gc.glyphCache.Fetch(gc, fontName, r)
|
||||
x += glyph.Fill(gc, x, y)
|
||||
prev, hasPrev = index, true
|
||||
}
|
||||
return x - startx
|
||||
}
|
||||
|
||||
// GetStringBounds returns the approximate pixel bounds of the string s at x, y.
|
||||
// The the left edge of the em square of the first character of s
|
||||
// and the baseline intersect at 0, 0 in the returned coordinates.
|
||||
// Therefore the top and left coordinates may well be negative.
|
||||
func (gc *GraphicContext) GetStringBounds(s string) (left, top, right, bottom float64) {
|
||||
panic("not implemented")
|
||||
f, err := gc.loadCurrentFont()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return 0, 0, 0, 0
|
||||
}
|
||||
top, left, bottom, right = 10e6, 10e6, -10e6, -10e6
|
||||
cursor := 0.0
|
||||
prev, hasPrev := truetype.Index(0), false
|
||||
for _, rune := range s {
|
||||
index := f.Index(rune)
|
||||
if hasPrev {
|
||||
cursor += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index))
|
||||
}
|
||||
if err := gc.glyphBuf.Load(gc.Current.Font, fixed.Int26_6(gc.Current.Scale), index, font.HintingNone); err != nil {
|
||||
log.Println(err)
|
||||
return 0, 0, 0, 0
|
||||
}
|
||||
e0 := 0
|
||||
for _, e1 := range gc.glyphBuf.Ends {
|
||||
ps := gc.glyphBuf.Points[e0:e1]
|
||||
for _, p := range ps {
|
||||
x, y := pointToF64Point(p)
|
||||
top = math.Min(top, y)
|
||||
bottom = math.Max(bottom, y)
|
||||
left = math.Min(left, x+cursor)
|
||||
right = math.Max(right, x+cursor)
|
||||
}
|
||||
}
|
||||
cursor += fUnitsToFloat64(f.HMetric(fixed.Int26_6(gc.Current.Scale), index).AdvanceWidth)
|
||||
prev, hasPrev = index, true
|
||||
}
|
||||
return left, top, right, bottom
|
||||
}
|
||||
|
||||
func (gc *GraphicContext) StrokeString(text string) (cursor float64) {
|
||||
// StrokeString draws the contour of the text at point (0, 0)
|
||||
func (gc *GraphicContext) StrokeString(text string) (width float64) {
|
||||
return gc.StrokeStringAt(text, 0, 0)
|
||||
}
|
||||
|
||||
func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64) {
|
||||
width := gc.CreateStringPath(text, x, y)
|
||||
gc.Stroke()
|
||||
return width
|
||||
// StrokeStringAt draws the contour of the text at point (x, y)
|
||||
func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (width float64) {
|
||||
f, err := gc.loadCurrentFont()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return 0.0
|
||||
}
|
||||
startx := x
|
||||
prev, hasPrev := truetype.Index(0), false
|
||||
fontName := gc.GetFontName()
|
||||
for _, r := range text {
|
||||
index := f.Index(r)
|
||||
if hasPrev {
|
||||
x += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index))
|
||||
}
|
||||
glyph := gc.glyphCache.Fetch(gc, fontName, r)
|
||||
x += glyph.Stroke(gc, x, y)
|
||||
prev, hasPrev = index, true
|
||||
}
|
||||
return x - startx
|
||||
}
|
||||
|
||||
// recalc recalculates scale and bounds values from the font size, screen
|
||||
// resolution and font metrics, and invalidates the glyph cache.
|
||||
func (gc *GraphicContext) recalc() {
|
||||
gc.Current.Scale = gc.Current.FontSize * float64(gc.DPI) * (64.0 / 72.0)
|
||||
}
|
||||
|
||||
func (gc *GraphicContext) SetDPI(dpi int) {
|
||||
gc.DPI = dpi
|
||||
gc.recalc()
|
||||
}
|
||||
|
||||
// SetFont sets the font used to draw text.
|
||||
func (gc *GraphicContext) SetFont(font *truetype.Font) {
|
||||
gc.Current.Font = font
|
||||
}
|
||||
|
||||
// SetFontSize sets the font size in points (as in ``a 12 point font'').
|
||||
func (gc *GraphicContext) SetFontSize(fontSize float64) {
|
||||
gc.Current.FontSize = fontSize
|
||||
gc.recalc()
|
||||
}
|
||||
|
||||
func (gc *GraphicContext) GetDPI() int {
|
||||
return -1
|
||||
return gc.DPI
|
||||
}
|
||||
|
||||
//TODO
|
||||
func (gc *GraphicContext) Clear() {
|
||||
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
//TODO
|
||||
func (gc *GraphicContext) ClearRect(x1, y1, x2, y2 int) {
|
||||
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
//TODO
|
||||
func (gc *GraphicContext) DrawImage(img image.Image) {
|
||||
|
||||
}
|
||||
|
||||
func (gc *GraphicContext) FillString(text string) (cursor float64) {
|
||||
return 0
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (gc *GraphicContext) paint(rasterizer *raster.Rasterizer, color color.Color) {
|
||||
|
|
82
draw2dgl/text.go
Normal file
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
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"image/color"
|
||||
"log"
|
||||
"math"
|
||||
|
||||
"github.com/llgcode/draw2d"
|
||||
"github.com/llgcode/draw2d/draw2dbase"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/draw2dbase"
|
||||
"git.fromouter.space/crunchy-rocks/emoji"
|
||||
|
||||
"github.com/golang/freetype/raster"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"git.fromouter.space/crunchy-rocks/freetype/raster"
|
||||
"git.fromouter.space/crunchy-rocks/freetype/truetype"
|
||||
|
||||
"golang.org/x/image/draw"
|
||||
"golang.org/x/image/font"
|
||||
|
@ -35,8 +35,11 @@ type GraphicContext struct {
|
|||
painter Painter
|
||||
fillRasterizer *raster.Rasterizer
|
||||
strokeRasterizer *raster.Rasterizer
|
||||
FontCache draw2d.FontCache
|
||||
glyphCache draw2dbase.GlyphCache
|
||||
glyphBuf *truetype.GlyphBuf
|
||||
DPI int
|
||||
Emojis emoji.Table
|
||||
}
|
||||
|
||||
// ImageFilter defines the type of filter to use
|
||||
|
@ -53,7 +56,6 @@ const (
|
|||
|
||||
// NewGraphicContext creates a new Graphic context from an image.
|
||||
func NewGraphicContext(img draw.Image) *GraphicContext {
|
||||
|
||||
var painter Painter
|
||||
switch selectImage := img.(type) {
|
||||
case *image.RGBA:
|
||||
|
@ -74,8 +76,11 @@ func NewGraphicContextWithPainter(img draw.Image, painter Painter) *GraphicConte
|
|||
painter,
|
||||
raster.NewRasterizer(width, height),
|
||||
raster.NewRasterizer(width, height),
|
||||
draw2d.GetGlobalFontCache(),
|
||||
draw2dbase.NewGlyphCache(),
|
||||
&truetype.GlyphBuf{},
|
||||
dpi,
|
||||
make(emoji.Table),
|
||||
}
|
||||
return gc
|
||||
}
|
||||
|
@ -108,7 +113,7 @@ func DrawImage(src image.Image, dest draw.Image, tr draw2d.Matrix, op draw.Op, f
|
|||
case BicubicFilter:
|
||||
transformer = draw.CatmullRom
|
||||
}
|
||||
transformer.Transform(dest, f64.Aff3{tr[0], tr[1], tr[4], tr[2], tr[3], tr[5]}, src, src.Bounds(), draw.Over, nil)
|
||||
transformer.Transform(dest, f64.Aff3{tr[0], tr[1], tr[4], tr[2], tr[3], tr[5]}, src, src.Bounds(), op, nil)
|
||||
}
|
||||
|
||||
// DrawImage draws the raster image in the current canvas
|
||||
|
@ -117,40 +122,98 @@ func (gc *GraphicContext) DrawImage(img image.Image) {
|
|||
}
|
||||
|
||||
// FillString draws the text at point (0, 0)
|
||||
func (gc *GraphicContext) FillString(text string) (cursor float64) {
|
||||
func (gc *GraphicContext) FillString(text string) (width float64) {
|
||||
return gc.FillStringAt(text, 0, 0)
|
||||
}
|
||||
|
||||
const emojiSpacing = 10
|
||||
const emojiScale = 110
|
||||
|
||||
// FillStringAt draws the text at the specified point (x, y)
|
||||
func (gc *GraphicContext) FillStringAt(text string, x, y float64) (cursor float64) {
|
||||
width := gc.CreateStringPath(text, x, y)
|
||||
gc.Fill()
|
||||
return width
|
||||
func (gc *GraphicContext) FillStringAt(text string, x, y float64) (width float64) {
|
||||
f, err := gc.loadCurrentFont()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return 0.0
|
||||
}
|
||||
startx := x
|
||||
prev, hasPrev := truetype.Index(0), false
|
||||
fontName := gc.GetFontName()
|
||||
for fragment := range gc.Emojis.Iterate(text) {
|
||||
if fragment.IsEmoji {
|
||||
img, err := LoadFromPngFile(fragment.Emoji.Path)
|
||||
if err == nil {
|
||||
gc.Save()
|
||||
scale := gc.GetFontSize() / 100
|
||||
gc.Translate(x+scale*emojiSpacing, y-scale*emojiScale)
|
||||
gc.Scale(scale, scale)
|
||||
gc.DrawImage(img)
|
||||
gc.Restore()
|
||||
x += scale*float64(img.Bounds().Size().X) + scale*emojiSpacing*2
|
||||
}
|
||||
continue
|
||||
}
|
||||
index := f.Index(fragment.Rune)
|
||||
if hasPrev {
|
||||
x += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index))
|
||||
}
|
||||
glyph := gc.glyphCache.Fetch(gc, fontName, fragment.Rune)
|
||||
x += glyph.Fill(gc, x, y)
|
||||
prev, hasPrev = index, true
|
||||
}
|
||||
return x - startx
|
||||
}
|
||||
|
||||
// StrokeString draws the contour of the text at point (0, 0)
|
||||
func (gc *GraphicContext) StrokeString(text string) (cursor float64) {
|
||||
func (gc *GraphicContext) StrokeString(text string) (width float64) {
|
||||
return gc.StrokeStringAt(text, 0, 0)
|
||||
}
|
||||
|
||||
// StrokeStringAt draws the contour of the text at point (x, y)
|
||||
func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64) {
|
||||
width := gc.CreateStringPath(text, x, y)
|
||||
gc.Stroke()
|
||||
return width
|
||||
func (gc *GraphicContext) StrokeStringAt(text string, x, y float64) (width float64) {
|
||||
f, err := gc.loadCurrentFont()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return 0.0
|
||||
}
|
||||
startx := x
|
||||
prev, hasPrev := truetype.Index(0), false
|
||||
fontName := gc.GetFontName()
|
||||
for fragment := range gc.Emojis.Iterate(text) {
|
||||
if fragment.IsEmoji {
|
||||
img, err := LoadFromPngFile(fragment.Emoji.Path)
|
||||
if err == nil {
|
||||
gc.Save()
|
||||
scale := gc.GetFontSize() / 100
|
||||
gc.Translate(x+scale*emojiSpacing, y-scale*emojiScale)
|
||||
gc.Scale(scale, scale)
|
||||
gc.DrawImage(img)
|
||||
gc.Restore()
|
||||
x += scale*float64(img.Bounds().Size().X) + scale*emojiSpacing*2
|
||||
}
|
||||
continue
|
||||
}
|
||||
index := f.Index(fragment.Rune)
|
||||
if hasPrev {
|
||||
x += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index))
|
||||
}
|
||||
glyph := gc.glyphCache.Fetch(gc, fontName, fragment.Rune)
|
||||
x += glyph.Stroke(gc, x, y)
|
||||
prev, hasPrev = index, true
|
||||
}
|
||||
return x - startx
|
||||
}
|
||||
|
||||
func (gc *GraphicContext) loadCurrentFont() (*truetype.Font, error) {
|
||||
font := draw2d.GetFont(gc.Current.FontData)
|
||||
if font == nil {
|
||||
font = draw2d.GetFont(draw2dbase.DefaultFontData)
|
||||
font, err := gc.FontCache.Load(gc.Current.FontData)
|
||||
if err != nil {
|
||||
font, err = gc.FontCache.Load(draw2dbase.DefaultFontData)
|
||||
}
|
||||
if font == nil {
|
||||
return nil, errors.New("No font set, and no default font available.")
|
||||
if font != nil {
|
||||
gc.SetFont(font)
|
||||
gc.SetFontSize(gc.Current.FontSize)
|
||||
}
|
||||
gc.SetFont(font)
|
||||
gc.SetFontSize(gc.Current.FontSize)
|
||||
return font, nil
|
||||
return font, err
|
||||
}
|
||||
|
||||
// p is a truetype.Point measured in FUnits and positive Y going upwards.
|
||||
|
@ -212,8 +275,39 @@ func (gc *GraphicContext) GetStringBounds(s string) (left, top, right, bottom fl
|
|||
top, left, bottom, right = 10e6, 10e6, -10e6, -10e6
|
||||
cursor := 0.0
|
||||
prev, hasPrev := truetype.Index(0), false
|
||||
for _, rune := range s {
|
||||
index := f.Index(rune)
|
||||
// Get sample letter for approximated emoji calculations
|
||||
const letter = 'M'
|
||||
mindex := f.Index(letter)
|
||||
mtop, mleft, mheight, mwidth := 10e6, 10e6, -10e6, -10e6
|
||||
if err := gc.glyphBuf.Load(gc.Current.Font, fixed.Int26_6(gc.Current.Scale), mindex, font.HintingNone); err != nil {
|
||||
log.Println(err)
|
||||
return 0, 0, 0, 0
|
||||
}
|
||||
e0 := 0
|
||||
for _, e1 := range gc.glyphBuf.Ends {
|
||||
ps := gc.glyphBuf.Points[e0:e1]
|
||||
for _, p := range ps {
|
||||
x, y := pointToF64Point(p)
|
||||
mtop = math.Min(mtop, y)
|
||||
mheight = math.Max(mheight, y)
|
||||
mleft = math.Min(mleft, x)
|
||||
mwidth = math.Max(mwidth, x)
|
||||
}
|
||||
}
|
||||
mtop *= 1.2
|
||||
mwidth *= 1.55
|
||||
mheight += math.Abs(mtop-mheight) * 0.2
|
||||
// Actually iterate through the string
|
||||
for fragment := range gc.Emojis.Iterate(s) {
|
||||
if fragment.IsEmoji {
|
||||
cursor += mwidth
|
||||
top = math.Min(top, mtop)
|
||||
bottom = math.Max(bottom, mheight)
|
||||
left = math.Min(left, mleft)
|
||||
right = math.Max(right, cursor)
|
||||
continue
|
||||
}
|
||||
index := f.Index(fragment.Rune)
|
||||
if hasPrev {
|
||||
cursor += fUnitsToFloat64(f.Kern(fixed.Int26_6(gc.Current.Scale), prev, index))
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
package draw2dimg
|
||||
|
||||
import (
|
||||
"github.com/golang/freetype/raster"
|
||||
"git.fromouter.space/crunchy-rocks/freetype/raster"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package draw2dimg
|
||||
|
||||
import (
|
||||
"github.com/golang/freetype/truetype"
|
||||
"github.com/llgcode/draw2d"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||
"git.fromouter.space/crunchy-rocks/freetype/truetype"
|
||||
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/golang/freetype/truetype"
|
||||
"git.fromouter.space/crunchy-rocks/freetype/truetype"
|
||||
|
||||
"github.com/jung-kurt/gofpdf"
|
||||
"github.com/llgcode/draw2d"
|
||||
|
|
176
draw2dsvg/converters.go
Normal file
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 (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/golang/freetype/truetype"
|
||||
)
|
||||
"sync"
|
||||
|
||||
var (
|
||||
fontFolder = "../resource/font/"
|
||||
fonts = make(map[string]*truetype.Font)
|
||||
fontNamer FontFileNamer = FontFileName
|
||||
"git.fromouter.space/crunchy-rocks/freetype/truetype"
|
||||
)
|
||||
|
||||
// FontStyle defines bold and italic styles for the font
|
||||
|
@ -69,41 +64,167 @@ func FontFileName(fontData FontData) string {
|
|||
}
|
||||
|
||||
func RegisterFont(fontData FontData, font *truetype.Font) {
|
||||
fonts[fontNamer(fontData)] = font
|
||||
fontCache.Store(fontData, font)
|
||||
}
|
||||
|
||||
func GetFont(fontData FontData) *truetype.Font {
|
||||
fontFileName := fontNamer(fontData)
|
||||
font := fonts[fontFileName]
|
||||
if font != nil {
|
||||
return font
|
||||
func GetFont(fontData FontData) (font *truetype.Font) {
|
||||
var err error
|
||||
|
||||
if font, err = fontCache.Load(fontData); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
fonts[fontFileName] = loadFont(fontFileName)
|
||||
return fonts[fontFileName]
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func GetFontFolder() string {
|
||||
return fontFolder
|
||||
return defaultFonts.folder
|
||||
}
|
||||
|
||||
func SetFontFolder(folder string) {
|
||||
fontFolder = filepath.Clean(folder)
|
||||
defaultFonts.setFolder(filepath.Clean(folder))
|
||||
}
|
||||
|
||||
func GetGlobalFontCache() FontCache {
|
||||
return fontCache
|
||||
}
|
||||
|
||||
func SetFontNamer(fn FontFileNamer) {
|
||||
fontNamer = fn
|
||||
defaultFonts.setNamer(fn)
|
||||
}
|
||||
|
||||
func loadFont(fontFileName string) *truetype.Font {
|
||||
fontBytes, err := ioutil.ReadFile(path.Join(fontFolder, fontFileName))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil
|
||||
}
|
||||
font, err := truetype.Parse(fontBytes)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil
|
||||
}
|
||||
return font
|
||||
// Types implementing this interface can be passed to SetFontCache to change the
|
||||
// way fonts are being stored and retrieved.
|
||||
type FontCache interface {
|
||||
// Loads a truetype font represented by the FontData object passed as
|
||||
// argument.
|
||||
// The method returns an error if the font could not be loaded, either
|
||||
// because it didn't exist or the resource it was loaded from was corrupted.
|
||||
Load(FontData) (*truetype.Font, error)
|
||||
|
||||
// Sets the truetype font that will be returned by Load when given the font
|
||||
// data passed as first argument.
|
||||
Store(FontData, *truetype.Font)
|
||||
}
|
||||
|
||||
// Changes the font cache backend used by the package. After calling this
|
||||
// functionSetFontFolder and SetFontNamer will not affect anymore how fonts are
|
||||
// loaded.
|
||||
// To restore the default font cache, call this function passing nil as argument.
|
||||
func SetFontCache(cache FontCache) {
|
||||
if cache == nil {
|
||||
fontCache = defaultFonts
|
||||
} else {
|
||||
fontCache = cache
|
||||
}
|
||||
}
|
||||
|
||||
// FolderFontCache can Load font from folder
|
||||
type FolderFontCache struct {
|
||||
fonts map[string]*truetype.Font
|
||||
folder string
|
||||
namer FontFileNamer
|
||||
}
|
||||
|
||||
// NewFolderFontCache creates FolderFontCache
|
||||
func NewFolderFontCache(folder string) *FolderFontCache {
|
||||
return &FolderFontCache{
|
||||
fonts: make(map[string]*truetype.Font),
|
||||
folder: folder,
|
||||
namer: FontFileName,
|
||||
}
|
||||
}
|
||||
|
||||
// Load a font from cache if exists otherwise it will load the font from file
|
||||
func (cache *FolderFontCache) Load(fontData FontData) (font *truetype.Font, err error) {
|
||||
if font = cache.fonts[cache.namer(fontData)]; font != nil {
|
||||
return font, nil
|
||||
}
|
||||
|
||||
var data []byte
|
||||
var file = cache.namer(fontData)
|
||||
|
||||
if data, err = ioutil.ReadFile(filepath.Join(cache.folder, file)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if font, err = truetype.Parse(data); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cache.fonts[file] = font
|
||||
return
|
||||
}
|
||||
|
||||
// Store a font to this cache
|
||||
func (cache *FolderFontCache) Store(fontData FontData, font *truetype.Font) {
|
||||
cache.fonts[cache.namer(fontData)] = font
|
||||
}
|
||||
|
||||
// SyncFolderFontCache can Load font from folder
|
||||
type SyncFolderFontCache struct {
|
||||
sync.RWMutex
|
||||
fonts map[string]*truetype.Font
|
||||
folder string
|
||||
namer FontFileNamer
|
||||
}
|
||||
|
||||
// NewSyncFolderFontCache creates SyncFolderFontCache
|
||||
func NewSyncFolderFontCache(folder string) *SyncFolderFontCache {
|
||||
return &SyncFolderFontCache{
|
||||
fonts: make(map[string]*truetype.Font),
|
||||
folder: folder,
|
||||
namer: FontFileName,
|
||||
}
|
||||
}
|
||||
|
||||
func (cache *SyncFolderFontCache) setFolder(folder string) {
|
||||
cache.Lock()
|
||||
cache.folder = folder
|
||||
cache.Unlock()
|
||||
}
|
||||
|
||||
func (cache *SyncFolderFontCache) setNamer(namer FontFileNamer) {
|
||||
cache.Lock()
|
||||
cache.namer = namer
|
||||
cache.Unlock()
|
||||
}
|
||||
|
||||
// Load a font from cache if exists otherwise it will load the font from file
|
||||
func (cache *SyncFolderFontCache) Load(fontData FontData) (font *truetype.Font, err error) {
|
||||
cache.RLock()
|
||||
font = cache.fonts[cache.namer(fontData)]
|
||||
cache.RUnlock()
|
||||
|
||||
if font != nil {
|
||||
return font, nil
|
||||
}
|
||||
|
||||
var data []byte
|
||||
var file = cache.namer(fontData)
|
||||
|
||||
if data, err = ioutil.ReadFile(filepath.Join(cache.folder, file)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if font, err = truetype.Parse(data); err != nil {
|
||||
return
|
||||
}
|
||||
cache.Lock()
|
||||
cache.fonts[file] = font
|
||||
cache.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Store a font to this cache
|
||||
func (cache *SyncFolderFontCache) Store(fontData FontData, font *truetype.Font) {
|
||||
cache.Lock()
|
||||
cache.fonts[cache.namer(fontData)] = font
|
||||
cache.Unlock()
|
||||
}
|
||||
|
||||
var (
|
||||
defaultFonts = NewSyncFolderFontCache("../resource/font")
|
||||
|
||||
fontCache FontCache = defaultFonts
|
||||
)
|
||||
|
|
30
gc.go
30
gc.go
|
@ -10,9 +10,12 @@ import (
|
|||
|
||||
// GraphicContext describes the interface for the various backends (images, pdf, opengl, ...)
|
||||
type GraphicContext interface {
|
||||
// PathBuilder describes the interface for path drawing
|
||||
PathBuilder
|
||||
// BeginPath creates a new path
|
||||
BeginPath()
|
||||
// GetPath copies the current path, then returns it
|
||||
GetPath() Path
|
||||
// GetMatrixTransform returns the current transformation matrix
|
||||
GetMatrixTransform() Matrix
|
||||
// SetMatrixTransform sets the current transformation matrix
|
||||
|
@ -27,7 +30,7 @@ type GraphicContext interface {
|
|||
Scale(sx, sy float64)
|
||||
// SetStrokeColor sets the current stroke color
|
||||
SetStrokeColor(c color.Color)
|
||||
// SetStrokeColor sets the current fill color
|
||||
// SetFillColor sets the current fill color
|
||||
SetFillColor(c color.Color)
|
||||
// SetFillRule sets the current fill rule
|
||||
SetFillRule(f FillRule)
|
||||
|
@ -37,27 +40,48 @@ type GraphicContext interface {
|
|||
SetLineCap(cap LineCap)
|
||||
// SetLineJoin sets the current line join
|
||||
SetLineJoin(join LineJoin)
|
||||
// SetLineJoin sets the current dash
|
||||
// SetLineDash sets the current dash
|
||||
SetLineDash(dash []float64, dashOffset float64)
|
||||
// SetFontSize
|
||||
// SetFontSize sets the current font size
|
||||
SetFontSize(fontSize float64)
|
||||
// GetFontSize gets the current font size
|
||||
GetFontSize() float64
|
||||
// SetFontData sets the current FontData
|
||||
SetFontData(fontData FontData)
|
||||
// GetFontData gets the current FontData
|
||||
GetFontData() FontData
|
||||
// GetFontName gets the current FontData as a string
|
||||
GetFontName() string
|
||||
// DrawImage draws the raster image in the current canvas
|
||||
DrawImage(image image.Image)
|
||||
// Save the context and push it to the context stack
|
||||
Save()
|
||||
// Restore remove the current context and restore the last one
|
||||
Restore()
|
||||
// Clear fills the current canvas with a default transparent color
|
||||
Clear()
|
||||
// ClearRect fills the specified rectangle with a default transparent color
|
||||
ClearRect(x1, y1, x2, y2 int)
|
||||
// SetDPI sets the current DPI
|
||||
SetDPI(dpi int)
|
||||
// GetDPI gets the current DPI
|
||||
GetDPI() int
|
||||
// GetStringBounds gets pixel bounds(dimensions) of given string
|
||||
GetStringBounds(s string) (left, top, right, bottom float64)
|
||||
// CreateStringPath creates a path from the string s at x, y
|
||||
CreateStringPath(text string, x, y float64) (cursor float64)
|
||||
// FillString draws the text at point (0, 0)
|
||||
FillString(text string) (cursor float64)
|
||||
// FillStringAt draws the text at the specified point (x, y)
|
||||
FillStringAt(text string, x, y float64) (cursor float64)
|
||||
// StrokeString draws the contour of the text at point (0, 0)
|
||||
StrokeString(text string) (cursor float64)
|
||||
// StrokeStringAt draws the contour of the text at point (x, y)
|
||||
StrokeStringAt(text string, x, y float64) (cursor float64)
|
||||
// Stroke strokes the paths with the color specified by SetStrokeColor
|
||||
Stroke(paths ...*Path)
|
||||
// Fill fills the paths with the color specified by SetFillColor
|
||||
Fill(paths ...*Path)
|
||||
// FillStroke first fills the paths and than strokes them
|
||||
FillStroke(paths ...*Path)
|
||||
}
|
||||
|
|
13
go.mod
Normal file
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
|
||||
func (p *Path) LineTo(x, y float64) {
|
||||
if len(p.Components) == 0 { //special case when no move has been done
|
||||
p.MoveTo(0, 0)
|
||||
p.MoveTo(x, y)
|
||||
} else {
|
||||
p.appendToPath(LineToCmp, x, y)
|
||||
}
|
||||
p.appendToPath(LineToCmp, x, y)
|
||||
p.x = x
|
||||
p.y = y
|
||||
}
|
||||
|
@ -86,9 +87,10 @@ func (p *Path) LineTo(x, y float64) {
|
|||
// QuadCurveTo adds a quadratic bezier curve to the current path
|
||||
func (p *Path) QuadCurveTo(cx, cy, x, y float64) {
|
||||
if len(p.Components) == 0 { //special case when no move has been done
|
||||
p.MoveTo(0, 0)
|
||||
p.MoveTo(x, y)
|
||||
} else {
|
||||
p.appendToPath(QuadCurveToCmp, cx, cy, x, y)
|
||||
}
|
||||
p.appendToPath(QuadCurveToCmp, cx, cy, x, y)
|
||||
p.x = x
|
||||
p.y = y
|
||||
}
|
||||
|
@ -96,9 +98,10 @@ func (p *Path) QuadCurveTo(cx, cy, x, y float64) {
|
|||
// CubicCurveTo adds a cubic bezier curve to the current path
|
||||
func (p *Path) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) {
|
||||
if len(p.Components) == 0 { //special case when no move has been done
|
||||
p.MoveTo(0, 0)
|
||||
p.MoveTo(x, y)
|
||||
} else {
|
||||
p.appendToPath(CubicCurveToCmp, cx1, cy1, cx2, cy2, x, y)
|
||||
}
|
||||
p.appendToPath(CubicCurveToCmp, cx1, cy1, cx2, cy2, x, y)
|
||||
p.x = x
|
||||
p.y = y
|
||||
}
|
||||
|
@ -187,3 +190,34 @@ func (p *Path) String() string {
|
|||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Returns new Path with flipped y axes
|
||||
func (path *Path) VerticalFlip() *Path {
|
||||
p := path.Copy()
|
||||
j := 0
|
||||
for _, cmd := range p.Components {
|
||||
switch cmd {
|
||||
case MoveToCmp, LineToCmp:
|
||||
p.Points[j+1] = -p.Points[j+1]
|
||||
j = j + 2
|
||||
case QuadCurveToCmp:
|
||||
p.Points[j+1] = -p.Points[j+1]
|
||||
p.Points[j+3] = -p.Points[j+3]
|
||||
j = j + 4
|
||||
case CubicCurveToCmp:
|
||||
p.Points[j+1] = -p.Points[j+1]
|
||||
p.Points[j+3] = -p.Points[j+3]
|
||||
p.Points[j+5] = -p.Points[j+5]
|
||||
j = j + 6
|
||||
case ArcToCmp:
|
||||
p.Points[j+1] = -p.Points[j+1]
|
||||
p.Points[j+3] = -p.Points[j+3]
|
||||
p.Points[j+4] = -p.Points[j+4] // start angle
|
||||
p.Points[j+5] = -p.Points[j+5] // angle
|
||||
j = j + 6
|
||||
case CloseCmp:
|
||||
}
|
||||
}
|
||||
p.y = -p.y
|
||||
return p
|
||||
}
|
||||
|
|
|
@ -8,9 +8,9 @@ import (
|
|||
"image/color"
|
||||
"math"
|
||||
|
||||
"github.com/llgcode/draw2d"
|
||||
"github.com/llgcode/draw2d/draw2dkit"
|
||||
"github.com/llgcode/draw2d/samples"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/samples"
|
||||
)
|
||||
|
||||
// Main draws a droid and returns the filename. This should only be
|
||||
|
|
|
@ -9,9 +9,9 @@ import (
|
|||
"image/png"
|
||||
"net/http"
|
||||
|
||||
"github.com/llgcode/draw2d/draw2dimg"
|
||||
"github.com/llgcode/draw2d/draw2dpdf"
|
||||
"github.com/llgcode/draw2d/samples/android"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/draw2dimg"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/draw2dpdf"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/samples/android"
|
||||
|
||||
"appengine"
|
||||
)
|
||||
|
|
|
@ -7,10 +7,10 @@ package frameimage
|
|||
import (
|
||||
"math"
|
||||
|
||||
"github.com/llgcode/draw2d"
|
||||
"github.com/llgcode/draw2d/draw2dimg"
|
||||
"github.com/llgcode/draw2d/draw2dkit"
|
||||
"github.com/llgcode/draw2d/samples"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/draw2dimg"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/samples"
|
||||
)
|
||||
|
||||
// Main draws the image frame and returns the filename.
|
||||
|
|
|
@ -9,10 +9,10 @@ import (
|
|||
"image/color"
|
||||
"math"
|
||||
|
||||
"github.com/llgcode/draw2d"
|
||||
"github.com/llgcode/draw2d/draw2dkit"
|
||||
"github.com/llgcode/draw2d/samples"
|
||||
"github.com/llgcode/draw2d/samples/gopher2"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/samples"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/samples/gopher2"
|
||||
)
|
||||
|
||||
// Main draws geometry and returns the filename. This should only be
|
||||
|
@ -189,6 +189,7 @@ func CubicCurve(gc draw2d.GraphicContext, x, y, width, height float64) {
|
|||
}
|
||||
|
||||
// FillString draws a filled and stroked string.
|
||||
// And filles/stroked path created from string. Which may have different - unselectable - output in non raster gc implementations.
|
||||
func FillString(gc draw2d.GraphicContext, x, y, width, height float64) {
|
||||
sx, sy := width/100, height/100
|
||||
gc.Save()
|
||||
|
@ -202,7 +203,8 @@ func FillString(gc draw2d.GraphicContext, x, y, width, height float64) {
|
|||
gc.SetFontData(draw2d.FontData{
|
||||
Name: "luxi",
|
||||
Family: draw2d.FontFamilyMono,
|
||||
Style: draw2d.FontStyleBold | draw2d.FontStyleItalic})
|
||||
Style: draw2d.FontStyleBold | draw2d.FontStyleItalic,
|
||||
})
|
||||
w := gc.FillString("Hug")
|
||||
gc.Translate(w+sx, 0)
|
||||
left, top, right, bottom := gc.GetStringBounds("cou")
|
||||
|
@ -214,6 +216,14 @@ func FillString(gc draw2d.GraphicContext, x, y, width, height float64) {
|
|||
gc.SetStrokeColor(color.NRGBA{0x33, 0x33, 0xff, 0xff})
|
||||
gc.SetLineWidth(height / 100)
|
||||
gc.StrokeString("Hug")
|
||||
|
||||
gc.Translate(-(w + sx), sy*24)
|
||||
w = gc.CreateStringPath("Hug", 0, 0)
|
||||
gc.Fill()
|
||||
gc.Translate(w+sx, 0)
|
||||
gc.CreateStringPath("Hug", 0, 0)
|
||||
path := gc.GetPath()
|
||||
gc.Stroke((&path).VerticalFlip())
|
||||
gc.Restore()
|
||||
}
|
||||
|
||||
|
|
|
@ -8,8 +8,8 @@ package gopher
|
|||
import (
|
||||
"image/color"
|
||||
|
||||
"github.com/llgcode/draw2d"
|
||||
"github.com/llgcode/draw2d/samples"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/samples"
|
||||
)
|
||||
|
||||
// Main draws a left hand and ear of a gopher. Afterwards it returns
|
||||
|
|
|
@ -10,9 +10,9 @@ import (
|
|||
"image/color"
|
||||
"math"
|
||||
|
||||
"github.com/llgcode/draw2d"
|
||||
"github.com/llgcode/draw2d/draw2dkit"
|
||||
"github.com/llgcode/draw2d/samples"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/samples"
|
||||
)
|
||||
|
||||
// Main draws a rotated face of the gopher. Afterwards it returns
|
||||
|
|
|
@ -9,9 +9,9 @@ import (
|
|||
"fmt"
|
||||
"image"
|
||||
|
||||
"github.com/llgcode/draw2d"
|
||||
"github.com/llgcode/draw2d/draw2dkit"
|
||||
"github.com/llgcode/draw2d/samples"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/samples"
|
||||
)
|
||||
|
||||
// Main draws "Hello World" and returns the filename. This should only be
|
||||
|
|
|
@ -6,11 +6,11 @@ import (
|
|||
"log"
|
||||
"runtime"
|
||||
|
||||
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/draw2dgl"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/go-gl/glfw/v3.1/glfw"
|
||||
"github.com/llgcode/draw2d"
|
||||
"github.com/llgcode/draw2d/draw2dgl"
|
||||
"github.com/llgcode/draw2d/draw2dkit"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -7,9 +7,9 @@ package line
|
|||
import (
|
||||
"image/color"
|
||||
|
||||
"github.com/llgcode/draw2d"
|
||||
"github.com/llgcode/draw2d/draw2dkit"
|
||||
"github.com/llgcode/draw2d/samples"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/draw2dkit"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/samples"
|
||||
)
|
||||
|
||||
// Main draws vertically spaced lines and returns the filename.
|
||||
|
|
|
@ -7,8 +7,8 @@ package linecapjoin
|
|||
import (
|
||||
"image/color"
|
||||
|
||||
"github.com/llgcode/draw2d"
|
||||
"github.com/llgcode/draw2d/samples"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/samples"
|
||||
)
|
||||
|
||||
// Main draws the different line caps and joins.
|
||||
|
|
|
@ -8,8 +8,8 @@ import (
|
|||
|
||||
"github.com/llgcode/ps"
|
||||
|
||||
"github.com/llgcode/draw2d"
|
||||
"github.com/llgcode/draw2d/samples"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/samples"
|
||||
)
|
||||
|
||||
// Main draws the tiger
|
||||
|
|
|
@ -10,9 +10,9 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/draw2dgl"
|
||||
"github.com/go-gl/gl/v2.1/gl"
|
||||
"github.com/go-gl/glfw/v3.1/glfw"
|
||||
"github.com/llgcode/draw2d/draw2dgl"
|
||||
"github.com/llgcode/ps"
|
||||
)
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import "fmt"
|
|||
// Resource returns a resource filename for testing.
|
||||
func Resource(folder, filename, ext string) string {
|
||||
var root string
|
||||
if ext == "pdf" {
|
||||
if ext == "pdf" || ext == "svg" {
|
||||
root = "../"
|
||||
}
|
||||
return fmt.Sprintf("%sresource/%s/%s", root, folder, filename)
|
||||
|
@ -17,7 +17,7 @@ func Resource(folder, filename, ext string) string {
|
|||
// Output returns the output filename for testing.
|
||||
func Output(name, ext string) string {
|
||||
var root string
|
||||
if ext == "pdf" {
|
||||
if ext == "pdf" || ext == "svg" {
|
||||
root = "../"
|
||||
}
|
||||
return fmt.Sprintf("%soutput/samples/%s.%s", root, name, ext)
|
||||
|
|
|
@ -5,16 +5,16 @@ package draw2d_test
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/llgcode/draw2d"
|
||||
"github.com/llgcode/draw2d/samples/android"
|
||||
"github.com/llgcode/draw2d/samples/frameimage"
|
||||
"github.com/llgcode/draw2d/samples/geometry"
|
||||
"github.com/llgcode/draw2d/samples/gopher"
|
||||
"github.com/llgcode/draw2d/samples/gopher2"
|
||||
"github.com/llgcode/draw2d/samples/helloworld"
|
||||
"github.com/llgcode/draw2d/samples/line"
|
||||
"github.com/llgcode/draw2d/samples/linecapjoin"
|
||||
"github.com/llgcode/draw2d/samples/postscript"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/samples/android"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/samples/frameimage"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/samples/geometry"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/samples/gopher"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/samples/gopher2"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/samples/helloworld"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/samples/line"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/samples/linecapjoin"
|
||||
"git.fromouter.space/crunchy-rocks/draw2d/samples/postscript"
|
||||
)
|
||||
|
||||
func TestSampleAndroid(t *testing.T) {
|
||||
|
|
46
sync_test.go
Normal file
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