simple_webp

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 17, 2026 License: MIT Imports: 11 Imported by: 0

README

simple-webp

English | 简体中文

Pure-Go WebP parse / decode / encode — no cgo, no third-party runtime dependencies. Container parsing, VP8 lossy decoding, VP8L lossless decoding+encoding, ALPH transparency, animation compositing — all implemented from scratch, returning standard image.Image.

webpDecoder mirrors the architecture of simple-png: chunked, raw bytes preserved, byte-exact write back.

Supported

Capability Status Notes
RIFF/WEBP container ✅ VP8 /VP8L/VP8X/ALPH/ANIM/ANMF/ICCP/EXIF/XMP , incl. padding
VP8L lossless decode ✅ 4 transforms (predictor/color/subtract-green/color-indexing), meta-Huffman, LZ77, color cache
VP8 lossy decode ✅ bool decoder, keyframe header, partitions, coefficient trees, 16x16/8x8/4x4 prediction, WHT+IDCT, loop filter
ALPH transparency ✅ uncompressed (3 row filters) and VP8L-compressed alpha
Animation ✅ ANIM/ANMF, dispose/blend compositing, canvas-sized frame sequence
image interface ✅ ToImage(), *Webp implements image.Image, image.RegisterFormat("webp", ...)
Metadata editing ✅ rewrite with ICCP/EXIF/XMP chunks, pixels never re-encoded, byte-exact when untouched
VP8L lossless encode ✅ color-indexing / subtract-green / predictor / LZ77 / Huffman (≤15 bits)
VP8 lossy encode ❌ out of scope (mode decision, RD optimization, rate control)

Install

go get github.com/knoweather/simple-webp

Usage

Parse + decode
f, _ := os.Open("a.webp")
w, err := simple_webp.ParseWebp(f)
if err != nil {
    panic(err)
}
log.Println(w.Width, w.Height, w.HasAlpha, w.HasAnimation, len(w.ANMFs))

img, err := w.ToImage() // image.Image (*image.NRGBA)
Standard library

The format is registered in init(), so image.Decode works out of the box:

img, format, err := image.Decode(r)  // format == "webp"
cfg, err := image.DecodeConfig(r)    // header only, no pixels touched
Use directly as an image.Image
var img image.Image = w // Bounds / ColorModel / At (lazy decode, cached)
draw.Draw(dst, dst.Bounds(), w, image.Point{}, draw.Src)
Edit metadata, lossless write back
w.XMP = &simple_webp.XMP{Data: []byte(xmpPacket)}
w.DeleteRawChunk("EXIF")

var buf bytes.Buffer
_, err := w.WriteTo(&buf) // untouched chunks are copied byte for byte, pixels not re-encoded
Animation
frames, durations, err := w.Frames(0) // 0 = default pixel budget (1<<27)
for i, f := range frames {
    log.Println(i, durations[i], f.Bounds())
}
Encode
err := simple_webp.Encode(out, img, &simple_webp.EncodeOptions{
    Lossless: true,
    Method:   2, // 0 literals only / 1 +color-indexing+LZ77 / 2 +predictor
})
Custom chunks

The same extension point as simple-png:

type myChunk struct{ Data []byte }

func (c *myChunk) ChunkName() simple_webp.FourCC { return simple_webp.FourCC("ABCD") }
func (c *myChunk) Parse(chunk *simple_webp.RawChunk) error { c.Data = chunk.Data; return nil }

Layout

webp.go        Webp struct / ParseWebp / chunk consumption & in-order snapshot
riff.go        RIFF primitives: FourCC, little-endian lengths, padding, R/W
chunk.go       VP8X / VP8 / VP8L / ALPH / ANIM / ANMF / ICCP / EXIF / XMP
writer.go      WriteTo / MarshalBytes (in-order write back, lengths & padding recomputed)
decode.go      ToImage / ToImageWith, lossy & lossless dispatch
alpha.go       ALPH inflate, alpha row-filter inverse transform, compositing
animation.go   ANMF frame compositing (blend / dispose)
vp8.go         VP8 → NRGBA (9:3:3:1 fancy upsampling + fixed-point BT.601)
image.go       image.RegisterFormat / Decode / DecodeConfig / image.Image
encode.go      image.Image → VP8L → RIFF
internal/vp8/  VP8 lossy decoder (bool decode, prediction, IDCT, loop filter)
internal/vp8l/ VP8L lossless decoder + encoder (bitstream, Huffman, LZ77, transforms)
testdata/      fixtures generated against libwebp (.webp + .rgba references)

Correctness

Fixtures are all generated by libwebp (Pillow) and compared pixel by pixel:

  • lossless: gradients, random noise, palette, text, alpha, checkerboard, 1×1, 320×240
  • lossy: smooth, sharp edges, noise, with alpha
  • real data: 6778×6028 satellite image, 400×300 photo crop
  • animation: 3 frames

VP8/VP8L decode produces YUV/ARGB planes byte-identical to the reference implementation (loop filter and animation compositing included), and the final RGBA output is pixel-identical to libwebp (fancy upsampling and fixed-point BT.601 included). Encoded files decode correctly in libwebp and match the source pixel-for-pixel.

Benchmark

Windows/amd64, Ryzen 7 7840HS, go test -bench . -benchtime 1s:

Scenario simple-webp reference (x/image) libwebp 1.5.0 (C)
container parse (320×240, 37 KB) 8.6 µs (4.3 GB/s) — —
VP8L lossless decode (320×240) 3.45 ms (22.3 MP/s) 2.86 ms (26.9 MP/s) 1.3–3.4× faster
VP8 lossy decode, YUV only (400×300) 1.02 ms (117.8 MP/s) 1.08 ms (111.4 MP/s) —
VP8 lossy decode + RGB (400×300) 4.52 ms → 1.42 ms (optimized, CT) not supported 0.208 ms (single-thread ≈ 6.8× slower)

The libwebp column comes from a single thread on a different machine (libwebp 1.5.0 WebPDecodeRGBA), scale reference only. Lossy decoding got a YUV→RGB lookup-table + 16-row concurrent optimization, hence the optimized numbers.

Lossless decoding is ~20% slower than the reference (bit-by-bit Huffman, no table-driven fast path yet); core VP8 decoding is slightly faster. Encoding runs at ~4 MP/s with output sizes 1.5–2× of cwebp -lossless -m 6 (no meta-Huffman / color transform yet).

Known limitations

  • Lossy encoding not implemented — EncodeOptions.Quality has no effect (lossless only)
  • The encoder implements neither color transform nor meta-Huffman blocked coding, so compression is weaker than cwebp
  • ALPH preprocessing=1 (level reduction) is passed through as-is, without the reference dithering; cwebp does not produce that mode by default
  • Lossless Huffman decodes bit by bit — large images ~20% slower than the reference
  • Animation is read-only (no animation encoder)
  • WebP contains no VP9: WebP's two codecs are VP8 (lossy, VP8 chunk) and VP8L (lossless, VP8L chunk). VP9 is a different codec used in WebM/Matroska containers and unrelated to this library

Safety / pixel budget

To guard against decompression bombs (test data includes a few-dozen-byte malformed file claiming 16383×16383), every decode path checks the pixel budget after reading the header and before allocating pixel buffers:

  • DecodeOptions.MaxPixels (default 1<<27 ≈ 134M pixels ≈ 512 MB ARGB) is forwarded to vp8.DecodeLimit / vp8l.DecodeARGBLimit; exceeding it returns ErrTooLarge / ErrImageTooLarge
  • single chunk size is capped by maxChunkSize
  • covered by TestToImageWithDefaultBudget and single-worker fuzzing: the worst-case bogus header is rejected with zero allocations, no OOM

Tighten the budget yourself, e.g. trust only small images:

img, err := w.ToImageWith(simple_webp.DecodeOptions{MaxPixels: 4096 * 4096})

Testing

go test ./...                 # unit tests + pixel-by-pixel fixture comparison
go test -run XXX -bench .     # benchmarks
go test -fuzz FuzzParseWebp   # fuzz (goal: no panic, no OOM)

The internal/vp8 plane-level tests and bench_test.go use golang.org/x/image as a comparison oracle — test-only, never a runtime dependency.

References


Acknowledgements

Thanks to Tencent WorkBuddy for the free quota of the Step 5 Preview model that made this project possible.

Documentation

Index

Constants

View Source
const (
	FlagICC       uint8 = 0x20
	FlagAlpha     uint8 = 0x10
	FlagEXIF      uint8 = 0x08
	FlagXMP       uint8 = 0x04
	FlagAnimation uint8 = 0x02
)

VP8X flags, the bit positions defined by the container specification.

View Source
const (
	ALPHCompressionNone     uint8 = 0
	ALPHCompressionLossless uint8 = 1
)

ALPH alpha channel compression methods.

View Source
const (
	ALPHFilterNone       uint8 = 0
	ALPHFilterHorizontal uint8 = 1
	ALPHFilterVertical   uint8 = 2
	ALPHFilterGradient   uint8 = 3
)

ALPH alpha channel filtering methods.

Variables

View Source
var (
	// ErrNotWebP is returned when the stream does not start with RIFF....WEBP.
	ErrNotWebP = errors.New("invalid webp: bad RIFF/WEBP signature")
	// ErrChunkNotFound is returned by ParseChunk when no chunk of the
	// requested type is left in the stream.
	ErrChunkNotFound = errors.New("chunk not found")
	// ErrUnsupported is returned for valid but unimplemented webp features.
	ErrUnsupported = errors.New("unsupported webp feature")
	// ErrImageTooLarge is returned when the image exceeds the configured
	// pixel budget.
	ErrImageTooLarge = errors.New("webp image too large")
)

Sentinel errors returned by the parser and the decoders.

Functions

func Decode

func Decode(r io.Reader) (image.Image, error)

Decode reads a webp stream and returns the decoded image, implementing the image.Decode signature.

func DecodeConfig

func DecodeConfig(r io.Reader) (image.Config, error)

DecodeConfig reads only the container header, it does not decode any pixel data.

func Encode

func Encode(w io.Writer, m image.Image, opt *EncodeOptions) error

Encode writes m as a lossless WebP file.

func EncodeBytes

func EncodeBytes(m image.Image, opt *EncodeOptions) ([]byte, error)

EncodeBytes encodes m as a lossless WebP file held in memory.

Types

type ALPH

type ALPH struct {
	Compression   uint8
	Filter        uint8
	Preprocessing uint8
	Data          []byte
}

ALPH carries the alpha plane of a lossy image.

0                   1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+
|C|F|P|Rsv|

C: compression method (0 none, 1 lossless), F: filtering method, P: preprocessing (0 none, 1 level reduction).

func (*ALPH) ChunkName

func (a *ALPH) ChunkName() FourCC

func (*ALPH) Parse

func (a *ALPH) Parse(c *riffChunk) error

type ANIM

type ANIM struct {
	// BackgroundColor is stored as 0xAARRGGBB in the file's BGRA byte order.
	BackgroundColor uint32
	LoopCount       uint16
}

ANIM holds the global animation parameters.

Background Color: 32 bits (BGRA)
Loop Count:       16 bits

func (*ANIM) ChunkName

func (a *ANIM) ChunkName() FourCC

func (*ANIM) Parse

func (a *ANIM) Parse(c *riffChunk) error

type ANMF

type ANMF struct {
	X, Y          int
	Width, Height int
	Duration      int
	// Blend is true when the frame is alpha blended onto the canvas, false
	// when it overwrites the canvas region.
	Blend bool
	// Dispose is true when the canvas is cleared to the background color
	// after the frame was displayed.
	Dispose bool

	ALPH *ALPH
	VP8  *VP8
	VP8L *VP8L
}

ANMF is one animation frame.

Frame X:      24 bits (actual x / 2)
Frame Y:      24 bits (actual y / 2)
Frame Width:  24 bits (width - 1)
Frame Height: 24 bits (height - 1)
Duration:     24 bits (milliseconds)
Flags:         8 bits (bit1 blending, bit0 disposal)
Frame Data:   ALPH (optional) + VP8 / VP8L

func (*ANMF) ChunkName

func (a *ANMF) ChunkName() FourCC

func (*ANMF) Parse

func (a *ANMF) Parse(c *riffChunk) error

type ChunkParse

type ChunkParse interface {
	ChunkName() FourCC
	Parse(chunk *riffChunk) error
}

ChunkParse is the interface a custom chunk parser has to implement, the same extension point simple-png exposes.

type DecodeOptions

type DecodeOptions struct {
	// Format selects the returned pixel layout.
	Format PixelFormat
	// Frame selects the frame of an animated file, 0 is the first frame.
	Frame int
	// MaxPixels rejects larger images, 0 uses defaultMaxPixels.
	MaxPixels int
}

DecodeOptions tunes the pixel decoding.

type EXIF

type EXIF struct {
	Data []byte
}

EXIF is the embedded EXIF metadata.

func (*EXIF) ChunkName

func (e *EXIF) ChunkName() FourCC

func (*EXIF) Parse

func (e *EXIF) Parse(c *riffChunk) error

type EncodeOptions

type EncodeOptions struct {
	// Lossless is informational: this library only writes lossless webp, so
	// the value is accepted and ignored.
	Lossless bool
	// Method trades speed for size: 0 stores literals only, 1 adds the
	// subtract green transform, colour indexing and LZ77, 2 (the default)
	// also applies a predictor transform.
	Method int
}

EncodeOptions tunes the lossless encoder.

type FourCC

type FourCC string

FourCC is a 4 byte chunk type code, e.g. "VP8L".

const (
	RIFFChunk FourCC = "RIFF"
	WEBPChunk FourCC = "WEBP"

	VP8XChunk FourCC = "VP8X"
	VP8Chunk  FourCC = "VP8 "
	VP8LChunk FourCC = "VP8L"
	ALPHChunk FourCC = "ALPH"
	ANIMChunk FourCC = "ANIM"
	ANMFChunk FourCC = "ANMF"
	ICCPChunk FourCC = "ICCP"
	EXIFChunk FourCC = "EXIF"
	XMPChunk  FourCC = "XMP "
)

The chunk types of the WebP container, see https://developers.google.com/speed/webp/docs/riff_container

type ICCP

type ICCP struct {
	Data []byte
}

ICCP is the embedded ICC color profile.

func (*ICCP) ChunkName

func (i *ICCP) ChunkName() FourCC

func (*ICCP) Parse

func (i *ICCP) Parse(c *riffChunk) error

type PixelFormat

type PixelFormat int

PixelFormat selects the pixel layout of the decoded image.

const (
	// PixelFormatNRGBA returns an *image.NRGBA, the straight alpha layout
	// used by the png decoder for RGBA images. It is the default.
	PixelFormatNRGBA PixelFormat = iota
	// PixelFormatRGBA returns an *image.RGBA, the premultiplied layout.
	PixelFormatRGBA
)

type RawChunk

type RawChunk struct {
	// Name is the 4 byte chunk type code.
	Name FourCC
	// Data is the chunk payload without header and padding.
	Data []byte
}

RawChunk is a chunk that was not decoded natively and is kept in its raw form, e.g. an unknown application specific chunk.

type VP8

type VP8 struct {
	Data []byte

	Width, Height int
	Version       uint8
	ShowFrame     bool
	FirstPartSize int
}

VP8 is the lossy bitstream chunk. The payload is kept untouched: WebP lossy images are single VP8 key frames, the header is parsed here so the dimensions are known without decoding the frame.

func (*VP8) ChunkName

func (v *VP8) ChunkName() FourCC

func (*VP8) Parse

func (v *VP8) Parse(c *riffChunk) error

type VP8L

type VP8L struct {
	Data []byte

	Width, Height int
	AlphaUsed     bool
	Version       uint8
}

VP8L is the lossless bitstream chunk, see https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification

The 5 byte header is:

signature       8 bits (0x2f)
width - 1      14 bits
height - 1     14 bits
alpha_is_used   1 bit
version_number  3 bits (must be 0)

func (*VP8L) ChunkName

func (v *VP8L) ChunkName() FourCC

func (*VP8L) Parse

func (v *VP8L) Parse(c *riffChunk) error

type VP8X

type VP8X struct {
	Flags        uint8
	CanvasWidth  int
	CanvasHeight int
}

VP8X

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Rsv|I|L|E|X|A|R|                   Reserved                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Canvas Width Minus One               |             ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
...  Canvas Height Minus One    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

I: ICC profile, L: alpha, E: EXIF, X: XMP, A: animation.

func (*VP8X) ChunkName

func (v *VP8X) ChunkName() FourCC

func (*VP8X) HasAlpha

func (v *VP8X) HasAlpha() bool

func (*VP8X) HasAnimation

func (v *VP8X) HasAnimation() bool

func (*VP8X) HasEXIF

func (v *VP8X) HasEXIF() bool

func (*VP8X) HasICC

func (v *VP8X) HasICC() bool

func (*VP8X) HasXMP

func (v *VP8X) HasXMP() bool

func (*VP8X) Parse

func (v *VP8X) Parse(c *riffChunk) error

type Webp

type Webp struct {
	Width, Height int
	HasAlpha      bool
	HasAnimation  bool

	VP8X  *VP8X
	VP8   *VP8
	VP8L  *VP8L
	ALPH  *ALPH
	ANIM  *ANIM
	ANMFs []*ANMF
	ICCP  *ICCP
	EXIF  *EXIF
	XMP   *XMP

	// OtherChunk holds the custom chunks parsed through ParseChunk, grouped
	// by chunk type.
	OtherChunk map[FourCC][]ChunkParse

	sync.RWMutex
	sync.Once
	// contains filtered or unexported fields
}

Webp is a parsed WebP container. The chunk payloads of the image data are kept as raw bytes, the parsed structs are views on them, so metadata can be edited and written back without re-encoding the pixels.

func ParseWebp

func ParseWebp(r io.Reader, verifySize ...bool) (*Webp, error)

ParseWebp parses a webp stream: it reads the RIFF header, all chunks and decodes the container chunks (VP8X, VP8, VP8L, ALPH, ANIM, ANMF, ICCP, EXIF, XMP). Chunks of unknown type are kept in raw form, see RawChunks.

The RIFF size field is validated only when verifySize is true.

func ParseWebpBytes

func ParseWebpBytes(b []byte) (*Webp, error)

ParseWebpBytes parses a webp stream held in memory.

func (*Webp) AddRawChunk

func (w *Webp) AddRawChunk(name FourCC, data []byte) error

AddRawChunk inserts a new raw chunk into the stream before the first image chunk (VP8, VP8L or ANMF); if there is no image chunk it is appended. Use it e.g. to add an EXIF or XMP chunk to an already parsed file.

func (*Webp) At

func (w *Webp) At(x, y int) color.Color

At implements image.Image. The image is decoded on the first call and then cached.

func (*Webp) Bounds

func (w *Webp) Bounds() image.Rectangle

Bounds implements image.Image: the canvas rectangle of the file.

func (*Webp) ColorModel

func (w *Webp) ColorModel() color.Model

ColorModel implements image.Image. The decoders produce straight alpha NRGBA pixels.

func (*Webp) DeleteRawChunk

func (w *Webp) DeleteRawChunk(name FourCC) int

DeleteRawChunk removes every chunk of the given type from the stream and returns how many chunks were removed. It works for the metadata chunks that are decoded natively (ICCP, EXIF, XMP) as well as for unknown raw chunks; a WriteTo afterwards writes the stream without those chunks.

func (*Webp) Frames

func (w *Webp) Frames(maxPixels int) ([]*image.NRGBA, []int, error)

Frames decodes every frame of an animated file and returns the composited frames at canvas size together with their display duration in milliseconds. The pixel budget is forwarded to the sub-decoders.

func (*Webp) GetOtherChunkByName

func (w *Webp) GetOtherChunkByName(name FourCC) ([]ChunkParse, error)

GetOtherChunkByName returns the custom chunk parsers recorded for a type.

func (*Webp) Image

func (w *Webp) Image() (image.Image, error)

Image decodes the image once and caches the result, it backs the image.Image implementation of Webp.

func (*Webp) MarshalBytes

func (w *Webp) MarshalBytes() ([]byte, error)

MarshalBytes returns the webp stream of the current state as a byte slice, see WriteTo for the details.

func (*Webp) ParseChunk

func (w *Webp) ParseChunk(c ChunkParse, notSave ...bool) error

ParseChunk parses the first remaining chunk whose type matches c.ChunkName(). After a successful parse the chunk is consumed and removed from the raw chunk list. Unless notSave is true, the parser instance is recorded in OtherChunk and can be retrieved later with GetOtherChunkByName.

func (*Webp) RawChunkByName

func (w *Webp) RawChunkByName(name FourCC) ([]byte, bool)

RawChunkByName returns the raw payload of the first still-present chunk of the given type.

func (*Webp) RawChunks

func (w *Webp) RawChunks() []RawChunk

RawChunks returns all chunks that are still left after base parsing, in the order they appear in the file.

func (*Webp) ToImage

func (w *Webp) ToImage() (image.Image, error)

ToImage decodes the image and returns it as an image.Image. Lossless files are decoded by the VP8L decoder, lossy files by the VP8 decoder with the ALPH alpha channel applied. For animated files the first frame is returned.

func (*Webp) ToImageWith

func (w *Webp) ToImageWith(opt DecodeOptions) (image.Image, error)

ToImageWith decodes the image with the given options.

func (*Webp) WriteTo

func (w *Webp) WriteTo(out io.Writer) (int64, error)

WriteTo writes the webp stream of the current state, implementing io.WriterTo. Chunks that were consumed while parsing are re-serialized from their parsed structs, so modifications made to the structs (or chunks added/deleted through AddRawChunk / DeleteRawChunk) are reflected, while the chunks that were left raw are copied verbatim. Parsing and writing back a file without touching anything reproduces it byte for byte.

The image payload (VP8 / VP8L / ALPH) is never re-encoded, so editing the metadata chunks does not touch the pixels at all.

type XMP

type XMP struct {
	Data []byte
}

XMP is the embedded XMP metadata.

func (*XMP) ChunkName

func (x *XMP) ChunkName() FourCC

func (*XMP) Parse

func (x *XMP) Parse(c *riffChunk) error

Directories

Path Synopsis
internal
vp8
vp8l
Package vp8l implements the WebP lossless bitstream decoder, see https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification
Package vp8l implements the WebP lossless bitstream decoder, see https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL