// Package optimizer provides an interface for optimizing images. It provides both an in-process // optimizer that supports JPEG outputs, and an imaginary optimizer that supports WEBP outputs. package optimizer import ( "bytes" "context" "image" "image/jpeg" "io" _ "image/png" "github.com/hay-kot/httpkit/errtrace" "github.com/nfnt/resize" ) const ( MaxWidth = 800 ) type OptimizerResult struct { Body io.ReadCloser ContentType string ContentLength int64 Extension string } type Optimizer interface { Optimize(ctx context.Context, body io.Reader, contentType string) (*OptimizerResult, error) } var _ Optimizer = &InProcessOptimizer{} type InProcessOptimizer struct { sem chan struct{} } func NewInProcessOptimizer() *InProcessOptimizer { return &InProcessOptimizer{ sem: make(chan struct{}, 10), } } // UnlimitedConcurrency disables the semaphore, allowing unlimited concurrency. // This is useful for testing purposes, or if you have significant compute resources. func (i *InProcessOptimizer) UnlimitedConcurrency() { i.sem = nil } func (i *InProcessOptimizer) Optimize(ctx context.Context, body io.Reader, contentType string) (*OptimizerResult, error) { if i.sem != nil { // Acquire a semaphore slot i.sem <- struct{}{} // Release the semaphore slot defer func() { <-i.sem }() } // Check if the content type is supported if contentType == "image/webp" { buff := &bytes.Buffer{} nRead, err := io.Copy(buff, body) if err != nil { return nil, errtrace.Wrap(err) } return &OptimizerResult{ Body: io.NopCloser(buff), ContentType: contentType, ContentLength: nRead, Extension: "webp", }, nil } // Decode the image img, _, err := image.Decode(body) if err != nil { return nil, errtrace.Wrap(err) // This may not be necessary, keeping it to refer to later. /* // try webp, sometimes it's not detected correctly */ /* var webpErr error */ /* img, webpErr = webp.Decode(body) */ /* if webpErr != nil { */ /* return nil, errtrace.Wrapf(err, "failed to decode image: %s", webpErr.Error()) */ /* } */ } // Calculate the new dimensions while maintaining the aspect ratio newWidth := uint(MaxWidth) newHeight := uint(float64(newWidth) / float64(img.Bounds().Dx()) * float64(img.Bounds().Dy())) // Resize the image resizedImg := resize.Resize(newWidth, newHeight, img, resize.Lanczos3) // Encode the resized image as JPEG with 75% quality jpegOptions := jpeg.Options{ Quality: 75, } // Encode the resized image buf := new(bytes.Buffer) err = jpeg.Encode(buf, resizedImg, &jpegOptions) if err != nil { return nil, errtrace.Wrap(err) } return &OptimizerResult{ Body: io.NopCloser(buf), ContentType: "image/jpeg", ContentLength: int64(buf.Len()), Extension: "jpg", }, nil }