go-libwebp

Experimental translation from libwebp to Go source.
Log | Files | Refs | README | LICENSE

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 }