| 
									
										
										
										
											2021-05-17 19:06:58 +02:00
										 |  |  | /* | 
					
						
							|  |  |  |    GoToSocial | 
					
						
							| 
									
										
										
										
											2021-12-20 18:42:19 +01:00
										 |  |  |    Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org | 
					
						
							| 
									
										
										
										
											2021-05-17 19:06:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |    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 ( | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	"bytes" | 
					
						
							| 
									
										
										
										
											2021-05-17 19:06:58 +02:00
										 |  |  | 	"errors" | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	"image" | 
					
						
							|  |  |  | 	"image/gif" | 
					
						
							|  |  |  | 	"image/jpeg" | 
					
						
							|  |  |  | 	"image/png" | 
					
						
							| 
									
										
										
										
											2022-01-16 18:52:55 +01:00
										 |  |  | 	"io" | 
					
						
							| 
									
										
										
										
											2021-05-17 19:06:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	"github.com/buckket/go-blurhash" | 
					
						
							|  |  |  | 	"github.com/nfnt/resize" | 
					
						
							| 
									
										
										
										
											2021-05-17 19:06:58 +02:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | const ( | 
					
						
							|  |  |  | 	thumbnailMaxWidth  = 512 | 
					
						
							|  |  |  | 	thumbnailMaxHeight = 512 | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-16 18:52:55 +01:00
										 |  |  | type imageMeta struct { | 
					
						
							| 
									
										
										
										
											2022-01-10 18:36:09 +01:00
										 |  |  | 	width    int | 
					
						
							|  |  |  | 	height   int | 
					
						
							|  |  |  | 	size     int | 
					
						
							|  |  |  | 	aspect   float64 | 
					
						
							| 
									
										
										
										
											2022-01-16 18:52:55 +01:00
										 |  |  | 	blurhash string // defined only for calls to deriveThumbnail if createBlurhash is true | 
					
						
							|  |  |  | 	small    []byte // defined only for calls to deriveStaticEmoji or deriveThumbnail | 
					
						
							| 
									
										
										
										
											2022-01-10 18:36:09 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-16 18:52:55 +01:00
										 |  |  | func decodeGif(r io.Reader) (*imageMeta, error) { | 
					
						
							|  |  |  | 	gif, err := gif.DecodeAll(r) | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// use the first frame to get the static characteristics | 
					
						
							| 
									
										
										
										
											2022-01-02 15:00:53 +01:00
										 |  |  | 	width := gif.Config.Width | 
					
						
							|  |  |  | 	height := gif.Config.Height | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	size := width * height | 
					
						
							|  |  |  | 	aspect := float64(width) / float64(height) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-16 18:52:55 +01:00
										 |  |  | 	return &imageMeta{ | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 		width:  width, | 
					
						
							|  |  |  | 		height: height, | 
					
						
							|  |  |  | 		size:   size, | 
					
						
							|  |  |  | 		aspect: aspect, | 
					
						
							|  |  |  | 	}, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-16 18:52:55 +01:00
										 |  |  | func decodeImage(r io.Reader, contentType string) (*imageMeta, error) { | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	var i image.Image | 
					
						
							|  |  |  | 	var err error | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	switch contentType { | 
					
						
							|  |  |  | 	case mimeImageJpeg: | 
					
						
							| 
									
										
										
										
											2022-01-16 18:52:55 +01:00
										 |  |  | 		i, err = jpeg.Decode(r) | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	case mimeImagePng: | 
					
						
							| 
									
										
										
										
											2022-04-25 14:45:44 +02:00
										 |  |  | 		i, err = StrippedPngDecode(r) | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	default: | 
					
						
							|  |  |  | 		err = fmt.Errorf("content type %s not recognised", contentType) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if i == nil { | 
					
						
							|  |  |  | 		return nil, errors.New("processed image was nil") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	width := i.Bounds().Size().X | 
					
						
							|  |  |  | 	height := i.Bounds().Size().Y | 
					
						
							|  |  |  | 	size := width * height | 
					
						
							|  |  |  | 	aspect := float64(width) / float64(height) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-16 18:52:55 +01:00
										 |  |  | 	return &imageMeta{ | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 		width:  width, | 
					
						
							|  |  |  | 		height: height, | 
					
						
							|  |  |  | 		size:   size, | 
					
						
							|  |  |  | 		aspect: aspect, | 
					
						
							|  |  |  | 	}, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-03 17:37:38 +01:00
										 |  |  | // deriveThumbnail returns a byte slice and metadata for a thumbnail | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | // of a given jpeg, png, or gif, or an error if something goes wrong. | 
					
						
							|  |  |  | // | 
					
						
							| 
									
										
										
										
											2022-01-10 18:36:09 +01:00
										 |  |  | // If createBlurhash is true, then a blurhash will also be generated from a tiny | 
					
						
							|  |  |  | // version of the image. This costs precious CPU cycles, so only use it if you | 
					
						
							|  |  |  | // really need a blurhash and don't have one already. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // If createBlurhash is false, then the blurhash field on the returned ImageAndMeta | 
					
						
							|  |  |  | // will be an empty string. | 
					
						
							| 
									
										
										
										
											2022-01-16 18:52:55 +01:00
										 |  |  | func deriveThumbnail(r io.Reader, contentType string, createBlurhash bool) (*imageMeta, error) { | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	var i image.Image | 
					
						
							|  |  |  | 	var err error | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	switch contentType { | 
					
						
							|  |  |  | 	case mimeImageJpeg: | 
					
						
							| 
									
										
										
										
											2022-01-16 18:52:55 +01:00
										 |  |  | 		i, err = jpeg.Decode(r) | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	case mimeImagePng: | 
					
						
							| 
									
										
										
										
											2022-04-25 14:45:44 +02:00
										 |  |  | 		i, err = StrippedPngDecode(r) | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	case mimeImageGif: | 
					
						
							| 
									
										
										
										
											2022-01-16 18:52:55 +01:00
										 |  |  | 		i, err = gif.Decode(r) | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	default: | 
					
						
							| 
									
										
										
										
											2022-01-10 18:36:09 +01:00
										 |  |  | 		err = fmt.Errorf("content type %s can't be thumbnailed", contentType) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							| 
									
										
										
										
											2022-03-21 19:46:51 +01:00
										 |  |  | 		return nil, fmt.Errorf("error decoding image as %s: %s", contentType, err) | 
					
						
							| 
									
										
										
										
											2022-01-10 18:36:09 +01:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if i == nil { | 
					
						
							|  |  |  | 		return nil, errors.New("processed image was nil") | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-03 17:37:38 +01:00
										 |  |  | 	thumb := resize.Thumbnail(thumbnailMaxWidth, thumbnailMaxHeight, i, resize.NearestNeighbor) | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	width := thumb.Bounds().Size().X | 
					
						
							|  |  |  | 	height := thumb.Bounds().Size().Y | 
					
						
							|  |  |  | 	size := width * height | 
					
						
							|  |  |  | 	aspect := float64(width) / float64(height) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-16 18:52:55 +01:00
										 |  |  | 	im := &imageMeta{ | 
					
						
							| 
									
										
										
										
											2022-01-09 18:41:22 +01:00
										 |  |  | 		width:  width, | 
					
						
							|  |  |  | 		height: height, | 
					
						
							|  |  |  | 		size:   size, | 
					
						
							|  |  |  | 		aspect: aspect, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if createBlurhash { | 
					
						
							| 
									
										
										
										
											2022-01-10 18:36:09 +01:00
										 |  |  | 		// for generating blurhashes, it's more cost effective to lose detail rather than | 
					
						
							|  |  |  | 		// pass a big image into the blurhash algorithm, so make a teeny tiny version | 
					
						
							| 
									
										
										
										
											2022-01-09 18:41:22 +01:00
										 |  |  | 		tiny := resize.Thumbnail(32, 32, thumb, resize.NearestNeighbor) | 
					
						
							|  |  |  | 		bh, err := blurhash.Encode(4, 3, tiny) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							| 
									
										
										
										
											2022-03-21 19:46:51 +01:00
										 |  |  | 			return nil, fmt.Errorf("error creating blurhash: %s", err) | 
					
						
							| 
									
										
										
										
											2022-01-09 18:41:22 +01:00
										 |  |  | 		} | 
					
						
							|  |  |  | 		im.blurhash = bh | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	out := &bytes.Buffer{} | 
					
						
							|  |  |  | 	if err := jpeg.Encode(out, thumb, &jpeg.Options{ | 
					
						
							| 
									
										
										
										
											2022-01-10 18:36:09 +01:00
										 |  |  | 		// Quality isn't extremely important for thumbnails, so 75 is "good enough" | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 		Quality: 75, | 
					
						
							|  |  |  | 	}); err != nil { | 
					
						
							| 
									
										
										
										
											2022-03-21 19:46:51 +01:00
										 |  |  | 		return nil, fmt.Errorf("error encoding thumbnail: %s", err) | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2022-01-16 18:52:55 +01:00
										 |  |  | 	im.small = out.Bytes() | 
					
						
							| 
									
										
										
										
											2022-01-09 18:41:22 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	return im, nil | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // deriveStaticEmojji takes a given gif or png of an emoji, decodes it, and re-encodes it as a static png. | 
					
						
							| 
									
										
										
										
											2022-01-16 18:52:55 +01:00
										 |  |  | func deriveStaticEmoji(r io.Reader, contentType string) (*imageMeta, error) { | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	var i image.Image | 
					
						
							|  |  |  | 	var err error | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	switch contentType { | 
					
						
							|  |  |  | 	case mimeImagePng: | 
					
						
							| 
									
										
										
										
											2022-04-25 14:45:44 +02:00
										 |  |  | 		i, err = StrippedPngDecode(r) | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	case mimeImageGif: | 
					
						
							| 
									
										
										
										
											2022-01-16 18:52:55 +01:00
										 |  |  | 		i, err = gif.Decode(r) | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 		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 | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2022-01-16 18:52:55 +01:00
										 |  |  | 	return &imageMeta{ | 
					
						
							|  |  |  | 		small: out.Bytes(), | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	}, nil | 
					
						
							|  |  |  | } |