最後活躍 1727271116

修訂 1ad06d5a67fb22662abc724fd1605277551d12b7

image_optimizer.go 原始檔案
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.
3package optimizer
4
5import (
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
18const (
19 MaxWidth = 800
20)
21
22type OptimizerResult struct {
23 Body io.ReadCloser
24 ContentType string
25 ContentLength int64
26 Extension string
27}
28
29type Optimizer interface {
30 Optimize(ctx context.Context, body io.Reader, contentType string) (*OptimizerResult, error)
31}
32
33var _ Optimizer = &InProcessOptimizer{}
34
35type InProcessOptimizer struct {
36 sem chan struct{}
37}
38
39func 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.
47func (i *InProcessOptimizer) UnlimitedConcurrency() {
48 i.sem = nil
49}
50
51func (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}