| 
									
										
										
										
											2023-11-10 19:29:26 +01:00
										 |  |  | // GoToSocial | 
					
						
							|  |  |  | // Copyright (C) GoToSocial Authors admin@gotosocial.org | 
					
						
							|  |  |  | // SPDX-License-Identifier: AGPL-3.0-or-later | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // 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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | import ( | 
					
						
							|  |  |  | 	"cmp" | 
					
						
							|  |  |  | 	"errors" | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							|  |  |  | 	"image" | 
					
						
							|  |  |  | 	"io" | 
					
						
							|  |  |  | 	"os" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | 	"golang.org/x/image/webp" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 	"codeberg.org/gruf/go-bytesize" | 
					
						
							|  |  |  | 	"codeberg.org/gruf/go-iotools" | 
					
						
							|  |  |  | 	"codeberg.org/gruf/go-mimetypes" | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 	"github.com/buckket/go-blurhash" | 
					
						
							|  |  |  | 	"github.com/disintegration/imaging" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // thumbSize returns the dimensions to use for an input | 
					
						
							|  |  |  | // image of given width / height, for its outgoing thumbnail. | 
					
						
							| 
									
										
										
										
											2024-07-28 19:10:41 +00:00
										 |  |  | // This attempts to maintains the original image aspect ratio. | 
					
						
							|  |  |  | func thumbSize(width, height int, aspect float32, rotation int) (int, int) { | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 	const ( | 
					
						
							|  |  |  | 		maxThumbWidth  = 512 | 
					
						
							|  |  |  | 		maxThumbHeight = 512 | 
					
						
							|  |  |  | 	) | 
					
						
							| 
									
										
										
										
											2024-07-28 19:10:41 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// If image is rotated by | 
					
						
							|  |  |  | 	// any odd multiples of 90, | 
					
						
							|  |  |  | 	// flip width / height to | 
					
						
							|  |  |  | 	// get the correct scale. | 
					
						
							|  |  |  | 	switch rotation { | 
					
						
							|  |  |  | 	case -90, 90, -270, 270: | 
					
						
							|  |  |  | 		width, height = height, width | 
					
						
							|  |  |  | 		aspect = 1 / aspect | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 	switch { | 
					
						
							|  |  |  | 	// Simplest case, within bounds! | 
					
						
							|  |  |  | 	case width < maxThumbWidth && | 
					
						
							|  |  |  | 		height < maxThumbHeight: | 
					
						
							|  |  |  | 		return width, height | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Width is larger side. | 
					
						
							|  |  |  | 	case width > height: | 
					
						
							| 
									
										
										
										
											2024-07-28 19:10:41 +00:00
										 |  |  | 		// i.e. height = newWidth * (height / width) | 
					
						
							|  |  |  | 		height = int(float32(maxThumbWidth) / aspect) | 
					
						
							|  |  |  | 		return maxThumbWidth, height | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Height is larger side. | 
					
						
							|  |  |  | 	case height > width: | 
					
						
							| 
									
										
										
										
											2024-07-28 19:10:41 +00:00
										 |  |  | 		// i.e. width = newHeight * (width / height) | 
					
						
							|  |  |  | 		width = int(float32(maxThumbHeight) * aspect) | 
					
						
							|  |  |  | 		return width, maxThumbHeight | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Square. | 
					
						
							|  |  |  | 	default: | 
					
						
							|  |  |  | 		return maxThumbWidth, maxThumbHeight | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | // webpDecode decodes the WebP at filepath into parsed image.Image. | 
					
						
							|  |  |  | func webpDecode(filepath string) (image.Image, error) { | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 	// Open the file at given path. | 
					
						
							|  |  |  | 	file, err := os.Open(filepath) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Decode image from file. | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | 	img, err := webp.Decode(file) | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Done with file. | 
					
						
							|  |  |  | 	_ = file.Close() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return img, err | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // generateBlurhash generates a blurhash for JPEG at filepath. | 
					
						
							|  |  |  | func generateBlurhash(filepath string) (string, error) { | 
					
						
							|  |  |  | 	// Decode JPEG file at given path. | 
					
						
							| 
									
										
										
										
											2024-07-19 15:28:43 +00:00
										 |  |  | 	img, err := webpDecode(filepath) | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return "", err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// for generating blurhashes, it's more cost effective to | 
					
						
							|  |  |  | 	// lose detail since it's blurry, so make a tiny version. | 
					
						
							|  |  |  | 	tiny := imaging.Resize(img, 64, 64, imaging.NearestNeighbor) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Drop the larger image | 
					
						
							|  |  |  | 	// ref as soon as possible | 
					
						
							|  |  |  | 	// to allow GC to claim. | 
					
						
							|  |  |  | 	img = nil //nolint | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Generate blurhash for thumbnail. | 
					
						
							|  |  |  | 	return blurhash.Encode(4, 3, tiny) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // getMimeType returns a suitable mimetype for file extension. | 
					
						
							|  |  |  | func getMimeType(ext string) string { | 
					
						
							|  |  |  | 	const defaultType = "application/octet-stream" | 
					
						
							|  |  |  | 	return cmp.Or(mimetypes.MimeTypes[ext], defaultType) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // drainToTmp drains data from given reader into a new temp file | 
					
						
							|  |  |  | // and closes it, returning the path of the resulting temp file. | 
					
						
							| 
									
										
										
										
											2023-11-10 19:29:26 +01:00
										 |  |  | // | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | // Note that this function specifically makes attempts to unwrap the | 
					
						
							|  |  |  | // io.ReadCloser as much as it can to underlying type, to maximise | 
					
						
							|  |  |  | // chance that Linux's sendfile syscall can be utilised for optimal | 
					
						
							|  |  |  | // draining of data source to temporary file storage. | 
					
						
							|  |  |  | func drainToTmp(rc io.ReadCloser) (string, error) { | 
					
						
							|  |  |  | 	tmp, err := os.CreateTemp(os.TempDir(), "gotosocial-*") | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return "", err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Close readers | 
					
						
							|  |  |  | 	// on func return. | 
					
						
							|  |  |  | 	defer tmp.Close() | 
					
						
							|  |  |  | 	defer rc.Close() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Extract file path. | 
					
						
							|  |  |  | 	path := tmp.Name() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Limited reader (if any). | 
					
						
							|  |  |  | 	var lr *io.LimitedReader | 
					
						
							|  |  |  | 	var limit int64 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Reader type to use | 
					
						
							|  |  |  | 	// for draining to tmp. | 
					
						
							|  |  |  | 	rd := (io.Reader)(rc) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Check if reader is actually wrapped, | 
					
						
							|  |  |  | 	// (as our http client wraps close func). | 
					
						
							|  |  |  | 	rct, ok := rc.(*iotools.ReadCloserType) | 
					
						
							|  |  |  | 	if ok { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Get unwrapped. | 
					
						
							|  |  |  | 		rd = rct.Reader | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Extract limited reader if wrapped. | 
					
						
							|  |  |  | 		lr, limit = iotools.GetReaderLimit(rd) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Drain reader into tmp. | 
					
						
							|  |  |  | 	_, err = tmp.ReadFrom(rd) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return path, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Check to see if limit was reached, | 
					
						
							|  |  |  | 	// (produces more useful error messages). | 
					
						
							|  |  |  | 	if lr != nil && !iotools.AtEOF(lr.R) { | 
					
						
							|  |  |  | 		return path, fmt.Errorf("reached read limit %s", bytesize.Size(limit)) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return path, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // remove only removes paths if not-empty. | 
					
						
							|  |  |  | func remove(paths ...string) error { | 
					
						
							|  |  |  | 	var errs []error | 
					
						
							|  |  |  | 	for _, path := range paths { | 
					
						
							|  |  |  | 		if path != "" { | 
					
						
							|  |  |  | 			if err := os.Remove(path); err != nil { | 
					
						
							|  |  |  | 				errs = append(errs, fmt.Errorf("error removing %s: %w", path, err)) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return errors.Join(errs...) | 
					
						
							| 
									
										
										
										
											2023-11-10 19:29:26 +01:00
										 |  |  | } |