webp_test.go (7001B)
1 package webp 2 3 import ( 4 "bytes" 5 _ "embed" 6 "fmt" 7 "image" 8 "image/color" 9 "image/png" 10 "io" 11 "math/rand" 12 "os" 13 "path/filepath" 14 "testing" 15 16 stdwebp "golang.org/x/image/webp" 17 ) 18 19 //go:embed testdata/golden-in.png 20 var goldenIn []byte 21 22 //go:embed testdata/golden-out.webp 23 var goldenOut []byte 24 25 func TestLossless(t *testing.T) { 26 t.Logf("selected backend: %q", Backend()) 27 28 m, err := png.Decode(bytes.NewReader(goldenIn)) 29 if err != nil { 30 t.Fatalf("decoding image: %v", err) 31 } 32 33 buf1 := bytes.NewBuffer(nil) 34 buf2 := bytes.NewBuffer(nil) 35 36 if err := Encode(io.MultiWriter(buf1, buf2), m, Lossless()); err != nil { 37 t.Fatalf("encoding webp: %v", err) 38 } 39 40 if err := os.WriteFile(filepath.Join("testdata", "golden-got.webp"), buf1.Bytes(), 0o644); err != nil { 41 t.Errorf("writing output png: %v", err) 42 } 43 44 t.Run("stdlib webp outupt", func(t *testing.T) { 45 assertOutput(t, m, buf1, stdwebp.Decode) 46 }) 47 48 t.Run("round trip through selected backend", func(t *testing.T) { 49 assertOutput(t, m, buf2, Decode) 50 }) 51 } 52 53 func BenchmarkDecode(b *testing.B) { 54 b.Run("stdwebp", func(b *testing.B) { 55 for ii := 0; ii < b.N; ii++ { 56 r := bytes.NewReader(goldenOut) 57 m, err := stdwebp.Decode(r) 58 if err != nil { 59 b.Fatalf("error: %v", err) 60 } 61 _ = m 62 } 63 }) 64 b.Run(Backend(), func(b *testing.B) { 65 for ii := 0; ii < b.N; ii++ { 66 m, err := Decode(bytes.NewReader(goldenOut)) 67 if err != nil { 68 b.Fatalf("error: %v", err) 69 } 70 _ = m 71 } 72 }) 73 } 74 75 func assertOutput(t *testing.T, m image.Image, src io.Reader, decode func(io.Reader) (image.Image, error)) { 76 t.Helper() 77 78 out, err := decode(src) 79 if err != nil { 80 t.Fatalf("decoding webp: %v", err) 81 } 82 83 outb := out.Bounds() 84 for xx := outb.Min.X; xx < outb.Max.X; xx++ { 85 for yy := outb.Min.Y; yy < outb.Max.Y; yy++ { 86 if got, want := out.At(xx, yy), m.At(xx, yy); !colorEqual(got, want) { 87 save(t, fmt.Sprintf("got-%d_%d.webp", xx, yy), out) 88 save(t, fmt.Sprintf("want-%d_%d.webp", xx, yy), m) 89 t.Fatalf("color mismatch after lossless encode: Point = (%d, %d) Got = %v Want = %v Bounds= %d", xx, yy, got, want, outb) 90 } 91 } 92 } 93 } 94 95 // goldenNRGBA decodes the golden PNG as NRGBA, the form the backend entry 96 // points take. 97 func goldenNRGBA(t *testing.T) *image.NRGBA { 98 t.Helper() 99 m, err := png.Decode(bytes.NewReader(goldenIn)) 100 if err != nil { 101 t.Fatalf("decoding golden image: %v", err) 102 } 103 return toNRGBA(m) 104 } 105 106 // encodeWith adapts a backend's encode entry point to the signature the 107 // shared fuzz body expects. 108 func encodeWith(fn func(io.Writer, *image.NRGBA, float32) error) func(io.Writer, image.Image, float32) error { 109 return func(w io.Writer, m image.Image, q float32) error { 110 return fn(w, toNRGBA(m), q) 111 } 112 } 113 114 // decodeReaderWith adapts a backend's byte-slice decoder to an io.Reader one. 115 func decodeReaderWith(fn func([]byte) (image.Image, error)) func(io.Reader) (image.Image, error) { 116 return func(r io.Reader) (image.Image, error) { 117 by, err := io.ReadAll(r) 118 if err != nil { 119 return nil, err 120 } 121 return fn(by) 122 } 123 } 124 125 // assertLossyDecode encodes lossily and decodes the result through the same 126 // backend, checking dimensions rather than pixels. 127 // 128 // This is the VP8 decode path, which is entirely separate from the VP8L path 129 // the golden and the fuzz seeds exercise. A transpiled build that fell back 130 // to inline-asm byte swapping passed every other test and aborted here. 131 func assertLossyDecode(t *testing.T, 132 encode func(io.Writer, *image.NRGBA, float32) error, 133 decode func([]byte) (image.Image, error), 134 ) { 135 t.Helper() 136 m := goldenNRGBA(t) 137 var buf bytes.Buffer 138 if err := encode(&buf, m, 0.75); err != nil { 139 t.Fatalf("lossy encode: %v", err) 140 } 141 got, err := decode(buf.Bytes()) 142 if err != nil { 143 t.Fatalf("lossy decode: %v", err) 144 } 145 if got.Bounds() != m.Bounds() { 146 t.Fatalf("bounds: got %v want %v", got.Bounds(), m.Bounds()) 147 } 148 } 149 150 func save(t *testing.T, name string, m image.Image) { 151 t.Helper() 152 153 if err := func() error { 154 f, err := os.Create(name) 155 if err != nil { 156 return err 157 } 158 defer f.Close() 159 defer f.Sync() 160 return Encode(f, m, Lossless()) 161 162 }(); err != nil { 163 t.Error(err) 164 } 165 } 166 167 func colorEqual(left, right color.Color) bool { 168 lr, lg, lb, la := left.RGBA() 169 rr, rg, rb, ra := right.RGBA() 170 return lr == rr && lg == rg && lb == rb && la == ra 171 } 172 173 func FuzzEncode(f *testing.F) { 174 f.Add(uint16(000), uint16(000), int64(0), float32(0.00), true) 175 f.Add(uint16(000), uint16(000), int64(0), float32(0.00), false) 176 f.Add(uint16(001), uint16(001), int64(1), float32(1.00), true) 177 f.Add(uint16(001), uint16(001), int64(1), float32(1.00), false) 178 f.Add(uint16(001), uint16(001), int64(2), float32(0.50), true) 179 f.Add(uint16(001), uint16(001), int64(2), float32(0.50), false) 180 f.Add(uint16(100), uint16(100), int64(3), float32(0.75), true) 181 f.Add(uint16(100), uint16(100), int64(3), float32(0.75), false) 182 f.Add(uint16(100), uint16(100), int64(3), float32(0.90), true) 183 f.Add(uint16(100), uint16(100), int64(3), float32(0.90), false) 184 f.Add(uint16(100), uint16(100), int64(3), float32(0.95), true) 185 f.Add(uint16(100), uint16(100), int64(3), float32(0.95), false) 186 f.Add(uint16(100), uint16(100), int64(3), float32(1.00), true) 187 f.Add(uint16(100), uint16(100), int64(3), float32(1.00), false) 188 189 f.Fuzz(func(t *testing.T, x1, y1 uint16, seed int64, quality float32, lossless bool) { 190 encode := func(w io.Writer, m image.Image, q float32) error { 191 opts := []EncodeOption{Quality(q)} 192 if lossless { 193 opts = append(opts, Lossless()) 194 } 195 return Encode(w, m, opts...) 196 } 197 fuzzEncode(t, x1, y1, seed, quality, lossless, encode, Decode) 198 }) 199 } 200 201 // addEncodeSeeds mirrors the seed corpus of FuzzEncode. 202 func addEncodeSeeds(f *testing.F) { 203 for _, q := range []float32{0.00, 1.00, 0.50, 0.75, 0.90, 0.95} { 204 for _, dim := range []uint16{0, 1, 100} { 205 for _, lossless := range []bool{true, false} { 206 f.Add(dim, dim, int64(dim), q, lossless) 207 } 208 } 209 } 210 } 211 212 func fuzzEncode( 213 t *testing.T, 214 x1, y1 uint16, 215 seed int64, 216 quality float32, 217 lossless bool, 218 encode func(io.Writer, image.Image, float32) error, 219 decode func(io.Reader) (image.Image, error), 220 ) { 221 { 222 if quality <= 0 || quality > 1 { 223 t.Skip() 224 return 225 } 226 227 if int(x1) <= 0 || int(y1) <= 0 { 228 t.Skip() 229 return 230 } 231 232 t.Logf("(%d, %d), quality=%.2f, lossless=%v", x1, y1, quality, lossless) 233 234 rng := rand.New(rand.NewSource(seed)) 235 236 m := image.NewNRGBA(image.Rect(0, 0, int(x1), int(y1))) 237 238 for x := uint16(0); x < x1; x += 1 { 239 for y := uint16(0); y < y1; y += 1 { 240 colors := rng.Uint32() 241 r := uint8(colors) 242 g := uint8(colors >> 8) 243 b := uint8(colors >> 16) 244 a := uint8(colors >> 24) 245 m.SetNRGBA(int(x), int(y), color.NRGBA{R: r, G: g, B: b, A: a}) 246 } 247 } 248 249 if lossless { 250 quality = 1.0 251 } 252 253 buf := bytes.NewBuffer(nil) 254 255 if err := encode(buf, m, quality); err != nil { 256 t.Errorf("encode error: %v", err) 257 } 258 259 if lossless { 260 // If lossless, we can do a pixel-wise comparison between the original and the 261 // decoded image. 262 assertOutput(t, m, buf, decode) 263 } 264 } 265 }