Fetch, resize, reformat, and cache Atmosphere avatar images atp.pics
atproto
at optimize-resolution 107 lines 3.0 kB view raw
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}