Pure Go encoder and decoder for the WebP image format. Zero dependencies, zero CGo.
go get github.com/deepteams/webp
Requires Go 1.24+
- Lossy encoding & decoding (VP8)
- Lossless encoding & decoding (VP8L)
- Alpha channel support (ALPH chunk with VP8L compression)
- Animation (ANIM/ANMF) with sub-frame optimization, keyframe control, mixed codec mode
- Extended format (VP8X) with ICC, EXIF, XMP metadata
- Sharp YUV conversion for high-quality chroma subsampling
- Presets for photos, pictures, drawings, icons, text
- Transparent integration with Go's
imagepackage (image.Decodejust works) - CLI tool (
gwebp) for encoding, decoding and inspecting WebP files
package main
import (
"image"
"image/png"
"os"
_ "github.com/deepteams/webp" // register WebP format
)
func main() {
// image.Decode auto-detects WebP thanks to init() registration
f, _ := os.Open("photo.webp")
defer f.Close()
img, _, _ := image.Decode(f)
out, _ := os.Create("photo.png")
defer out.Close()
png.Encode(out, img)
}Lossy VP8 pixels are returned as *image.NRGBA. VP8 stores limited-range
BT.601 YUV, while Go's image.YCbCr uses full-range JPEG semantics; converting
during decode prevents black/white range contraction in standard image
consumers and repeated WebP transcodes.
When decoding many images of similar size (thumbnails, video frames, batch
processing), DecodeReuse recycles the pixel buffers of the previous result,
bringing steady-state allocations near zero:
var img image.Image
for _, path := range paths {
f, _ := os.Open(path)
img, _ = webp.DecodeReuse(f, img) // reuses img's buffers when compatible
f.Close()
// ... process img (do not keep references across iterations)
}package main
import (
"image"
_ "image/jpeg"
"os"
"github.com/deepteams/webp"
)
func main() {
f, _ := os.Open("photo.jpg")
defer f.Close()
img, _, _ := image.Decode(f)
out, _ := os.Create("photo.webp")
defer out.Close()
webp.Encode(out, img, &webp.EncoderOptions{
Quality: 80,
Method: 4, // 0=fast, 6=best compression
})
}webp.Encode(out, img, &webp.EncoderOptions{
Lossless: true,
Quality: 75, // controls compression effort
})package main
import (
"image"
"image/color"
"os"
"time"
"github.com/deepteams/webp/animation"
)
func main() {
out, _ := os.Create("anim.webp")
defer out.Close()
enc := animation.NewEncoder(out, 256, 256, &animation.EncodeOptions{
Quality: 80,
LoopCount: 0, // infinite loop
})
for i := 0; i < 10; i++ {
img := image.NewNRGBA(image.Rect(0, 0, 256, 256))
// ... draw frame ...
enc.AddFrame(img, 100*time.Millisecond)
}
enc.Close()
}f, _ := os.Open("image.webp")
defer f.Close()
feat, _ := webp.GetFeatures(f)
fmt.Printf("Size: %dx%d\n", feat.Width, feat.Height)
fmt.Printf("Format: %s\n", feat.Format) // "lossy", "lossless", "extended"
fmt.Printf("Alpha: %v\n", feat.HasAlpha)
fmt.Printf("Animated: %v\n", feat.HasAnimation)
fmt.Printf("Frames: %d\n", feat.FrameCount)go install github.com/deepteams/webp/cmd/gwebp@latest# JPEG/PNG to WebP (lossy, quality 80)
gwebp enc -q 80 photo.jpg -o photo.webp
# Lossless encoding
gwebp enc -lossless input.png -o output.webp
# Sharp YUV for better chroma edges
gwebp enc -q 90 -sharp_yuv photo.jpg
# Content-specific preset
gwebp enc -preset photo -q 85 landscape.jpg
# GIF to animated WebP
gwebp enc -q 75 animation.gif -o animation.webp# WebP to PNG
gwebp dec input.webp -o output.png
# Animated WebP to GIF
gwebp dec animation.webp -o animation.gifgwebp info photo.webp| Option | Type | Default | Description |
|---|---|---|---|
Lossless |
bool |
false |
VP8L lossless encoding |
Quality |
float32 |
75 |
Compression quality (0-100) |
Method |
int |
4 |
Effort level (0=fast, 6=slowest/best) |
Preset |
Preset |
Default |
Content preset (Picture, Photo, Drawing, Icon, Text) |
UseSharpYUV |
bool |
false |
Sharp RGB-to-YUV conversion |
Exact |
bool |
false |
Preserve RGB under transparent areas |
TargetSize |
int |
0 |
Target output size in bytes |
TargetPSNR |
float32 |
0 |
Target PSNR in dB |
SNSStrength |
int |
50 |
Spatial noise shaping (0-100) |
FilterStrength |
int |
60 |
Loop filter strength (0-100) |
FilterSharpness |
int |
0 |
Loop filter sharpness (0-7) |
FilterType |
int |
1 |
Filter type (0=simple, 1=strong) |
Segments |
int |
4 |
Number of segments (1-4) |
Pass |
int |
1 |
Entropy analysis passes (1-10) |
AlphaCompression |
int |
1 |
Alpha compression (0=none, 1=lossless) |
AlphaFiltering |
int |
1 |
Alpha filter (0=none, 1=fast, 2=best) |
AlphaQuality |
int |
100 |
Alpha quality (0-100) |
Benchmarked on Apple M5 Max (macOS, arm64), Go 1.27.0, GOMAXPROCS=18, on 2026-11-10 at v1.2.8-13-gb0f8d34 (b0f8d34). Source: testdata/test_color.png, 1536x1024. Values are medians of 10 runs using the default 1-second benchmark duration.
| Library | Mode | Time | MB/s | B/op | Allocs |
|---|---|---|---|---|---|
| deepteams/webp (Pure Go) | Lossy | 48.8 ms | 3.96 | 1.26 MB | 176 |
| gen2brain/webp (WASM) | Lossy | 56.0 ms | 4.51 | 12.9 KB | 12 |
| chai2010/webp (CGo) | Lossy | 74.0 ms | 2.83 | 227.4 KB | 4 |
| deepteams/webp (Pure Go) | Lossless | 118.1 ms | 15.47 | 21.04 MB | 1,177 |
| gen2brain/webp (WASM) | Lossless | 197.8 ms | 10.39 | 342.9 KB | 12 |
| nativewebp (Pure Go) | Lossless | 282.1 ms | 7.13 | 89.29 MB | 2,155 |
| chai2010/webp (CGo) | Lossless | 930.1 ms | 1.88 | 2.63 MB | 4 |
| Library | Mode | Time | MB/s | B/op | Allocs |
|---|---|---|---|---|---|
| chai2010/webp (CGo) | Lossy | 9.72 ms | 21.52 | 6.76 MB | 23 |
| deepteams/webp (Pure Go) | Lossy | 10.9 ms | 17.66 | 6.51 MB | 7 |
| golang.org/x/image/webp | Lossy | 18.7 ms | 10.36 | 2.59 MB | 13 |
| gen2brain/webp (WASM) | Lossy | 22.8 ms | 11.07 | 622.1 KB | 40 |
| deepteams/webp (Pure Go) | Lossless | 18.0 ms | 101.79 | 8.64 MB | 225 |
| chai2010/webp (CGo) | Lossless | 19.5 ms | 89.96 | 10.65 MB | 30 |
| gen2brain/webp (WASM) | Lossless | 34.9 ms | 58.85 | 4.66 MB | 46 |
| nativewebp (Pure Go) | Lossless | 36.6 ms | 54.98 | 6.35 MB | 50 |
| golang.org/x/image/webp | Lossless | 40.3 ms | 45.39 | 7.14 MB | 966 |
These results describe this image and the benchmark's existing settings. MB/s uses compressed bytes, not decoded pixels; B/op and allocation counts cover Go heap allocations, not total native/WASM memory. Decoder inputs and output color models differ between libraries, so timings are not a comparison of identical work. In particular, deepteams/webp now returns range-correct *image.NRGBA for lossy images; older YCbCr decode timings are not equivalent.
DecodeReuse can reuse compatible output pixel buffers. The tables above measure Decode with a fresh output image; see BenchmarkDecodeLossyReuse / BenchmarkDecodeLosslessReuse in the root package for reuse benchmarks.
Lossy encoding uses row-pipelined parallelism that scales with available cores. Hot DSP kernels are SIMD-accelerated on arm64 (NEON) and amd64 (SSE2/AVX2), with pure Go fallbacks on other architectures. This run measures ARM64 only. See benchmark/ for library versions, methodology and small-image results.
cd benchmark && go test -bench=. -benchmem -count=10 -run='^$' -timeout=30mOutput files are compatible with all WebP decoders (Chrome, Firefox, Safari, libwebp dwebp, ImageMagick, etc.). The encoder produces bitstream-conformant VP8/VP8L output matching the behavior of Google's C reference implementation (libwebp).
webp.go / encode.go Public API (Decode, Encode, Options)
animation/ Animation encoder/decoder (ANIM/ANMF)
cmd/gwebp/ CLI tool
mux/ WebP mux/demux (RIFF container)
sharpyuv/ Sharp YUV color space conversion
internal/
bitio/ Bit-level I/O (boolean arithmetic, lossless streams)
container/ RIFF/WEBP container parsing
dsp/ DSP (YUV conversion, filters, prediction, cost)
lossless/ VP8L encoder/decoder
lossy/ VP8 encoder/decoder
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-change) - Run tests (
go test ./...) - Run race detector (
go test -race ./...) - Submit a pull request
- Keep zero external dependencies
- All codec changes must pass round-trip tests (encode -> decode -> verify)
- Run
go vet ./...and fix any issues before submitting - Bitstream code is precision-critical: test thoroughly against reference files
MIT License - see LICENSE for details.