forked from jlelse/GoBlog
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmediaCompression.go
More file actions
185 lines (172 loc) · 5.01 KB
/
Copy pathmediaCompression.go
File metadata and controls
185 lines (172 loc) · 5.01 KB
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
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 = 1280
const defaultCompressionHeight = 800
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
pr, pw := io.Pipe()
go func() {
_ = pw.CloseWithError(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,
},
}).
ToWriter(pw).
Fetch(context.Background()))
}()
// Upload compressed file
res, err := uploadCompressedFile(fileExtension, pr, upload)
_ = pr.CloseWithError(err)
return res, err
}
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
pr, pw := io.Pipe()
go func() {
_ = pw.CloseWithError(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).
ToWriter(pw).
Fetch(context.Background()))
}()
// Upload compressed file
res, err := uploadCompressedFile(fileExtension, pr, upload)
_ = pr.CloseWithError(err)
return res, err
}
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 and decode image
pr, pw := io.Pipe()
go func() {
_ = pw.CloseWithError(requests.URL(url).Client(hc).ToWriter(pw).Fetch(context.Background()))
}()
img, err := imaging.Decode(pr, imaging.AutoOrientation(true))
_ = pr.CloseWithError(err)
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
pr, pw = io.Pipe()
go func() {
switch fileExtension {
case "png":
_ = pw.CloseWithError(imaging.Encode(pw, resizedImage, imaging.PNG, imaging.PNGCompressionLevel(png.BestCompression)))
default:
_ = pw.CloseWithError(imaging.Encode(pw, resizedImage, imaging.JPEG, imaging.JPEGQuality(99)))
}
}()
// Upload compressed file
res, err := uploadCompressedFile(fileExtension, pr, upload)
_ = pr.CloseWithError(err)
return res, err
}
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)
_, err := io.Copy(io.MultiWriter(tempBuffer, hash), r)
if err != nil {
return "", err
}
// Upload buffer
return upload(fmt.Sprintf("%x.%s", hash.Sum(nil), fileExtension), tempBuffer)
}