github.com/linzeyan/webp-go

Pure-Go WebP encoder (VP8 lossy + VP8L lossless, animations, alpha) — no cgo, no libwebp.


License
MIT
Install
go get github.com/linzeyan/webp-go

Documentation

Tests Coverage Status Go Reference License: MIT

gowebp

A pure-Go WebP codec. No libwebp, no cgo, no runtime dependencies beyond golang.org/x/image (which is used for decoding only).

Encodes to either WebP format:

  • VP8L (lossless) — default. Every pixel preserved exactly.
  • VP8 (lossy) — opt-in via Options.Lossy. Full intra-only VP8 keyframe encoder with I16 + B_PRED + UV8 intra prediction, DCT + Walsh-Hadamard transforms, boolean arithmetic coding, loop filter, and reconstruction-based RD mode selection.

Also encodes WebP animations (ANIM/ANMF) with either codec, and preserves alpha channels via ALPH chunks (raw or VP8L-compressed, static or in-animation).

Installation

go get github.com/linzeyan/webp-go

Command-line tool

A gowebp binary converts images to WebP from the shell. Pre-built binaries for Linux, macOS, and Windows are attached to each release, or install from source:

go install github.com/linzeyan/webp-go/cmd/gowebp@latest
gowebp photo.png                 # -> photo.webp (lossless, the default)
gowebp -lossy -q 80 photo.jpg    # -> photo.webp (lossy VP8)
gowebp anim.gif                  # -> anim.webp (animated GIF -> animated WebP)
gowebp a.png b.png c.png         # batch: each -> .webp in place
gowebp -o out/ *.png             # batch into a directory
cat a.png | gowebp - > a.webp    # stdin -> stdout

Inputs may be PNG, JPEG, GIF, or WebP (auto-detected). Flags: -o (output file / directory / - for stdout), -lossy, -lossless, -q (quality 0–100), -m (method/effort 0–6), -near (near-lossless bits), and -max-pixels (reject inputs over N pixels — width × height × frames — to guard against decompression bombs; 0 disables it). Run gowebp -h for the full list.

Usage

Lossless

import "github.com/linzeyan/webp-go"

f, _ := os.Create(name)
defer f.Close()
if err := gowebp.Encode(f, img, nil); err != nil {
    log.Fatal(err)
}

For a smaller (but slower) lossless file, set Method to any positive value. It makes the encoder search for the best predictor tile size and color-cache size, and switch to a palette when the image has 256 or fewer colors (logos, screenshots); the decoded pixels are identical, only the file size changes. Method 0 (the default) keeps the fast path.

NearLossless trades a small, bounded error for smaller files. It is the maximum number of low R/G/B bits the encoder may drop in smooth regions (alpha and edges stay exact); the per-channel error is at most 2^NearLossless - 1. The encoder also keeps the exact encoding when it is smaller, so the output is never larger than plain lossless. It helps most on noisy/photographic content. NearLossless 0 (the default) is exact lossless.

err := gowebp.Encode(f, img, &gowebp.Options{Method: 1})

Lossy

err := gowebp.Encode(f, img, &gowebp.Options{
    Lossy:   true,
    Quality: 75, // 0 (smallest) to 100 (best), default 75
    Method:  4,  // 0 (fastest) to 6 (highest quality), default 0
})

Animation

ani := gowebp.Animation{
    Images:          []image.Image{frame1, frame2, frame3},
    Durations:       []uint{100, 100, 100}, // ms per frame
    Disposals:       []uint{0, 0, 0},       // 0=keep, 1=clear
    LoopCount:       0,                     // 0 = infinite
    BackgroundColor: 0xffffffff,            // BGRA
}
// Pass Options{Lossy: true} for VP8 frames; nil for VP8L.
gowebp.EncodeAll(f, &ani, nil)

Metadata (ICC / EXIF / XMP)

Setting any of ICCProfile, EXIF, or XMP automatically wraps the output in a VP8X container and writes the corresponding chunks in the spec-mandated order (VP8X → ICCP → image data → EXIF → XMP). Works with lossless, lossy, and animated output.

err := gowebp.Encode(f, img, &gowebp.Options{
    ICCProfile: iccBytes, // raw ICC profile
    EXIF:       exifBytes, // raw EXIF block
    XMP:        xmpBytes,  // raw XMP packet
})

Cancellation

EncodeContext and EncodeAllContext accept a context.Context and abort with ctx.Err() when it is cancelled. Cancellation is polled before each animation frame and between macroblock rows of a lossy image; a lossless single image runs to completion once started.

err := gowebp.EncodeContext(ctx, f, img, &gowebp.Options{Lossy: true})

Decoding

img, err := gowebp.Decode(r)

Wraps golang.org/x/image/webp. Also provides DecodeIgnoreAlphaFlag for VP8X files where a misset alpha flag trips up the standard decoder.

Lossy method tiers

The Method field picks between different speed/quality/size tradeoffs. Higher values spend more CPU per macroblock to produce smaller or higher-quality output.

Method Strategy Notes
0 I16 with DC_PRED only fastest baseline
1 I16 with 4-mode SSE search +1-3 dB over M0
2 B_PRED with 10 I4 modes per sub-block best on textured content
3 Per-MB I16/B_PRED arbitration (prediction SSE) fast, good default
4 Per-MB arbitration with reconstruction-aware RDO recommended
5 Dual-path (measure both I16 and B_PRED) principled reference
6 Adds trailing-coefficient trellis trim helps on high-freq content

Encoding is goroutine-safe: each Encode/EncodeAll call is self-contained and has no shared mutable state. EncodeAll also encodes its frames concurrently (up to GOMAXPROCS), so multi-frame animations scale across cores while producing byte-identical output.

Benchmarks

Measured on Intel i9-14900K (Linux, Go 1.25) against the 5 natural photos linked from Google's WebP Gallery. Baseline is Go's image/png with png.BestCompression.

Reproduce:

NATIVEWEBP_FETCH=1 go test -run TestGalleryPSNR -v    # populate testdata/
go test -bench=. -benchtime=5x

File sizes (bytes per frame)

Image PNG Lossless Lossy Q=50 M=4 Lossy Q=75 M=3 Lossy Q=75 M=4 Lossy Q=90 M=4
1.png (400×301) 120 188 97 720 16 766 17 648 24 906 43 518
2.png (386×395) 45 659 37 018 15 050 13 808 21 888 33 632
3.png (800×600) 236 018 194 714 80 992 80 552 95 330 130 828
4.png (421×163) 52 681 41 554 27 304 24 838 35 736 47 510
5.png (300×300) 138 919 123 932 74 202 78 300 97 588 133 548
  • Lossless (VP8L) averages 13-23 % smaller than PNG's best compression.
  • Lossy Q=75 M=3 is 1.8×-3.3× smaller than lossless on the same images.
  • Lossy Q=50 M=4 can reach 7× smaller than PNG while still producing visually acceptable output on photos.

Encode time (ns/op)

Image PNG Lossless Lossy Q=75 M=3 Lossy Q=75 M=4
1.png (400×301) 38 034 25 745 30 476 30 629
2.png (386×395) 82 888 29 999 34 298 49 051
3.png (800×600) 152 804 100 415 110 645 117 259
4.png (421×163) 23 707 24 294 17 631 19 426
5.png (300×300) 54 435 20 294 27 094 27 931

All timings in microseconds for brevity: lossless WebP is ≈ 1.5-2.8× faster than PNG across the gallery, while producing smaller files. Lossy encoding is typically within a small constant factor of lossless speed.

Quality (spec-correct Y-PSNR, lossy Q=75 M=3)

Y-PSNR measured against VP8 limited-range BT.601 (what a real decoder shows). Higher is better; >40 dB is visually lossless on photographic content.

Image Y-PSNR
1.png 39.2 dB
2.png 42.2 dB
3.png 43.5 dB
4.png 38.7 dB
5.png 36.7 dB

Threshold floors on these numbers are asserted by TestGalleryPSNR so future encoder changes can't quietly regress quality.

BT.601 color handling

VP8 stores pixels in limited-range BT.601 YCbCr (luma 16-235, chroma 16-240) per RFC 6386. Go's stdlib image/color.YCbCrToRGB uses the JFIF full-range inverse, which shifts the resulting RGB by 2-5 units per channel when applied to VP8 samples.

Decode handles this automatically: for VP8 sources the returned image is a *gowebp.BT601YCbCr (or *gowebp.BT601NYCbCrA when the source has an ALPH chunk) whose At() method uses the spec-correct limited-range BT.601 inverse. Calling img.At(x, y).RGBA() gives the same RGB values a libwebp or browser decoder would produce.

Users who want the raw *image.YCbCr for zero-copy plane access can type-assert to *gowebp.BT601YCbCr and read the embedded field:

img, _ := gowebp.Decode(r)
if y, ok := img.(*gowebp.BT601YCbCr); ok {
    rawYCbCr := y.YCbCr     // *image.YCbCr with limited-range samples
    _ = rawYCbCr
}

VP8L (lossless) sources decode to *image.NRGBA with exact pixel fidelity and don't go through YCbCr, so no wrapping is applied.

Implementation notes

The lossy VP8 encoder lives in internal/vp8enc/ and is a pure-Go implementation of RFC 6386 (The VP8 Data Format and Decoding Guide). The following open-source implementations were consulted as references for bit-exact compatibility; no code was copied, but table values (which are specification constants) were transcribed:

  • golang.org/x/image/vp8 — pure-Go VP8 decoder, used as the round-trip test oracle (BSD-3-Clause).
  • libwebp — the reference C encoder/decoder from Google (BSD-3-Clause).
  • libvpx — VP8/VP9 reference codec (BSD-3-Clause).

Every file in internal/vp8enc/ references the relevant RFC 6386 section and the matching path in the x/image/vp8 decoder, which is the oracle used by the test suite's round-trip integration tests.

Remaining work (tracked in the source, not here): full rate-distortion optimization with calibrated λ; proper Viterbi trellis quantization with context-aware token-tree rate estimation.

License

MIT, same as the upstream project this was forked from (HugoSmits86/nativewebp).