From 81bd5b488c6fdb1245599dba57fefa6a75602d7e Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Mon, 8 Aug 2022 01:05:55 +0900 Subject: [PATCH] ebiten: add (*Image).ReadPixels Closes #1995 --- image.go | 35 ++++++++++++++++++++++++++++++++--- image_test.go | 15 +++++++++++++++ internal/ui/image.go | 16 ++++++++++------ 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/image.go b/image.go index dd45ecb4a..16fc6a07a 100644 --- a/image.go +++ b/image.go @@ -777,6 +777,37 @@ func (i *Image) ColorModel() color.Model { return color.RGBAModel } +// ReadPixels reads the image's pixels from the image. +// +// ReadPixels loads pixels from GPU to system memory if necessary, which means that ReadPixels can be slow. +// +// ReadPixels always sets a transparent color if the image is disposed. +// +// len(pixels) must be 4*width*height. If the sizes don't match, ReadPixels returns an error. +// +// Note that an important logic should not rely on values returned by ReadPixels, since +// the returned values can include very slight differences between some machines. +// +// ReadPixels can't be called outside the main loop (ebiten.Run's updating function) starts. +func (i *Image) ReadPixels(pixels []byte) error { + b := i.Bounds() + if got, want := len(pixels), 4*b.Dx()*b.Dy(); got != want { + return fmt.Errorf("ebiten: len(pixels) must be %d but %d at ReadPixels", want, got) + } + + if i.isDisposed() { + for i := range pixels { + pixels[i] = 0 + } + return nil + } + + i.resolveSetVerticesCacheIfNeeded() + + x, y := i.adjustPosition(b.Min.X, b.Min.Y) + return i.image.ReadPixels(pixels, x, y, b.Dx(), b.Dy()) +} + // At returns the color of the image at (x, y). // // At loads pixels from GPU to system memory if necessary, which means that At can be slow. @@ -819,9 +850,7 @@ func (i *Image) at(x, y int) (r, g, b, a byte) { if c, ok := i.setVerticesCache[[2]int{x, y}]; ok { return c[0], c[1], c[2], c[3] } - var pix [4]byte - i.image.ReadPixels(pix[:], x, y, 1, 1) - return pix[0], pix[1], pix[2], pix[3] + return i.image.At(x, y) } // Set sets the color at (x, y). diff --git a/image_test.go b/image_test.go index 650e4aba2..84e26ae16 100644 --- a/image_test.go +++ b/image_test.go @@ -110,6 +110,21 @@ func TestImagePixels(t *testing.T) { } } } + + pix := make([]byte, 4*w*h) + if err := img0.ReadPixels(pix); err != nil { + t.Fatal(err) + } + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + idx := 4 * (j*w + i) + got := color.RGBA{pix[idx], pix[idx+1], pix[idx+2], pix[idx+3]} + want := color.RGBAModel.Convert(img.At(i, j)) + if got != want { + t.Errorf("(%d, %d): got %v; want %v", i, j, got, want) + } + } + } } func TestImageComposition(t *testing.T) { diff --git a/internal/ui/image.go b/internal/ui/image.go index 062b74e98..a4519c12a 100644 --- a/internal/ui/image.go +++ b/internal/ui/image.go @@ -75,21 +75,25 @@ func (i *Image) ReplacePixels(pix []byte, x, y, width, height int) { i.mipmap.ReplacePixels(pix, x, y, width, height) } -func (i *Image) ReadPixels(pixels []byte, x, y, width, height int) { +func (i *Image) ReadPixels(pixels []byte, x, y, width, height int) error { + return theUI.readPixels(i.mipmap, pixels, x, y, width, height) +} + +func (i *Image) At(x, y int) (r, g, b, a byte) { // Check the error existence and avoid unnecessary calls. if theGlobalState.error() != nil { - return + return 0, 0, 0, 0 } - if err := theUI.readPixels(i.mipmap, pixels, x, y, width, height); err != nil { + var pix [4]byte + if err := theUI.readPixels(i.mipmap, pix[:], x, y, 1, 1); err != nil { if panicOnErrorOnReadingPixels { panic(err) } theGlobalState.setError(err) - for i := range pixels { - pixels[i] = 0 - } + return 0, 0, 0, 0 } + return pix[0], pix[1], pix[2], pix[3] } func (i *Image) DumpScreenshot(name string, blackbg bool) error {