Pure Go WebP decoder and encoder. No cgo, no external dependencies.
go get github.com/SeriousBug/webp-go-pure
Decodes any WebP still image and decodes animated WebP into a composited frame sequence, ready to display. Encodes still images, lossy or lossless.
Transparency works in every mode, including lossy: your alpha channel comes back exactly as you gave it, and only the color is compressed with loss. Encoder output is byte-identical on amd64 and arm64.
The std subpackage implements the standard library's codec interfaces, so
image.Image goes in and comes out and image.Decode works. Underneath it, the
root package is the codec itself, working on plain byte buffers.
If you can use cgo, use libwebp itself. It is the faster decoder in every mode, and the faster encoder in most of them.
Without cgo, the alternative is libwebp compiled to WebAssembly and run through wazero, such as gen2brain/webp. That costs both time and memory, and webp-go-pure comes out ahead of it:
- Lossy: 1.2-3.4x faster, at 1.6-2.9x lower peak memory.
- Lossless: 1.9-3.5x faster, at 1.3-1.8x lower peak memory.
Against libwebp itself, lossless is a close match on size, half a percent apart at our effort 6, and on time we range from level with it to 1.47x slower depending on the machine. Our lossy effort 8 writes 1.7% more than its method 6 at the same PSNR, for 1.0-1.2x the time.
For lossless encoding only, there is another pure Go encoder, nativewebp. We are smaller and faster than it at the same time: our effort 0 writes 9.8% smaller files than its best setting in a sixth of the time, and everything up to our effort 5 still beats that setting on both. It does use less memory than we do, 0.43-0.63x our peak, and that gap is the reason to reach for it.
Peak memory is the number to check before using lossless on large images: we cost 1.4-2.1x libwebp's peak there, and around 400 MiB to encode a 5.5 megapixel image. The lossy modes are cheaper for everyone, and there we are the lightest of the three.
Effort is the knob to reach for either way. The figure below is every setting of every encoder: pick the time and size you want, then read the setting off the point.
Those figures are for encoding. For decoding, the comparison to make is
golang.org/x/image/webp, which decodes but does not encode. We are faster than
it in every mode on both machines, by 2% to 20% on the geometric mean; on arm64
lossless the per-image results go either way. Against libwebp we are 2.2-5.8x
slower on lossy and 1.5-3.1x on lossless.
The numbers above are for photographs. Flat graphics with alpha are a different encoding problem and the rankings shift; full tables, PSNR and peak-memory figures, both test corpora and the method are in benchmark/results.md.
import "github.com/SeriousBug/webp-go-pure/std"Decode, DecodeConfig and Encode have the same signatures as the ones in
image/png and image/jpeg:
func describe(r io.Reader) (string, error) {
img, err := webp.Decode(r)
if err != nil {
return "", err
}
bounds := img.Bounds()
return fmt.Sprintf("%dx%d %T", bounds.Dx(), bounds.Dy(), img), nil
}The concrete type depends on the file: *image.YCbCr for lossy,
*image.NYCbCrA for lossy with transparency, *image.NRGBA for lossless. All
of them are an image.Image, so At, Bounds and draw.Draw work as usual.
If you would rather always get the same type, DecodeNRGBA returns an
*image.NRGBA whatever the file holds.
Encoding takes an options struct, or nil for the defaults (lossy, quality 90):
func writeWebP(w io.Writer, img image.Image) error {
return webp.Encode(w, img, &webp.Options{Quality: 80})
}Set Lossless to encode with VP8L instead, which reproduces the input exactly.
Effort runs 0..9 for lossy and 0..6 for lossless, and trades encode time for
file size.
JPEG to WebP is a decode and an encode, with nothing in between:
func jpegToWebP(dst io.Writer, src io.Reader) error {
img, err := jpeg.Decode(src)
if err != nil {
return err
}
return webp.Encode(dst, img, &webp.Options{Quality: 80})
}This path is faster and about a third lighter on memory than transcoding through RGBA.
Importing the package registers WebP with image.Decode and
image.DecodeConfig, the way image/png and golang.org/x/image/webp do:
func sniffFormat(r io.Reader) (string, error) {
_, format, err := image.Decode(r)
return format, err
}To migrate from golang.org/x/image/webp, swapping the import path is enough:
we support the same API, and add encoding and animations. One difference is that
lossy colors come out with a wider range here. x/image/webp crushes blacks and
whites slightly, decoding a white pixel to 235 where we give you 255.
Keep only one WebP package in your binary. image.RegisterFormat has no way to
unregister, and every WebP package claims the same magic bytes, so with two of
them linked in it is import order that decides which one image.Decode uses.
- docs/std.md covers the
stdpackage in full: which concrete types the fast paths recognize, alpha and premultiplication, animations, and reusing buffers. - docs/codec-api.md covers the root package: the byte oriented API, EXIF, raw planar YUV, and animation decoding.