discord / lilliput

Resize images and animated GIFs in Go
https://discord.com/blog/how-discord-resizes-150-million-images-every-day-with-go-and-c
Other
1.96k stars 123 forks source link
cgo crop gif golang image image-resizer imageops images jpeg png resize-images resized-images thumbnail webp

lilliput

lilliput resizes images in Go.

Lilliput relies on mature, high-performance C libraries to do most of the work of decompressing, resizing and compressing images. It aims to do as little memory allocation as possible and especially not to create garbage in Go. As a result, it is suitable for very high throughput image resizing services.

Lilliput supports resizing JPEG, PNG, static WEBP, and animated GIFs & WEBPs. It can also convert formats. Lilliput also has some support for getting the first frame from MOV and WEBM videos.

Lilliput presently only supports OSX ARM64 and Linux.

Example

Lilliput comes with a fully working example that runs on the command line. The example takes a user supplied filename and prints some basic info about the file. It then resizes and transcodes the image (if flags are supplied) and saves the resulting file.

To use the example, go get github.com/discord/lilliput and then run go build from the examples/ directory.

License

Lilliput is released under MIT license (see LICENSE). Additionally, lilliput ships with other libraries, each provided under its own license. See third-party-licenses for more info.

Usage

First, import "github.com/discord/lilliput".

Decoder

Lilliput is concerned with in-memory images, so the decoder requires image data to be in a []byte buffer.

func lilliput.NewDecoder([]byte buf) (lilliput.Decoder, error)

Create a new Decoder object from the compressed image contained by buf. This will return an error when the magic bytes of the buffer don't match one of the supported image types.

func (d lilliput.Decoder) Header() (lilliput.ImageHeader, error)

Read and return the image's header. The header contains the image's metadata. Returns error if the image has a malformed header. An image with a malformed header cannot be decoded.

func (d lilliput.Decoder) Description() string

Returns a string describing the image's type, e.g. "JPEG" or "PNG".

func (h lilliput.Decoder) Duration() time.Duration

Returns the length of the content. Returns 0 for static images and animated GIFs.

func (d lilliput.Decoder) DecodeTo(f *lilliput.Framebuffer) error

Fully decodes the image and writes its pixel data to f. Returns an error if the decoding process fails. If the image contains multiple frames, then each call returns a subsequent frame. io.EOF is returned when the image does not contain any more data to be decoded.

Users of lilliput generally should not call DecodeTo and should instead use an ImageOps object.

func (d lilliput.Decoder) Close()

Closes the decoder and releases resources. The Decoder object must have .Close() called when it is no longer in use.

ImageOps

Lilliput provides a convenience object to handle image resizing and encoding from an open Decoder object. The ImageOps object can be created and then reused, which reduces memory allocations. Generally, users should prefer the ImageOps object over manually controlling the resize and encode process.

func lilliput.NewImageOps(dimension int) *lilliput.ImageOps

Create an ImageOps object that can operate on images up to dimension x dimension pixels in size. This object can be reused for multiple operations.

func (o *lilliput.ImageOps) Transform(decoder lilliput.Decoder, opts *lilliput.ImageOptions, dst []byte) ([]byte, error)

Transform the compressed image contained in a Decoder object into the desired output type. The decoder must not have DecodeTo() called on it already. However, it is ok to call decoder.Header() if you would like to check image properties before transforming the image. Returns an error if the resize or encoding process fails.

The resulting compressed image will be written into dst. The returned []byte slice will point to the same region as dst but with a different length, so that you can tell where the image ends.

Fields for lilliput.ImageOptions are as follows

func (o *lilliput.ImageOps) Clear()

Clear out all pixel data contained in ImageOps object from any previous operations. This function does not need to be called between Transform() calls. The user may choose to do this if they want to remove image data from memory.

func (o *lilliput.ImageOps) Close()

Close the ImageOps object and release resources. The ImageOps object must have .Close() called when it is no longer in use.

ImageHeader

This interface returns basic metadata about an image. It is created by calling Decoder.Header().

func (h lilliput.ImageHeader) Width() int

Returns the image's width in number of pixels.

func (h lilliput.ImageHeader) Height() int

Returns the image's height in number of pixels.

func (h lilliput.ImageHeader) PixelType() lilliput.PixelType

Returns the basic pixel type for the image's pixels.

func (h lilliput.ImageHeader) Orientation() lilliput.ImageOrientation

Returns the metadata-based orientation of the image. This function can be called on all image types but presently only detects orientation in JPEG images. An orientation value of 1 indicates default orientation. All other values indicate some kind of rotation or mirroring.

PixelType

func (p lilliput.PixelType) Depth() int

Returns the number of bits per pixel.

func (p lilliput.PixelType) Channels() int

Returns the number of channels per pixel, e.g. 3 for RGB or 4 for RGBA.

Framebuffer

This type contains a raw array of pixels, decompressed from an image. In general, you will want to use the ImageOps object instead of operating on Framebuffers manually.

func lilliput.NewFramebuffer(width, height int) *lilliput.Framebuffer

Create a new Framebuffer with given dimensions without any pixel data.

func (f *lilliput.Framebuffer) Clear()

Set contents of framebuffer to 0, clearing out any previous pixel data.

func (f *lilliput.Framebuffer) Width() int

Returns the width in number of pixels of the contained pixel data, if any. This does not return the capacity of the buffer.

func (f *lilliput.Framebuffer) Height() int

Returns the height in number of pixels of the contained pixel data, if any. This does not return the capacity of the buffer.

func (f *lilliput.Framebuffer) PixelType() lilliput.PixelType

Returns the PixelType of the contained pixel data, if any.

func (f *lilliput.Framebuffer) OrientationTransform(orientation lilliput.ImageOrientation)

Rotate and/or mirror framebuffer according to orientation value. If you pass the orientation value given by the image's ImageHeader, then the resulting image has its orientation normalized to the default orientation.

func (f *lilliput.Framebuffer) ResizeTo(width, height int, dst *lilliput.Framebuffer) error

Perform a resize into dst of f according to given dimensions. This function does not preserve the source's aspect ratio if the new dimensions have a different ratio. The resize can fail if the destination is not large enough to hold the new image.

func (f *lilliput.Framebuffer) Fit(width, height int, dst *lilliput.Framebuffer) error

Perform a cropping resize into dst of f according to given dimensions. This function does preserve the source's aspect ratio. The image will be cropped along one axis if the new dimensions have a different ratio than the source. The cropping will occur equally on the edges, e.g. if the source image is too tall for the new ratio, then the destination will have rows of pixels from the top and bottom removed. Returns error if the destination is not large enough to contain the resized image.

func (f *lilliput.Framebuffer) Close()

Closes the framebuffer and releases resources. The Framebuffer object must have .Close() called when it is no longer in use.

Encoder

The Encoder takes a Framebuffer and writes the pixels into a compressed format.

func lilliput.NewEncoder(extension string, decodedBy lilliput.Decoder, dst []byte) (lilliput.Encoder, error)

Create a new Encoder object that writes to dst. extension should be a file extension-like string, e.g. ".jpeg" or ".png". decodedBy should be the Decoder used to decompress the image, if any. decodedBy may be left as nil in most cases but is required when creating a .gif encoder. That is, .gif outputs can only be created from source GIFs.

func (e lilliput.Encoder) Encode(buffer lilliput.Framebuffer, opts map[int]int) ([]byte, error)

Encodes the Framebuffer supplied into the output dst given when the Encoder was created. The returned []byte will point to the same buffer as dst but can be a shorter slice, so that if dst has 50MB of capacity but the image only occupies 30KB, you can tell where the image data ends. This function returns an error if the encoding process fails.

opts is optional and may be left nil. It is used to control encoder behavior e.g. map[int]int{lilliput.JpegQuality: 80} to set JPEG outputquality to 80.

Valid keys/values for opts are

func (e lilliput.Encoder) Close()

Close the Encoder and release resources. The Encoder object must have .Close() called when it is no longer in use.

Building Dependencies

Go does not provide any mechanism for arbitrary building of dependencies, e.g. invoking make or cmake. In order to make lilliput usable as a standard Go package, prebuilt static libraries have been provided for all of lilliput's dependencies on Linux and OSX. In order to automate this process, lilliput ships with build scripts alongside compressed archives of the sources of its dependencies. These build scripts are provided for OSX and Linux.