From 76e4833b7a3f1ab2d8f30e497d00809b1f6cf1e6 Mon Sep 17 00:00:00 2001 From: Alexander Efremov Date: Mon, 10 Oct 2022 13:24:36 +0000 Subject: [PATCH] image/png: optimise RGBA encoding MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Optimised RGBA image encoding to PNG. Improved test coverage. Reworked benchmark. Performance improvement with both old and new versions of the benchmark: name old speed new speed delta EncodeRGBA_OriginalVersion-10 115MB/s ± 1% 308MB/s ± 1% +166.70% (p=0.000 n=19+17) EncodeRGBA_NewVersion-10 40.3MB/s ± 1% 51.1MB/s ± 2% +26.93% (p=0.000 n=18+20) name old allocs/op new allocs/op delta EncodeRGBA_OriginalVersion-10 614k ± 0% 0k ± 0% -99.99% (p=0.000 n=20+20) EncodeRGBA_NewVersion-10 614k ± 0% 0k ± 0% -99.99% (p=0.000 n=20+20) Change-Id: I450013909c2410b043cd9c1239facd5bd6e3f3f9 GitHub-Last-Rev: 329d6ac011b08efcba5c1d737ba5395b0a66a6ea GitHub-Pull-Request: golang/go#55119 Reviewed-on: https://go-review.googlesource.com/c/go/+/431575 Reviewed-by: Dmitri Shuralyov Run-TryBot: Nigel Tao Reviewed-by: Nigel Tao (INACTIVE; USE @golang.org INSTEAD) TryBot-Result: Gopher Robot Reviewed-by: Nigel Tao --- src/image/png/writer.go | 30 +++++++++++++++ src/image/png/writer_test.go | 71 ++++++++++++++++++++++++++++++++++-- 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/src/image/png/writer.go b/src/image/png/writer.go index cbcdb9e798..0d747da170 100644 --- a/src/image/png/writer.go +++ b/src/image/png/writer.go @@ -450,6 +450,36 @@ func (e *encoder) writeImage(w io.Writer, m image.Image, cb int, level int) erro if nrgba != nil { offset := (y - b.Min.Y) * nrgba.Stride copy(cr[0][1:], nrgba.Pix[offset:offset+b.Dx()*4]) + } else if rgba != nil { + dst := cr[0][1:] + src := rgba.Pix[rgba.PixOffset(b.Min.X, y):rgba.PixOffset(b.Max.X, y)] + for ; len(src) >= 4; dst, src = dst[4:], src[4:] { + d := (*[4]byte)(dst) + s := (*[4]byte)(src) + if s[3] == 0x00 { + d[0] = 0 + d[1] = 0 + d[2] = 0 + d[3] = 0 + } else if s[3] == 0xff { + copy(d[:], s[:]) + } else { + // This code does the same as color.NRGBAModel.Convert( + // rgba.At(x, y)).(color.NRGBA) but with no extra memory + // allocations or interface/function call overhead. + // + // The multiplier m combines 0x101 (which converts + // 8-bit color to 16-bit color) and 0xffff (which, when + // combined with the division-by-a, converts from + // alpha-premultiplied to non-alpha-premultiplied). + const m = 0x101 * 0xffff + a := uint32(s[3]) * 0x101 + d[0] = uint8((uint32(s[0]) * m / a) >> 8) + d[1] = uint8((uint32(s[1]) * m / a) >> 8) + d[2] = uint8((uint32(s[2]) * m / a) >> 8) + d[3] = s[3] + } + } } else { // Convert from image.Image (which is alpha-premultiplied) to PNG's non-alpha-premultiplied. for x := b.Min.X; x < b.Max.X; x++ { diff --git a/src/image/png/writer_test.go b/src/image/png/writer_test.go index 47aa861339..6dac8ec4d8 100644 --- a/src/image/png/writer_test.go +++ b/src/image/png/writer_test.go @@ -11,6 +11,7 @@ import ( "fmt" "image" "image/color" + "image/draw" "io" "testing" ) @@ -29,7 +30,7 @@ func diff(m0, m1 image.Image) error { r0, g0, b0, a0 := c0.RGBA() r1, g1, b1, a1 := c1.RGBA() if r0 != r1 || g0 != g1 || b0 != b1 || a0 != a1 { - return fmt.Errorf("colors differ at (%d, %d): %v vs %v", x, y, c0, c1) + return fmt.Errorf("colors differ at (%d, %d): %T%v vs %T%v", x, y, c0, c0, c1, c1) } } } @@ -45,6 +46,13 @@ func encodeDecode(m image.Image) (image.Image, error) { return Decode(&b) } +func convertToNRGBA(m image.Image) *image.NRGBA { + b := m.Bounds() + ret := image.NewNRGBA(b) + draw.Draw(ret, b, m, b.Min, draw.Src) + return ret +} + func TestWriter(t *testing.T) { // The filenames variable is declared in reader_test.go. names := filenames @@ -227,6 +235,49 @@ func TestSubImage(t *testing.T) { } } +func TestWriteRGBA(t *testing.T) { + const width, height = 640, 480 + transparentImg := image.NewRGBA(image.Rect(0, 0, width, height)) + opaqueImg := image.NewRGBA(image.Rect(0, 0, width, height)) + mixedImg := image.NewRGBA(image.Rect(0, 0, width, height)) + translucentImg := image.NewRGBA(image.Rect(0, 0, width, height)) + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + opaqueColor := color.RGBA{uint8(x), uint8(y), uint8(y + x), 255} + translucentColor := color.RGBA{uint8(x) % 128, uint8(y) % 128, uint8(y+x) % 128, 128} + opaqueImg.Set(x, y, opaqueColor) + translucentImg.Set(x, y, translucentColor) + if y%2 == 0 { + mixedImg.Set(x, y, opaqueColor) + } + } + } + + testCases := []struct { + name string + img image.Image + }{ + {"Transparent RGBA", transparentImg}, + {"Opaque RGBA", opaqueImg}, + {"50/50 Transparent/Opaque RGBA", mixedImg}, + {"RGBA with variable alpha", translucentImg}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + m0 := tc.img + m1, err := encodeDecode(m0) + if err != nil { + t.Fatal(err) + } + err = diff(convertToNRGBA(m0), m1) + if err != nil { + t.Error(err) + } + }) + } +} + func BenchmarkEncodeGray(b *testing.B) { img := image.NewGray(image.Rect(0, 0, 640, 480)) b.SetBytes(640 * 480 * 1) @@ -329,11 +380,25 @@ func BenchmarkEncodeRGBOpaque(b *testing.B) { } func BenchmarkEncodeRGBA(b *testing.B) { - img := image.NewRGBA(image.Rect(0, 0, 640, 480)) + const width, height = 640, 480 + img := image.NewRGBA(image.Rect(0, 0, width, height)) + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + percent := (x + y) % 100 + switch { + case percent < 10: // 10% of pixels are translucent (have alpha >0 and <255) + img.Set(x, y, color.NRGBA{uint8(x), uint8(y), uint8(x * y), uint8(percent)}) + case percent < 40: // 30% of pixels are transparent (have alpha == 0) + img.Set(x, y, color.NRGBA{uint8(x), uint8(y), uint8(x * y), 0}) + default: // 60% of pixels are opaque (have alpha == 255) + img.Set(x, y, color.NRGBA{uint8(x), uint8(y), uint8(x * y), 255}) + } + } + } if img.Opaque() { b.Fatal("expected image not to be opaque") } - b.SetBytes(640 * 480 * 4) + b.SetBytes(width * height * 4) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { -- 2.50.0