media.go raw
1 package variants
2
3 import (
4 "bytes"
5 "fmt"
6 "image"
7 "image/jpeg"
8 _ "image/png" // Register PNG decoder
9 "io"
10
11 "github.com/disintegration/imaging"
12 )
13
14 // GeneratedVariant holds the result of generating a single variant.
15 type GeneratedVariant struct {
16 Name VariantName
17 Data []byte
18 Width int
19 Height int
20 MimeType string
21 }
22
23 // GenerateResponsiveVariants creates all applicable variants for an image.
24 // It returns the variants in order from smallest to largest.
25 // EXIF metadata is automatically stripped by re-encoding.
26 func GenerateResponsiveVariants(r io.Reader) ([]GeneratedVariant, error) {
27 // Decode the image (this strips EXIF by only reading pixel data)
28 img, format, err := image.Decode(r)
29 if err != nil {
30 return nil, fmt.Errorf("failed to decode image: %w", err)
31 }
32
33 bounds := img.Bounds()
34 originalWidth := bounds.Dx()
35 originalHeight := bounds.Dy()
36
37 variantsToGenerate := GetVariantsToGenerate(originalWidth)
38 results := make([]GeneratedVariant, 0, len(variantsToGenerate))
39
40 for _, name := range variantsToGenerate {
41 var data []byte
42 var width, height int
43
44 if name == Original {
45 // Re-encode at original size to strip EXIF
46 data, err = encodeJPEG(img, OriginalQuality)
47 if err != nil {
48 return nil, fmt.Errorf("failed to encode original: %w", err)
49 }
50 width = originalWidth
51 height = originalHeight
52 } else {
53 config := VariantSizes[name]
54
55 // Resize using Lanczos (high quality)
56 resized := imaging.Resize(img, config.Width, 0, imaging.Lanczos)
57 resizedBounds := resized.Bounds()
58 width = resizedBounds.Dx()
59 height = resizedBounds.Dy()
60
61 data, err = encodeJPEG(resized, config.Quality)
62 if err != nil {
63 return nil, fmt.Errorf("failed to encode %s: %w", name, err)
64 }
65 }
66
67 results = append(results, GeneratedVariant{
68 Name: name,
69 Data: data,
70 Width: width,
71 Height: height,
72 MimeType: "image/jpeg",
73 })
74
75 _ = format // Keep for potential format-preserving in future
76 }
77
78 return results, nil
79 }
80
81 // encodeJPEG encodes an image as JPEG with the specified quality.
82 func encodeJPEG(img image.Image, quality int) ([]byte, error) {
83 var buf bytes.Buffer
84 opts := &jpeg.Options{Quality: quality}
85 if err := jpeg.Encode(&buf, img, opts); err != nil {
86 return nil, err
87 }
88 return buf.Bytes(), nil
89 }
90
91 // GenerateThumbnail creates just a thumbnail variant for an image.
92 // Useful for on-demand thumbnail generation.
93 func GenerateThumbnail(r io.Reader, width int) ([]byte, error) {
94 img, _, err := image.Decode(r)
95 if err != nil {
96 return nil, fmt.Errorf("failed to decode image: %w", err)
97 }
98
99 if width <= 0 {
100 width = VariantSizes[Thumb].Width
101 }
102
103 resized := imaging.Resize(img, width, 0, imaging.Lanczos)
104
105 quality := VariantSizes[Thumb].Quality
106 return encodeJPEG(resized, quality)
107 }
108