From 869376395c758951ed119e2aa7f9350a9cf3e4e3 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Mon, 24 Nov 2025 18:36:13 +0900 Subject: [PATCH] ebiten: add premultipliedAlpha flag to imageToBytes This is a preparation for ColorEncoding option at NewImageFromImage. Updates #3314 --- image.go | 2 +- imagetobytes.go | 79 ++++++++++++++++++++----------- imagetobytes_test.go | 108 ++++++++++++++++++++++++++++++++++--------- 3 files changed, 137 insertions(+), 52 deletions(-) diff --git a/image.go b/image.go index 6c06e08e1..6c275d212 100644 --- a/image.go +++ b/image.go @@ -1511,7 +1511,7 @@ func NewImageFromImageWithOptions(source image.Image, options *NewImageFromImage return i } - i.WritePixels(imageToBytes(source)) + i.WritePixels(imageToBytes(source, true)) return i } diff --git a/imagetobytes.go b/imagetobytes.go index 44db508b4..fa3310ea5 100644 --- a/imagetobytes.go +++ b/imagetobytes.go @@ -21,12 +21,13 @@ import ( ) // imageToBytes gets RGBA bytes from img. +// premultipliedAlpha specifies whether the returned bytes are in premultiplied alpha format or not. // // Basically imageToBytes just calls draw.Draw. // If img is a paletted image, an optimized copying method is used. // -// If img is *image.RGBA and its length is same as 4*width*height, imageToBytes returns its Pix. -func imageToBytes(img image.Image) []byte { +// imageToBytes might return img.Pix directly without copying when possible. +func imageToBytes(img image.Image, premultipliedAlpha bool) []byte { size := img.Bounds().Size() w, h := size.X, size.Y @@ -41,48 +42,70 @@ func imageToBytes(img image.Image) []byte { y1 := b.Max.Y palette := make([]uint8, len(img.Palette)*4) - for i, c := range img.Palette { - // Create a temporary slice to reduce boundary checks. - pl := palette[4*i : 4*i+4] - rgba := color.RGBAModel.Convert(c).(color.RGBA) - pl[0] = rgba.R - pl[1] = rgba.G - pl[2] = rgba.B - pl[3] = rgba.A + if premultipliedAlpha { + for i, c := range img.Palette { + // Create a temporary slice to reduce boundary checks. + pl := palette[4*i : 4*i+4] + rgba := color.RGBAModel.Convert(c).(color.RGBA) + pl[0] = rgba.R + pl[1] = rgba.G + pl[2] = rgba.B + pl[3] = rgba.A + } + } else { + for i, c := range img.Palette { + // Create a temporary slice to reduce boundary checks. + pl := palette[4*i : 4*i+4] + nrgba := color.NRGBAModel.Convert(c).(color.NRGBA) + pl[0] = nrgba.R + pl[1] = nrgba.G + pl[2] = nrgba.B + pl[3] = nrgba.A + } } // Even img is a subimage of another image, Pix starts with 0-th index. - idx0 := 0 - idx1 := 0 + var srcIdx, dstIdx int d := img.Stride - (x1 - x0) - for j := 0; j < y1-y0; j++ { - for i := 0; i < x1-x0; i++ { - p := int(img.Pix[idx0]) - copy(bs[idx1:idx1+4], palette[4*p:4*p+4]) - idx0++ - idx1 += 4 + for range y1 - y0 { + for range x1 - x0 { + p := int(img.Pix[srcIdx]) + copy(bs[dstIdx:dstIdx+4], palette[4*p:4*p+4]) + srcIdx++ + dstIdx += 4 } - idx0 += d + srcIdx += d } return bs case *image.RGBA: - if len(img.Pix) == 4*w*h { + if premultipliedAlpha && len(img.Pix) == 4*w*h { + return img.Pix + } + case *image.NRGBA: + if !premultipliedAlpha && len(img.Pix) == 4*w*h { return img.Pix } - return imageToBytesSlow(img) - default: - return imageToBytesSlow(img) } + return imageToBytesSlow(img, premultipliedAlpha) } -func imageToBytesSlow(img image.Image) []byte { +func imageToBytesSlow(img image.Image, premultipliedAlpha bool) []byte { size := img.Bounds().Size() w, h := size.X, size.Y bs := make([]byte, 4*w*h) - dstImg := &image.RGBA{ - Pix: bs, - Stride: 4 * w, - Rect: image.Rect(0, 0, w, h), + var dstImg draw.Image + if premultipliedAlpha { + dstImg = &image.RGBA{ + Pix: bs, + Stride: 4 * w, + Rect: image.Rect(0, 0, w, h), + } + } else { + dstImg = &image.NRGBA{ + Pix: bs, + Stride: 4 * w, + Rect: image.Rect(0, 0, w, h), + } } draw.Draw(dstImg, image.Rect(0, 0, w, h), img, img.Bounds().Min, draw.Src) return bs diff --git a/imagetobytes_test.go b/imagetobytes_test.go index 39d6d6ce8..9fdc5cddd 100644 --- a/imagetobytes_test.go +++ b/imagetobytes_test.go @@ -39,61 +39,123 @@ func TestImageToBytes(t *testing.T) { } bigPalette := color.Palette(p) cases := []struct { - In image.Image - Out []uint8 + Image image.Image + Premul bool + Out []uint8 }{ { - In: &image.Paletted{ - Pix: []uint8{0, 1, 1, 0}, + Image: &image.Paletted{ + Pix: []uint8{0, 1, 1, 2}, Stride: 2, Rect: image.Rect(0, 0, 2, 2), Palette: color.Palette([]color.Color{ - color.Transparent, color.White, + color.Transparent, color.White, color.RGBA{0x80, 0x80, 0x80, 0x80}, }), }, - Out: []uint8{0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0}, + Premul: true, + Out: []uint8{0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x80, 0x80, 0x80}, }, { - In: image.NewPaletted(image.Rect(0, 0, 240, 160), pal).SubImage(image.Rect(238, 158, 240, 160)), - Out: []uint8{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + Image: &image.Paletted{ + Pix: []uint8{0, 1, 1, 2}, + Stride: 2, + Rect: image.Rect(0, 0, 2, 2), + Palette: color.Palette([]color.Color{ + color.Transparent, color.White, color.RGBA{0x80, 0x80, 0x80, 0x80}, + }), + }, + Premul: false, + Out: []uint8{0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80}, }, { - In: &image.RGBA{ + Image: image.NewPaletted(image.Rect(0, 0, 240, 160), pal).SubImage(image.Rect(238, 158, 240, 160)), + Premul: true, + Out: []uint8{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + }, + { + Image: image.NewPaletted(image.Rect(0, 0, 240, 160), pal).SubImage(image.Rect(238, 158, 240, 160)), + Premul: false, + Out: []uint8{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + }, + { + Image: &image.RGBA{ Pix: []uint8{0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0}, Stride: 8, Rect: image.Rect(0, 0, 2, 2), }, - Out: []uint8{0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0}, + Premul: true, + Out: []uint8{0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0}, }, { - In: &image.NRGBA{ + Image: &image.RGBA{ + Pix: []uint8{0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0}, + Stride: 8, + Rect: image.Rect(0, 0, 2, 2), + }, + Premul: false, + Out: []uint8{0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0}, + }, + { + Image: &image.NRGBA{ Pix: []uint8{0, 0, 0, 0, 0xff, 0xff, 0xff, 0x80, 0x80, 0x80, 0x80, 0x80, 0, 0, 0, 0}, Stride: 8, Rect: image.Rect(0, 0, 2, 2), }, - Out: []uint8{0, 0, 0, 0, 0x80, 0x80, 0x80, 0x80, 0x40, 0x40, 0x40, 0x80, 0, 0, 0, 0}, + Premul: true, + Out: []uint8{0, 0, 0, 0, 0x80, 0x80, 0x80, 0x80, 0x40, 0x40, 0x40, 0x80, 0, 0, 0, 0}, }, { - In: &image.Paletted{ + Image: &image.NRGBA{ + Pix: []uint8{0, 0, 0, 0, 0xff, 0xff, 0xff, 0x80, 0x80, 0x80, 0x80, 0x80, 0, 0, 0, 0}, + Stride: 8, + Rect: image.Rect(0, 0, 2, 2), + }, + Premul: false, + Out: []uint8{0, 0, 0, 0, 0xff, 0xff, 0xff, 0x80, 0x80, 0x80, 0x80, 0x80, 0, 0, 0, 0}, + }, + { + Image: &image.Paletted{ Pix: []uint8{0, 64, 0, 0}, Stride: 2, Rect: image.Rect(0, 0, 2, 2), Palette: bigPalette, }, - Out: []uint8{0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0, 0, 0, 0, 0}, + Premul: true, + Out: []uint8{0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0, 0, 0, 0, 0}, }, { - In: (&image.Paletted{ + Image: &image.Paletted{ + Pix: []uint8{0, 64, 0, 0}, + Stride: 2, + Rect: image.Rect(0, 0, 2, 2), + Palette: bigPalette, + }, + Premul: false, + Out: []uint8{0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0, 0, 0, 0, 0}, + }, + { + Image: (&image.Paletted{ Pix: []uint8{0, 64, 0, 0}, Stride: 2, Rect: image.Rect(0, 0, 2, 2), Palette: bigPalette, }).SubImage(image.Rect(1, 0, 2, 1)), - Out: []uint8{0xff, 0xff, 0xff, 0xff}, + Premul: true, + Out: []uint8{0xff, 0xff, 0xff, 0xff}, + }, + { + Image: (&image.Paletted{ + Pix: []uint8{0, 64, 0, 0}, + Stride: 2, + Rect: image.Rect(0, 0, 2, 2), + Palette: bigPalette, + }).SubImage(image.Rect(1, 0, 2, 1)), + Premul: false, + Out: []uint8{0xff, 0xff, 0xff, 0xff}, }, } for i, c := range cases { - got := ebiten.ImageToBytes(c.In) + got := ebiten.ImageToBytes(c.Image, c.Premul) want := c.Out if !bytes.Equal(got, want) { t.Errorf("Test %d: got: %v, want: %v", i, got, want) @@ -104,23 +166,23 @@ func TestImageToBytes(t *testing.T) { func BenchmarkImageToBytesRGBA(b *testing.B) { img := image.NewRGBA(image.Rect(0, 0, 4096, 4096)) b.ResetTimer() - for i := 0; i < b.N; i++ { - ebiten.ImageToBytes(img) + for range b.N { + ebiten.ImageToBytes(img, true) } } func BenchmarkImageToBytesNRGBA(b *testing.B) { img := image.NewNRGBA(image.Rect(0, 0, 4096, 4096)) b.ResetTimer() - for i := 0; i < b.N; i++ { - ebiten.ImageToBytes(img) + for range b.N { + ebiten.ImageToBytes(img, true) } } func BenchmarkImageToBytesPaletted(b *testing.B) { img := image.NewPaletted(image.Rect(0, 0, 4096, 4096), palette.Plan9) b.ResetTimer() - for i := 0; i < b.N; i++ { - ebiten.ImageToBytes(img) + for range b.N { + ebiten.ImageToBytes(img, true) } }