diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d816c07..c1069f99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,13 +11,11 @@ jobs: strategy: matrix: go-version: [1.23.x, 1.22.x] - platform: [ubuntu-20.04, macos-latest] + platform: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.platform }} env: GO111MODULE: on GOPATH: ${{ github.workspace }} - DISPLAY: ":99.0" - EGL_PLATFORM: "x11" defaults: run: working-directory: ${{ env.GOPATH }}/src/gonum.org/v1/plot @@ -51,14 +49,6 @@ jobs: restore-keys: | ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - - name: Install Linux packages - if: matrix.platform == 'ubuntu-20.04' - run: | - sudo apt-get update - sudo apt-get install -qq gcc pkg-config libwayland-dev libx11-dev libx11-xcb-dev libxkbcommon-x11-dev libgles2-mesa-dev libegl1-mesa-dev libffi-dev libxcursor-dev libvulkan-dev xvfb xdotool - # start a virtual frame buffer - Xvfb :99 -screen 0 1920x1024x24 & - - name: Check copyrights+formatting run: | # Required for format check. @@ -76,7 +66,7 @@ jobs: go install -v ./... - name: Test Linux - if: matrix.platform == 'ubuntu-20.04' + if: matrix.platform == 'ubuntu-latest' run: | go test -v ./... ./.ci/check-imports.sh @@ -93,5 +83,5 @@ jobs: go test -v ./... - name: Upload-Coverage - if: matrix.platform == 'ubuntu-20.04' + if: matrix.platform == 'ubuntu-latest' uses: codecov/codecov-action@v2 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e864fd4c..736f53b8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,16 +9,10 @@ jobs: runs-on: ubuntu-latest env: GO111MODULE: on - DISPLAY: ":99.0" - EGL_PLATFORM: "x11" steps: - uses: actions/checkout@v4 with: fetch-depth: 1 - - name: cgo-dependencies - run: | - sudo apt-get update - sudo apt-get install -qq gcc pkg-config libwayland-dev libx11-dev libx11-xcb-dev libxkbcommon-x11-dev libgles2-mesa-dev libegl1-mesa-dev libffi-dev libxcursor-dev libvulkan-dev xvfb xdotool - uses: dominikh/staticcheck-action@v1 with: - version: "2024.1" + version: "2025.1" diff --git a/go.mod b/go.mod index b1e95a5e..58e0f223 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,8 @@ module gonum.org/v1/plot go 1.22.0 require ( - gioui.org v0.2.0 - gioui.org/x v0.2.0 git.sr.ht/~sbinet/gg v0.6.0 github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b - github.com/andybalholm/stroke v0.0.0-20221221101821-bd29b49d73f0 github.com/go-fonts/latin-modern v0.3.3 github.com/go-fonts/liberation v0.3.3 github.com/go-latex/latex v0.0.0-20240709081214-31cef3c7570e @@ -19,13 +16,8 @@ require ( ) require ( - gioui.org/cpu v0.0.0-20220412190645-f1e9e8c3b1f7 // indirect - gioui.org/shader v1.0.6 // indirect github.com/campoy/embedmd v1.0.0 // indirect - github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/exp/shiny v0.0.0-20241009180824-f66d83c29e7c // indirect - golang.org/x/sys v0.26.0 // indirect golang.org/x/text v0.19.0 // indirect ) diff --git a/go.sum b/go.sum index bb1bd52d..ef574b42 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,3 @@ -eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY= -eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA= -gioui.org v0.2.0 h1:RbzDn1h/pCVf/q44ImQSa/J3MIFpY3OWphzT/Tyei+w= -gioui.org v0.2.0/go.mod h1:1H72sKEk/fNFV+l0JNeM2Dt3co3Y4uaQcD+I+/GQ0e4= -gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= -gioui.org/cpu v0.0.0-20220412190645-f1e9e8c3b1f7 h1:tNJdnP5CgM39PRc+KWmBRRYX/zJ+rd5XaYxY5d5veqA= -gioui.org/cpu v0.0.0-20220412190645-f1e9e8c3b1f7/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= -gioui.org/shader v1.0.6 h1:cvZmU+eODFR2545X+/8XucgZdTtEjR3QWW6W65b0q5Y= -gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= -gioui.org/x v0.2.0 h1:/MbdjKH19F16auv19UiQxli2n6BYPw7eyh9XBOTgmEw= -gioui.org/x v0.2.0/go.mod h1:rCGN2nZ8ZHqrtseJoQxCMZpt2xrZUrdZ2WuMRLBJmYs= git.sr.ht/~sbinet/cmpimg v0.1.0 h1:E0zPRk2muWuCqSKSVZIWsgtU9pjsw3eKHi8VmQeScxo= git.sr.ht/~sbinet/cmpimg v0.1.0/go.mod h1:FU12psLbF4TfNXkKH2ZZQ29crIqoiqTZmeQ7dkp/pxE= git.sr.ht/~sbinet/gg v0.6.0 h1:RIzgkizAk+9r7uPzf/VfbJHBMKUr0F5hRFxTUGMnt38= @@ -18,8 +7,6 @@ github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= -github.com/andybalholm/stroke v0.0.0-20221221101821-bd29b49d73f0 h1:uF5Q/hWnDU1XZeT6CsrRSxHLroUSEYYO3kgES+yd+So= -github.com/andybalholm/stroke v0.0.0-20221221101821-bd29b49d73f0/go.mod h1:ccdDYaY5+gO+cbnQdFxEXqfy0RkoV25H3jLXUDNM3wg= github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY= github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= github.com/go-fonts/dejavu v0.3.4 h1:Qqyx9IOs5CQFxyWTdvddeWzrX0VNwUAvbmAzL0fpjbc= @@ -32,10 +19,6 @@ github.com/go-latex/latex v0.0.0-20240709081214-31cef3c7570e h1:xcdj0LWnMSIU1j8+ github.com/go-latex/latex v0.0.0-20240709081214-31cef3c7570e/go.mod h1:J4SAGzkcl+28QWi7yz72tyC/4aGnppOvya+AEv4TaAQ= github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw= github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y= -github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo= -github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k= -github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI= -github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= 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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -47,8 +30,6 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= -golang.org/x/exp/shiny v0.0.0-20241009180824-f66d83c29e7c h1:jTMrjjZRcSH/BDxWhXCP6OWsfVgmnwI7J+F4/nyVXaU= -golang.org/x/exp/shiny v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o= golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -61,8 +42,6 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= diff --git a/vg/vggio/context.go b/vg/vggio/context.go deleted file mode 100644 index 509fa352..00000000 --- a/vg/vggio/context.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright ©2020 The Gonum Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package vggio // import "gonum.org/v1/plot/vg/vggio" - -import ( - "image/color" - - "gioui.org/f32" - "gioui.org/op" - - "gonum.org/v1/plot/vg" -) - -// ctxops holds a stack of Gio operations. -type ctxops struct { - ops *op.Ops // ops is the Gio operations vggio is drawing on. - ctx []context // ctx is the stack of Gio operations vggio is manipulating. - - w vg.Length // w is the canvas window width. - h vg.Length // h is the canvas window height. - dpi float64 // dpi is the canvas window dots per inch resolution. -} - -// context holds state about the Gio backing store. -// context provides methods to translate between Gio values (reference frame, -// operations and stack) and their plot/vg counterparts. -type context struct { - color color.Color // color is the current color. - linew vg.Length // linew is the current line width. - pattern []vg.Length // pattern is the current line style. - offset vg.Length // offset is the current line style. - - trans op.TransformStack // trans is the Gio transform context stack. -} - -func (ctx *ctxops) cur() *context { - return &ctx.ctx[len(ctx.ctx)-1] -} - -func (ctx *ctxops) push() { - ctx.ctx = append(ctx.ctx, *ctx.cur()) - ctx.cur().trans = op.TransformOp{}.Push(ctx.ops) -} - -func (ctx *ctxops) pop() { - ctx.cur().trans.Pop() - ctx.ctx = ctx.ctx[:len(ctx.ctx)-1] -} - -func (ctx *ctxops) scale(x, y float64) { - op.Affine(f32.Affine2D{}.Scale( - f32.Pt(0, 0), - f32.Pt(float32(x), float32(y)), - )).Add(ctx.ops) -} - -func (ctx *ctxops) translate(x, y float64) { - op.Affine(f32.Affine2D{}.Offset( - f32.Pt(float32(x), float32(y)), - )).Add(ctx.ops) -} - -func (ctx *ctxops) rotate(rad float64) { - op.Affine(f32.Affine2D{}.Rotate( - f32.Pt(0, 0), float32(rad), - )).Add(ctx.ops) -} - -func (ctx *ctxops) invertY() { - ctx.translate(0, ctx.h.Dots(ctx.dpi)) - ctx.scale(1, -1) -} - -func (ctx *ctxops) pt32(p vg.Point) f32.Point { - return f32.Point{ - X: float32(p.X.Dots(ctx.dpi)), - Y: float32(p.Y.Dots(ctx.dpi)), - } -} diff --git a/vg/vggio/testdata/func_golden.png b/vg/vggio/testdata/func_golden.png deleted file mode 100644 index 0eb247be..00000000 Binary files a/vg/vggio/testdata/func_golden.png and /dev/null differ diff --git a/vg/vggio/testdata/image_golden.png b/vg/vggio/testdata/image_golden.png deleted file mode 100644 index e09b1712..00000000 Binary files a/vg/vggio/testdata/image_golden.png and /dev/null differ diff --git a/vg/vggio/testdata/labels_golden.png b/vg/vggio/testdata/labels_golden.png deleted file mode 100644 index 6398e6dc..00000000 Binary files a/vg/vggio/testdata/labels_golden.png and /dev/null differ diff --git a/vg/vggio/testdata/paths_golden.png b/vg/vggio/testdata/paths_golden.png deleted file mode 100644 index a2f57b49..00000000 Binary files a/vg/vggio/testdata/paths_golden.png and /dev/null differ diff --git a/vg/vggio/vggio.go b/vg/vggio/vggio.go deleted file mode 100644 index 43ba321c..00000000 --- a/vg/vggio/vggio.go +++ /dev/null @@ -1,530 +0,0 @@ -// Copyright ©2020 The Gonum Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package vggio provides a vg.Canvas implementation backed by Gio, -// a toolkit that implements portable immediate GUI mode in Go. -// -// More informations about Gio can be found at https://gioui.org/. -package vggio // import "gonum.org/v1/plot/vg/vggio" - -import ( - "bytes" - "fmt" - "image" - "image/color" - "strings" - "sync" - - "gioui.org/f32" - giofont "gioui.org/font" - "gioui.org/font/opentype" - "gioui.org/gpu/headless" - "gioui.org/layout" - "gioui.org/op" - "gioui.org/op/clip" - "gioui.org/op/paint" - "gioui.org/text" - "gioui.org/unit" - "gioui.org/widget/material" - "gioui.org/x/stroke" - bstroke "github.com/andybalholm/stroke" - "golang.org/x/image/draw" - "golang.org/x/image/font/sfnt" - - "gonum.org/v1/plot/font" - "gonum.org/v1/plot/vg" -) - -var ( - _ vg.Canvas = (*Canvas)(nil) - _ vg.CanvasSizer = (*Canvas)(nil) -) - -// Canvas implements the vg.Canvas interface, -// drawing to an image.Image using vgimg and painting that image -// into a Gioui context. -type Canvas struct { - gtx layout.Context - ctx ctxops - - bkg color.Color // bkg is the background color. -} - -// DefaultDPI is the default dot resolution for image -// drawing in dots per inch. -const DefaultDPI = 96 - -// New returns a new image canvas with the provided dimensions and options. -// The currently accepted options are UseDPI and UseBackgroundColor. -// If the resolution or background color are not specified, defaults are used. -func New(gtx layout.Context, w, h vg.Length, opts ...option) *Canvas { - cfg := &config{ - dpi: DefaultDPI, - bkg: color.White, - } - for _, opt := range opts { - opt(cfg) - } - c := &Canvas{ - gtx: gtx, - ctx: ctxops{ - ops: gtx.Ops, - ctx: []context{ - {color: color.Black}, - }, - w: w, - h: h, - dpi: cfg.dpi, - }, - bkg: cfg.bkg, - } - - // flip the Y-axis so that Y grows from bottom to top and - // Y=0 is at the bottom of the image. - c.ctx.invertY() - - vg.Initialize(c) - - return c -} - -type config struct { - dpi float64 - bkg color.Color -} - -type option func(*config) - -// UseDPI sets the dots per inch of a canvas. It should only be -// used as an option argument when initializing a new canvas. -func UseDPI(dpi int) option { - if dpi <= 0 { - panic("DPI must be > 0.") - } - return func(c *config) { - c.dpi = float64(dpi) - } -} - -// UseBackgroundColor specifies the image background color. -// Without UseBackgroundColor, the default color is white. -func UseBackgroundColor(c color.Color) option { - return func(cfg *config) { - cfg.bkg = c - } -} - -// Size implement vg.CanvasSizer. -func (c *Canvas) Size() (w, h vg.Length) { - return c.ctx.w, c.ctx.h -} - -// DPI returns the resolution of the receiver in pixels per inch. -func (c *Canvas) DPI() float64 { - return c.ctx.dpi -} - -// Paint returns the painting operations. -func (c *Canvas) Paint() *op.Ops { - return c.gtx.Ops -} - -// Screenshot returns a screenshot of the canvas as an image. -func (c *Canvas) Screenshot() (image.Image, error) { - win, err := headless.NewWindow( - int(c.ctx.w.Dots(c.ctx.dpi)), - int(c.ctx.h.Dots(c.ctx.dpi)), - ) - if err != nil { - return nil, fmt.Errorf("vggio: could not create headless window: %w", err) - } - - err = win.Frame(c.gtx.Ops) - if err != nil { - return nil, fmt.Errorf("vggio: could not run headless frame: %w", err) - } - - img := image.NewRGBA(image.Rectangle{Max: win.Size()}) - err = win.Screenshot(img) - if err != nil { - return nil, fmt.Errorf("vggio: could not create screenshot: %w", err) - } - - return img, nil -} - -// SetLineWidth sets the width of stroked paths. -// If the width is not positive then stroked lines -// are not drawn. -// -// The initial line width is 1 point. -func (c *Canvas) SetLineWidth(w vg.Length) { - c.ctx.cur().linew = w -} - -// SetLineDash sets the dash pattern for lines. -// The pattern slice specifies the lengths of -// alternating dashes and gaps, and the offset -// specifies the distance into the dash pattern -// to start the dash. -// -// The initial dash pattern is a solid line. -func (c *Canvas) SetLineDash(pattern []vg.Length, offset vg.Length) { - cur := c.ctx.cur() - cur.pattern = pattern - cur.offset = offset -} - -// SetColor sets the current drawing color. -// Note that fill color and stroke color are -// the same, so if you want different fill -// and stroke colors then you must set a color, -// draw fills, set a new color and then draw lines. -// -// The initial color is black. -// If SetColor is called with a nil color then black is used. -func (c *Canvas) SetColor(clr color.Color) { - if clr == nil { - clr = color.Black - } - c.ctx.cur().color = clr -} - -// Rotate applies a rotation transform to the context. -// The parameter is specified in radians. -func (c *Canvas) Rotate(rad float64) { - c.ctx.rotate(rad) -} - -// Translate applies a translational transform -// to the context. -func (c *Canvas) Translate(pt vg.Point) { - c.ctx.translate(pt.X.Dots(c.ctx.dpi), pt.Y.Dots(c.ctx.dpi)) -} - -// Scale applies a scaling transform to the -// context. -func (c *Canvas) Scale(x, y float64) { - c.ctx.scale(x, y) -} - -// Push saves the current line width, the -// current dash pattern, the current -// transforms, and the current color -// onto a stack so that the state can later -// be restored by calling Pop(). -func (c *Canvas) Push() { - c.ctx.push() -} - -// Pop restores the context saved by the -// corresponding call to Push(). -func (c *Canvas) Pop() { - c.ctx.pop() -} - -// Stroke strokes the given path. -func (c *Canvas) Stroke(p vg.Path) { - if c.ctx.cur().linew <= 0 { - return - } - c.ctx.push() - defer c.ctx.pop() - - var ( - cur = c.ctx.cur() - dashes stroke.Dashes - ) - dashes.Phase = float32(cur.offset.Dots(c.ctx.dpi)) - dashes.Dashes = make([]float32, len(cur.pattern)) - for i, v := range cur.pattern { - dashes.Dashes[i] = float32(v.Dots(c.ctx.dpi)) - } - - shape := stroke.Stroke{ - Path: c.stroke(p), - Width: float32(cur.linew.Dots(c.ctx.dpi)), - Cap: stroke.FlatCap, - Dashes: dashes, - }.Op(c.ctx.ops) - - clr := c.ctx.cur().color - paint.FillShape(c.ctx.ops, rgba(clr), shape) -} - -// Fill fills the given path. -func (c *Canvas) Fill(p vg.Path) { - c.ctx.push() - defer c.ctx.pop() - - shape := clip.Outline{ - Path: c.outline(p), - }.Op() - - clr := c.ctx.cur().color - paint.FillShape(c.ctx.ops, rgba(clr), shape) -} - -func rgba(c color.Color) color.NRGBA { - r, g, b, a := c.RGBA() - return color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: uint8(a)} -} - -func (c *Canvas) outline(p vg.Path) clip.PathSpec { - var path clip.Path - path.Begin(c.ctx.ops) - for _, comp := range p { - switch comp.Type { - case vg.MoveComp: - pt := c.ctx.pt32(comp.Pos) - path.MoveTo(pt) - - case vg.LineComp: - pt := c.ctx.pt32(comp.Pos) - path.LineTo(pt) - - case vg.ArcComp: - center := c.ctx.pt32(comp.Pos) - path.ArcTo(center, center, float32(comp.Angle)) - - case vg.CurveComp: - switch len(comp.Control) { - case 1: - ctl := c.ctx.pt32(comp.Control[0]) - end := c.ctx.pt32(comp.Pos) - path.QuadTo(ctl, end) - case 2: - ctl0 := c.ctx.pt32(comp.Control[0]) - ctl1 := c.ctx.pt32(comp.Control[1]) - end := c.ctx.pt32(comp.Pos) - path.CubeTo(ctl0, ctl1, end) - default: - panic("vggio: invalid number of control points") - } - - case vg.CloseComp: - path.Close() - - default: - panic(fmt.Sprintf("vggio: unknown path component %d", comp.Type)) - } - } - return path.End() -} - -func (c *Canvas) stroke(p vg.Path) stroke.Path { - var ( - path stroke.Path - add = func(seg stroke.Segment) { - path.Segments = append(path.Segments, seg) - } - pen f32.Point - beg f32.Point - ) - - for i, comp := range p { - if i == 0 { - beg = c.ctx.pt32(comp.Pos) - } - switch comp.Type { - case vg.MoveComp: - pt := c.ctx.pt32(comp.Pos) - add(stroke.MoveTo(pt)) - pen = pt - - case vg.LineComp: - pt := c.ctx.pt32(comp.Pos) - add(stroke.LineTo(pt)) - pen = pt - - case vg.ArcComp: - center := c.ctx.pt32(comp.Pos) - arcs := arcTo(pen, center, center, float32(comp.Angle)) - path.Segments = append(path.Segments, xStroke(arcs)...) - pen = f32.Point(arcs[len(arcs)-1].End) - - case vg.CurveComp: - switch len(comp.Control) { - case 1: - var ( - ctl = c.ctx.pt32(comp.Control[0]) - end = c.ctx.pt32(comp.Pos) - ) - add(stroke.QuadTo(ctl, end)) - pen = end - case 2: - var ( - ctl0 = c.ctx.pt32(comp.Control[0]) - ctl1 = c.ctx.pt32(comp.Control[1]) - end = c.ctx.pt32(comp.Pos) - ) - add(stroke.CubeTo(ctl0, ctl1, end)) - pen = end - default: - panic("vggio: invalid number of control points") - } - - case vg.CloseComp: - add(stroke.LineTo(beg)) - pen = beg - - default: - panic(fmt.Sprintf("vggio: unknown path component %d", comp.Type)) - } - } - return path -} - -// FillString fills in text at the specified -// location using the given font. -// If the font size is zero, the text is not drawn. -func (c *Canvas) FillString(fnt font.Face, pt vg.Point, txt string) { - if fnt.Font.Size == 0 { - return - } - c.ctx.push() - defer c.ctx.pop() - - e := fnt.Extents() - x := pt.X.Dots(c.ctx.dpi) - y := pt.Y.Dots(c.ctx.dpi) - e.Descent.Dots(c.ctx.dpi) - h := c.ctx.h.Dots(c.ctx.dpi) - - c.ctx.invertY() - c.ctx.translate(x, h-y-fnt.Font.Size.Dots(c.ctx.dpi)) - - th := material.NewTheme() - th.Shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(collectionFor(fnt))) - lbl := material.Label( - th, - unit.Sp(float32(fnt.Font.Size.Dots(c.ctx.dpi))), - txt, - ) - lbl.Color = rgba(c.ctx.cur().color) - lbl.Alignment = text.Start - lbl.Layout(c.gtx) -} - -// DrawImage draws the image, scaled to fit -// the destination rectangle. -func (c *Canvas) DrawImage(rect vg.Rectangle, img image.Image) { - c.ctx.push() - defer c.ctx.pop() - - var ( - ops = c.ctx.ops - dpi = c.DPI() - min = rect.Min - xmin = min.X.Dots(dpi) - ymin = min.Y.Dots(dpi) - rsz = rect.Size() - width = rsz.X.Dots(dpi) - height = rsz.Y.Dots(dpi) - dst = image.NewRGBA(image.Rect(0, 0, int(width), int(height))) - ) - - draw.NearestNeighbor.Scale(dst, dst.Rect, img, img.Bounds(), draw.Src, nil) - - c.ctx.scale(1, -1) - c.ctx.translate(xmin, -ymin-height) - paint.NewImageOp(dst).Add(ops) - paint.PaintOp{}.Add(ops) -} - -var dbfonts = &gioFontsCache{ - cache: make(map[string][]giofont.FontFace), - fonts: make(map[string]struct{}), -} - -type gioFontsCache struct { - sync.RWMutex - cache map[string][]giofont.FontFace - fonts map[string]struct{} - buf sfnt.Buffer -} - -func (cache *gioFontsCache) get(fnt font.Face) ([]giofont.FontFace, bool) { - cache.RLock() - defer cache.RUnlock() - - _, ok := cache.fonts[fnt.Name()] - if !ok { - return nil, false - } - name := collectionName(fnt.Name()) - return cache.cache[name], ok -} - -func (cache *gioFontsCache) add(fnt font.Face) []giofont.FontFace { - cache.Lock() - defer cache.Unlock() - - name := fnt.Name() - if fnt.Face == nil { - panic(fmt.Errorf("vggio: nil plot/font.Face %q", name)) - } - buf := new(bytes.Buffer) - _, err := fnt.Face.WriteSourceTo(&cache.buf, buf) - if err != nil { - panic(fmt.Errorf("vggio: could not load font %q: %+v", name, err)) - } - - gioFace, err := opentype.Parse(buf.Bytes()) - if err != nil { - panic(fmt.Errorf("vggio: could not parse font %q: %+v", name, err)) - } - - gioFnt := gonumToGioFont(fnt.Font) - - colName := collectionName(fnt.Name()) - cache.cache[colName] = append(cache.cache[colName], giofont.FontFace{ - Font: gioFnt, - Face: gioFace, - }) - cache.fonts[name] = struct{}{} - - return cache.cache[colName] -} - -func gonumToGioFont(fnt font.Font) giofont.Font { - o := giofont.Font{ - Typeface: giofont.Typeface(fnt.Typeface), - Style: giofont.Style(fnt.Style), - Weight: giofont.Weight(fnt.Weight), - } - return o -} - -func collectionFor(fnt font.Face) []giofont.FontFace { - coll, ok := dbfonts.get(fnt) - if !ok { - coll = dbfonts.add(fnt) - } - return coll -} - -func collectionName(name string) string { - // regroup fonts with name "Liberation-Italic", "Liberation-Bold", ... - // under the same collection "Liberation". - if strings.Contains(name, "-") { - i := strings.Index(name, "-") - name = name[:i] - } - return name -} - -func arcTo(start, f1, f2 f32.Point, angle float32) []bstroke.Segment { - if f1 == f2 { - return bstroke.AppendArc(nil, bstroke.Pt(start.X, start.Y), bstroke.Pt(f1.X, f1.Y), angle) - } - return bstroke.AppendEllipticalArc(nil, bstroke.Pt(start.X, start.Y), bstroke.Pt(f1.X, f1.Y), bstroke.Pt(f2.X, f2.Y), angle) -} - -func xStroke(bs []bstroke.Segment) []stroke.Segment { - vs := make([]stroke.Segment, len(bs)) - for i, b := range bs { - vs[i] = stroke.CubeTo(f32.Point(b.CP1), f32.Point(b.CP2), f32.Point(b.End)) - } - return vs -} diff --git a/vg/vggio/vggio_example_test.go b/vg/vggio/vggio_example_test.go deleted file mode 100644 index 97f6ac23..00000000 --- a/vg/vggio/vggio_example_test.go +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright ©2020 The Gonum Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package vggio_test - -import ( - "image/color" - "math" - "os" - "time" - - "gioui.org/app" - "gioui.org/io/key" - "gioui.org/io/system" - "gioui.org/layout" - "gioui.org/op" - "gioui.org/op/clip" - "gioui.org/unit" - - "gonum.org/v1/plot" - "gonum.org/v1/plot/plotter" - "gonum.org/v1/plot/vg" - "gonum.org/v1/plot/vg/draw" - "gonum.org/v1/plot/vg/vggio" -) - -func ExampleCanvas() { - const ( - w = 20 * vg.Centimeter - h = 15 * vg.Centimeter - dpi = 96 - ) - go func(w, h vg.Length) { - defer os.Exit(0) - - win := app.NewWindow( - app.Title("Gonum"), - app.Size( - unit.Dp(float32(w.Dots(dpi))), - unit.Dp(float32(h.Dots(dpi))), - ), - ) - - done := time.NewTimer(2 * time.Second) - defer done.Stop() - - for e := range win.Events() { - switch e := e.(type) { - case system.FrameEvent: - var ( - ops op.Ops - gtx = layout.NewContext(&ops, e) - ) - // register a global key listener for the escape key wrapping our entire UI. - area := clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops) - key.InputOp{ - Tag: win, - Keys: key.NameEscape + "|Ctrl-Q|Alt-Q", - }.Add(gtx.Ops) - - for _, e := range gtx.Events(win) { - switch e := e.(type) { - case key.Event: - switch e.Name { - case key.NameEscape: - return - case "Q": - if e.Modifiers.Contain(key.ModCtrl) { - return - } - if e.Modifiers.Contain(key.ModAlt) { - return - } - } - } - } - area.Pop() - - p := plot.New() - p.Title.Text = "My title" - p.X.Label.Text = "X" - p.Y.Label.Text = "Y" - - quad := plotter.NewFunction(func(x float64) float64 { - return x * x - }) - quad.Color = color.RGBA{B: 255, A: 255} - - exp := plotter.NewFunction(func(x float64) float64 { - return math.Pow(2, x) - }) - exp.Dashes = []vg.Length{vg.Points(2), vg.Points(2)} - exp.Width = vg.Points(2) - exp.Color = color.RGBA{G: 255, A: 255} - - sin := plotter.NewFunction(func(x float64) float64 { - return 10*math.Sin(x) + 50 - }) - sin.Dashes = []vg.Length{vg.Points(4), vg.Points(5)} - sin.Width = vg.Points(4) - sin.Color = color.RGBA{R: 255, A: 255} - - p.Add(quad, exp, sin) - p.Legend.Add("x^2", quad) - p.Legend.Add("2^x", exp) - p.Legend.Add("10*sin(x)+50", sin) - p.Legend.ThumbnailWidth = 0.5 * vg.Inch - - p.X.Min = 0 - p.X.Max = 10 - p.Y.Min = 0 - p.Y.Max = 100 - - p.Add(plotter.NewGrid()) - - cnv := vggio.New(gtx, w, h, vggio.UseDPI(dpi)) - p.Draw(draw.New(cnv)) - e.Frame(cnv.Paint()) - - case system.DestroyEvent: - return - } - } - }(w, h) - - app.Main() -} diff --git a/vg/vggio/vggio_test.go b/vg/vggio/vggio_test.go deleted file mode 100644 index 1ecf6fba..00000000 --- a/vg/vggio/vggio_test.go +++ /dev/null @@ -1,482 +0,0 @@ -// Copyright ©2020 The Gonum Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package vggio - -import ( - "fmt" - "image" - "image/color" - "image/png" - "math" - "os" - "runtime" - "testing" - - "gioui.org/layout" - "gioui.org/op" - "gonum.org/v1/plot" - "gonum.org/v1/plot/cmpimg" - "gonum.org/v1/plot/plotter" - "gonum.org/v1/plot/vg" - "gonum.org/v1/plot/vg/draw" -) - -const deltaGio = 0.05 // empirical value from experimentation. - -// init makes sure the headless display is ready for tests with Gio. -// On GitHub Actions and on linux, that headless display may take some time to -// be properly available and appears to be setup "on demand". -// So we request it by trying to take a screenshot twice: -// - the first time around might fail -// - the second time shouldn't. -func init() { - if runtime.GOOS != "linux" { - return - } - - const ( - w = 20 * vg.Centimeter - h = 15 * vg.Centimeter - dpi = 96 - ) - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Exact(image.Pt( - int(w.Dots(dpi)), - int(h.Dots(dpi)), - )), - } - - var err error - for try := 0; try < 2; try++ { - _, err = New(gtx, w, h, UseDPI(dpi)).Screenshot() - if err == nil { - return - } - } - - panic(fmt.Errorf("vg/vggio_test: could not setup headless display: %+v", err)) -} - -func TestCanvas(t *testing.T) { - if runtime.GOOS == "darwin" { - t.Skip("TODO: github actions for darwin with headless setup.") - } - - const fname = "testdata/func.png" - - const ( - w = 20 * vg.Centimeter - h = 15 * vg.Centimeter - dpi = 96 - ) - - cmpimg.CheckPlotApprox(func() { - p := plot.New() - p.Title.Text = "My title" - p.X.Label.Text = "X" - p.Y.Label.Text = "Y" - - quad := plotter.NewFunction(func(x float64) float64 { return x * x }) - quad.Color = color.RGBA{B: 255, A: 255} - - exp := plotter.NewFunction(func(x float64) float64 { return math.Pow(2, x) }) - exp.Dashes = []vg.Length{vg.Points(2), vg.Points(2)} - exp.Width = vg.Points(2) - exp.Color = color.RGBA{G: 255, A: 255} - - sin := plotter.NewFunction(func(x float64) float64 { return 10*math.Sin(x) + 50 }) - sin.Dashes = []vg.Length{vg.Points(4), vg.Points(5)} - sin.Width = vg.Points(4) - sin.Color = color.RGBA{R: 255, A: 255} - - p.Add(quad, exp, sin) - p.Legend.Add("x^2", quad) - p.Legend.Add("2^x", exp) - p.Legend.Add("10*sin(x)+50", sin) - p.Legend.ThumbnailWidth = 0.5 * vg.Inch - - p.X.Min = 0 - p.X.Max = 10 - p.Y.Min = 0 - p.Y.Max = 100 - - p.Add(plotter.NewGrid()) - - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Exact(image.Pt( - int(w.Dots(dpi)), - int(h.Dots(dpi)), - )), - } - cnv := New(gtx, w, h, UseDPI(dpi)) - p.Draw(draw.New(cnv)) - - img, err := cnv.Screenshot() - if err != nil { - t.Fatalf("could not create screenshot: %+v", err) - } - f, err := os.Create(fname) - if err != nil { - t.Fatalf("could not create output file: %+v", err) - } - defer f.Close() - - err = png.Encode(f, img) - if err != nil { - t.Fatalf("could not encode screenshot: %+v", err) - } - - err = f.Close() - if err != nil { - t.Fatalf("could not save screenshot: %+v", err) - } - }, t, deltaGio, "func.png", - ) -} - -func TestCollectionName(t *testing.T) { - for _, tc := range []struct { - name string - want string - }{ - {"Liberation", "Liberation"}, - {"LiberationSerif-Bold", "LiberationSerif"}, - {"LiberationSerif-BoldItalic", "LiberationSerif"}, - {"LiberationSerif-BoldItalic-Extra", "LiberationSerif"}, - - {"LiberationMono", "LiberationMono"}, - {"LiberationMono-Regular", "LiberationMono"}, - - {"Times-Roman", "Times"}, - {"Times-Bold", "Times"}, - } { - got := collectionName(tc.name) - if got != tc.want { - t.Errorf( - "%s: invalid collection name: got=%q, want=%q", - tc.name, got, tc.want, - ) - } - } -} - -func TestLabels(t *testing.T) { - if runtime.GOOS == "darwin" { - t.Skip("TODO: github actions for darwin with headless setup.") - } - - const fname = "testdata/labels.png" - - const ( - w = 20 * vg.Centimeter - h = 15 * vg.Centimeter - dpi = 96 - ) - - cmpimg.CheckPlotApprox(func() { - p := plot.New() - p.Title.Text = "Labels" - p.X.Min = -1 - p.X.Max = +1 - p.Y.Min = -1 - p.Y.Max = +1 - - const ( - left = 0.00 - middle = 0.02 - right = 0.04 - ) - - labels, err := plotter.NewLabels(plotter.XYLabels{ - XYs: []plotter.XY{ - {X: -0.8 + left, Y: -0.5}, // Aq + y-align bottom - {X: -0.6 + middle, Y: -0.5}, // Aq + y-align center - {X: -0.4 + right, Y: -0.5}, // Aq + y-align top - - {X: -0.8 + left, Y: +0.5}, // ditto for Aq\nAq - {X: -0.6 + middle, Y: +0.5}, - {X: -0.4 + right, Y: +0.5}, - - {X: +0.0 + left, Y: +0}, // ditto for Bg\nBg\nBg - {X: +0.2 + middle, Y: +0}, - {X: +0.4 + right, Y: +0}, - }, - Labels: []string{ - "Aq", "Aq", "Aq", - "Aq\nAq", "Aq\nAq", "Aq\nAq", - - "Bg\nBg\nBg", - "Bg\nBg\nBg", - "Bg\nBg\nBg", - }, - }) - if err != nil { - t.Fatalf("could not creates labels plotter: %+v", err) - } - for i := range labels.TextStyle { - sty := &labels.TextStyle[i] - sty.Font.Size = vg.Length(34) - } - labels.TextStyle[0].YAlign = draw.YBottom - labels.TextStyle[1].YAlign = draw.YCenter - labels.TextStyle[2].YAlign = draw.YTop - - labels.TextStyle[3].YAlign = draw.YBottom - labels.TextStyle[4].YAlign = draw.YCenter - labels.TextStyle[5].YAlign = draw.YTop - - labels.TextStyle[6].YAlign = draw.YBottom - labels.TextStyle[7].YAlign = draw.YCenter - labels.TextStyle[8].YAlign = draw.YTop - - lred, err := plotter.NewLabels(plotter.XYLabels{ - XYs: []plotter.XY{ - {X: -0.8 + left, Y: +0.5}, - {X: +0.0 + left, Y: +0}, - }, - Labels: []string{ - "Aq", "Bg", - }, - }) - if err != nil { - t.Fatalf("could not creates labels plotter: %+v", err) - } - for i := range lred.TextStyle { - sty := &lred.TextStyle[i] - sty.Font.Size = vg.Length(34) - sty.Color = color.RGBA{R: 255, A: 255} - sty.YAlign = draw.YBottom - } - - m5 := plotter.NewFunction(func(float64) float64 { return -0.5 }) - m5.LineStyle.Color = color.RGBA{R: 255, A: 255} - - l0 := plotter.NewFunction(func(float64) float64 { return 0 }) - l0.LineStyle.Color = color.RGBA{G: 255, A: 255} - - p5 := plotter.NewFunction(func(float64) float64 { return +0.5 }) - p5.LineStyle.Color = color.RGBA{B: 255, A: 255} - - p.Add(labels, lred, m5, l0, p5) - p.Add(plotter.NewGrid()) - p.Add(plotter.NewGlyphBoxes()) - - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Exact(image.Pt( - int(w.Dots(dpi)), - int(h.Dots(dpi)), - )), - } - cnv := New(gtx, w, h, UseDPI(dpi)) - p.Draw(draw.New(cnv)) - - img, err := cnv.Screenshot() - if err != nil { - t.Fatalf("could not create screenshot: %+v", err) - } - f, err := os.Create(fname) - if err != nil { - t.Fatalf("could not create output file: %+v", err) - } - defer f.Close() - - err = png.Encode(f, img) - if err != nil { - t.Fatalf("could not encode screenshot: %+v", err) - } - - err = f.Close() - if err != nil { - t.Fatalf("could not save screenshot: %+v", err) - } - }, t, deltaGio, "labels.png", - ) -} - -func TestPaths(t *testing.T) { - if runtime.GOOS == "darwin" { - t.Skip("TODO: github actions for darwin with headless setup.") - } - - const fname = "testdata/paths.png" - - const ( - w = 20 * vg.Centimeter - h = 15 * vg.Centimeter - dpi = 96 - ) - - cmpimg.CheckPlotApprox(func() { - p := plot.New() - p.Title.Text = "Paths" - p.X.Min = -1 - p.X.Max = +1 - p.Y.Min = -1 - p.Y.Max = +1 - - newScatter := func(c color.Color, sty draw.GlyphDrawer, x, y float64) *plotter.Scatter { - t.Helper() - - pts := make(plotter.XYs, 1) - pts[0].X = x - pts[0].Y = y - - plt, err := plotter.NewScatter(pts) - if err != nil { - t.Fatal(err) - } - plt.GlyphStyle.Color = c - plt.GlyphStyle.Radius = vg.Points(10) - plt.GlyphStyle.Shape = sty - return plt - } - - p.Add( - newScatter( - color.RGBA{R: 255, A: 255}, - draw.CircleGlyph{}, - -0.8, -0.8, - ), - newScatter( - color.RGBA{B: 255, A: 255}, - draw.RingGlyph{}, - -0.6, -0.6, - ), - newScatter( - color.RGBA{R: 255, A: 255}, - draw.SquareGlyph{}, - -0.4, -0.4, - ), - newScatter( - color.RGBA{B: 255, A: 255}, - draw.BoxGlyph{}, - -0.2, -0.2, - ), - newScatter( - color.RGBA{R: 255, A: 255}, - draw.TriangleGlyph{}, - 0, 0, - ), - newScatter( - color.RGBA{B: 255, A: 255}, - draw.PyramidGlyph{}, - 0.2, 0.2, - ), - newScatter( - color.RGBA{R: 255, A: 255}, - draw.PlusGlyph{}, - 0.4, 0.4, - ), - newScatter( - color.RGBA{B: 255, A: 255}, - draw.CrossGlyph{}, - 0.6, 0.6, - ), - ) - - p.Add(plotter.NewGrid()) - p.Add(plotter.NewGlyphBoxes()) - - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Exact(image.Pt( - int(w.Dots(dpi)), - int(h.Dots(dpi)), - )), - } - cnv := New(gtx, w, h, UseDPI(dpi), UseBackgroundColor(color.Transparent)) - p.Draw(draw.New(cnv)) - - img, err := cnv.Screenshot() - if err != nil { - t.Fatalf("could not create screenshot: %+v", err) - } - f, err := os.Create(fname) - if err != nil { - t.Fatalf("could not create output file: %+v", err) - } - defer f.Close() - - err = png.Encode(f, img) - if err != nil { - t.Fatalf("could not encode screenshot: %+v", err) - } - - err = f.Close() - if err != nil { - t.Fatalf("could not save screenshot: %+v", err) - } - }, t, deltaGio, "paths.png", - ) -} - -// An example of embedding an image in a plot. -func TestImage(t *testing.T) { - if runtime.GOOS == "darwin" { - t.Skip("TODO: github actions for darwin with headless setup.") - } - - const fname = "testdata/image.png" - - const ( - w = 20 * vg.Centimeter - h = 15 * vg.Centimeter - dpi = 96 - ) - - cmpimg.CheckPlotApprox(func() { - p := plot.New() - p.Title.Text = "A Logo" - - // load an image - src, err := os.Open("../../plotter/testdata/gopher.png") - if err != nil { - t.Fatalf("error opening image file: %v\n", err) - } - defer src.Close() - - img, err := png.Decode(src) - if err != nil { - t.Fatalf("error decoding image file: %v\n", err) - } - - p.Add(plotter.NewImage(img, 100, 100, 200, 200)) - - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Exact(image.Pt( - int(w.Dots(dpi)), - int(h.Dots(dpi)), - )), - } - cnv := New(gtx, w, h, UseDPI(dpi), UseBackgroundColor(color.Transparent)) - p.Draw(draw.New(cnv)) - - scr, err := cnv.Screenshot() - if err != nil { - t.Fatalf("could not create screenshot: %+v", err) - } - - out, err := os.Create(fname) - if err != nil { - t.Fatalf("could not create output file: %+v", err) - } - defer out.Close() - - err = png.Encode(out, scr) - if err != nil { - t.Fatalf("could not encode screenshot: %+v", err) - } - - err = out.Close() - if err != nil { - t.Fatalf("could not save screenshot: %+v", err) - } - }, t, deltaGio, "image.png", - ) -}