/
thumbnail.go
132 lines (111 loc) · 3.65 KB
/
thumbnail.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
package thumbnail
import (
"bytes"
"fmt"
"image"
"image/gif"
"image/jpeg"
"image/png"
"io/ioutil"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/Chatterino/api/pkg/cache"
"github.com/Chatterino/api/pkg/config"
"github.com/Chatterino/api/pkg/resolver"
"github.com/Chatterino/api/pkg/utils"
"github.com/nfnt/resize"
)
var (
supportedThumbnails = []string{"image/jpeg", "image/png", "image/gif", "image/webp"}
cfg config.APIConfig
)
func InitializeConfig(passedCfg config.APIConfig) {
cfg = passedCfg
}
// buildStaticThumbnailByteArray is used when we fail to build an animated thumbnail using lilliput
func buildStaticThumbnailByteArray(inputBuf []byte, resp *http.Response) ([]byte, error) {
image, _, err := image.Decode(bytes.NewReader(inputBuf))
if err != nil {
return []byte{}, fmt.Errorf("could not decode image from url: %s", resp.Request.URL)
}
resized := resize.Thumbnail(cfg.MaxThumbnailSize, cfg.MaxThumbnailSize, image, resize.Bilinear)
buffer := new(bytes.Buffer)
if resp.Header.Get("content-type") == "image/png" {
err = png.Encode(buffer, resized)
} else if resp.Header.Get("content-type") == "image/gif" {
err = gif.Encode(buffer, resized, nil)
} else if resp.Header.Get("content-type") == "image/jpeg" {
err = jpeg.Encode(buffer, resized, nil)
}
if err != nil {
return []byte{}, fmt.Errorf("could not encode image from url: %s", resp.Request.URL)
}
return buffer.Bytes(), nil
}
func DoThumbnailRequest(urlString string, r *http.Request) (interface{}, time.Duration, error) {
url, err := url.Parse(urlString)
if err != nil {
return resolver.InvalidURL, cache.NoSpecialDur, nil
}
resp, err := resolver.RequestGET(url.String())
if err != nil {
if strings.HasSuffix(err.Error(), "no such host") {
return resolver.NoLinkInfoFound, cache.NoSpecialDur, nil
}
return utils.MarshalNoDur(&resolver.Response{
Status: http.StatusInternalServerError,
Message: resolver.CleanResponse(err.Error()),
})
}
defer resp.Body.Close()
if contentLength := resp.Header.Get("Content-Length"); contentLength != "" {
contentLengthBytes, err := strconv.Atoi(contentLength)
if err != nil {
return nil, cache.NoSpecialDur, err
}
if uint64(contentLengthBytes) > cfg.MaxContentLength {
return resolver.ResponseTooLarge, cache.NoSpecialDur, nil
}
}
if resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusMultipleChoices {
fmt.Println("Skipping url", resp.Request.URL, "because status code is", resp.StatusCode)
return resolver.NoLinkInfoFound, cache.NoSpecialDur, nil
}
if !IsSupportedThumbnail(resp.Header.Get("content-type")) {
return resolver.NoLinkInfoFound, cache.NoSpecialDur, nil
}
inputBuf, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println("Error reading body from request:", err)
return resolver.NoLinkInfoFound, cache.NoSpecialDur, nil
}
var image []byte
// attempt building an animated image
if cfg.EnableLilliput {
image, err = buildThumbnailByteArray(inputBuf, resp)
}
// fallback to static image if animated image building failed or is disabled
if !cfg.EnableLilliput || err != nil {
if err != nil {
log.Println("Error trying to build animated thumbnail:", err.Error(), "falling back to static thumbnail building")
}
image, err = buildStaticThumbnailByteArray(inputBuf, resp)
if err != nil {
log.Println("Error trying to build static thumbnail:", err.Error())
return resolver.NoLinkInfoFound, cache.NoSpecialDur, nil
}
}
return image, 10 * time.Minute, nil
}
func IsSupportedThumbnail(contentType string) bool {
for _, supportedType := range supportedThumbnails {
if contentType == supportedType {
return true
}
}
return false
}