ebiten: add premultipliedAlpha flag to imageToBytes

This is a preparation for ColorEncoding option at NewImageFromImage.

Updates #3314
This commit is contained in:
Hajime Hoshi
2025-11-24 18:36:13 +09:00
parent 43f1312136
commit 869376395c
3 changed files with 137 additions and 52 deletions

View File

@@ -1511,7 +1511,7 @@ func NewImageFromImageWithOptions(source image.Image, options *NewImageFromImage
return i
}
i.WritePixels(imageToBytes(source))
i.WritePixels(imageToBytes(source, true))
return i
}

View File

@@ -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

View File

@@ -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)
}
}