Skip to content

Latest commit

 

History

History
297 lines (245 loc) · 8.1 KB

File metadata and controls

297 lines (245 loc) · 8.1 KB

The std package

github.com/SeriousBug/webp-go-pure/std implements the standard library's codec interfaces. Its package name is webp, so a bare import reads the way image/png and image/jpeg do at the call site, and importing it registers the format with image.Decode.

The root package is also named webp, so on the rare occasion you want both, give one of them a name:

import (
	"github.com/SeriousBug/webp-go-pure/std"
	codec "github.com/SeriousBug/webp-go-pure"
)
import "github.com/SeriousBug/webp-go-pure/std"
Decode(r io.Reader) (image.Image, error)
DecodeConfig(r io.Reader) (image.Config, error)
DecodeAll(r io.Reader) (*Animation, error)
Encode(w io.Writer, m image.Image, o *Options) error

DecodeBytes and EncodeBytes are the same two operations for callers who already hold, or want, a []byte. They skip the copy that io.ReadAll and Write would make.

What Decode returns

Decode returns the type that matches how the file stores its pixels, so no conversion happens on the way out:

input type
lossy VP8 *image.YCbCr, 4:2:0
lossy VP8 with an ALPH chunk *image.NYCbCrA, 4:2:0
lossless VP8L *image.NRGBA
animated first frame, as *image.NRGBA
func decodedType(path string) (string, error) {
	f, err := os.Open(path)
	if err != nil {
		return "", err
	}
	defer f.Close()

	img, err := webp.Decode(f)
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("%T", img), nil
}

All four satisfy image.Image, so At, Bounds and draw.Draw work the same way whichever one you get.

If you would rather have one predictable layout, DecodeNRGBA and DecodeNRGBABytes always return an *image.NRGBA, with straight (not premultiplied) alpha. That is cheaper than converting the result of Decode yourself, because the codec converts its own planes directly.

func decodeAsNRGBA(path string) (string, error) {
	f, err := os.Open(path)
	if err != nil {
		return "", err
	}
	defer f.Close()

	img, err := webp.DecodeNRGBA(f)
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("%T %v", img, img.Bounds()), nil
}

What Encode recognizes

Encode looks for the types it can feed to the encoder without converting:

  • *image.YCbCr at 4:2:0 goes straight through as planes.
  • *image.NYCbCrA at 4:2:0 goes straight through, its alpha plane included.
  • *image.NRGBA is already the layout the byte-oriented API takes.

Encode takes any image.Image. Anything not on that list, including *image.RGBA, sub-images, non-4:2:0 chroma and odd crop origins, is drawn into an *image.NRGBA first, which costs a pass over the pixels.

*image.RGBA is one of those. Its pixels are alpha-premultiplied and WebP stores straight alpha, so the conversion keeps semi-transparent pixels from coming out dark.

func encodeHalfTransparentRed() (color.NRGBA, error) {
	src := image.NewRGBA(image.Rect(0, 0, 4, 4))
	src.Set(0, 0, color.NRGBA{R: 255, A: 128})

	data, err := webp.EncodeBytes(src, &webp.Options{Lossless: true})
	if err != nil {
		return color.NRGBA{}, err
	}
	decoded, err := webp.DecodeBytes(data)
	if err != nil {
		return color.NRGBA{}, err
	}
	return color.NRGBAModel.Convert(decoded.At(0, 0)).(color.NRGBA), nil
}

That returns {255 0 0 128}. A library that passed *image.RGBA pixels through unchanged would return {128 0 0 128}.

Options

func encodeLossless(img image.Image) ([]byte, error) {
	return webp.EncodeBytes(img, &webp.Options{Lossless: true, Effort: 6})
}
field meaning
Quality 1..100 for lossy. Zero means 90. Ignored when Lossless is set.
Effort 0..9 for lossy, 0..6 for lossless (7..9 accepted, but does not enable any additional options beyond 6 at the moment). Higher is slower and smaller. Zero means the default for the mode, 0 lossy and 6 lossless. Pass webp.EffortFastest to ask for 0 explicitly.
Lossless Encode with VP8L, reproducing the input exactly.
EXIF Raw EXIF bytes to embed as a metadata chunk.

A nil *Options means all of the above defaults, so Encode(w, img, nil) is lossy at quality 90.

Alpha

Both encoders keep transparency. Lossy stores the color channels in a VP8 chunk and the alpha channel losslessly in an ALPH chunk, so alpha survives a lossy encode unchanged while the color is quantized:

func encodeTransparent() string {
	src := image.NewNRGBA(image.Rect(0, 0, 4, 4))
	src.SetNRGBA(0, 0, color.NRGBA{R: 255, A: 128})

	data, err := webp.EncodeBytes(src, nil)
	if err != nil {
		return fmt.Sprint(err)
	}
	decoded, err := webp.DecodeBytes(data)
	if err != nil {
		return fmt.Sprint(err)
	}
	_, _, _, a := decoded.At(0, 0).RGBA()
	return fmt.Sprint(a >> 8)
}

An image that is fully opaque encodes without an ALPH chunk at all. Decoding transparency works either way: lossy files carrying an ALPH chunk decode to *image.NYCbCrA.

Animations

DecodeAll returns every frame, already composited onto the canvas, in the shape of gif.GIF. Unlike gif.GIF, the delays are in milliseconds, because that is what the WebP container stores.

func summarizeAnimation(path string) (string, error) {
	f, err := os.Open(path)
	if err != nil {
		return "", err
	}
	defer f.Close()

	anim, err := webp.DecodeAll(f)
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("%d frames, %dms first", len(anim.Image), anim.Delay[0]), nil
}

A still image decodes as a single-frame animation, so a caller that handles both does not need to branch. In the other direction, Decode on an animated file returns the first frame rather than failing, so image.Decode works on animations.

Encoding animations is not implemented.

DecodeConfig

DecodeConfig reports the dimensions and the color model Decode would produce, without decoding the pixels. It reads a few hundred bytes of a typical file, more only when metadata chunks sit in front of the image data. Use it to check an image's size before committing to decoding it.