| 
									
										
										
										
											2024-08-08 17:12:13 +00: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 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"context" | 
					
						
							|  |  |  | 	"image" | 
					
						
							|  |  |  | 	"image/gif" | 
					
						
							|  |  |  | 	"image/jpeg" | 
					
						
							|  |  |  | 	"image/png" | 
					
						
							|  |  |  | 	"io" | 
					
						
							|  |  |  | 	"strings" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-26 15:34:10 +02:00
										 |  |  | 	"code.superseriousbusiness.org/gotosocial/internal/gtserror" | 
					
						
							|  |  |  | 	"code.superseriousbusiness.org/gotosocial/internal/log" | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 	"github.com/buckket/go-blurhash" | 
					
						
							|  |  |  | 	"golang.org/x/image/webp" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-29 17:43:14 +02:00
										 |  |  | // thumbSize returns the dimensions to use for an input | 
					
						
							|  |  |  | // image of given width / height, for its outgoing thumbnail. | 
					
						
							|  |  |  | // This attempts to maintains the original image aspect ratio. | 
					
						
							| 
									
										
										
										
											2025-06-10 15:43:31 +02:00
										 |  |  | func thumbSize(max, width, height int, aspect float32) (int, int) { | 
					
						
							| 
									
										
										
										
											2024-08-29 17:43:14 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	switch { | 
					
						
							|  |  |  | 	// Simplest case, within bounds! | 
					
						
							| 
									
										
										
										
											2025-06-10 15:43:31 +02:00
										 |  |  | 	case width < max && height < max: | 
					
						
							| 
									
										
										
										
											2024-08-29 17:43:14 +02:00
										 |  |  | 		return width, height | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Width is larger side. | 
					
						
							|  |  |  | 	case width > height: | 
					
						
							| 
									
										
										
										
											2025-06-10 15:43:31 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-29 17:43:14 +02:00
										 |  |  | 		// i.e. height = newWidth * (height / width) | 
					
						
							| 
									
										
										
										
											2025-06-10 15:43:31 +02:00
										 |  |  | 		height = int(float32(max) / aspect) | 
					
						
							|  |  |  | 		return max, height | 
					
						
							| 
									
										
										
										
											2024-08-29 17:43:14 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Height is larger side. | 
					
						
							|  |  |  | 	case height > width: | 
					
						
							| 
									
										
										
										
											2025-06-10 15:43:31 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-29 17:43:14 +02:00
										 |  |  | 		// i.e. width = newHeight * (width / height) | 
					
						
							| 
									
										
										
										
											2025-06-10 15:43:31 +02:00
										 |  |  | 		width = int(float32(max) * aspect) | 
					
						
							|  |  |  | 		return width, max | 
					
						
							| 
									
										
										
										
											2024-08-29 17:43:14 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Square. | 
					
						
							|  |  |  | 	default: | 
					
						
							| 
									
										
										
										
											2025-06-10 15:43:31 +02:00
										 |  |  | 		return max, max | 
					
						
							| 
									
										
										
										
											2024-08-29 17:43:14 +02:00
										 |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | // generateThumb generates a thumbnail for the | 
					
						
							|  |  |  | // input file at path, resizing it to the given | 
					
						
							|  |  |  | // dimensions and generating a blurhash if needed. | 
					
						
							|  |  |  | // This wraps much of the complex thumbnailing | 
					
						
							|  |  |  | // logic in which where possible we use native | 
					
						
							|  |  |  | // Go libraries for generating thumbnails, else | 
					
						
							|  |  |  | // always falling back to slower but much more | 
					
						
							|  |  |  | // widely supportive ffmpeg. | 
					
						
							|  |  |  | func generateThumb( | 
					
						
							|  |  |  | 	ctx context.Context, | 
					
						
							|  |  |  | 	filepath string, | 
					
						
							|  |  |  | 	width, height int, | 
					
						
							|  |  |  | 	orientation int, | 
					
						
							|  |  |  | 	pixfmt string, | 
					
						
							|  |  |  | 	needBlurhash bool, | 
					
						
							|  |  |  | ) ( | 
					
						
							|  |  |  | 	outpath string, | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 	mimeType string, | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 	blurhash string, | 
					
						
							|  |  |  | 	err error, | 
					
						
							|  |  |  | ) { | 
					
						
							|  |  |  | 	var ext string | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 	// Default type is webp. | 
					
						
							|  |  |  | 	mimeType = "image/webp" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-24 15:12:25 +02:00
										 |  |  | 	// Generate thumb output path REPLACING file extension. | 
					
						
							|  |  |  | 	if i := strings.LastIndexByte(filepath, '.'); i != -1 { | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 		outpath = filepath[:i] + "_thumb.webp" | 
					
						
							|  |  |  | 		ext = filepath[i+1:] // old extension | 
					
						
							|  |  |  | 	} else { | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 		return "", "", "", gtserror.New("input file missing extension") | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Check for the few media types we | 
					
						
							|  |  |  | 	// have native Go decoding that allow | 
					
						
							|  |  |  | 	// us to generate thumbs natively. | 
					
						
							|  |  |  | 	switch { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	case ext == "jpeg": | 
					
						
							|  |  |  | 		// Replace the "webp" with "jpeg", as we'll | 
					
						
							|  |  |  | 		// use our native Go thumbnailing generation. | 
					
						
							|  |  |  | 		outpath = outpath[:len(outpath)-4] + "jpeg" | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 		mimeType = "image/jpeg" | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		log.Debug(ctx, "generating thumb from jpeg") | 
					
						
							|  |  |  | 		blurhash, err := generateNativeThumb( | 
					
						
							|  |  |  | 			filepath, | 
					
						
							|  |  |  | 			outpath, | 
					
						
							|  |  |  | 			width, | 
					
						
							|  |  |  | 			height, | 
					
						
							|  |  |  | 			orientation, | 
					
						
							|  |  |  | 			jpeg.Decode, | 
					
						
							|  |  |  | 			needBlurhash, | 
					
						
							|  |  |  | 		) | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 		return outpath, mimeType, blurhash, err | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// We specifically only allow generating native | 
					
						
							|  |  |  | 	// thumbnails from gif IF it doesn't contain an | 
					
						
							|  |  |  | 	// alpha channel. We'll ultimately be encoding to | 
					
						
							|  |  |  | 	// jpeg which doesn't support transparency layers. | 
					
						
							|  |  |  | 	case ext == "gif" && !containsAlpha(pixfmt): | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Replace the "webp" with "jpeg", as we'll | 
					
						
							|  |  |  | 		// use our native Go thumbnailing generation. | 
					
						
							|  |  |  | 		outpath = outpath[:len(outpath)-4] + "jpeg" | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 		mimeType = "image/jpeg" | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		log.Debug(ctx, "generating thumb from gif") | 
					
						
							|  |  |  | 		blurhash, err := generateNativeThumb( | 
					
						
							|  |  |  | 			filepath, | 
					
						
							|  |  |  | 			outpath, | 
					
						
							|  |  |  | 			width, | 
					
						
							|  |  |  | 			height, | 
					
						
							|  |  |  | 			orientation, | 
					
						
							|  |  |  | 			gif.Decode, | 
					
						
							|  |  |  | 			needBlurhash, | 
					
						
							|  |  |  | 		) | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 		return outpath, mimeType, blurhash, err | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// We specifically only allow generating native | 
					
						
							|  |  |  | 	// thumbnails from png IF it doesn't contain an | 
					
						
							|  |  |  | 	// alpha channel. We'll ultimately be encoding to | 
					
						
							|  |  |  | 	// jpeg which doesn't support transparency layers. | 
					
						
							|  |  |  | 	case ext == "png" && !containsAlpha(pixfmt): | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Replace the "webp" with "jpeg", as we'll | 
					
						
							|  |  |  | 		// use our native Go thumbnailing generation. | 
					
						
							|  |  |  | 		outpath = outpath[:len(outpath)-4] + "jpeg" | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 		mimeType = "image/jpeg" | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		log.Debug(ctx, "generating thumb from png") | 
					
						
							|  |  |  | 		blurhash, err := generateNativeThumb( | 
					
						
							|  |  |  | 			filepath, | 
					
						
							|  |  |  | 			outpath, | 
					
						
							|  |  |  | 			width, | 
					
						
							|  |  |  | 			height, | 
					
						
							|  |  |  | 			orientation, | 
					
						
							|  |  |  | 			png.Decode, | 
					
						
							|  |  |  | 			needBlurhash, | 
					
						
							|  |  |  | 		) | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 		return outpath, mimeType, blurhash, err | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// We specifically only allow generating native | 
					
						
							|  |  |  | 	// thumbnails from webp IF it doesn't contain an | 
					
						
							|  |  |  | 	// alpha channel. We'll ultimately be encoding to | 
					
						
							|  |  |  | 	// jpeg which doesn't support transparency layers. | 
					
						
							|  |  |  | 	case ext == "webp" && !containsAlpha(pixfmt): | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Replace the "webp" with "jpeg", as we'll | 
					
						
							|  |  |  | 		// use our native Go thumbnailing generation. | 
					
						
							|  |  |  | 		outpath = outpath[:len(outpath)-4] + "jpeg" | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 		mimeType = "image/jpeg" | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		log.Debug(ctx, "generating thumb from webp") | 
					
						
							|  |  |  | 		blurhash, err := generateNativeThumb( | 
					
						
							|  |  |  | 			filepath, | 
					
						
							|  |  |  | 			outpath, | 
					
						
							|  |  |  | 			width, | 
					
						
							|  |  |  | 			height, | 
					
						
							|  |  |  | 			orientation, | 
					
						
							|  |  |  | 			webp.Decode, | 
					
						
							|  |  |  | 			needBlurhash, | 
					
						
							|  |  |  | 		) | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 		return outpath, mimeType, blurhash, err | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// The fallback for thumbnail generation, which | 
					
						
							|  |  |  | 	// encompasses most media types is with ffmpeg. | 
					
						
							|  |  |  | 	log.Debug(ctx, "generating thumb with ffmpeg") | 
					
						
							|  |  |  | 	if err := ffmpegGenerateWebpThumb(ctx, | 
					
						
							|  |  |  | 		filepath, | 
					
						
							|  |  |  | 		outpath, | 
					
						
							|  |  |  | 		width, | 
					
						
							|  |  |  | 		height, | 
					
						
							|  |  |  | 		pixfmt, | 
					
						
							|  |  |  | 	); err != nil { | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 		return outpath, "", "", err | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if needBlurhash { | 
					
						
							|  |  |  | 		// Generate new blurhash from webp output thumb. | 
					
						
							|  |  |  | 		blurhash, err = generateWebpBlurhash(outpath) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 			return outpath, "", "", gtserror.Newf("error generating blurhash: %w", err) | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-04 13:58:15 +00:00
										 |  |  | 	return outpath, mimeType, blurhash, nil | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // generateNativeThumb generates a thumbnail | 
					
						
							|  |  |  | // using native Go code, using given decode | 
					
						
							|  |  |  | // function to get image, resize to given dimens, | 
					
						
							|  |  |  | // and write to output filepath as JPEG. If a | 
					
						
							|  |  |  | // blurhash is required it will also generate | 
					
						
							|  |  |  | // this from the image.Image while in-memory. | 
					
						
							|  |  |  | func generateNativeThumb( | 
					
						
							|  |  |  | 	inpath, outpath string, | 
					
						
							|  |  |  | 	width, height int, | 
					
						
							|  |  |  | 	orientation int, | 
					
						
							|  |  |  | 	decode func(io.Reader) (image.Image, error), | 
					
						
							|  |  |  | 	needBlurhash bool, | 
					
						
							|  |  |  | ) ( | 
					
						
							|  |  |  | 	string, // blurhash | 
					
						
							|  |  |  | 	error, | 
					
						
							|  |  |  | ) { | 
					
						
							|  |  |  | 	// Open input file at given path. | 
					
						
							| 
									
										
										
										
											2025-09-24 15:12:25 +02:00
										 |  |  | 	infile, err := openRead(inpath) | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return "", gtserror.Newf("error opening input file %s: %w", inpath, err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Decode image into memory. | 
					
						
							|  |  |  | 	img, err := decode(infile) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Done with file. | 
					
						
							|  |  |  | 	_ = infile.Close() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return "", gtserror.Newf("error decoding file %s: %w", inpath, err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Apply orientation BEFORE any resize, | 
					
						
							|  |  |  | 	// as our image dimensions are calculated | 
					
						
							|  |  |  | 	// taking orientation into account. | 
					
						
							|  |  |  | 	switch orientation { | 
					
						
							|  |  |  | 	case orientationFlipH: | 
					
						
							| 
									
										
										
										
											2024-08-31 08:41:38 +00:00
										 |  |  | 		img = flipH(img) | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 	case orientationFlipV: | 
					
						
							| 
									
										
										
										
											2024-08-31 08:41:38 +00:00
										 |  |  | 		img = flipV(img) | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 	case orientationRotate90: | 
					
						
							| 
									
										
										
										
											2024-08-31 08:41:38 +00:00
										 |  |  | 		img = rotate90(img) | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 	case orientationRotate180: | 
					
						
							| 
									
										
										
										
											2024-08-31 08:41:38 +00:00
										 |  |  | 		img = rotate180(img) | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 	case orientationRotate270: | 
					
						
							| 
									
										
										
										
											2024-08-31 08:41:38 +00:00
										 |  |  | 		img = rotate270(img) | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 	case orientationTranspose: | 
					
						
							| 
									
										
										
										
											2024-08-31 08:41:38 +00:00
										 |  |  | 		img = transpose(img) | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 	case orientationTransverse: | 
					
						
							| 
									
										
										
										
											2024-08-31 08:41:38 +00:00
										 |  |  | 		img = transverse(img) | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-31 08:41:38 +00:00
										 |  |  | 	// Resize image to dimens. | 
					
						
							|  |  |  | 	img = resizeDownLinear(img, | 
					
						
							|  |  |  | 		width, height, | 
					
						
							|  |  |  | 	) | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Open output file at given path. | 
					
						
							| 
									
										
										
										
											2025-09-24 15:12:25 +02:00
										 |  |  | 	outfile, err := openWrite(outpath) | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return "", gtserror.Newf("error opening output file %s: %w", outpath, err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Encode in-memory image to output file. | 
					
						
							|  |  |  | 	// (nil uses defaults, i.e. quality=75). | 
					
						
							|  |  |  | 	err = jpeg.Encode(outfile, img, nil) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Done with file. | 
					
						
							|  |  |  | 	_ = outfile.Close() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return "", gtserror.Newf("error encoding image: %w", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if needBlurhash { | 
					
						
							| 
									
										
										
										
											2024-08-31 08:41:38 +00:00
										 |  |  | 		// for generating blurhashes, it's more | 
					
						
							|  |  |  | 		// cost effective to lose detail since | 
					
						
							|  |  |  | 		// it's blurry, so make a tiny version. | 
					
						
							|  |  |  | 		tiny := resizeDownLinear(img, 32, 0) | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		// Drop the larger image | 
					
						
							|  |  |  | 		// ref as soon as possible | 
					
						
							|  |  |  | 		// to allow GC to claim. | 
					
						
							|  |  |  | 		img = nil //nolint | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Generate blurhash for the tiny thumbnail. | 
					
						
							|  |  |  | 		blurhash, err := blurhash.Encode(4, 3, tiny) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return "", gtserror.Newf("error generating blurhash: %w", err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return blurhash, nil | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return "", nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // generateWebpBlurhash generates a blurhash for Webp at filepath. | 
					
						
							|  |  |  | func generateWebpBlurhash(filepath string) (string, error) { | 
					
						
							| 
									
										
										
										
											2025-09-24 15:12:25 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 	// Open the file at given path. | 
					
						
							| 
									
										
										
										
											2025-09-24 15:12:25 +02:00
										 |  |  | 	file, err := openRead(filepath) | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return "", gtserror.Newf("error opening input file %s: %w", filepath, err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Decode image from file. | 
					
						
							|  |  |  | 	img, err := webp.Decode(file) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Done with file. | 
					
						
							|  |  |  | 	_ = file.Close() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return "", gtserror.Newf("error decoding file %s: %w", filepath, err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-31 08:41:38 +00:00
										 |  |  | 	// for generating blurhashes, it's more | 
					
						
							|  |  |  | 	// cost effective to lose detail since | 
					
						
							|  |  |  | 	// it's blurry, so make a tiny version. | 
					
						
							|  |  |  | 	tiny := resizeDownLinear(img, 32, 0) | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Drop the larger image | 
					
						
							|  |  |  | 	// ref as soon as possible | 
					
						
							|  |  |  | 	// to allow GC to claim. | 
					
						
							|  |  |  | 	img = nil //nolint | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Generate blurhash for the tiny thumbnail. | 
					
						
							|  |  |  | 	blurhash, err := blurhash.Encode(4, 3, tiny) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return "", gtserror.Newf("error generating blurhash: %w", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return blurhash, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // containsAlpha returns whether given pixfmt | 
					
						
							|  |  |  | // (i.e. colorspace) contains an alpha channel. | 
					
						
							| 
									
										
										
										
											2025-06-14 12:27:53 +02:00
										 |  |  | // | 
					
						
							|  |  |  | // generated with: for entry in $(ffprobe.wasm -show_entries pixel_format=name:flags=alpha 2>/dev/null | grep -B1 alpha=1 | sed -n '/name=/{ s|name=||; p }'); do printf '\tcase "%s":\n\t\treturn true\n' "$entry"; done | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | func containsAlpha(pixfmt string) bool { | 
					
						
							| 
									
										
										
										
											2025-06-14 12:27:53 +02:00
										 |  |  | 	switch pixfmt { | 
					
						
							|  |  |  | 	case "pal8": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "argb": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "rgba": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "abgr": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "bgra": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva420p": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "ya8": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva422p": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva444p": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva420p9be": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva420p9le": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva422p9be": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva422p9le": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva444p9be": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva444p9le": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva420p10be": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva420p10le": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva422p10be": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva422p10le": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva444p10be": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva444p10le": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva420p16be": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva420p16le": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva422p16be": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva422p16le": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva444p16be": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva444p16le": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "rgba64be": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "rgba64le": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "bgra64be": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "bgra64le": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "ya16be": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "ya16le": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "gbrap": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "gbrap16be": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "gbrap16le": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "ayuv64le": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "ayuv64be": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "gbrap12be": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "gbrap12le": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "gbrap10be": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "gbrap10le": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "gbrapf32be": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "gbrapf32le": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva422p12be": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva422p12le": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva444p12be": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	case "yuva444p12le": | 
					
						
							|  |  |  | 		return true | 
					
						
							|  |  |  | 	default: | 
					
						
							| 
									
										
										
										
											2024-08-08 17:12:13 +00:00
										 |  |  | 		return false | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } |