arez

package module
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Nov 4, 2026 License: MIT Imports: 10 Imported by: 0

README

arez

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 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 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 rows are limited to MaxRowBytes bytes (8191 RGBA or 4095 RGBA64 pixels) and interlaced inputs need at least 2 rows per field in every plane (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.

Documentation

Overview

Package arez resizes images, using SIMD where available.

It is based on 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 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 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 rows are limited to MaxRowBytes bytes (8191 RGBA or 4095 RGBA64 pixels) and interlaced inputs need at least 2 rows per field in every plane (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 an input or output image is too small to resize.
	ErrTooSmall = errors.New("arez: size too small")

	// ErrTooLarge is returned with 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 interface {
	// Convert resizes src into dst, which must match the descriptors the
	// converter was created for, returning an error otherwise. The result is
	// undefined if dst and src overlap.
	Convert(dst, src image.Image) error
}

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

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
}
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.
	Support() int
	// Name returns the name of the filter.
	Name() string
	// Get returns the weight of an input pixel at the distance dx from the
	// center of an output pixel, in input pixels when upscaling and in output
	// pixels when downscaling. The weights of an output pixel are normalized by
	// their sum. When dx >= Support(), the result is zero.
	Get(dx float64) float64
}

Filter is an interpolation filter, which gives the weights of the input pixels around an output pixel. Filters must be safe for concurrent use.

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 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 Lanczos

func Lanczos(alpha int) Filter

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

type Resizer

type Resizer interface {
	// 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.
	Resize(dst, src []byte, width, height, dstPitch, srcPitch int)
}

Resizer resizes planes of raw samples in one direction, see NewResizer.

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]

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