6cd033449f
Remote media is now dereferenced and attached properly to incoming federated statuses. Mentions are now dereferenced and attached properly to incoming federated statuses. Small fixes to status visibility. Allow URL params for filtering statuses: // ExcludeRepliesKey is for specifying whether to exclude replies in a list of returned statuses by an account. // PinnedKey is for specifying whether to include pinned statuses in a list of returned statuses by an account. // MaxIDKey is for specifying the maximum ID of the status to retrieve. // MediaOnlyKey is for specifying that only statuses with media should be returned in a list of returned statuses by an account. Add endpoint for fetching an account's statuses.
340 lines
8.0 KiB
Go
340 lines
8.0 KiB
Go
/*
|
|
GoToSocial
|
|
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package media
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"image/gif"
|
|
"image/jpeg"
|
|
"image/png"
|
|
|
|
"github.com/buckket/go-blurhash"
|
|
"github.com/h2non/filetype"
|
|
"github.com/nfnt/resize"
|
|
"github.com/superseriousbusiness/exifremove/pkg/exifremove"
|
|
)
|
|
|
|
const (
|
|
// MIMEImage is the mime type for image
|
|
MIMEImage = "image"
|
|
// MIMEJpeg is the jpeg image mime type
|
|
MIMEJpeg = "image/jpeg"
|
|
// MIMEGif is the gif image mime type
|
|
MIMEGif = "image/gif"
|
|
// MIMEPng is the png image mime type
|
|
MIMEPng = "image/png"
|
|
|
|
// MIMEVideo is the mime type for video
|
|
MIMEVideo = "video"
|
|
// MIMEMp4 is the mp4 video mime type
|
|
MIMEMp4 = "video/mp4"
|
|
// MIMEMpeg is the mpeg video mime type
|
|
MIMEMpeg = "video/mpeg"
|
|
// MIMEWebm is the webm video mime type
|
|
MIMEWebm = "video/webm"
|
|
)
|
|
|
|
// parseContentType parses the MIME content type from a file, returning it as a string in the form (eg., "image/jpeg").
|
|
// Returns an error if the content type is not something we can process.
|
|
func parseContentType(content []byte) (string, error) {
|
|
head := make([]byte, 261)
|
|
_, err := bytes.NewReader(content).Read(head)
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not read first magic bytes of file: %s", err)
|
|
}
|
|
|
|
kind, err := filetype.Match(head)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if kind == filetype.Unknown {
|
|
return "", errors.New("filetype unknown")
|
|
}
|
|
|
|
return kind.MIME.Value, nil
|
|
}
|
|
|
|
// SupportedImageType checks mime type of an image against a slice of accepted types,
|
|
// and returns True if the mime type is accepted.
|
|
func SupportedImageType(mimeType string) bool {
|
|
acceptedImageTypes := []string{
|
|
MIMEJpeg,
|
|
MIMEGif,
|
|
MIMEPng,
|
|
}
|
|
for _, accepted := range acceptedImageTypes {
|
|
if mimeType == accepted {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// SupportedVideoType checks mime type of a video against a slice of accepted types,
|
|
// and returns True if the mime type is accepted.
|
|
func SupportedVideoType(mimeType string) bool {
|
|
acceptedVideoTypes := []string{
|
|
MIMEMp4,
|
|
MIMEMpeg,
|
|
MIMEWebm,
|
|
}
|
|
for _, accepted := range acceptedVideoTypes {
|
|
if mimeType == accepted {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// supportedEmojiType checks that the content type is image/png -- the only type supported for emoji.
|
|
func supportedEmojiType(mimeType string) bool {
|
|
acceptedEmojiTypes := []string{
|
|
MIMEGif,
|
|
MIMEPng,
|
|
}
|
|
for _, accepted := range acceptedEmojiTypes {
|
|
if mimeType == accepted {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// purgeExif is a little wrapper for the action of removing exif data from an image.
|
|
// Only pass pngs or jpegs to this function.
|
|
func purgeExif(b []byte) ([]byte, error) {
|
|
if len(b) == 0 {
|
|
return nil, errors.New("passed image was not valid")
|
|
}
|
|
|
|
clean, err := exifremove.Remove(b)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not purge exif from image: %s", err)
|
|
}
|
|
if len(clean) == 0 {
|
|
return nil, errors.New("purged image was not valid")
|
|
}
|
|
return clean, nil
|
|
}
|
|
|
|
func deriveGif(b []byte, extension string) (*imageAndMeta, error) {
|
|
var g *gif.GIF
|
|
var err error
|
|
switch extension {
|
|
case MIMEGif:
|
|
g, err = gif.DecodeAll(bytes.NewReader(b))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("extension %s not recognised", extension)
|
|
}
|
|
|
|
// use the first frame to get the static characteristics
|
|
width := g.Config.Width
|
|
height := g.Config.Height
|
|
size := width * height
|
|
aspect := float64(width) / float64(height)
|
|
|
|
bh, err := blurhash.Encode(4, 3, g.Image[0])
|
|
if err != nil || bh == "" {
|
|
return nil, err
|
|
}
|
|
|
|
out := &bytes.Buffer{}
|
|
if err := gif.EncodeAll(out, g); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &imageAndMeta{
|
|
image: out.Bytes(),
|
|
width: width,
|
|
height: height,
|
|
size: size,
|
|
aspect: aspect,
|
|
blurhash: bh,
|
|
}, nil
|
|
}
|
|
|
|
func deriveImage(b []byte, contentType string) (*imageAndMeta, error) {
|
|
var i image.Image
|
|
var err error
|
|
|
|
switch contentType {
|
|
case MIMEJpeg:
|
|
i, err = jpeg.Decode(bytes.NewReader(b))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
case MIMEPng:
|
|
i, err = png.Decode(bytes.NewReader(b))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("content type %s not recognised", contentType)
|
|
}
|
|
|
|
width := i.Bounds().Size().X
|
|
height := i.Bounds().Size().Y
|
|
size := width * height
|
|
aspect := float64(width) / float64(height)
|
|
|
|
bh, err := blurhash.Encode(4, 3, i)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out := &bytes.Buffer{}
|
|
if err := jpeg.Encode(out, i, &jpeg.Options{
|
|
Quality: 100,
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &imageAndMeta{
|
|
image: out.Bytes(),
|
|
width: width,
|
|
height: height,
|
|
size: size,
|
|
aspect: aspect,
|
|
blurhash: bh,
|
|
}, nil
|
|
}
|
|
|
|
// deriveThumbnail returns a byte slice and metadata for a thumbnail of width x and height y,
|
|
// of a given jpeg, png, or gif, or an error if something goes wrong.
|
|
//
|
|
// Note that the aspect ratio of the image will be retained,
|
|
// so it will not necessarily be a square, even if x and y are set as the same value.
|
|
func deriveThumbnail(b []byte, contentType string, x uint, y uint) (*imageAndMeta, error) {
|
|
var i image.Image
|
|
var err error
|
|
|
|
switch contentType {
|
|
case MIMEJpeg:
|
|
i, err = jpeg.Decode(bytes.NewReader(b))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
case MIMEPng:
|
|
i, err = png.Decode(bytes.NewReader(b))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
case MIMEGif:
|
|
i, err = gif.Decode(bytes.NewReader(b))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("content type %s not recognised", contentType)
|
|
}
|
|
|
|
thumb := resize.Thumbnail(x, y, i, resize.NearestNeighbor)
|
|
width := thumb.Bounds().Size().X
|
|
height := thumb.Bounds().Size().Y
|
|
size := width * height
|
|
aspect := float64(width) / float64(height)
|
|
|
|
out := &bytes.Buffer{}
|
|
if err := jpeg.Encode(out, thumb, &jpeg.Options{
|
|
Quality: 100,
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
return &imageAndMeta{
|
|
image: out.Bytes(),
|
|
width: width,
|
|
height: height,
|
|
size: size,
|
|
aspect: aspect,
|
|
}, nil
|
|
}
|
|
|
|
// deriveStaticEmojji takes a given gif or png of an emoji, decodes it, and re-encodes it as a static png.
|
|
func deriveStaticEmoji(b []byte, contentType string) (*imageAndMeta, error) {
|
|
var i image.Image
|
|
var err error
|
|
|
|
switch contentType {
|
|
case MIMEPng:
|
|
i, err = png.Decode(bytes.NewReader(b))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
case MIMEGif:
|
|
i, err = gif.Decode(bytes.NewReader(b))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("content type %s not allowed for emoji", contentType)
|
|
}
|
|
|
|
out := &bytes.Buffer{}
|
|
if err := png.Encode(out, i); err != nil {
|
|
return nil, err
|
|
}
|
|
return &imageAndMeta{
|
|
image: out.Bytes(),
|
|
}, nil
|
|
}
|
|
|
|
type imageAndMeta struct {
|
|
image []byte
|
|
width int
|
|
height int
|
|
size int
|
|
aspect float64
|
|
blurhash string
|
|
}
|
|
|
|
// ParseMediaType converts s to a recognized MediaType, or returns an error if unrecognized
|
|
func ParseMediaType(s string) (Type, error) {
|
|
switch Type(s) {
|
|
case Attachment:
|
|
return Attachment, nil
|
|
case Header:
|
|
return Header, nil
|
|
case Avatar:
|
|
return Avatar, nil
|
|
case Emoji:
|
|
return Emoji, nil
|
|
}
|
|
return "", fmt.Errorf("%s not a recognized MediaType", s)
|
|
}
|
|
|
|
// ParseMediaSize converts s to a recognized MediaSize, or returns an error if unrecognized
|
|
func ParseMediaSize(s string) (Size, error) {
|
|
switch Size(s) {
|
|
case Small:
|
|
return Small, nil
|
|
case Original:
|
|
return Original, nil
|
|
case Static:
|
|
return Static, nil
|
|
}
|
|
return "", fmt.Errorf("%s not a recognized MediaSize", s)
|
|
}
|