mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-29 19:52:24 -05:00 
			
		
		
		
	* initial work replacing our media decoding / encoding pipeline with ffprobe + ffmpeg
* specify the video codec to use when generating static image from emoji
* update go-storage library (fixes incompatibility after updating go-iotools)
* maintain image aspect ratio when generating a thumbnail for it
* update readme to show go-ffmpreg
* fix a bunch of media tests, move filesize checking to callers of media manager for more flexibility
* remove extra debug from error message
* fix up incorrect function signatures
* update PutFile to just use regular file copy, as changes are file is on separate partition
* fix remaining tests, remove some unneeded tests now we're working with ffmpeg/ffprobe
* update more tests, add more code comments
* add utilities to generate processed emoji / media outputs
* fix remaining tests
* add test for opus media file, add license header to utility cmds
* limit the number of concurrently available ffmpeg / ffprobe instances
* reduce number of instances
* further reduce number of instances
* fix envparsing test with configuration variables
* update docs and configuration with new media-{local,remote}-max-size variables
		
	
			
		
			
				
	
	
		
			177 lines
		
	
	
	
		
			4.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			177 lines
		
	
	
	
		
			4.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // 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 (
 | |
| 	"cmp"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"image"
 | |
| 	"image/jpeg"
 | |
| 	"io"
 | |
| 	"os"
 | |
| 
 | |
| 	"codeberg.org/gruf/go-bytesize"
 | |
| 	"codeberg.org/gruf/go-iotools"
 | |
| 	"codeberg.org/gruf/go-mimetypes"
 | |
| 	"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.
 | |
| // This maintains the original image aspect ratio.
 | |
| func thumbSize(width, height int) (int, int) {
 | |
| 	const (
 | |
| 		maxThumbWidth  = 512
 | |
| 		maxThumbHeight = 512
 | |
| 	)
 | |
| 	switch {
 | |
| 	// Simplest case, within bounds!
 | |
| 	case width < maxThumbWidth &&
 | |
| 		height < maxThumbHeight:
 | |
| 		return width, height
 | |
| 
 | |
| 	// Width is larger side.
 | |
| 	case width > height:
 | |
| 		p := float32(width) / float32(maxThumbWidth)
 | |
| 		return maxThumbWidth, int(float32(height) / p)
 | |
| 
 | |
| 	// Height is larger side.
 | |
| 	case height > width:
 | |
| 		p := float32(height) / float32(maxThumbHeight)
 | |
| 		return int(float32(width) / p), maxThumbHeight
 | |
| 
 | |
| 	// Square.
 | |
| 	default:
 | |
| 		return maxThumbWidth, maxThumbHeight
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // jpegDecode decodes the JPEG at filepath into parsed image.Image.
 | |
| func jpegDecode(filepath string) (image.Image, error) {
 | |
| 	// Open the file at given path.
 | |
| 	file, err := os.Open(filepath)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// Decode image from file.
 | |
| 	img, err := jpeg.Decode(file)
 | |
| 
 | |
| 	// 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.
 | |
| 	img, err := jpegDecode(filepath)
 | |
| 	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.
 | |
| //
 | |
| // 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...)
 | |
| }
 |