Skip to content

Commit

Permalink
vg/vgimg: migrate to fogleman/gg
Browse files Browse the repository at this point in the history
Fixes gonum#231.
Fixes gonum#470.
  • Loading branch information
sbinet committed Feb 18, 2019
1 parent 5d3656e commit 15d4d9b
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 84 deletions.
8 changes: 8 additions & 0 deletions vg/font.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

"gonum.org/v1/plot/vg/fonts"

"golang.org/x/image/font"
"golang.org/x/image/math/fixed"

"github.com/golang/freetype"
Expand Down Expand Up @@ -110,6 +111,13 @@ func (f *Font) Font() *truetype.Font {
return f.font
}

func (f *Font) FontFace(dpi float64) font.Face {
return truetype.NewFace(f.font, &truetype.Options{
Size: f.Size.Points(),
DPI: dpi,
})
}

// SetName sets the name of the font, effectively
// changing the font. If an error is returned then
// the font is left unchanged.
Expand Down
File renamed without changes
128 changes: 57 additions & 71 deletions vg/vgimg/vgimg.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
// license that can be found in the LICENSE file.

// Package vgimg implements the vg.Canvas interface using
// draw2d (github.com/llgcode/draw2d)
// as a backend to output raster images.
// github.com/fogleman/gg as a backend to output raster images.
package vgimg // import "gonum.org/v1/plot/vg/vgimg"

import (
Expand All @@ -16,20 +15,17 @@ import (
"image/jpeg"
"image/png"
"io"
"sync"

"github.com/fogleman/gg"
"golang.org/x/image/tiff"

"github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/draw2dimg"

"gonum.org/v1/plot/vg"
)

// Canvas implements the vg.Canvas interface,
// drawing to an image.Image using draw2d.
type Canvas struct {
gc draw2d.GraphicContext
ctx *gg.Context
img draw.Image
w, h vg.Length
color []color.Color
Expand Down Expand Up @@ -99,12 +95,11 @@ func NewWith(o ...option) *Canvas {
h := c.h / vg.Inch * vg.Length(c.dpi)
c.img = draw.Image(image.NewRGBA(image.Rect(0, 0, int(w+0.5), int(h+0.5))))
}
if c.gc == nil {
h := float64(c.img.Bounds().Max.Y - c.img.Bounds().Min.Y)
c.gc = draw2dimg.NewGraphicContext(c.img)
c.gc.SetDPI(c.dpi)
c.gc.Scale(1, -1)
c.gc.Translate(0, -h)
if c.ctx == nil {
c.ctx = gg.NewContextForImage(c.img)
c.img = c.ctx.Image().(draw.Image)
// c.gc.SetDPI(c.dpi) // FIXME(sbinet) customize DPI ?
c.ctx.InvertY()
}
draw.Draw(c.img, c.img.Bounds(), &image.Uniform{c.backgroundColor}, image.ZP, draw.Src)
c.color = []color.Color{color.Black}
Expand Down Expand Up @@ -162,11 +157,11 @@ func UseImage(img draw.Image) option {
// and a graphic context to create the canvas from.
// The minimum point of the given image
// should probably be 0,0.
func UseImageWithContext(img draw.Image, gc draw2d.GraphicContext) option {
func UseImageWithContext(img draw.Image, ctx *gg.Context) option {
return func(c *Canvas) uint32 {
c.img = img
c.gc = gc
c.dpi = gc.GetDPI()
c.ctx = ctx
//c.dpi = gc.GetDPI() // FIXME(sbinet) customize DPI ?
return setsDPI | setsSize | setsBackground
}
}
Expand All @@ -193,92 +188,97 @@ func (c *Canvas) Size() (w, h vg.Length) {

func (c *Canvas) SetLineWidth(w vg.Length) {
c.width = w
c.gc.SetLineWidth(w.Dots(c.DPI()))
c.ctx.SetLineWidth(w.Dots(c.DPI()))
}

func (c *Canvas) SetLineDash(ds []vg.Length, offs vg.Length) {
dashes := make([]float64, len(ds))
for i, d := range ds {
dashes[i] = d.Dots(c.DPI())
dashes := make([]float64, 0, len(ds))
for i := range ds {
if i > 0 {
dashes = append(dashes, offs.Dots(c.DPI()))
}
dashes = append(dashes, ds[i].Dots(c.DPI()))
}
c.gc.SetLineDash(dashes, offs.Dots(c.DPI()))
c.ctx.SetDash(dashes...)
}

func (c *Canvas) SetColor(clr color.Color) {
if clr == nil {
clr = color.Black
}
c.gc.SetFillColor(clr)
c.gc.SetStrokeColor(clr)
c.ctx.SetColor(clr)
c.color[len(c.color)-1] = clr
}

func (c *Canvas) Rotate(t float64) {
c.gc.Rotate(t)
c.ctx.Rotate(t)
}

func (c *Canvas) Translate(pt vg.Point) {
c.gc.Translate(pt.X.Dots(c.DPI()), pt.Y.Dots(c.DPI()))
c.ctx.Translate(pt.X.Dots(c.DPI()), pt.Y.Dots(c.DPI()))
}

func (c *Canvas) Scale(x, y float64) {
c.gc.Scale(x, y)
c.ctx.Scale(x, y)
}

func (c *Canvas) Push() {
c.color = append(c.color, c.color[len(c.color)-1])
c.gc.Save()
c.ctx.Push()
}

func (c *Canvas) Pop() {
c.color = c.color[:len(c.color)-1]
c.gc.Restore()
c.ctx.Pop()
}

func (c *Canvas) Stroke(p vg.Path) {
if c.width <= 0 {
return
}
c.outline(p)
c.gc.Stroke()
c.ctx.Stroke()
}

func (c *Canvas) Fill(p vg.Path) {
c.outline(p)
c.gc.Fill()
c.ctx.Fill()
}

func (c *Canvas) outline(p vg.Path) {
c.gc.BeginPath()
for _, comp := range p {
switch comp.Type {
case vg.MoveComp:
c.gc.MoveTo(comp.Pos.X.Dots(c.DPI()), comp.Pos.Y.Dots(c.DPI()))
c.ctx.MoveTo(comp.Pos.X.Dots(c.DPI()), comp.Pos.Y.Dots(c.DPI()))

case vg.LineComp:
c.gc.LineTo(comp.Pos.X.Dots(c.DPI()), comp.Pos.Y.Dots(c.DPI()))
c.ctx.LineTo(comp.Pos.X.Dots(c.DPI()), comp.Pos.Y.Dots(c.DPI()))

case vg.ArcComp:
c.gc.ArcTo(comp.Pos.X.Dots(c.DPI()), comp.Pos.Y.Dots(c.DPI()),
comp.Radius.Dots(c.DPI()), comp.Radius.Dots(c.DPI()),
comp.Start, comp.Angle)
c.ctx.DrawArc(comp.Pos.X.Dots(c.DPI()), comp.Pos.Y.Dots(c.DPI()),
comp.Radius.Dots(c.DPI()),
comp.Start, comp.Angle,
)

case vg.CurveComp:
switch len(comp.Control) {
case 1:
c.gc.QuadCurveTo(comp.Control[0].X.Dots(c.DPI()), comp.Control[0].Y.Dots(c.DPI()),
comp.Pos.X.Dots(c.DPI()), comp.Pos.Y.Dots(c.DPI()))
c.ctx.QuadraticTo(
comp.Control[0].X.Dots(c.DPI()), comp.Control[0].Y.Dots(c.DPI()),
comp.Pos.X.Dots(c.DPI()), comp.Pos.Y.Dots(c.DPI()),
)
case 2:
c.gc.CubicCurveTo(
c.ctx.CubicTo(
comp.Control[0].X.Dots(c.DPI()), comp.Control[0].Y.Dots(c.DPI()),
comp.Control[1].X.Dots(c.DPI()), comp.Control[1].Y.Dots(c.DPI()),
comp.Pos.X.Dots(c.DPI()), comp.Pos.Y.Dots(c.DPI()))
comp.Pos.X.Dots(c.DPI()), comp.Pos.Y.Dots(c.DPI()),
)
default:
panic("vgimg: invalid number of control points")
}

case vg.CloseComp:
c.gc.Close()
c.ctx.ClosePath()

default:
panic(fmt.Sprintf("Unknown path component: %d", comp.Type))
Expand All @@ -288,25 +288,18 @@ func (c *Canvas) outline(p vg.Path) {

// DPI returns the resolution of the receiver in pixels per inch.
func (c *Canvas) DPI() float64 {
return float64(c.gc.GetDPI())
return float64(c.dpi)
}

func (c *Canvas) FillString(font vg.Font, pt vg.Point, str string) {
c.gc.Save()
defer c.gc.Restore()

data := draw2d.FontData{Name: font.Name()}
registeredFont.Lock()
if !registeredFont.m[font.Name()] {
draw2d.RegisterFont(data, font.Font())
registeredFont.m[font.Name()] = true
}
registeredFont.Unlock()
c.gc.SetFontData(data)
c.gc.SetFontSize(font.Size.Points())
c.gc.Translate(pt.X.Dots(c.DPI()), pt.Y.Dots(c.DPI()))
c.gc.Scale(1, -1)
c.gc.FillString(str)
c.ctx.Push()
defer c.ctx.Pop()

c.ctx.SetFontFace(font.FontFace(c.DPI()))

c.ctx.Translate(pt.X.Dots(c.DPI()), pt.Y.Dots(c.DPI()))
c.ctx.Scale(1, -1)
c.ctx.DrawString(str, 0, 0)
}

// DrawImage implements the vg.Canvas.DrawImage method.
Expand All @@ -322,20 +315,13 @@ func (c *Canvas) DrawImage(rect vg.Rectangle, img image.Image) {
dx = float64(img.Bounds().Dx())
dy = float64(img.Bounds().Dy())
)
c.gc.Save()
c.gc.Scale(1, -1)
c.gc.Translate(xmin, -ymin-height)
c.gc.Scale(width/dx, height/dy)
c.gc.DrawImage(img)
c.gc.Restore()
}

// registeredFont contains the set of font names
// that have already been registered with draw2d.
var registeredFont = struct {
sync.Mutex
m map[string]bool
}{m: map[string]bool{}}
c.ctx.Push()
c.ctx.Scale(1, -1)
c.ctx.Translate(xmin, -ymin-height)
c.ctx.Scale(width/dx, height/dy)
c.ctx.DrawImage(img, 0, 0)
c.ctx.Pop()
}

// WriterCounter implements the io.Writer interface, and counts
// the total number of bytes written.
Expand Down
28 changes: 15 additions & 13 deletions vg/vgimg/vgimg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ import (
"image/color"
"io/ioutil"
"log"
"os"
"path/filepath"
"reflect"
"sync"
"testing"

"gonum.org/v1/plot"
"gonum.org/v1/plot/internal/cmpimg"
"gonum.org/v1/plot/plotter"
"gonum.org/v1/plot/vg"
"gonum.org/v1/plot/vg/draw"
Expand All @@ -39,36 +38,39 @@ func TestIssue179(t *testing.T) {
p.Draw(draw.New(c))
b := bytes.NewBuffer([]byte{})
if _, err = c.WriteTo(b); err != nil {
t.Error(err)
t.Fatal(err)
}

f, err := os.Open(filepath.Join("testdata", "issue179.jpg"))
want, err := ioutil.ReadFile("testdata/issue179_golden.jpg")
if err != nil {
t.Error(err)
t.Fatal(err)
}
defer f.Close()

want, err := ioutil.ReadAll(f)
ok, err := cmpimg.Equal("jpg", b.Bytes(), want)
if err != nil {
t.Error(err)
t.Fatal(err)
}
if !bytes.Equal(b.Bytes(), want) {
t.Error("Image mismatch")
if !ok {
ioutil.WriteFile("testdata/issue179.jpg", b.Bytes(), 0644)
t.Fatalf("images differ")
}
}

func TestConcurrentInit(t *testing.T) {
vg.MakeFont("Helvetica", 10)
ft, err := vg.MakeFont("Helvetica", 10)
if err != nil {
t.Fatal(err)
}
var wg sync.WaitGroup
wg.Add(2)
go func() {
c := vgimg.New(215, 215)
c.FillString(vg.Font{Size: 10}, vg.Point{}, "hi")
c.FillString(ft, vg.Point{}, "hi")
wg.Done()
}()
go func() {
c := vgimg.New(215, 215)
c.FillString(vg.Font{Size: 10}, vg.Point{}, "hi")
c.FillString(ft, vg.Point{}, "hi")
wg.Done()
}()
wg.Wait()
Expand Down

0 comments on commit 15d4d9b

Please sign in to comment.