-
-
Notifications
You must be signed in to change notification settings - Fork 57
/
image.go
132 lines (106 loc) · 2.89 KB
/
image.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 api
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"net/http"
"github.com/diamondburned/arikawa/v3/utils/json"
)
var ErrInvalidImageCT = errors.New("unknown image content-type")
var ErrInvalidImageData = errors.New("invalid image data")
type ImageTooLargeError struct {
Size, Max int
}
func (err ImageTooLargeError) Error() string {
return fmt.Sprintf("Image is %.02fkb, larger than %.02fkb",
float64(err.Size)/1000, float64(err.Max)/1000)
}
// Image wraps around the Data URI Scheme that Discord uses:
// https://discord.com/developers/docs/reference#image-data
type Image struct {
// ContentType is optional and will be automatically detected. However, it
// should always return "image/jpeg," "image/png" or "image/gif".
ContentType string
// Just raw content of the file.
Content []byte
}
// NullImage is an *Image value that marshals to a null value. Use this to unset
// the image. It exists mostly for documentation purposes.
var NullImage = &Image{}
func DecodeImage(data []byte) (*Image, error) {
parts := bytes.SplitN(data, []byte{';'}, 2)
if len(parts) < 2 {
return nil, ErrInvalidImageData
}
if !bytes.HasPrefix(parts[0], []byte("data:")) {
return nil, fmt.Errorf("invalid header: %w", ErrInvalidImageData)
}
if !bytes.HasPrefix(parts[1], []byte("base64,")) {
return nil, fmt.Errorf("invalid base64: %w", ErrInvalidImageData)
}
var b64 = parts[1][len("base64,"):]
var img = Image{
ContentType: string(parts[0][len("data:"):]),
Content: make([]byte, base64.StdEncoding.DecodedLen(len(b64))),
}
base64.StdEncoding.Decode(img.Content, b64)
return &img, nil
}
func (i Image) Validate(maxSize int) error {
if maxSize > 0 && len(i.Content) > maxSize {
return ImageTooLargeError{len(i.Content), maxSize}
}
switch i.ContentType {
case "image/png", "image/jpeg", "image/gif":
return nil
default:
return ErrInvalidImageCT
}
}
func (i Image) Encode() ([]byte, error) {
if i.ContentType == "" {
var max = 512
if len(i.Content) < max {
max = len(i.Content)
}
i.ContentType = http.DetectContentType(i.Content[:max])
}
if err := i.Validate(0); err != nil {
return nil, err
}
b64enc := make([]byte, base64.StdEncoding.EncodedLen(len(i.Content)))
base64.StdEncoding.Encode(b64enc, i.Content)
return bytes.Join([][]byte{
[]byte("data:"),
[]byte(i.ContentType),
[]byte(";base64,"),
b64enc,
}, nil), nil
}
var _ json.Marshaler = (*Image)(nil)
var _ json.Unmarshaler = (*Image)(nil)
func (i Image) MarshalJSON() ([]byte, error) {
if len(i.Content) == 0 {
return []byte("null"), nil
}
b, err := i.Encode()
if err != nil {
return nil, err
}
return bytes.Join([][]byte{{'"'}, b, {'"'}}, nil), nil
}
func (i *Image) UnmarshalJSON(v []byte) error {
// Trim string
v = bytes.Trim(v, `"`)
// Accept a nil image.
if string(v) == "null" {
return nil
}
img, err := DecodeImage(v)
if err != nil {
return err
}
*i = *img
return nil
}