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 ¶
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 ¶
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 CustomBicubic ¶
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.
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.