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 ¶
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 ¶
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
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 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.
func Kernel ¶ added in v0.0.2
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.
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 ¶
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.
