arez

package module
v0.0.2 Latest Latest
Warning

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

Go to latest
Published: Nov 8, 2026 License: MIT Imports: 11 Imported by: 0

README

arez

Go Reference Test Fuzz

Go image resizing library with amd64/arm64/wasm SIMD support. Based on rez, but rewritten to use archsimd, with amd64/arm64/wasm support, more image formats, and a modernized API. Requires GOEXPERIMENT=simd.

Features
Original
  • Supports YCbCr, Gray, RGBA, and NRGBA images.
  • Supports YCbCr subsample ratio conversions.
  • Supports interlaced resizes.
  • Supports parallel resizing.
  • Supports SIMD on amd64.
New
  • Modernized code.
  • Proper error types.
  • Requires Go 1.27 or later.
  • Slightly faster on amd64.
  • Supports nearest neighbour, box, and fractional lanczos filters.
  • Supports SIMD on arm64 and wasm.
  • Supports Alpha, CMYK, NYCbCrA, Gray16, Alpha16, RGBA64, and NRGBA64 images.
  • Supports premultiplying alpha for NRGBA, NRGBA64.
  • Supports rows larger than 32767 bytes (i.e., 8192 pixels RGBA).
  • Supports concurrent use of a Converter.
  • All outputs are bit-identical to a fallback Go implementation, which itself is almost identical to the original one.
  • Fixed many buffer overflows and out-of-bound reads.
  • Fixed many crashes with weirder image layouts.
  • Added fuzzers and tests against simpler (but slower) versions.
Usage

To convert a single image:

err := arez.Convert(output, input, arez.Bicubic())

If you need to convert video, where resize parameters are the same for multiple images, you can do something like:

cfg, err := arez.Prepare(output, input)
converter, err := arez.NewConverter(cfg, arez.Bicubic())
for i := range input {
	err := converter.Convert(output[i], input[i])
}

You can also use arez.NewResizer to resize raw image data.

Notes

Paletted images and other image.Image implementations are not supported and need to be converted first (e.g., with image/draw).

Channels are filtered independently, which is correct for premultiplied alpha (RGBA, RGBA64), but may bleed transparent colors into visible pixels for images with straight alpha (NRGBA, NRGBA64, NYCbCrA).

By default, images are resized in parallel with GOMAXPROCS slices.

Input planes need at least 2 pixels in each direction (2 rows per field when interlaced), and rows are limited to 2^31-1 bytes (NewConverter returns an error otherwise).

I used Claude Fable 5.1 to help port and verify parts of the SIMD code (it's a lot of tedious and mostly mechanical work), write the tests, and help find/test micro-optimizations, but the rest is mostly done by hand.

Filters

Note: The comparison should be viewed at full size (open the image in a new tab and zoom to 100%).

filter comparison

Documentation

Overview

Package arez resizes images, using SIMD where available.

It is based on github.com/bamiaux/rez, but rewritten to use simd/archsimd, with amd64/arm64/wasm support, more image formats, and a modernized API.

As of Go 1.27, SIMD requires GOEXPERIMENT=simd. If SIMD is not available or disabled, regular Go scalers are used, but the output is still bit-identical.

Features:

  • Supports YCbCr, Gray, RGBA, and NRGBA images.
  • Supports YCbCr subsample ratio conversions.
  • Supports interlaced resizes.
  • Supports parallel resizing.
  • Supports SIMD on amd64.

New features:

  • Modernized code.
  • Proper error types.
  • Requires Go 1.27 or later.
  • Slightly faster on amd64.
  • Supports SIMD on arm64 and wasm.
  • Supports nearest neighbour, box, and fractional lanczos filters.
  • Supports Alpha, CMYK, NYCbCrA, Gray16, Alpha16, RGBA64, and NRGBA64 images.
  • Supports premultiplying alpha for NRGBA, NRGBA64.
  • Supports rows larger than 32767 bytes (i.e., 8192 pixels RGBA).
  • Supports concurrent use of a Converter.
  • All outputs are bit-identical to a fallback Go implementation, which itself is almost identical to the original one.
  • Fixed many buffer overflows and out-of-bound reads.
  • Fixed many crashes with weirder image layouts.
  • Added fuzzers and tests against simpler (but slower) versions.

Paletted images and other image.Image implementations are not supported and need to be converted first (e.g., with image/draw).

Channels are filtered independently, which is correct for premultiplied alpha (RGBA, RGBA64), but may bleed transparent colors into visible pixels for images with straight alpha (NRGBA, NRGBA64, NYCbCrA).

By default, images are resized in parallel with GOMAXPROCS slices.

Input planes need at least 2 pixels in each direction (2 rows per field when interlaced), and rows are limited to 2^31-1 bytes (NewConverter returns an error otherwise).

I used Claude Fable 5.1 to help port and verify parts of the SIMD code (it's a lot of tedious and mostly mechanical work), write the tests, and help find/test micro-optimizations, but the rest is mostly done by hand.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrUnsupportedImage is returned for unsupported image types (Paletted,
	// Uniform, other [image.Image] implementations), nil images, YCbCr images
	// of an unknown subsample ratio, images with a negative stride, or ones
	// which otherwise overflow/overlap.
	ErrUnsupportedImage = errors.New("arez: unsupported image")

	// ErrInvalidDescriptor is returned by [Descriptor.Check], by [NewConverter]
	// for an invalid input or output descriptor, and by [NewResizer] for an
	// invalid depth or pack.
	ErrInvalidDescriptor = errors.New("arez: invalid descriptor")

	// ErrIncompatible is returned when the input and output of a conversion
	// differ in interlacing, pack, planes or depth, or are images of different
	// types.
	ErrIncompatible = errors.New("arez: incompatible formats")

	// ErrTooSmall is returned when an input or output image is too small to
	// resize.
	ErrTooSmall = errors.New("arez: size too small")

	// ErrTooLarge is returned when the width/height of an image is too large
	// and would overflow.
	ErrTooLarge = errors.New("arez: size too large")

	// ErrMismatch is returned by [Converter.Convert] for images that do not
	// match the configuration the converter was created for.
	ErrMismatch = errors.New("arez: image does not match the converter")

	// ErrInvalidFilter is returned for a nil or invalid [Filter].
	ErrInvalidFilter = errors.New("arez: invalid filter")
)

Base errors (compare with errors.Is).

Functions

func Convert

func Convert(output, input image.Image, filter Filter) error

Convert resizes input into output with filter. The images must be the same type. A Converter is faster for repeated conversions of identical images.

Example
package main

import (
	"fmt"
	"image"
	"image/color"
	"image/draw"
	"log"

	"github.com/pgaskin/arez"
)

func main() {
	src := image.NewRGBA(image.Rect(0, 0, 640, 480))
	draw.Draw(src, src.Bounds(), image.NewUniform(color.RGBA{R: 200, G: 100, B: 50, A: 255}), image.Point{}, draw.Src)

	dst := image.NewRGBA(image.Rect(0, 0, 320, 240))
	if err := arez.Convert(dst, src, arez.Lanczos(3)); err != nil {
		log.Fatal(err)
	}
	fmt.Println(dst.Bounds(), dst.RGBAAt(160, 120))
}
Output:
(0,0)-(320,240) {200 100 50 255}

Types

type ChromaRatio

type ChromaRatio int

ChromaRatio is the chroma subsampling ratio of a YCbCr image, see ChromaRatioOf.

const (
	Ratio410 ChromaRatio = iota // 4:1:0
	Ratio411                    // 4:1:1
	Ratio420                    // 4:2:0
	Ratio422                    // 4:2:2
	Ratio440                    // 4:4:0
	Ratio444                    // 4:4:4, no subsampling
)

func ChromaRatioOf

func ChromaRatioOf(value image.YCbCrSubsampleRatio) (ChromaRatio, bool)

ChromaRatioOf returns the ChromaRatio of an image.YCbCrSubsampleRatio, and whether it is one of the supported ratios.

func (ChromaRatio) String

func (r ChromaRatio) String() string

String returns the ratio in J:a:b notation, such as "4:2:0".

type Converter

type Converter struct {
	// contains filtered or unexported fields
}

Converter resizes images of one format and size into another, see NewConverter. It converts the chroma subsampling of YCbCr images. A Converter is safe for concurrent use, and may use up to ConverterConfig.Threads goroutines per plane.

func NewConverter

func NewConverter(cfg *ConverterConfig, filter Filter) (*Converter, error)

NewConverter returns a Converter for cfg (which is copied), resizing with filter (which must not be nil).

Example
package main

import (
	"fmt"
	"image"
	"log"

	"github.com/pgaskin/arez"
)

func main() {
	// frames of a 4:2:0 video, downscaled and converted to 4:4:4
	frames := make([]*image.YCbCr, 3)
	for i := range frames {
		frames[i] = image.NewYCbCr(image.Rect(0, 0, 1280, 720), image.YCbCrSubsampleRatio420)
	}
	dst := image.NewYCbCr(image.Rect(0, 0, 640, 360), image.YCbCrSubsampleRatio444)

	cfg, err := arez.Prepare(dst, frames[0])
	if err != nil {
		log.Fatal(err)
	}
	converter, err := arez.NewConverter(cfg, arez.Bicubic())
	if err != nil {
		log.Fatal(err)
	}
	for _, frame := range frames {
		if err := converter.Convert(dst, frame); err != nil {
			log.Fatal(err)
		}
	}
	fmt.Println(cfg.Input)
	fmt.Println(cfg.Output)
}
Output:
1280x720 3 planes 4:2:0 1-packed 8-bit progressive
640x360 3 planes 4:4:4 1-packed 8-bit progressive

func (*Converter) Convert

func (ctx *Converter) Convert(dst, src image.Image) error

Convert resizes src into dst, which must match the descriptors the converter was created for, returning an error otherwise. The result is undefined if output and input overlap.

type ConverterConfig

type ConverterConfig struct {
	Input            Descriptor // input format and size
	Output           Descriptor // output format and size
	Threads          int        // goroutines per plane, <= 0 for GOMAXPROCS
	DisableSIMD      bool       // only use the non-accelerated Go implementation
	PremultiplyAlpha bool       // premultiply straight images before filtering
}

ConverterConfig contains configuration for a Converter.

Additional options may be added in future versions, but the zero value will always have the same behaviour within a major version.

Example (PremultiplyAlpha)
package main

import (
	"fmt"
	"image"
	"image/color"
	"log"

	"github.com/pgaskin/arez"
)

func main() {
	// opaque red on the left, transparent green on the right
	src := image.NewNRGBA(image.Rect(0, 0, 8, 8))
	for y := range 8 {
		for x := range 8 {
			c := color.NRGBA{R: 255, A: 255}
			if x >= 4 {
				c = color.NRGBA{G: 255}
			}
			src.SetNRGBA(x, y, c)
		}
	}
	dst := image.NewNRGBA(image.Rect(0, 0, 4, 4))

	for _, premultiply := range []bool{false, true} {
		cfg, err := arez.Prepare(dst, src)
		if err != nil {
			log.Fatal(err)
		}
		cfg.PremultiplyAlpha = premultiply
		converter, err := arez.NewConverter(cfg, arez.Bilinear())
		if err != nil {
			log.Fatal(err)
		}
		if err := converter.Convert(dst, src); err != nil {
			log.Fatal(err)
		}
		fmt.Printf("premultiply=%v: %v %v\n", premultiply, dst.NRGBAAt(1, 0), dst.NRGBAAt(2, 0))
	}
}
Output:
premultiply=false: {223 32 0 223} {32 223 0 32}
premultiply=true: {255 0 0 223} {255 0 0 32}

func Prepare

func Prepare(output, input image.Image) (*ConverterConfig, error)

Prepare returns the configuration of a conversion from images like input to images like output, for NewConverter.

type Descriptor

type Descriptor struct {
	Rect       image.Rectangle // bounds of the image, in pixels
	Ratio      ChromaRatio     // chroma subsampling, not used for single plane images
	Pack       int             // samples per pixel, 1 to 4
	Interlaced bool            // whether the rows are two interlaced fields
	Planes     int             // number of planes, 1 to 4
	Depth      int             // bits per sample, 8 (or 0) or 16
}

Descriptor describes the format and size of an image. Prepare returns the descriptors of two images.

func (Descriptor) Check

func (d Descriptor) Check() error

func (Descriptor) Height

func (d Descriptor) Height(plane int) int

func (Descriptor) String

func (d Descriptor) String() string

String describes the image in a human-readable form.

func (Descriptor) Width

func (d Descriptor) Width(plane int) int

type Filter

type Filter interface {
	// Support returns the radius of the filter, which must be positive and
	// finite.
	Support(scale float64) float64
	// Weight returns the weight of the input pixel at the signed distance dx
	// from the center of an output pixel (the output center minus the input
	// center). The weights of an output pixel are normalized by their sum. The
	// result must be zero unless -Support(scale) < dx <= Support(scale).
	Weight(dx, scale float64) float64
}

Filter is an interpolation filter, which gives the weights of the input pixels around an output pixel for a resize by scale (output/input). Filters must be safe for concurrent use.

Distances are in input pixels, or in rows of a field for interlaced resizes.

func Bicubic

func Bicubic() Filter

Bicubic returns the Catmull-Rom bicubic filter, CustomBicubic(0, 0.5).

func Bilinear

func Bilinear() Filter

Bilinear returns a bilinear filter.

func Box added in v0.0.2

func Box() Filter

Box returns a box filter.

func CustomBicubic

func CustomBicubic(b, c float64) Filter

CustomBicubic returns the Mitchell-Netravali bicubic filter of the parameters b and c. For example, b = c = 1/3 is the filter Mitchell and Netravali recommend, and b = 0, c = 0.5 the Catmull-Rom spline of Bicubic.

func Kernel added in v0.0.2

func Kernel(support float64, k func(dx float64) float64) Filter

Kernel returns a filter for a symmetric convolution kernel with the specified support radius, which is called with distances from 0 to support. The kernel is stretched over the output pixels when downscaling.

func Lanczos

func Lanczos(alpha float64) Filter

Lanczos returns a Lanczos filter with a support of alpha pixels, commonly 2 or 3.

func Nearest added in v0.0.2

func Nearest() Filter

Nearest returns a nearest-neighbor filter.

type Resizer

type Resizer struct {
	// contains filtered or unexported fields
}

Resizer resizes planes of raw samples in one direction, see NewResizer. A resizer is safe for concurrent use.

func NewResizer

func NewResizer(cfg *ResizerConfig, filter Filter) (*Resizer, error)

NewResizer returns a Resizer for the resize cfg describes, with the weights of filter. It copies cfg. NewResizer returns an error matching ErrInvalidDescriptor if cfg/filter is not valid, or ErrTooSmall/ErrTooLarge if unsupported.

Example
package main

import (
	"fmt"
	"log"

	"github.com/pgaskin/arez"
)

func main() {
	// a horizontal ramp of 2 rows of 4 gray pixels, doubled in width
	src := []byte{
		0, 80, 160, 240,
		240, 160, 80, 0,
	}
	dst := make([]byte, 2*8)
	r, err := arez.NewResizer(&arez.ResizerConfig{Input: 4, Output: 8}, arez.Bilinear())
	if err != nil {
		log.Fatal(err)
	}
	r.Resize(dst, src, 4, 2, 8, 4)
	fmt.Println(dst[:8])
	fmt.Println(dst[8:])
}
Output:
[0 20 60 100 140 180 220 240]
[240 220 180 140 100 60 20 0]

func (*Resizer) Resize

func (c *Resizer) Resize(dst, src []byte, width, height, dstPitch, srcPitch int)

Resize resizes the plane src of width x height pixels into dst. The rows of dst and src start dstPitch and srcPitch bytes apart, and 16-bit samples are big endian, as in the image package.

The resized dimension of the source, width for a horizontal resize and height for a vertical one, must be the configured Input, and the other one is kept.

The pitches must not be negative and the slices must be sized correctly for the planes (src at least srcPitch*(height-1) + width*bpp bytes, dst at least dstPitch*(rows-1) + columns*bpp bytes, where rows and columns are the destination dimensions, and bpp = Pack*Depth/8 the bytes per pixel).

The rows of dst are written from several goroutines and must not overlap. When there is more than one goroutine, dstPitch must be at least columns*bpp. The rows of src may overlap.

Resize will panic if any of these requirements are not met.

type ResizerConfig

type ResizerConfig struct {
	Depth       int  // bits per sample, 8 (the default, also 0) or 16
	Input       int  // input size in pixels (must be at least 2, or 2 per field when interlaced)
	Output      int  // output size in pixels (must be at least 1)
	Vertical    bool // resize the height instead of the width
	Interlaced  bool // whether the rows are two interlaced fields, vertical resizes only
	Pack        int  // samples per pixel, 1 (the default, also 0) or more (must not be negative)
	Threads     int  // number of goroutines, 0 for GOMAXPROCS
	DisableSIMD bool // use the Go scalers only
}

ResizerConfig configures a Resizer, see NewResizer.

Rows must not be more than 2^31-1 bytes, or an error matching ErrTooLarge will be returned.

Jump to

Keyboard shortcuts

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