go-libwebp

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

commit 91d4abc35e3ec60ee61f6ba7456d18c7c6bc8451
parent 55f202710aba7f22fc3fd31da03651c1d48fe100
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Wed,  3 Jan 2024 18:10:14 +0800

all: use NRGBA images (not pre-multiplied)

This is what WebP is expecting.

Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>

Diffstat:
Mlib/common/common.go | 8++++----
Mlib/dynamic/webp/encode_cgo.go | 2+-
Mlib/dynamic/webp/encode_default.go | 2+-
Mlib/dynamic/webp/lib.go | 2+-
Mlib/transpiled/webp/lib.go | 2+-
Mwebp/encode.go | 10+++++-----
Mwebp/webp_test.go | 93++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
7 files changed, 82 insertions(+), 37 deletions(-)

diff --git a/lib/common/common.go b/lib/common/common.go @@ -18,7 +18,7 @@ type DecoderFunc func(data uintptr, data_size uint64, width uintptr, height uint type FreeFunc func(uintptr) // Encode an RGBA webp image into the provided writer. -func Encode(w io.Writer, m *image.RGBA, q float32, enc EncodeFunc, free FreeFunc) error { +func Encode(w io.Writer, m *image.NRGBA, q float32, enc EncodeFunc, free FreeFunc) error { p := runtime.Pinner{} defer p.Unpin() @@ -62,8 +62,8 @@ func Decode(buf []byte, dec DecoderFunc, free FreeFunc) (image.Image, error) { defer p.Unpin() data := unsafe.SliceData(buf) - width := new(int) - height := new(int) + width := new(int32) + height := new(int32) p.Pin(data) p.Pin(width) @@ -92,7 +92,7 @@ func Decode(buf []byte, dec DecoderFunc, free FreeFunc) (image.Image, error) { // longer reachable. That way we could avoid this copy. copy(pix, unsafe.Slice(samples, size)) - m := &image.RGBA{ + m := &image.NRGBA{ Pix: pix, Rect: image.Rectangle{Max: image.Point{X: w, Y: h}}, Stride: w * 4, diff --git a/lib/dynamic/webp/encode_cgo.go b/lib/dynamic/webp/encode_cgo.go @@ -11,6 +11,6 @@ import ( // HACK: always use lossless when cgo is activated on linux. cgo on linux provokes // a bug in purego when using float arguments. -func encodeImpl(w io.Writer, m *image.RGBA, quality float32) error { +func encodeImpl(w io.Writer, m *image.NRGBA, quality float32) error { return common.Encode(w, m, quality, wrappedLossless, WebPFree) } diff --git a/lib/dynamic/webp/encode_default.go b/lib/dynamic/webp/encode_default.go @@ -9,7 +9,7 @@ import ( "git.sr.ht/~jackmordaunt/go-libwebp/lib/common" ) -func encodeImpl(w io.Writer, m *image.RGBA, quality float32) error { +func encodeImpl(w io.Writer, m *image.NRGBA, quality float32) error { if quality >= 1.0 { return common.Encode(w, m, quality, wrappedLossless, WebPFree) } diff --git a/lib/dynamic/webp/lib.go b/lib/dynamic/webp/lib.go @@ -48,7 +48,7 @@ func DecodeImpl(buf []byte) (image.Image, error) { return common.Decode(buf, WebPDecodeRGBA, WebPFree) } -func EncodeImpl(w io.Writer, m *image.RGBA, quality float32) error { +func EncodeImpl(w io.Writer, m *image.NRGBA, quality float32) error { return encodeImpl(w, m, quality) } diff --git a/lib/transpiled/webp/lib.go b/lib/transpiled/webp/lib.go @@ -15,7 +15,7 @@ func DecodeImpl(buf []byte) (image.Image, error) { return common.Decode(buf, makeDecodeImpl(tls), makeFreeImpl(tls)) } -func EncodeImpl(w io.Writer, m *image.RGBA, quality float32) error { +func EncodeImpl(w io.Writer, m *image.NRGBA, quality float32) error { tls := libc.NewTLS() defer tls.Close() return common.Encode(w, m, quality, makeEncodeImpl(tls), makeFreeImpl(tls)) diff --git a/webp/encode.go b/webp/encode.go @@ -55,16 +55,16 @@ type Encoder struct { } // Encode specified image as webp to w. -// If the image is RGBA, the pixel buffer will be encoded directly. -// If the image is not RGBA, it will be converted to RGBA first. +// If the image is NRGBA, the pixel buffer will be encoded directly. +// If the image is not NRGBA, it will be converted to NRGBA first. func (enc *Encoder) Encode(w io.Writer, m image.Image) error { if enc.Quality <= 0.0 || enc.Quality > 1 { enc.Quality = 1.0 } - if rgba, ok := m.(*image.RGBA); ok { + if rgba, ok := m.(*image.NRGBA); ok { return enc.encode(w, rgba) } - rgba := image.NewRGBA(m.Bounds()) + rgba := image.NewNRGBA(m.Bounds()) b := m.Bounds() for y := b.Min.Y; y < b.Max.Y; y++ { for x := b.Min.X; x < b.Max.X; x++ { @@ -74,7 +74,7 @@ func (enc *Encoder) Encode(w io.Writer, m image.Image) error { return enc.encode(w, rgba) } -func (enc *Encoder) encode(w io.Writer, m *image.RGBA) error { +func (enc *Encoder) encode(w io.Writer, m *image.NRGBA) error { if err := dynamic.Init(); err == nil { return dynamic.EncodeImpl(w, m, enc.Quality) } diff --git a/webp/webp_test.go b/webp/webp_test.go @@ -3,6 +3,7 @@ package webp import ( "bytes" _ "embed" + "fmt" "image" "image/color" "image/png" @@ -59,12 +60,31 @@ func assertOutput(t *testing.T, m image.Image, src io.Reader, decode func(io.Rea for xx := outb.Min.X; xx < outb.Max.X; xx++ { for yy := outb.Min.Y; yy < outb.Max.Y; yy++ { if got, want := out.At(xx, yy), m.At(xx, yy); !colorEqual(got, want) { - t.Fatalf("color mismatch after lossless encode: Point = (%d, %d) Got = %v Want = %v", xx, yy, got, want) + save(t, fmt.Sprintf("got-%d_%d.webp", xx, yy), out) + save(t, fmt.Sprintf("want-%d_%d.webp", xx, yy), m) + t.Fatalf("color mismatch after lossless encode: Point = (%d, %d) Got = %v Want = %v Bounds= %d", xx, yy, got, want, outb) } } } } +func save(t *testing.T, name string, m image.Image) { + t.Helper() + + if err := func() error { + f, err := os.Create(name) + if err != nil { + return err + } + defer f.Close() + defer f.Sync() + return Encode(f, m, Lossless()) + + }(); err != nil { + t.Error(err) + } +} + func colorEqual(left, right color.Color) bool { lr, lg, lb, la := left.RGBA() rr, rg, rb, ra := right.RGBA() @@ -72,40 +92,65 @@ func colorEqual(left, right color.Color) bool { } func FuzzEncode(f *testing.F) { - f.Add(uint16(0), uint16(0), uint16(0), uint16(0), int64(0), float32(0)) - f.Add(uint16(0), uint16(0), uint16(1), uint16(1), int64(1), float32(1)) - f.Add(uint16(0), uint16(0), uint16(1), uint16(1), int64(2), float32(0.5)) - f.Add(uint16(0), uint16(0), uint16(100), uint16(100), int64(3), float32(0.75)) - f.Add(uint16(0), uint16(0), uint16(100), uint16(100), int64(3), float32(0.9)) - f.Add(uint16(0), uint16(0), uint16(100), uint16(100), int64(3), float32(0.95)) - f.Add(uint16(0), uint16(0), uint16(100), uint16(100), int64(3), float32(1.0)) - f.Fuzz(func(t *testing.T, x0, y0, x1, y1 uint16, seed int64, quality float32) { + f.Add(uint16(000), uint16(000), int64(0), float32(0.00), true) + f.Add(uint16(000), uint16(000), int64(0), float32(0.00), false) + f.Add(uint16(001), uint16(001), int64(1), float32(1.00), true) + f.Add(uint16(001), uint16(001), int64(1), float32(1.00), false) + f.Add(uint16(001), uint16(001), int64(2), float32(0.50), true) + f.Add(uint16(001), uint16(001), int64(2), float32(0.50), false) + f.Add(uint16(100), uint16(100), int64(3), float32(0.75), true) + f.Add(uint16(100), uint16(100), int64(3), float32(0.75), false) + f.Add(uint16(100), uint16(100), int64(3), float32(0.90), true) + f.Add(uint16(100), uint16(100), int64(3), float32(0.90), false) + f.Add(uint16(100), uint16(100), int64(3), float32(0.95), true) + f.Add(uint16(100), uint16(100), int64(3), float32(0.95), false) + f.Add(uint16(100), uint16(100), int64(3), float32(1.00), true) + f.Add(uint16(100), uint16(100), int64(3), float32(1.00), false) + + f.Fuzz(func(t *testing.T, x1, y1 uint16, seed int64, quality float32, lossless bool) { + if quality <= 0 || quality > 1 { + t.Skip() + return + } + + if int(x1) <= 0 || int(y1) <= 0 { + t.Skip() + return + } + + t.Logf("(%d, %d), quality=%.2f, lossless=%v", x1, y1, quality, lossless) + rng := rand.New(rand.NewSource(seed)) - m := image.NewNRGBA(image.Rect(int(x0), int(y0), int(x1), int(y1))) - for x := x0; x < x1; x += 1 { - for y := y0; y < y1; y += 1 { + + m := image.NewNRGBA(image.Rect(0, 0, int(x1), int(y1))) + + for x := uint16(0); x < x1; x += 1 { + for y := uint16(0); y < y1; y += 1 { colors := rng.Uint32() r := uint8(colors) g := uint8(colors >> 8) b := uint8(colors >> 16) a := uint8(colors >> 24) - m.Set(int(x), int(y), color.RGBA{R: r, G: g, B: b, A: a}) + m.SetNRGBA(int(x), int(y), color.NRGBA{R: r, G: g, B: b, A: a}) } } - if quality <= 0 || quality > 1 { - t.Skip() - return - } - if int(x1)-int(x0) <= 0 || int(y1)-int(y0) <= 0 { - t.Skip() - return + + opts := []EncodeOption{Quality(quality)} + + if lossless { + opts = append(opts, Lossless()) } + buf := bytes.NewBuffer(nil) - if err := Encode(buf, m, Quality(quality)); err != nil { - t.Errorf("encode error: %v (x0: %d, y0: %d, x1: %d, y1: %d)", err, x0, y0, x1, y1) + + if err := Encode(buf, m, opts...); err != nil { + t.Errorf("encode error: %v", err) } - if _, err := Decode(buf); err != nil { - t.Errorf("decode error: %v (x0: %d, y0: %d, x1: %d, y1: %d)", err, x0, y0, x1, y1) + + if lossless { + // If lossless, we can do a pixel-wise comparison between the original and the + // decoded image. + assertOutput(t, m, buf, Decode) } }) }