image_optimizer.go
· 2.8 KiB · Go
Brut
// 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
}
| 1 | // Package optimizer provides an interface for optimizing images. It provides both an in-process |
| 2 | // optimizer that supports JPEG outputs, and an imaginary optimizer that supports WEBP outputs. |
| 3 | package optimizer |
| 4 | |
| 5 | import ( |
| 6 | "bytes" |
| 7 | "context" |
| 8 | "image" |
| 9 | "image/jpeg" |
| 10 | "io" |
| 11 | |
| 12 | _ "image/png" |
| 13 | |
| 14 | "github.com/hay-kot/httpkit/errtrace" |
| 15 | "github.com/nfnt/resize" |
| 16 | ) |
| 17 | |
| 18 | const ( |
| 19 | MaxWidth = 800 |
| 20 | ) |
| 21 | |
| 22 | type OptimizerResult struct { |
| 23 | Body io.ReadCloser |
| 24 | ContentType string |
| 25 | ContentLength int64 |
| 26 | Extension string |
| 27 | } |
| 28 | |
| 29 | type Optimizer interface { |
| 30 | Optimize(ctx context.Context, body io.Reader, contentType string) (*OptimizerResult, error) |
| 31 | } |
| 32 | |
| 33 | var _ Optimizer = &InProcessOptimizer{} |
| 34 | |
| 35 | type InProcessOptimizer struct { |
| 36 | sem chan struct{} |
| 37 | } |
| 38 | |
| 39 | func NewInProcessOptimizer() *InProcessOptimizer { |
| 40 | return &InProcessOptimizer{ |
| 41 | sem: make(chan struct{}, 10), |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | // UnlimitedConcurrency disables the semaphore, allowing unlimited concurrency. |
| 46 | // This is useful for testing purposes, or if you have significant compute resources. |
| 47 | func (i *InProcessOptimizer) UnlimitedConcurrency() { |
| 48 | i.sem = nil |
| 49 | } |
| 50 | |
| 51 | func (i *InProcessOptimizer) Optimize(ctx context.Context, body io.Reader, contentType string) (*OptimizerResult, error) { |
| 52 | if i.sem != nil { |
| 53 | // Acquire a semaphore slot |
| 54 | i.sem <- struct{}{} |
| 55 | |
| 56 | // Release the semaphore slot |
| 57 | defer func() { |
| 58 | <-i.sem |
| 59 | }() |
| 60 | } |
| 61 | |
| 62 | // Check if the content type is supported |
| 63 | if contentType == "image/webp" { |
| 64 | buff := &bytes.Buffer{} |
| 65 | nRead, err := io.Copy(buff, body) |
| 66 | if err != nil { |
| 67 | return nil, errtrace.Wrap(err) |
| 68 | } |
| 69 | |
| 70 | return &OptimizerResult{ |
| 71 | Body: io.NopCloser(buff), |
| 72 | ContentType: contentType, |
| 73 | ContentLength: nRead, |
| 74 | Extension: "webp", |
| 75 | }, nil |
| 76 | } |
| 77 | |
| 78 | // Decode the image |
| 79 | img, _, err := image.Decode(body) |
| 80 | if err != nil { |
| 81 | return nil, errtrace.Wrap(err) |
| 82 | |
| 83 | // This may not be necessary, keeping it to refer to later. |
| 84 | /* // try webp, sometimes it's not detected correctly */ |
| 85 | /* var webpErr error */ |
| 86 | /* img, webpErr = webp.Decode(body) */ |
| 87 | /* if webpErr != nil { */ |
| 88 | /* return nil, errtrace.Wrapf(err, "failed to decode image: %s", webpErr.Error()) */ |
| 89 | /* } */ |
| 90 | } |
| 91 | |
| 92 | // Calculate the new dimensions while maintaining the aspect ratio |
| 93 | newWidth := uint(MaxWidth) |
| 94 | newHeight := uint(float64(newWidth) / float64(img.Bounds().Dx()) * float64(img.Bounds().Dy())) |
| 95 | |
| 96 | // Resize the image |
| 97 | resizedImg := resize.Resize(newWidth, newHeight, img, resize.Lanczos3) |
| 98 | |
| 99 | // Encode the resized image as JPEG with 75% quality |
| 100 | jpegOptions := jpeg.Options{ |
| 101 | Quality: 75, |
| 102 | } |
| 103 | |
| 104 | // Encode the resized image |
| 105 | buf := new(bytes.Buffer) |
| 106 | err = jpeg.Encode(buf, resizedImg, &jpegOptions) |
| 107 | if err != nil { |
| 108 | return nil, errtrace.Wrap(err) |
| 109 | } |
| 110 | |
| 111 | return &OptimizerResult{ |
| 112 | Body: io.NopCloser(buf), |
| 113 | ContentType: "image/jpeg", |
| 114 | ContentLength: int64(buf.Len()), |
| 115 | Extension: "jpg", |
| 116 | }, nil |
| 117 | } |