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