2020-01-04 04:19:24 +00:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/base64"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
|
2021-06-02 02:53:19 +00:00
|
|
|
"github.com/diamondburned/arikawa/v3/utils/json"
|
2020-01-04 04:19:24 +00:00
|
|
|
"github.com/pkg/errors"
|
|
|
|
)
|
|
|
|
|
2020-05-16 21:14:49 +00:00
|
|
|
var ErrInvalidImageCT = errors.New("unknown image content-type")
|
|
|
|
var ErrInvalidImageData = errors.New("invalid image data")
|
2020-01-04 04:19:24 +00:00
|
|
|
|
2021-06-06 19:40:24 +00:00
|
|
|
type ImageTooLargeError struct {
|
2020-01-04 04:19:24 +00:00
|
|
|
Size, Max int
|
|
|
|
}
|
|
|
|
|
2021-06-06 19:40:24 +00:00
|
|
|
func (err ImageTooLargeError) Error() string {
|
2020-01-04 04:19:24 +00:00
|
|
|
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:
|
2020-11-03 17:16:42 +00:00
|
|
|
// https://discord.com/developers/docs/reference#image-data
|
2020-01-04 04:19:24 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-02-17 21:05:15 +00:00
|
|
|
// 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{}
|
|
|
|
|
2020-01-04 04:19:24 +00:00
|
|
|
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, errors.Wrap(ErrInvalidImageData, "invalid header")
|
|
|
|
}
|
|
|
|
|
|
|
|
if !bytes.HasPrefix(parts[1], []byte("base64,")) {
|
|
|
|
return nil, errors.Wrap(ErrInvalidImageData, "invalid base64")
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-05-07 19:32:56 +00:00
|
|
|
func (i Image) Validate(maxSize int) error {
|
|
|
|
if maxSize > 0 && len(i.Content) > maxSize {
|
2021-06-06 19:40:24 +00:00
|
|
|
return ImageTooLargeError{len(i.Content), maxSize}
|
2020-01-04 04:19:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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])
|
|
|
|
}
|
|
|
|
|
2020-05-07 19:32:56 +00:00
|
|
|
if err := i.Validate(0); err != nil {
|
2020-01-04 04:19:24 +00:00
|
|
|
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) {
|
2020-01-06 03:48:39 +00:00
|
|
|
if len(i.Content) == 0 {
|
|
|
|
return []byte("null"), nil
|
|
|
|
}
|
|
|
|
|
2020-01-04 04:19:24 +00:00
|
|
|
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, `"`)
|
|
|
|
|
2020-05-07 19:32:56 +00:00
|
|
|
// Accept a nil image.
|
2020-01-06 03:48:39 +00:00
|
|
|
if string(v) == "null" {
|
2020-05-07 19:32:56 +00:00
|
|
|
return nil
|
2020-01-06 03:48:39 +00:00
|
|
|
}
|
|
|
|
|
2020-01-04 04:19:24 +00:00
|
|
|
img, err := DecodeImage(v)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
*i = *img
|
|
|
|
return nil
|
|
|
|
}
|