// Package transform decodes, resizes, and encodes avatar images. package transform import ( "bytes" "fmt" "image" "image/jpeg" "image/png" "github.com/chai2010/webp" "github.com/disintegration/imaging" ) // Params holds the requested output dimensions, quality, and format. type Params struct { Width int // 0 = not specified Height int // 0 = not specified Quality int // 1–100; ignored for PNG; default 85 Format string // "webp" | "jpg" | "png" } // DefaultQuality is used when Params.Quality is 0. const DefaultQuality = 85 // Transform decodes src, applies the requested resize, and encodes to the // target format. Returns the encoded bytes and the output Content-Type. func Transform(src []byte, mimeType string, p Params) ([]byte, string, error) { img, err := decode(src, mimeType) if err != nil { return nil, "", fmt.Errorf("decode: %w", err) } img = resize(img, p.Width, p.Height) q := p.Quality if q == 0 { q = DefaultQuality } return encode(img, p.Format, q) } func decode(data []byte, mimeType string) (image.Image, error) { r := bytes.NewReader(data) switch mimeType { case "image/jpeg": img, err := jpeg.Decode(r) if err != nil { return nil, fmt.Errorf("jpeg decode: %w", err) } return img, nil case "image/png": img, err := png.Decode(r) if err != nil { return nil, fmt.Errorf("png decode: %w", err) } return img, nil case "image/webp": img, err := webp.Decode(r) if err != nil { return nil, fmt.Errorf("webp decode: %w", err) } return img, nil default: return nil, fmt.Errorf("unsupported source format: %s", mimeType) } } // resize applies the correct resize strategy based on which dimensions are set. // // - Both w and h: cover-fit (scale to fill, center-crop to exact size) // - Only w or only h: proportional scale, preserving aspect ratio // - Neither: no resize, return original func resize(img image.Image, w, h int) image.Image { if w == 0 && h == 0 { return img } if w > 0 && h > 0 { return imaging.Fill(img, w, h, imaging.Center, imaging.Lanczos) } // Proportional: pass 0 for the unconstrained dimension. return imaging.Resize(img, w, h, imaging.Lanczos) } func encode(img image.Image, format string, quality int) ([]byte, string, error) { var buf bytes.Buffer switch format { case "jpg", "jpeg": opts := &jpeg.Options{Quality: quality} if err := jpeg.Encode(&buf, img, opts); err != nil { return nil, "", fmt.Errorf("jpeg encode: %w", err) } return buf.Bytes(), "image/jpeg", nil case "png": if err := png.Encode(&buf, img); err != nil { return nil, "", fmt.Errorf("png encode: %w", err) } return buf.Bytes(), "image/png", nil default: // "webp" and anything else opts := &webp.Options{Lossless: false, Quality: float32(quality)} if err := webp.Encode(&buf, img, opts); err != nil { return nil, "", fmt.Errorf("webp encode: %w", err) } return buf.Bytes(), "image/webp", nil } }