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
+1 -1
View File
@@ -1511,7 +1511,7 @@ func NewImageFromImageWithOptions(source image.Image, options *NewImageFromImage
return i return i
} }
i.WritePixels(imageToBytes(source)) i.WritePixels(imageToBytes(source, true))
return i return i
} }
+51 -28
View File
@@ -21,12 +21,13 @@ import (
) )
// imageToBytes gets RGBA bytes from img. // 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. // Basically imageToBytes just calls draw.Draw.
// If img is a paletted image, an optimized copying method is used. // 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. // imageToBytes might return img.Pix directly without copying when possible.
func imageToBytes(img image.Image) []byte { func imageToBytes(img image.Image, premultipliedAlpha bool) []byte {
size := img.Bounds().Size() size := img.Bounds().Size()
w, h := size.X, size.Y w, h := size.X, size.Y
@@ -41,48 +42,70 @@ func imageToBytes(img image.Image) []byte {
y1 := b.Max.Y y1 := b.Max.Y
palette := make([]uint8, len(img.Palette)*4) palette := make([]uint8, len(img.Palette)*4)
for i, c := range img.Palette { if premultipliedAlpha {
// Create a temporary slice to reduce boundary checks. for i, c := range img.Palette {
pl := palette[4*i : 4*i+4] // Create a temporary slice to reduce boundary checks.
rgba := color.RGBAModel.Convert(c).(color.RGBA) pl := palette[4*i : 4*i+4]
pl[0] = rgba.R rgba := color.RGBAModel.Convert(c).(color.RGBA)
pl[1] = rgba.G pl[0] = rgba.R
pl[2] = rgba.B pl[1] = rgba.G
pl[3] = rgba.A 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. // Even img is a subimage of another image, Pix starts with 0-th index.
idx0 := 0 var srcIdx, dstIdx int
idx1 := 0
d := img.Stride - (x1 - x0) d := img.Stride - (x1 - x0)
for j := 0; j < y1-y0; j++ { for range y1 - y0 {
for i := 0; i < x1-x0; i++ { for range x1 - x0 {
p := int(img.Pix[idx0]) p := int(img.Pix[srcIdx])
copy(bs[idx1:idx1+4], palette[4*p:4*p+4]) copy(bs[dstIdx:dstIdx+4], palette[4*p:4*p+4])
idx0++ srcIdx++
idx1 += 4 dstIdx += 4
} }
idx0 += d srcIdx += d
} }
return bs return bs
case *image.RGBA: 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 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() size := img.Bounds().Size()
w, h := size.X, size.Y w, h := size.X, size.Y
bs := make([]byte, 4*w*h) bs := make([]byte, 4*w*h)
dstImg := &image.RGBA{ var dstImg draw.Image
Pix: bs, if premultipliedAlpha {
Stride: 4 * w, dstImg = &image.RGBA{
Rect: image.Rect(0, 0, w, h), 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) draw.Draw(dstImg, image.Rect(0, 0, w, h), img, img.Bounds().Min, draw.Src)
return bs return bs
+85 -23
View File
@@ -39,61 +39,123 @@ func TestImageToBytes(t *testing.T) {
} }
bigPalette := color.Palette(p) bigPalette := color.Palette(p)
cases := []struct { cases := []struct {
In image.Image Image image.Image
Out []uint8 Premul bool
Out []uint8
}{ }{
{ {
In: &image.Paletted{ Image: &image.Paletted{
Pix: []uint8{0, 1, 1, 0}, Pix: []uint8{0, 1, 1, 2},
Stride: 2, Stride: 2,
Rect: image.Rect(0, 0, 2, 2), Rect: image.Rect(0, 0, 2, 2),
Palette: color.Palette([]color.Color{ 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)), Image: &image.Paletted{
Out: []uint8{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, 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}, Pix: []uint8{0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0},
Stride: 8, Stride: 8,
Rect: image.Rect(0, 0, 2, 2), 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}, Pix: []uint8{0, 0, 0, 0, 0xff, 0xff, 0xff, 0x80, 0x80, 0x80, 0x80, 0x80, 0, 0, 0, 0},
Stride: 8, Stride: 8,
Rect: image.Rect(0, 0, 2, 2), 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}, Pix: []uint8{0, 64, 0, 0},
Stride: 2, Stride: 2,
Rect: image.Rect(0, 0, 2, 2), Rect: image.Rect(0, 0, 2, 2),
Palette: bigPalette, 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}, Pix: []uint8{0, 64, 0, 0},
Stride: 2, Stride: 2,
Rect: image.Rect(0, 0, 2, 2), Rect: image.Rect(0, 0, 2, 2),
Palette: bigPalette, Palette: bigPalette,
}).SubImage(image.Rect(1, 0, 2, 1)), }).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 { for i, c := range cases {
got := ebiten.ImageToBytes(c.In) got := ebiten.ImageToBytes(c.Image, c.Premul)
want := c.Out want := c.Out
if !bytes.Equal(got, want) { if !bytes.Equal(got, want) {
t.Errorf("Test %d: got: %v, want: %v", i, 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) { func BenchmarkImageToBytesRGBA(b *testing.B) {
img := image.NewRGBA(image.Rect(0, 0, 4096, 4096)) img := image.NewRGBA(image.Rect(0, 0, 4096, 4096))
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for range b.N {
ebiten.ImageToBytes(img) ebiten.ImageToBytes(img, true)
} }
} }
func BenchmarkImageToBytesNRGBA(b *testing.B) { func BenchmarkImageToBytesNRGBA(b *testing.B) {
img := image.NewNRGBA(image.Rect(0, 0, 4096, 4096)) img := image.NewNRGBA(image.Rect(0, 0, 4096, 4096))
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for range b.N {
ebiten.ImageToBytes(img) ebiten.ImageToBytes(img, true)
} }
} }
func BenchmarkImageToBytesPaletted(b *testing.B) { func BenchmarkImageToBytesPaletted(b *testing.B) {
img := image.NewPaletted(image.Rect(0, 0, 4096, 4096), palette.Plan9) img := image.NewPaletted(image.Rect(0, 0, 4096, 4096), palette.Plan9)
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for range b.N {
ebiten.ImageToBytes(img) ebiten.ImageToBytes(img, true)
} }
} }