]> Cypherpunks repositories - gostls13.git/commitdiff
image/png: optimise RGBA encoding
authorAlexander Efremov <alexander.efremov@id.thesoul.io>
Mon, 10 Oct 2022 13:24:36 +0000 (13:24 +0000)
committerNigel Tao <nigeltao@golang.org>
Fri, 14 Oct 2022 08:14:05 +0000 (08:14 +0000)
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 <dmitshur@google.com>
Run-TryBot: Nigel Tao <nigeltao@golang.org>
Reviewed-by: Nigel Tao (INACTIVE; USE @golang.org INSTEAD) <nigeltao@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Nigel Tao <nigeltao@golang.org>
src/image/png/writer.go
src/image/png/writer_test.go

index cbcdb9e798519488c1a5925968a9661230b60460..0d747da17055ccaec7972954644bcb2ca6f6d164 100644 (file)
@@ -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++ {
index 47aa861339893a801a0ad13a0a5a68aee739e2c6..6dac8ec4d835fafa962b95c81dc4090c30cd0c8d 100644 (file)
@@ -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++ {