Skip to content

Commit

Permalink
vg/vgimg: migrate to fogleman/gg
Browse files Browse the repository at this point in the history
This CL modifies the vgimg backend implementation to use fogleman/gg
instead of llgcode/draw2d.

This CL adds consistent handling of DPI for the fogleman/gg backend.
The old backend required to be communicated the current DPI for the
image.
This new backend does not, as the DPI is only needed for the font.
gg handles fonts via golang.org/x/image/font.Face that embeds the DPI
upon creation of that font.

The vg/vgimg.Canvas carries its own DPI information and propagates it
correctly upon font registration and creation.
This evacuates the need to communicate the current DPI to gg.

Note that the fogleman/gg backend does not have support for dash lines with
offsets and needs to be eventually implemented upstream.

Updates fogleman/gg#64.
Fixes #231.
Fixes #470.
  • Loading branch information
sbinet committed Feb 19, 2019
1 parent 8e62c0c commit af8bb81
Show file tree
Hide file tree
Showing 58 changed files with 86 additions and 91 deletions.
2 changes: 1 addition & 1 deletion go.mod
Expand Up @@ -2,9 +2,9 @@ module gonum.org/v1/plot

require (
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af
github.com/fogleman/gg v1.2.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/jung-kurt/gofpdf v1.0.0
github.com/llgcode/draw2d v0.0.0-20180817132918-587a55234ca2
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4
Expand Down
8 changes: 2 additions & 6 deletions go.sum
@@ -1,15 +1,11 @@
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af h1:wVe6/Ea46ZMeNkQjjBW6xcqyQA/j5e0D6GytH95g0gQ=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/go-gl/gl v0.0.0-20180407155706-68e253793080/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=
github.com/go-gl/glfw v0.0.0-20180426074136-46a8d530c326/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/fogleman/gg v1.2.0 h1:Z0uOlqo+EbPQwdbrpKV1/jEcefXGPICDtGmS/gwly30=
github.com/fogleman/gg v1.2.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/jung-kurt/gofpdf v1.0.0 h1:EroSdlP9BOoL5ssLYf3uLJXhCQMMM2fFxCJDKA3RhnA=
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/llgcode/draw2d v0.0.0-20180817132918-587a55234ca2 h1:3xDkT1Tbsw2yDtKWUrROAlr15+dzp76kwucDvAPPnQo=
github.com/llgcode/draw2d v0.0.0-20180817132918-587a55234ca2/go.mod h1:mVa0dA29Db2S4LVqDYLlsePDzRJLDfdhVZiI15uY0FA=
github.com/llgcode/ps v0.0.0-20150911083025-f1443b32eedb h1:61ndUreYSlWFeCY44JxDDkngVoI7/1MVhEl98Nm0KOk=
github.com/llgcode/ps v0.0.0-20150911083025-f1443b32eedb/go.mod h1:1l8ky+Ew27CMX29uG+a2hNOKpeNYEQjjtiALiBlFQbY=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de h1:xSjD6HQTqT0H/k60N5yYBtnN1OEkVy7WIo/DYyxKRO0=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f h1:9kQ594xxPWRNKfTOnPjPcgrIJ19zM3ic57aI7PbMyAA=
Expand Down
Binary file modified palette/moreland/testdata/moreland_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified palette/testdata/reverse_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified palette/testdata/reverse_palette_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/barChart2_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/barChart_positiveNegative_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/bubbles_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/clippedFilledLine_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/colorBarHorizontalLog_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/colorBarHorizontal_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/colorBarVertical_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/errorBars_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/filledLine_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/functions_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/groupedBoxPlot_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/groupedQuartPlot_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/heatMap_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/histogram_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/histogram_logy_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/horizontalBarChart_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/horizontalBoxPlot_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/horizontalQuartPlot_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/image_plot_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/image_plot_log_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/invertedlogscale_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/logscale_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified plotter/testdata/plotLogo_golden.png
Binary file modified plotter/testdata/polygon_hexagons_golden.png
Binary file modified plotter/testdata/polygon_holes_golden.png
Binary file modified plotter/testdata/precision_golden.png
Binary file modified plotter/testdata/rotation_golden.png
Binary file modified plotter/testdata/sankeyGrouped_golden.png
Binary file modified plotter/testdata/sankeySimple_golden.png
Binary file modified plotter/testdata/scatterColor_golden.png
Binary file modified plotter/testdata/scatter_golden.png
Binary file modified plotter/testdata/stackedBarChart_golden.png
Binary file modified plotter/testdata/step_golden.png
Binary file modified plotter/testdata/timeseries_golden.png
Binary file modified plotter/testdata/verticalBarChart_golden.png
Binary file modified plotter/testdata/verticalBoxPlot_golden.png
Binary file modified plotter/testdata/verticalQuartPlot_golden.png
Binary file modified testdata/align_golden.png
Binary file modified testdata/legend_standalone_golden.png
11 changes: 9 additions & 2 deletions vg/font.go
Expand Up @@ -18,9 +18,9 @@ import (

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

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

"github.com/golang/freetype"
"github.com/golang/freetype/truetype"
)

Expand Down Expand Up @@ -110,6 +110,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 Expand Up @@ -190,7 +197,7 @@ func getFont(name string) (*truetype.Font, error) {
return nil, err
}

font, err := freetype.ParseFont(bytes)
font, err := truetype.Parse(bytes)
if err == nil {
fontLock.Lock()
loadedFonts[name] = font
Expand Down
Binary file modified vg/testdata/width_-1.jpg
Binary file modified vg/testdata/width_-1.png
Binary file modified vg/testdata/width_-1.tiff
Binary file not shown.
Binary file modified vg/testdata/width_0.jpg
Binary file modified vg/testdata/width_0.png
Binary file modified vg/testdata/width_0.tiff
Binary file not shown.
Binary file modified vg/testdata/width_1.jpg
Binary file modified vg/testdata/width_1.png
Binary file modified vg/testdata/width_1.tiff
Binary file not shown.
Binary file removed vg/vgimg/testdata/issue179.jpg
Diff not rendered.
Binary file added vg/vgimg/testdata/issue179_golden.jpg
128 changes: 59 additions & 69 deletions vg/vgimg/vgimg.go
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.ctx.SetLineCapButt()
c.img = c.ctx.Image().(draw.Image)
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,12 +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()
return setsDPI | setsSize | setsBackground
c.ctx = ctx
return setsSize | setsBackground
}
}

Expand All @@ -193,92 +187,99 @@ 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())
}
c.gc.SetLineDash(dashes, offs.Dots(c.DPI()))
// FIXME(sbinet): handle offs (the distance into the dash pattern
// to start the dash.)
// this needs to be implemented on the gg side.
// see:
// - https://github.com/fogleman/gg/issues/64
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 +289,21 @@ 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()))

x := pt.X.Dots(c.DPI())
y := pt.Y.Dots(c.DPI())
h := c.h.Dots(c.DPI())

c.ctx.InvertY()
c.ctx.DrawString(str, x, h-y)
}

// DrawImage implements the vg.Canvas.DrawImage method.
Expand All @@ -322,20 +319,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
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 af8bb81

Please sign in to comment.