2020-01-06 14:32:15 +01:00
|
|
|
package thumb
|
|
|
|
|
|
|
|
import (
|
2020-01-06 17:50:05 +01:00
|
|
|
"errors"
|
2020-01-06 14:32:15 +01:00
|
|
|
"fmt"
|
|
|
|
"image"
|
|
|
|
"image/png"
|
|
|
|
"os"
|
|
|
|
"path"
|
|
|
|
"path/filepath"
|
|
|
|
|
2021-12-09 07:00:39 +01:00
|
|
|
"github.com/disintegration/imaging"
|
|
|
|
|
2022-04-15 09:42:07 +02:00
|
|
|
"github.com/photoprism/photoprism/pkg/clean"
|
2020-01-12 14:00:56 +01:00
|
|
|
"github.com/photoprism/photoprism/pkg/fs"
|
2020-01-06 14:32:15 +01:00
|
|
|
)
|
|
|
|
|
2021-09-03 17:42:37 +02:00
|
|
|
// Suffix returns the thumb cache file suffix.
|
|
|
|
func Suffix(width, height int, opts ...ResampleOption) (result string) {
|
2020-01-06 14:32:15 +01:00
|
|
|
method, _, format := ResampleOptions(opts...)
|
|
|
|
|
|
|
|
result = fmt.Sprintf("%dx%d_%s.%s", width, height, ResampleMethods[method], format)
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2022-07-06 23:01:54 +02:00
|
|
|
// FileName returns the file name of the thumbnail for the matching size.
|
|
|
|
func FileName(hash, thumbPath string, width, height int, opts ...ResampleOption) (fileName string, err error) {
|
2020-05-05 15:42:54 +02:00
|
|
|
if InvalidSize(width) {
|
2022-07-06 23:01:54 +02:00
|
|
|
return "", fmt.Errorf("thumb: width exceeds limit (%d)", width)
|
2020-01-06 14:32:15 +01:00
|
|
|
}
|
|
|
|
|
2020-05-05 15:42:54 +02:00
|
|
|
if InvalidSize(height) {
|
2022-07-06 23:01:54 +02:00
|
|
|
return "", fmt.Errorf("thumb: height exceeds limit (%d)", height)
|
2020-01-06 14:32:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(hash) < 4 {
|
2022-07-06 23:01:54 +02:00
|
|
|
return "", fmt.Errorf("thumb: file hash is empty or too short (%s)", clean.Log(hash))
|
2020-01-06 14:32:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(thumbPath) == 0 {
|
2022-07-06 23:01:54 +02:00
|
|
|
return "", errors.New("thumb: folder is empty")
|
2020-01-06 14:32:15 +01:00
|
|
|
}
|
|
|
|
|
2021-09-03 17:42:37 +02:00
|
|
|
suffix := Suffix(width, height, opts...)
|
2020-01-06 14:32:15 +01:00
|
|
|
p := path.Join(thumbPath, hash[0:1], hash[1:2], hash[2:3])
|
|
|
|
|
2022-10-31 15:01:48 +01:00
|
|
|
if err := os.MkdirAll(p, fs.ModeDir); err != nil {
|
2020-01-06 14:32:15 +01:00
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2021-09-03 17:42:37 +02:00
|
|
|
fileName = fmt.Sprintf("%s/%s_%s", p, hash, suffix)
|
2020-01-06 14:32:15 +01:00
|
|
|
|
2021-09-03 17:42:37 +02:00
|
|
|
return fileName, nil
|
2020-01-06 14:32:15 +01:00
|
|
|
}
|
|
|
|
|
2022-07-06 23:01:54 +02:00
|
|
|
// ResolvedName returns the file name of the thumbnail for the matching size with all symlinks resolved.
|
|
|
|
func ResolvedName(hash, thumbPath string, width, height int, opts ...ResampleOption) (fileName string, err error) {
|
|
|
|
if fileName, err = FileName(hash, thumbPath, width, height, opts...); err != nil {
|
|
|
|
return fileName, err
|
|
|
|
} else {
|
|
|
|
return fs.Resolve(fileName)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// FromCache returns the filename if a thumbnail image with the matching size is in the cache.
|
2020-05-05 15:42:54 +02:00
|
|
|
func FromCache(imageFilename, hash, thumbPath string, width, height int, opts ...ResampleOption) (fileName string, err error) {
|
2020-01-06 14:32:15 +01:00
|
|
|
if len(hash) < 4 {
|
2022-07-06 23:01:54 +02:00
|
|
|
return "", fmt.Errorf("thumb: invalid file hash %s", clean.Log(hash))
|
2020-01-06 14:32:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(imageFilename) < 4 {
|
2022-07-06 23:01:54 +02:00
|
|
|
return "", fmt.Errorf("thumb: invalid file name %s", clean.Log(imageFilename))
|
2020-01-06 14:32:15 +01:00
|
|
|
}
|
|
|
|
|
2022-07-06 23:01:54 +02:00
|
|
|
if fileName, err = FileName(hash, thumbPath, width, height, opts...); err != nil {
|
|
|
|
log.Debugf("thumb: %s in %s (get filename)", err, clean.Log(imageFilename))
|
2020-01-06 14:32:15 +01:00
|
|
|
return "", err
|
2022-07-06 23:01:54 +02:00
|
|
|
} else if fileName, err = fs.Resolve(fileName); err != nil {
|
|
|
|
return "", ErrNotCached
|
|
|
|
} else if fs.FileExists(fileName) {
|
2020-01-06 14:32:15 +01:00
|
|
|
return fileName, nil
|
|
|
|
}
|
|
|
|
|
2022-07-06 23:01:54 +02:00
|
|
|
return "", ErrNotCached
|
2020-05-05 15:42:54 +02:00
|
|
|
}
|
|
|
|
|
2022-07-06 23:01:54 +02:00
|
|
|
// FromFile creates a new thumbnail with the specified size if it was not found in the cache, and returns the filename.
|
2021-02-21 22:53:25 +01:00
|
|
|
func FromFile(imageFilename, hash, thumbPath string, width, height, orientation int, opts ...ResampleOption) (fileName string, err error) {
|
2022-07-06 23:01:54 +02:00
|
|
|
if fileName, err = FromCache(imageFilename, hash, thumbPath, width, height, opts...); err == nil {
|
2020-05-05 15:42:54 +02:00
|
|
|
return fileName, err
|
2022-07-06 23:01:54 +02:00
|
|
|
} else if err != ErrNotCached {
|
2020-05-05 15:42:54 +02:00
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2021-12-09 07:00:39 +01:00
|
|
|
// Generate thumb cache filename.
|
2021-09-03 17:42:37 +02:00
|
|
|
fileName, err = FileName(hash, thumbPath, width, height, opts...)
|
2020-05-05 15:42:54 +02:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Error(err)
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2021-12-09 07:00:39 +01:00
|
|
|
// Load image from storage.
|
|
|
|
img, err := Open(imageFilename, orientation)
|
2020-01-06 14:32:15 +01:00
|
|
|
|
|
|
|
if err != nil {
|
2022-07-06 23:01:54 +02:00
|
|
|
log.Debugf("thumb: %s in %s", err, clean.Log(filepath.Base(imageFilename)))
|
2020-01-06 14:32:15 +01:00
|
|
|
return "", err
|
|
|
|
}
|
2021-02-21 22:53:25 +01:00
|
|
|
|
2021-12-09 07:00:39 +01:00
|
|
|
// Create thumb from image.
|
2022-07-06 23:01:54 +02:00
|
|
|
if _, err = Create(img, fileName, width, height, opts...); err != nil {
|
2020-01-06 14:32:15 +01:00
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
return fileName, nil
|
|
|
|
}
|
|
|
|
|
2021-09-03 17:42:37 +02:00
|
|
|
// Create creates an image thumbnail.
|
2020-05-29 18:04:30 +02:00
|
|
|
func Create(img image.Image, fileName string, width, height int, opts ...ResampleOption) (result image.Image, err error) {
|
2020-05-05 15:42:54 +02:00
|
|
|
if InvalidSize(width) {
|
2022-07-06 23:01:54 +02:00
|
|
|
return img, fmt.Errorf("thumb: width has an invalid value (%d)", width)
|
2020-01-06 14:32:15 +01:00
|
|
|
}
|
|
|
|
|
2020-05-05 15:42:54 +02:00
|
|
|
if InvalidSize(height) {
|
2022-07-06 23:01:54 +02:00
|
|
|
return img, fmt.Errorf("thumb: height has an invalid value (%d)", height)
|
2020-01-06 14:32:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
result = Resample(img, width, height, opts...)
|
|
|
|
|
2022-04-01 13:25:25 +02:00
|
|
|
var quality imaging.EncodeOption
|
2020-01-06 14:32:15 +01:00
|
|
|
|
2022-04-15 09:42:07 +02:00
|
|
|
if filepath.Ext(fileName) == "."+string(fs.ImagePNG) {
|
2022-04-01 13:25:25 +02:00
|
|
|
quality = imaging.PNGCompressionLevel(png.DefaultCompression)
|
2020-01-06 14:32:15 +01:00
|
|
|
} else if width <= 150 && height <= 150 {
|
2022-04-01 13:25:25 +02:00
|
|
|
quality = JpegQualitySmall.EncodeOption()
|
2020-01-06 14:32:15 +01:00
|
|
|
} else {
|
2022-04-01 13:25:25 +02:00
|
|
|
quality = JpegQuality.EncodeOption()
|
2020-01-06 14:32:15 +01:00
|
|
|
}
|
|
|
|
|
2022-04-01 13:25:25 +02:00
|
|
|
err = imaging.Save(result, fileName, quality)
|
2020-01-06 14:32:15 +01:00
|
|
|
|
|
|
|
if err != nil {
|
2022-07-06 23:01:54 +02:00
|
|
|
log.Debugf("thumb: failed to save %s", clean.Log(filepath.Base(fileName)))
|
2020-01-06 14:32:15 +01:00
|
|
|
return result, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|