Files
gio-patched/gpu/internal/rendertest/util_test.go
T
Pierre Curto af63c089f6 gpu/headless: make Screenshot take an input image to tranfer into
When extracting headless.Window's content via screenshots,
it can be useful to keep reusing the same image for output,
as well as specify which area of the Window is to be
extracted.
The updated Screenshot method does this by using the supplied
image.

API change: users must pass an existing image to Window.Screenshot.

Signed-off-by: Pierre Curto <pierre.curto@gmail.com>
2021-12-15 16:05:19 +01:00

291 lines
7.0 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package rendertest
import (
"bytes"
"flag"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"io/ioutil"
"path/filepath"
"strconv"
"testing"
"golang.org/x/image/colornames"
"gioui.org/f32"
"gioui.org/gpu/headless"
"gioui.org/internal/f32color"
"gioui.org/op"
"gioui.org/op/paint"
)
var (
dumpImages = flag.Bool("saveimages", false, "save test images")
squares paint.ImageOp
smallSquares paint.ImageOp
)
var (
red = f32color.RGBAToNRGBA(colornames.Red)
green = f32color.RGBAToNRGBA(colornames.Green)
blue = f32color.RGBAToNRGBA(colornames.Blue)
magenta = f32color.RGBAToNRGBA(colornames.Magenta)
black = f32color.RGBAToNRGBA(colornames.Black)
white = f32color.RGBAToNRGBA(colornames.White)
transparent = color.RGBA{}
)
func init() {
squares = buildSquares(512)
smallSquares = buildSquares(50)
}
func buildSquares(size int) paint.ImageOp {
sub := size / 4
im := image.NewNRGBA(image.Rect(0, 0, size, size))
c1, c2 := image.NewUniform(colornames.Green), image.NewUniform(colornames.Blue)
for r := 0; r < 4; r++ {
for c := 0; c < 4; c++ {
c1, c2 = c2, c1
draw.Draw(im, image.Rect(r*sub, c*sub, r*sub+sub, c*sub+sub), c1, image.Point{}, draw.Over)
}
c1, c2 = c2, c1
}
return paint.NewImageOp(im)
}
func drawImage(t *testing.T, size int, ops *op.Ops, draw func(o *op.Ops)) (*image.RGBA, error) {
sz := image.Point{X: size, Y: size}
w := newWindow(t, sz.X, sz.Y)
defer w.Release()
draw(ops)
if err := w.Frame(ops); err != nil {
return nil, err
}
img := image.NewRGBA(image.Rectangle{Max: sz})
err := w.Screenshot(img)
return img, err
}
func run(t *testing.T, f func(o *op.Ops), c func(r result)) {
// Draw a few times and check that it is correct each time, to
// ensure any caching effects still generate the correct images.
var img *image.RGBA
var err error
ops := new(op.Ops)
for i := 0; i < 3; i++ {
ops.Reset()
img, err = drawImage(t, 128, ops, f)
if err != nil {
t.Error("error rendering:", err)
return
}
// Check for a reference image and make sure it is identical.
if !verifyRef(t, img, 0) {
name := fmt.Sprintf("%s-%d-bad.png", t.Name(), i)
saveImage(t, name, img)
}
c(result{t: t, img: img})
}
}
func frame(f func(o *op.Ops), c func(r result)) frameT {
return frameT{f: f, c: c}
}
type frameT struct {
f func(o *op.Ops)
c func(r result)
}
// multiRun is used to run test cases over multiple frames, typically
// to test caching interactions.
func multiRun(t *testing.T, frames ...frameT) {
// draw a few times and check that it is correct each time, to
// ensure any caching effects still generate the correct images.
var err error
sz := image.Point{X: 128, Y: 128}
w := newWindow(t, sz.X, sz.Y)
defer w.Release()
ops := new(op.Ops)
for i := range frames {
ops.Reset()
frames[i].f(ops)
if err := w.Frame(ops); err != nil {
t.Errorf("rendering failed: %v", err)
continue
}
img := image.NewRGBA(image.Rectangle{Max: sz})
err = w.Screenshot(img)
if err != nil {
t.Errorf("screenshot failed: %v", err)
continue
}
// Check for a reference image and make sure they are identical.
ok := verifyRef(t, img, i)
if frames[i].c != nil {
frames[i].c(result{t: t, img: img})
}
if !ok {
name := t.Name() + ".png"
if i != 0 {
name = t.Name() + "_" + strconv.Itoa(i) + ".png"
}
saveImage(t, name, img)
}
}
}
func verifyRef(t *testing.T, img *image.RGBA, frame int) (ok bool) {
// ensure identical to ref data
path := filepath.Join("refs", t.Name()+".png")
if frame != 0 {
path = filepath.Join("refs", t.Name()+"_"+strconv.Itoa(frame)+".png")
}
if *dumpImages {
saveImage(t, path, img)
return true
}
b, err := ioutil.ReadFile(path)
if err != nil {
t.Error("could not open ref:", err)
return
}
r, err := png.Decode(bytes.NewReader(b))
if err != nil {
t.Error("could not decode ref:", err)
return
}
if img.Bounds() != r.Bounds() {
t.Errorf("reference image is %v, expected %v", r.Bounds(), img.Bounds())
return false
}
var ref *image.RGBA
switch r := r.(type) {
case *image.RGBA:
ref = r
case *image.NRGBA:
ref = image.NewRGBA(r.Bounds())
bnd := r.Bounds()
for x := bnd.Min.X; x < bnd.Max.X; x++ {
for y := bnd.Min.Y; y < bnd.Max.Y; y++ {
ref.SetRGBA(x, y, f32color.NRGBAToRGBA(r.NRGBAAt(x, y)))
}
}
default:
t.Fatalf("reference image is a %T, expected *image.NRGBA or *image.RGBA", r)
}
bnd := img.Bounds()
for x := bnd.Min.X; x < bnd.Max.X; x++ {
for y := bnd.Min.Y; y < bnd.Max.Y; y++ {
exp := ref.RGBAAt(x, y)
got := img.RGBAAt(x, y)
if !colorsClose(exp, got) {
t.Error("not equal to ref at", x, y, " ", got, exp)
return false
}
}
}
return true
}
func colorsClose(c1, c2 color.RGBA) bool {
const delta = 0.01 // magic value obtained from experimentation.
return yiqEqApprox(c1, c2, delta)
}
// yiqEqApprox compares the colors of 2 pixels, in the NTSC YIQ color space,
// as described in:
//
// Measuring perceived color difference using YIQ NTSC
// transmission color space in mobile applications.
// Yuriy Kotsarenko, Fernando Ramos.
//
// An electronic version is available at:
//
// - http://www.progmat.uaem.mx:8080/artVol2Num2/Articulo3Vol2Num2.pdf
func yiqEqApprox(c1, c2 color.RGBA, d2 float64) bool {
const max = 35215.0 // difference between 2 maximally different pixels.
var (
r1 = float64(c1.R)
g1 = float64(c1.G)
b1 = float64(c1.B)
r2 = float64(c2.R)
g2 = float64(c2.G)
b2 = float64(c2.B)
y1 = r1*0.29889531 + g1*0.58662247 + b1*0.11448223
i1 = r1*0.59597799 - g1*0.27417610 - b1*0.32180189
q1 = r1*0.21147017 - g1*0.52261711 + b1*0.31114694
y2 = r2*0.29889531 + g2*0.58662247 + b2*0.11448223
i2 = r2*0.59597799 - g2*0.27417610 - b2*0.32180189
q2 = r2*0.21147017 - g2*0.52261711 + b2*0.31114694
y = y1 - y2
i = i1 - i2
q = q1 - q2
diff = 0.5053*y*y + 0.299*i*i + 0.1957*q*q
)
return diff <= max*d2
}
func (r result) expect(x, y int, col color.RGBA) {
r.t.Helper()
if r.img == nil {
return
}
c := r.img.RGBAAt(x, y)
if !colorsClose(c, col) {
r.t.Error("expected ", col, " at ", "(", x, ",", y, ") but got ", c)
}
}
type result struct {
t *testing.T
img *image.RGBA
}
func saveImage(t testing.TB, file string, img *image.RGBA) {
if !*dumpImages {
return
}
// Only NRGBA images are losslessly encoded by png.Encode.
nrgba := image.NewNRGBA(img.Bounds())
bnd := img.Bounds()
for x := bnd.Min.X; x < bnd.Max.X; x++ {
for y := bnd.Min.Y; y < bnd.Max.Y; y++ {
nrgba.SetNRGBA(x, y, f32color.RGBAToNRGBA(img.RGBAAt(x, y)))
}
}
var buf bytes.Buffer
if err := png.Encode(&buf, nrgba); err != nil {
t.Error(err)
return
}
if err := ioutil.WriteFile(file, buf.Bytes(), 0666); err != nil {
t.Error(err)
return
}
}
func newWindow(t testing.TB, width, height int) *headless.Window {
w, err := headless.NewWindow(width, height)
if err != nil {
t.Skipf("failed to create headless window, skipping: %v", err)
}
return w
}
func scale(sx, sy float32) op.TransformOp {
return op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(sx, sy)))
}