forked from jlelse/GoBlog
-
Notifications
You must be signed in to change notification settings - Fork 0
/
mediaCompression.go
192 lines (179 loc) · 5.43 KB
/
mediaCompression.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
package main
import (
"context"
"crypto/sha256"
"errors"
"fmt"
"image/png"
"io"
"log"
"net/http"
"github.com/carlmjohnson/requests"
"github.com/disintegration/imaging"
"go.goblog.app/app/pkgs/bufferpool"
)
const defaultCompressionWidth = 2000
const defaultCompressionHeight = 3000
type mediaCompression interface {
compress(url string, save mediaStorageSaveFunc, hc *http.Client) (location string, err error)
}
func (a *goBlog) compressMediaFile(url string) (location string, err error) {
// Init compressors
a.compressorsInit.Do(a.initMediaCompressors)
// Try all compressors until success
for _, c := range a.compressors {
location, err = c.compress(url, a.saveMediaFile, a.httpClient)
if location != "" && err == nil {
break
}
}
// Return result
return location, err
}
func (a *goBlog) initMediaCompressors() {
if a.cfg.Micropub == nil || a.cfg.Micropub.MediaStorage == nil {
return
}
config := a.cfg.Micropub.MediaStorage
if key := config.TinifyKey; key != "" {
a.compressors = append(a.compressors, &tinify{key})
}
if config.CloudflareCompressionEnabled {
a.compressors = append(a.compressors, &cloudflare{})
}
if config.LocalCompressionEnabled {
a.compressors = append(a.compressors, &localMediaCompressor{})
}
}
type tinify struct {
key string
}
func (tf *tinify) compress(url string, upload mediaStorageSaveFunc, hc *http.Client) (string, error) {
tinifyErr := errors.New("failed to compress image using tinify")
// Check url
fileExtension, allowed := urlHasExt(url, "jpg", "jpeg", "png")
if !allowed {
return "", nil
}
// Compress
headers := http.Header{}
err := requests.
URL("https://api.tinify.com/shrink").
Client(hc).
Method(http.MethodPost).
BasicAuth("api", tf.key).
BodyJSON(map[string]any{
"source": map[string]any{
"url": url,
},
}).
ToHeaders(headers).
Fetch(context.Background())
if err != nil {
log.Println("Tinify error:", err.Error())
return "", tinifyErr
}
compressedLocation := headers.Get("Location")
if compressedLocation == "" {
log.Println("Tinify error: location header missing")
return "", tinifyErr
}
// Resize and download image
imgBuffer := bufferpool.Get()
defer bufferpool.Put(imgBuffer)
err = requests.
URL(compressedLocation).
Client(hc).
Method(http.MethodPost).
BasicAuth("api", tf.key).
BodyJSON(map[string]any{
"resize": map[string]any{
"method": "fit",
"width": defaultCompressionWidth,
"height": defaultCompressionHeight,
},
}).
ToBytesBuffer(imgBuffer).
Fetch(context.Background())
if err != nil {
log.Println("Tinify error:", err.Error())
return "", tinifyErr
}
// Upload compressed file
return uploadCompressedFile(fileExtension, imgBuffer, upload)
}
type cloudflare struct{}
func (*cloudflare) compress(url string, upload mediaStorageSaveFunc, hc *http.Client) (string, error) {
// Check url
if _, allowed := urlHasExt(url, "jpg", "jpeg", "png"); !allowed {
return "", nil
}
// Force jpeg
fileExtension := "jpeg"
// Compress
imgBuffer := bufferpool.Get()
defer bufferpool.Put(imgBuffer)
err := requests.
URL(fmt.Sprintf("https://www.cloudflare.com/cdn-cgi/image/f=jpeg,q=75,metadata=none,fit=scale-down,w=%d,h=%d/%s", defaultCompressionWidth, defaultCompressionHeight, url)).
Client(hc).
ToBytesBuffer(imgBuffer).
Fetch(context.Background())
if err != nil {
log.Println("Cloudflare error:", err.Error())
return "", errors.New("failed to compress image using cloudflare")
}
// Upload compressed file
return uploadCompressedFile(fileExtension, imgBuffer, upload)
}
type localMediaCompressor struct{}
func (*localMediaCompressor) compress(url string, upload mediaStorageSaveFunc, hc *http.Client) (string, error) {
// Check url
fileExtension, allowed := urlHasExt(url, "jpg", "jpeg", "png")
if !allowed {
return "", nil
}
// Download image
imgBuffer := bufferpool.Get()
defer bufferpool.Put(imgBuffer)
err := requests.
URL(url).
Client(hc).
ToBytesBuffer(imgBuffer).
Fetch(context.Background())
if err != nil {
log.Println("Local compressor error:", err.Error())
return "", errors.New("failed to download image using local compressor")
}
// Decode image
img, err := imaging.Decode(imgBuffer, imaging.AutoOrientation(true))
if err != nil {
log.Println("Local compressor error:", err.Error())
return "", errors.New("failed to compress image using local compressor")
}
// Resize image
resizedImage := imaging.Fit(img, defaultCompressionWidth, defaultCompressionHeight, imaging.Lanczos)
// Encode image
resizedBuffer := bufferpool.Get()
defer bufferpool.Put(resizedBuffer)
switch fileExtension {
case "jpg", "jpeg":
err = imaging.Encode(resizedBuffer, resizedImage, imaging.JPEG, imaging.JPEGQuality(75))
case "png":
err = imaging.Encode(resizedBuffer, resizedImage, imaging.PNG, imaging.PNGCompressionLevel(png.BestCompression))
}
if err != nil {
log.Println("Local compressor error:", err.Error())
return "", errors.New("failed to compress image using local compressor")
}
// Upload compressed file
return uploadCompressedFile(fileExtension, resizedBuffer, upload)
}
func uploadCompressedFile(fileExtension string, r io.Reader, upload mediaStorageSaveFunc) (string, error) {
// Copy file to temporary buffer to generate hash and filename
hash := sha256.New()
tempBuffer := bufferpool.Get()
defer bufferpool.Put(tempBuffer)
_, _ = io.Copy(io.MultiWriter(tempBuffer, hash), r)
// Upload buffer
return upload(fmt.Sprintf("%x.%s", hash.Sum(nil), fileExtension), tempBuffer)
}