Documentation
¶
Overview ¶
Package image is a chainable image pipeline: decode → transform → encode, pure Go with only the standard library and golang.org/x/image as dependencies. The API is shaped after Bun.Image; the implementation is independent.
Construction:
img, err := image.Decode(r)
img, err := image.DecodeBytes(data)
img, err := image.Open("path/to/file.jpg")
img, err := image.OpenFS(fsys, "name")
Chain (each method returns a new *Image):
img.Resize(800, 600, image.Lanczos3).
AutoOrient().
Modulate(image.Modulation{Brightness: image.Float64(1.1)})
Encode (terminal):
data, err := img.JPEG(image.JPEGOptions{Quality: 80}).Bytes()
err = img.PNG().Write(w)
durl, err := img.WebP().DataURL() // zero-value = lossless
Placeholders:
durl, err := img.Placeholder() // base64-encoded tiny JPEG hash, err := img.BlurHash(4, 3) // BlurHash string
Index ¶
- Constants
- Variables
- func Float64(v float64) *float64
- type Config
- type Encoder
- type Filter
- type Fit
- type Format
- type GIFOptions
- type Image
- func Decode(r io.Reader) (*Image, error)
- func DecodeBytes(data []byte) (*Image, error)
- func DecodeBytesWithConfig(data []byte, cfg Config) (*Image, error)
- func DecodeWithConfig(r io.Reader, cfg Config) (*Image, error)
- func FromImage(img stdimage.Image, format Format) *Image
- func Open(path string) (*Image, error)
- func OpenFS(fsys fs.FS, name string) (*Image, error)
- func (i *Image) AutoOrient() *Image
- func (i *Image) BMP() *Encoder
- func (i *Image) BlurHash(xComp, yComp int) (string, error)
- func (i *Image) Bounds() stdimage.Rectangle
- func (i *Image) Flip() *Image
- func (i *Image) Flop() *Image
- func (i *Image) Format() Format
- func (i *Image) GIF(opts ...GIFOptions) *Encoder
- func (i *Image) GoImage() stdimage.Image
- func (i *Image) JPEG(opts ...JPEGOptions) *Encoder
- func (i *Image) Metadata() Metadata
- func (i *Image) Modulate(m Modulation) *Image
- func (i *Image) PNG(opts ...PNGOptions) *Encoder
- func (i *Image) Placeholder(opts ...PlaceholderOptions) (string, error)
- func (i *Image) Resize(width, height int, opts ...ResizeOption) *Image
- func (i *Image) Rotate(degrees int) *Image
- func (i *Image) TIFF(opts ...TIFFOptions) *Encoder
- func (i *Image) WebP(opts ...WebPOptions) *Encoder
- type JPEGOptions
- type Metadata
- type Modulation
- type PNGOptions
- type PlaceholderOptions
- type ResizeOption
- type StreamResult
- type TIFFOptions
- type Variant
- type VariantHeader
- type VariantOutput
- type VariantResult
- type VariantSet
- type VariantSink
- type WebPOptions
Constants ¶
const DefaultMaxPixels int64 = 64 * 1024 * 1024
DefaultMaxPixels caps decoded image area at 64 MP — an 8192×8192 square. Tightened from Bun.Image's 268 MP default after round-4 found a 45-byte PNG declaring 16383×16383 (~268 M pixels) passed the guard and triggered ~1 GiB of stdimage allocation. Callers who need bigger inputs configure Config.MaxPixels explicitly.
const MaxVariantsPerSet = 64
MaxVariantsPerSet caps Variants to a sane bound. Attacker-controlled variant lists are an obvious DoS vector for upload pipelines: each variant is an encode + allocation; a 10k-variant request would chew several CPU-seconds and ~10 GB of throughput. 64 variants covers every realistic responsive-srcset scenario (5-10 widths × 3-5 formats); legitimate callers that need more should split into multiple sets.
Variables ¶
var ( // ErrFormatUnsupported is returned by encoders for formats this package // cannot produce. Currently: WebP lossy, HEIC, AVIF. ErrFormatUnsupported = errors.New("image: format not supported for encoding") // ErrDecompressionBomb is returned when an input image's reported // dimensions exceed the configured MaxPixels guard. ErrDecompressionBomb = errors.New("image: decompression bomb guard tripped") // ErrInvalidInput is returned when a source cannot be identified as a // supported image format. ErrInvalidInput = errors.New("image: invalid input") // ErrAnimatedSource is returned by VariantSet.Process / // VariantSet.ProcessTo when RejectAnimated is set and the source // has FrameCount > 1. Callers wanting to flatten or split frames // instead must do that explicitly before the variant pipeline. ErrAnimatedSource = errors.New("image: animated source rejected (set VariantSet.RejectAnimated=false to flatten to first frame)") )
var ( Nearest = Filter{"nearest", draw.NearestNeighbor} BiLinear = Filter{"bilinear", draw.BiLinear} ApproxBiLinear = Filter{"approx-bilinear", draw.ApproxBiLinear} CatmullRom = Filter{"catmull-rom", draw.CatmullRom} Lanczos3 = CatmullRom Lanczos2 = BiLinear )
Resampling kernels. x/image/draw does not ship a Lanczos kernel, so Lanczos3 and Lanczos2 are aliases for the highest-quality interpolators available pure-Go (CatmullRom and BiLinear respectively). Names mirror Bun.Image so callers porting from a JS pipeline find what they expect.
var ErrReaderClosed = errors.New("image: VariantSink reader used after sink returned")
ErrReaderClosed is returned when a sink reads from its VariantHeader reader after the sink has returned. The reader is intentionally one-shot to surface the "I stashed the reader and read it later" foot-gun loudly instead of returning corrupted bytes from a later variant.
Functions ¶
Types ¶
type Config ¶
type Config struct {
// MaxPixels caps decoded image area. Inputs whose reported width*height
// exceed this return ErrDecompressionBomb at decode time. Zero means
// DefaultMaxPixels.
MaxPixels int64
}
Config holds knobs that propagate through a pipeline.
type Encoder ¶
type Encoder struct {
// contains filtered or unexported fields
}
Encoder is a configured terminal of an image pipeline. It captures the chosen output format and any per-format options. Call Bytes, Write, Base64, or DataURL to materialise the encoded image. Repeat calls against the same Encoder reuse the first encode's bytes instead of re-running the codec — even across multiple goroutines.
Note: the cache is keyed by *Encoder identity, not by source pixels. Mutating the underlying image.Image via Image.GoImage() after the first terminal call will not invalidate the cache; the second call returns the original encoded bytes. Build a fresh Encoder after any direct pixel mutation.
func (*Encoder) Bytes ¶
Bytes returns a freshly-allocated copy of the encoded image. The codec runs at most once across all terminal calls (goroutine-safe), but each Bytes call hands the caller its own slice so in-place mutation of the returned bytes cannot corrupt the shared cache or another caller's view. Use Write to avoid the copy when streaming to an io.Writer.
type Filter ¶
type Filter struct {
// contains filtered or unexported fields
}
Filter selects a resampling kernel for Resize.
type Fit ¶
type Fit int
Fit controls how Resize maps the source into the target box.
const ( // FitFill stretches the source to fill the target exactly. May change // aspect ratio. Default. FitFill Fit = iota // FitInside fits the source within the target while preserving aspect // ratio; output may be smaller than the target on one axis. FitInside // FitOutside fills the target while preserving aspect ratio; output // may exceed the target on one axis. FitOutside )
type Format ¶
type Format int
Format identifies an image container format.
type GIFOptions ¶
type GIFOptions struct {
// NumColors is the palette size 1..256. 0 defaults to 256.
NumColors int
}
GIFOptions configures GIF encoding.
type Image ¶
type Image struct {
// contains filtered or unexported fields
}
Image is the chainable pipeline value. Transformations return a new *Image, so the same source may be branched into independent pipelines.
func DecodeBytes ¶
DecodeBytes decodes an in-memory image buffer.
func DecodeBytesWithConfig ¶
DecodeBytesWithConfig is DecodeBytes with a non-default Config.
func DecodeWithConfig ¶
DecodeWithConfig is Decode with a non-default Config.
func FromImage ¶
FromImage wraps an existing image.Image without decoding. Use when you already have pixel data (e.g., generated, not loaded from a file).
The returned *Image carries Orientation = 0 and FrameCount = 0; AutoOrient() is a no-op against it. If you need EXIF orientation handling or animated-source detection, route the bytes through Decode / Open / OpenFS instead.
func Open ¶
Open reads an image from a filesystem path. Rejects paths containing ".." segments as a defense-in-depth measure — callers handling user-supplied paths must validate before this layer, but the framework boundary catches the obvious traversal patterns rather than blindly handing them to os.ReadFile.
For paths rooted in an embed.FS or a constrained directory tree, prefer OpenFS — fs.FS implementations reject traversal natively.
func (*Image) AutoOrient ¶
AutoOrient applies the EXIF orientation tag from decode time, if any, and clears it. Safe to call when no orientation is known. On the no-op path (orient ≤ 1, or no EXIF metadata) the returned *Image shares the underlying image.Image with the receiver — if a caller plans to mutate via GoImage(), they should clone first.
EXIF tag values:
1: identity 5: transpose (rotate 90 then flop) 2: flop 6: rotate 90 CW 3: rotate 180 7: transverse (rotate 270 then flop) 4: flip 8: rotate 270 CW
func (*Image) BlurHash ¶
BlurHash returns a compact base83 placeholder string for the current image, following the spec at https://blurha.sh. xComp and yComp control the number of DCT components on each axis; both must be in 1..9. Typical values are 4×3 (landscape) or 3×4 (portrait).
The algorithm scales with width × height × components, so callers processing large images should resize first:
hash, _ := img.Resize(32, 32, WithFit(FitInside)).BlurHash(4, 3)
func (*Image) Format ¶
Format returns the source format. May be FormatUnknown when the Image was constructed via FromImage.
func (*Image) GoImage ¶
GoImage returns the underlying image.Image. Useful for callers that need to pass the pixels into another library or do custom drawing.
func (*Image) Modulate ¶
func (i *Image) Modulate(m Modulation) *Image
Modulate returns a new *Image with brightness and saturation applied. A Modulation with both fields nil leaves the source unchanged.
NaN values are treated as nil (no change for that channel) — they usually indicate a config-parsing bug (e.g., int(NaN)=0 or a JSON null surfacing as NaN). +Inf/-Inf clamp via the channel limits as you'd expect: +Inf brightness → 255, -Inf → 0.
func (*Image) Placeholder ¶
func (i *Image) Placeholder(opts ...PlaceholderOptions) (string, error)
Placeholder returns a small base64 data: URL suitable for use as an LQIP (low-quality image placeholder). The default produces ~500-byte JPEGs that decode instantly and can be inlined into HTML.
func (*Image) Resize ¶
func (i *Image) Resize(width, height int, opts ...ResizeOption) *Image
Resize returns a new *Image scaled to the target box. If height is 0 or negative it is computed from width preserving aspect; same for width when height is 0 or negative. Width and height both 0 (or both negative) return the source unchanged. Use Sharp-style "skip if larger than source" via WithoutEnlargement().
func (*Image) Rotate ¶
Rotate returns a new *Image rotated by 0/90/180/270 degrees clockwise. Other values are normalised modulo 360 and rounded down to the nearest 90.
func (*Image) WebP ¶
func (i *Image) WebP(opts ...WebPOptions) *Encoder
WebP selects WebP output. The default mode is lossless; setting WebPOptions.Lossy = true returns ErrFormatUnsupported.
type JPEGOptions ¶
type JPEGOptions struct {
// Quality is the JPEG quality 1..100. 0 defaults to 80.
Quality int
}
JPEGOptions configures JPEG encoding.
type Metadata ¶
type Metadata struct {
Width int
Height int
Format Format
HasAlpha bool
Orientation int // EXIF orientation tag 1..8; 0 if absent or already applied
// FrameCount surfaces animated-image frame totals. The decoder
// returns only the first frame; FrameCount > 1 lets callers detect
// "I just dropped N-1 frames" instead of silently mishandling
// animated GIFs. 0 or 1 indicates a still image.
FrameCount int
}
Metadata summarises an image's identifying attributes.
type Modulation ¶
type Modulation struct {
// Brightness multiplies each colour channel. 1.0 is identity;
// values above 1 brighten; values below 1 darken; 0 produces
// pure black. Channels are clamped to 0..255. nil = unchanged.
Brightness *float64
// Saturation interpolates between grayscale (0.0) and source
// (1.0). Values above 1 over-saturate. nil = unchanged.
Saturation *float64
}
Modulation tweaks brightness and saturation in one pass over the image. Fields are pointers so the zero value Modulation{} means "no change" and a literal zero (e.g. grayscale via Saturation=0) is unambiguous. Use the Float64 helper to construct values:
img.Modulate(image.Modulation{Saturation: image.Float64(0)}) // grayscale
img.Modulate(image.Modulation{Brightness: image.Float64(1.4)})
type PNGOptions ¶
type PNGOptions struct {
// Compression maps to image/png.CompressionLevel. Zero is
// png.DefaultCompression.
Compression png.CompressionLevel
}
PNGOptions configures PNG encoding.
type PlaceholderOptions ¶
type PlaceholderOptions struct {
// Width is the target placeholder width in pixels. Height is computed
// from the source aspect ratio. Default 16.
Width int
// Quality is the JPEG quality for the placeholder (1..100). Default 40.
Quality int
}
PlaceholderOptions configures Placeholder.
type ResizeOption ¶
type ResizeOption func(*resizeOpts)
ResizeOption configures Resize.
func WithFilter ¶
func WithFilter(f Filter) ResizeOption
WithFilter selects the resampling kernel. Default is Lanczos3 (CatmullRom).
func WithFit ¶
func WithFit(f Fit) ResizeOption
WithFit selects the fit strategy. Default is FitFill.
func WithoutEnlargement ¶
func WithoutEnlargement() ResizeOption
WithoutEnlargement skips the resize when the target would enlarge the source on either axis (mirrors Sharp / Bun.Image semantics).
type StreamResult ¶
StreamResult is the typed return of ProcessTo. Variants are streamed via the sink rather than carried here.
type TIFFOptions ¶
type TIFFOptions struct {
Compression tiff.CompressionType
Predictor bool
}
TIFFOptions configures TIFF encoding.
type Variant ¶
type Variant struct {
// Width is the target box width in pixels. Required, must be >= 1.
Width int
// Format is the output encoding. Required.
Format Format
// Quality applies to JPEG (1..100). 0 uses the format default.
Quality int
// Suffix is appended to BaseName when forming the output Name.
// Empty defaults to the variant width (e.g., "photo-800.jpg").
Suffix string
}
Variant describes one output produced by a VariantSet. Height is derived from the source aspect ratio (FitInside semantics).
type VariantHeader ¶
VariantHeader is the metadata for one streaming variant, delivered to the sink callback alongside an io.Reader carrying the encoded bytes. ProcessTo emits one VariantHeader per Variant in input order.
type VariantOutput ¶
type VariantOutput struct {
// Name is a storage-friendly identifier composed from BaseName,
// Suffix (or width), and the format extension. Example:
// "photo-800.jpg".
Name string
// Format is the encoding format.
Format Format
// Width and Height are the rendered output dimensions.
Width int
Height int
// Bytes is the encoded image. Callers typically hand this to a
// storage backend (battery/storage) or to an HTTP response writer.
Bytes []byte
// MIME is the canonical Content-Type for Format.
MIME string
}
VariantOutput is one fully rendered variant.
type VariantResult ¶
type VariantResult struct {
// SourceWidth and SourceHeight reflect the input image's bounds at
// the moment Process was called.
SourceWidth int
SourceHeight int
// Variants holds one VariantOutput per VariantSet.Variants entry,
// in the same order.
Variants []VariantOutput
// Placeholder is the LQIP data: URL when requested.
Placeholder string
// BlurHash is the base83 hash string when requested.
BlurHash string
}
VariantResult is the typed result of Process. Fields are populated only when requested by VariantSet — Placeholder is empty when VariantSet.Placeholder is nil, BlurHash is empty when both BlurHash component counts are zero.
type VariantSet ¶
type VariantSet struct {
// Variants is the ordered list of outputs to produce. Empty means
// "no encoded variants — just the placeholder and/or hash".
Variants []Variant
// Placeholder, if non-nil, produces a base64 data: URL LQIP.
Placeholder *PlaceholderOptions
// BlurHashX and BlurHashY are the BlurHash component counts (1..9).
// Both zero means no BlurHash. Setting only one is an error.
BlurHashX int
BlurHashY int
// BaseName is the prefix used for VariantOutput.Name. Default "image".
BaseName string
// AllowUpscale opts back in to producing variants larger than the
// source. The default (false) clamps every variant's effective
// width to min(Variant.Width, source width). Without the clamp a
// 16×16 source with Variant{Width: 2048} would silently produce a
// 2048×2048 pixel-multiplied-garbage output — almost never what a
// caller wants.
AllowUpscale bool
// RejectAnimated returns ErrAnimatedSource when the source's
// Metadata.FrameCount is > 1 (today: animated GIFs). Use this on
// upload pipelines where the silent first-frame-flatten behavior
// of the default decoder would be a surprise — e.g. avatar
// uploads, profile photos. Default (false) preserves the legacy
// "first frame wins" behavior.
RejectAnimated bool
}
VariantSet is a declarative spec for producing N outputs from one source image. The same Process call also handles optional LQIP (Placeholder) and BlurHash generation, so callers get one typed result bag that captures every artefact derived from the source.
VariantSet is intentionally headless: no UI, no HTTP, no storage dependency. Pair its output with framework/ui.PipelineImage to render or with a battery/storage backend to persist.
func (VariantSet) Process ¶
func (s VariantSet) Process(src *Image) (VariantResult, error)
Process produces every variant + placeholder + hash declared by the set. The first error halts processing — callers wanting per-variant resilience can call Process repeatedly with single-element sets.
func (VariantSet) ProcessTo ¶
func (s VariantSet) ProcessTo(src *Image, sink VariantSink) (StreamResult, error)
ProcessTo is the streaming variant of Process. Only one variant lives in memory at a time — once the sink returns, the buffer is released and the next variant is encoded. Wire the sink directly to a storage backend (e.g., core/upload.Storage.Save) to avoid holding all variants resident.
type VariantSink ¶
type VariantSink func(h VariantHeader, r io.Reader) error
VariantSink is the callback ProcessTo invokes once per variant. Implementations must read r before returning; the framework reuses the underlying buffer on the next variant and the reader handed in is one-shot — it returns ErrReaderClosed on any access after the sink returns. Returning an error stops emission and propagates out of ProcessTo.
type WebPOptions ¶
type WebPOptions struct {
// Lossy requests VP8 lossy WebP. Not implemented — setting this
// to true returns ErrFormatUnsupported on terminal calls. The
// zero value (false) selects lossless.
Lossy bool
}
WebPOptions configures WebP encoding. The zero value selects lossless (VP8L), the only mode currently implemented.