Fetch, resize, reformat, and cache Atmosphere avatar images
atp.pics
atproto
1// Package transform decodes, resizes, and encodes avatar images.
2package transform
3
4import (
5 "bytes"
6 "fmt"
7 "image"
8 "image/jpeg"
9 "image/png"
10
11 "github.com/chai2010/webp"
12 "github.com/disintegration/imaging"
13)
14
15// Params holds the requested output dimensions, quality, and format.
16type Params struct {
17 Width int // 0 = not specified
18 Height int // 0 = not specified
19 Quality int // 1–100; ignored for PNG; default 85
20 Format string // "webp" | "jpg" | "png"
21}
22
23// DefaultQuality is used when Params.Quality is 0.
24const DefaultQuality = 85
25
26// Transform decodes src, applies the requested resize, and encodes to the
27// target format. Returns the encoded bytes and the output Content-Type.
28func Transform(src []byte, mimeType string, p Params) ([]byte, string, error) {
29 img, err := decode(src, mimeType)
30 if err != nil {
31 return nil, "", fmt.Errorf("decode: %w", err)
32 }
33
34 img = resize(img, p.Width, p.Height)
35
36 q := p.Quality
37 if q == 0 {
38 q = DefaultQuality
39 }
40
41 return encode(img, p.Format, q)
42}
43
44func decode(data []byte, mimeType string) (image.Image, error) {
45 r := bytes.NewReader(data)
46 switch mimeType {
47 case "image/jpeg":
48 img, err := jpeg.Decode(r)
49 if err != nil {
50 return nil, fmt.Errorf("jpeg decode: %w", err)
51 }
52 return img, nil
53 case "image/png":
54 img, err := png.Decode(r)
55 if err != nil {
56 return nil, fmt.Errorf("png decode: %w", err)
57 }
58 return img, nil
59 case "image/webp":
60 img, err := webp.Decode(r)
61 if err != nil {
62 return nil, fmt.Errorf("webp decode: %w", err)
63 }
64 return img, nil
65 default:
66 return nil, fmt.Errorf("unsupported source format: %s", mimeType)
67 }
68}
69
70// resize applies the correct resize strategy based on which dimensions are set.
71//
72// - Both w and h: cover-fit (scale to fill, center-crop to exact size)
73// - Only w or only h: proportional scale, preserving aspect ratio
74// - Neither: no resize, return original
75func resize(img image.Image, w, h int) image.Image {
76 if w == 0 && h == 0 {
77 return img
78 }
79 if w > 0 && h > 0 {
80 return imaging.Fill(img, w, h, imaging.Center, imaging.Lanczos)
81 }
82 // Proportional: pass 0 for the unconstrained dimension.
83 return imaging.Resize(img, w, h, imaging.Lanczos)
84}
85
86func encode(img image.Image, format string, quality int) ([]byte, string, error) {
87 var buf bytes.Buffer
88 switch format {
89 case "jpg", "jpeg":
90 opts := &jpeg.Options{Quality: quality}
91 if err := jpeg.Encode(&buf, img, opts); err != nil {
92 return nil, "", fmt.Errorf("jpeg encode: %w", err)
93 }
94 return buf.Bytes(), "image/jpeg", nil
95 case "png":
96 if err := png.Encode(&buf, img); err != nil {
97 return nil, "", fmt.Errorf("png encode: %w", err)
98 }
99 return buf.Bytes(), "image/png", nil
100 default: // "webp" and anything else
101 opts := &webp.Options{Lossless: false, Quality: float32(quality)}
102 if err := webp.Encode(&buf, img, opts); err != nil {
103 return nil, "", fmt.Errorf("webp encode: %w", err)
104 }
105 return buf.Bytes(), "image/webp", nil
106 }
107}