mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2024-11-26 22:30:25 +00:00
f77005128a
* wrap thumbnailing code to handle generation natively where possible * more code comments! * add even more code comments! * add code comments about blurhash generation * maintain image rotation if contained in exif data * move rotation before resizing * ensure pix_fmt actually selected by ffprobe, check for alpha layer with gifs * use linear instead of nearest-neighbour for resizing * work with image "orientation" instead of "rotation". use default 75% quality for both webp and jpeg generation * add header to new file * use thumb extension when getting thumb mime type * update test models and tests with new media processing * add suggested code comments * add note about thumbnail filter count reducing memory usage
150 lines
3.8 KiB
Go
150 lines
3.8 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"
|
|
"io"
|
|
"os"
|
|
|
|
"codeberg.org/gruf/go-bytesize"
|
|
"codeberg.org/gruf/go-iotools"
|
|
"codeberg.org/gruf/go-mimetypes"
|
|
)
|
|
|
|
// getExtension splits file extension from path.
|
|
func getExtension(path string) string {
|
|
for i := len(path) - 1; i >= 0 && path[i] != '/'; i-- {
|
|
if path[i] == '.' {
|
|
return path[i+1:]
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// 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.
|
|
func thumbSize(width, height int, aspect float32) (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:
|
|
// i.e. height = newWidth * (height / width)
|
|
height = int(float32(maxThumbWidth) / aspect)
|
|
return maxThumbWidth, height
|
|
|
|
// Height is larger side.
|
|
case height > width:
|
|
// i.e. width = newHeight * (width / height)
|
|
width = int(float32(maxThumbHeight) * aspect)
|
|
return width, maxThumbHeight
|
|
|
|
// Square.
|
|
default:
|
|
return maxThumbWidth, maxThumbHeight
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
defer rc.Close()
|
|
|
|
// Open new temporary file.
|
|
tmp, err := os.CreateTemp(
|
|
os.TempDir(),
|
|
"gotosocial-*",
|
|
)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer tmp.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...)
|
|
}
|