| 
									
										
										
										
											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" | 
					
						
							| 
									
										
										
										
											2022-08-10 14:05:14 +02:00
										 |  |  | 	"github.com/disintegration/imaging" | 
					
						
							| 
									
										
										
										
											2022-12-06 14:15:25 +01:00
										 |  |  | 	_ "golang.org/x/image/webp" // blank import to support WebP decoding | 
					
						
							| 
									
										
										
										
											2021-05-17 19:06:58 +02:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | const ( | 
					
						
							|  |  |  | 	thumbnailMaxWidth  = 512 | 
					
						
							|  |  |  | 	thumbnailMaxHeight = 512 | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-17 05:38:56 +01:00
										 |  |  | func decodeGif(r io.Reader) (*mediaMeta, error) { | 
					
						
							| 
									
										
										
										
											2022-01-16 18:52:55 +01:00
										 |  |  | 	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 | 
					
						
							| 
									
										
										
										
											2022-12-22 11:48:28 +01:00
										 |  |  | 	aspect := float32(width) / float32(height) | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-17 05:38:56 +01:00
										 |  |  | 	return &mediaMeta{ | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 		width:  width, | 
					
						
							|  |  |  | 		height: height, | 
					
						
							|  |  |  | 		size:   size, | 
					
						
							|  |  |  | 		aspect: aspect, | 
					
						
							|  |  |  | 	}, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-17 05:38:56 +01:00
										 |  |  | func decodeImage(r io.Reader, contentType string) (*mediaMeta, error) { | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	var i image.Image | 
					
						
							|  |  |  | 	var err error | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	switch contentType { | 
					
						
							| 
									
										
										
										
											2022-12-06 14:15:25 +01:00
										 |  |  | 	case mimeImageJpeg, mimeImageWebp: | 
					
						
							| 
									
										
										
										
											2022-08-10 15:54:15 +02:00
										 |  |  | 		i, err = imaging.Decode(r, imaging.AutoOrientation(true)) | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	case mimeImagePng: | 
					
						
							| 
									
										
										
										
											2022-08-10 15:54:15 +02:00
										 |  |  | 		strippedPngReader := io.Reader(&PNGAncillaryChunkStripper{ | 
					
						
							|  |  |  | 			Reader: r, | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 		i, err = imaging.Decode(strippedPngReader, imaging.AutoOrientation(true)) | 
					
						
							| 
									
										
										
										
											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 | 
					
						
							| 
									
										
										
										
											2022-12-22 11:48:28 +01:00
										 |  |  | 	aspect := float32(width) / float32(height) | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-17 05:38:56 +01:00
										 |  |  | 	return &mediaMeta{ | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 		width:  width, | 
					
						
							|  |  |  | 		height: height, | 
					
						
							|  |  |  | 		size:   size, | 
					
						
							|  |  |  | 		aspect: aspect, | 
					
						
							|  |  |  | 	}, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-17 05:38:56 +01:00
										 |  |  | // deriveStaticEmojji takes a given gif or png of an emoji, decodes it, and re-encodes it as a static png. | 
					
						
							|  |  |  | func deriveStaticEmoji(r io.Reader, contentType string) (*mediaMeta, error) { | 
					
						
							|  |  |  | 	var i image.Image | 
					
						
							|  |  |  | 	var err error | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	switch contentType { | 
					
						
							|  |  |  | 	case mimeImagePng: | 
					
						
							|  |  |  | 		i, err = StrippedPngDecode(r) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	case mimeImageGif: | 
					
						
							|  |  |  | 		i, err = gif.Decode(r) | 
					
						
							|  |  |  | 		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 &mediaMeta{ | 
					
						
							|  |  |  | 		small: out.Bytes(), | 
					
						
							|  |  |  | 	}, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // deriveThumbnailFromImage returns a byte slice and metadata for a thumbnail | 
					
						
							|  |  |  | // of a given piece of media, or an error if something goes wrong. | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | // | 
					
						
							| 
									
										
										
										
											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-12-17 05:38:56 +01:00
										 |  |  | func deriveThumbnailFromImage(r io.Reader, contentType string, createBlurhash bool) (*mediaMeta, error) { | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	var i image.Image | 
					
						
							|  |  |  | 	var err error | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	switch contentType { | 
					
						
							| 
									
										
										
										
											2022-12-06 14:15:25 +01:00
										 |  |  | 	case mimeImageJpeg, mimeImageGif, mimeImageWebp: | 
					
						
							| 
									
										
										
										
											2022-08-10 14:05:14 +02:00
										 |  |  | 		i, err = imaging.Decode(r, imaging.AutoOrientation(true)) | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	case mimeImagePng: | 
					
						
							| 
									
										
										
										
											2022-08-10 14:05:14 +02:00
										 |  |  | 		strippedPngReader := io.Reader(&PNGAncillaryChunkStripper{ | 
					
						
							|  |  |  | 			Reader: r, | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 		i, err = imaging.Decode(strippedPngReader, imaging.AutoOrientation(true)) | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	default: | 
					
						
							| 
									
										
										
										
											2022-12-17 05:38:56 +01:00
										 |  |  | 		err = fmt.Errorf("content type %s can't be thumbnailed as an image", contentType) | 
					
						
							| 
									
										
										
										
											2022-01-10 18:36:09 +01:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							| 
									
										
										
										
											2022-08-10 14:05:14 +02:00
										 |  |  | 		return nil, fmt.Errorf("error decoding %s: %s", contentType, err) | 
					
						
							| 
									
										
										
										
											2022-01-10 18:36:09 +01:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-10 14:05:14 +02:00
										 |  |  | 	originalX := i.Bounds().Size().X | 
					
						
							|  |  |  | 	originalY := i.Bounds().Size().Y | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	var thumb image.Image | 
					
						
							|  |  |  | 	if originalX <= thumbnailMaxWidth && originalY <= thumbnailMaxHeight { | 
					
						
							|  |  |  | 		// it's already small, no need to resize | 
					
						
							|  |  |  | 		thumb = i | 
					
						
							|  |  |  | 	} else { | 
					
						
							|  |  |  | 		thumb = imaging.Fit(i, thumbnailMaxWidth, thumbnailMaxHeight, imaging.Linear) | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-10 14:05:14 +02:00
										 |  |  | 	thumbX := thumb.Bounds().Size().X | 
					
						
							|  |  |  | 	thumbY := thumb.Bounds().Size().Y | 
					
						
							|  |  |  | 	size := thumbX * thumbY | 
					
						
							| 
									
										
										
										
											2022-12-22 11:48:28 +01:00
										 |  |  | 	aspect := float32(thumbX) / float32(thumbY) | 
					
						
							| 
									
										
										
										
											2021-12-28 16:36:00 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-17 05:38:56 +01:00
										 |  |  | 	im := &mediaMeta{ | 
					
						
							| 
									
										
										
										
											2022-08-10 14:05:14 +02:00
										 |  |  | 		width:  thumbX, | 
					
						
							|  |  |  | 		height: thumbY, | 
					
						
							| 
									
										
										
										
											2022-01-09 18:41:22 +01:00
										 |  |  | 		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-08-10 14:05:14 +02:00
										 |  |  | 		tiny := imaging.Resize(thumb, 32, 0, imaging.NearestNeighbor) | 
					
						
							| 
									
										
										
										
											2022-01-09 18:41:22 +01:00
										 |  |  | 		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
										 |  |  | } |