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"
|
|
|
|
|
2020-01-12 14:00:56 +01:00
|
|
|
"github.com/photoprism/photoprism/pkg/fs"
|
2020-05-03 18:00:50 +02:00
|
|
|
"github.com/photoprism/photoprism/pkg/txt"
|
2020-01-06 14:32:15 +01:00
|
|
|
|
|
|
|
"github.com/disintegration/imaging"
|
|
|
|
)
|
|
|
|
|
2020-12-12 17:20:31 +01:00
|
|
|
func ResampleOptions(opts ...ResampleOption) (method ResampleOption, filter imaging.ResampleFilter, format fs.FileFormat) {
|
2020-01-06 14:32:15 +01:00
|
|
|
method = ResampleFit
|
|
|
|
filter = imaging.Lanczos
|
2020-12-12 17:20:31 +01:00
|
|
|
format = fs.FormatJpeg
|
2020-01-06 14:32:15 +01:00
|
|
|
|
|
|
|
for _, option := range opts {
|
|
|
|
switch option {
|
|
|
|
case ResamplePng:
|
2020-12-12 17:20:31 +01:00
|
|
|
format = fs.FormatPng
|
2020-01-06 14:32:15 +01:00
|
|
|
case ResampleNearestNeighbor:
|
|
|
|
filter = imaging.NearestNeighbor
|
2020-01-13 11:07:09 +01:00
|
|
|
case ResampleDefault:
|
2020-01-13 13:46:05 +01:00
|
|
|
filter = Filter.Imaging()
|
2020-01-06 14:32:15 +01:00
|
|
|
case ResampleFillTopLeft:
|
|
|
|
method = ResampleFillTopLeft
|
|
|
|
case ResampleFillCenter:
|
|
|
|
method = ResampleFillCenter
|
|
|
|
case ResampleFillBottomRight:
|
|
|
|
method = ResampleFillBottomRight
|
|
|
|
case ResampleFit:
|
|
|
|
method = ResampleFit
|
|
|
|
case ResampleResize:
|
|
|
|
method = ResampleResize
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return method, filter, format
|
|
|
|
}
|
|
|
|
|
2020-05-29 18:04:30 +02:00
|
|
|
func Resample(img image.Image, width, height int, opts ...ResampleOption) image.Image {
|
2020-01-31 18:34:20 +01:00
|
|
|
var resImg image.Image
|
|
|
|
|
2020-01-06 14:32:15 +01:00
|
|
|
method, filter, _ := ResampleOptions(opts...)
|
|
|
|
|
|
|
|
if method == ResampleFit {
|
2020-05-29 18:04:30 +02:00
|
|
|
resImg = imaging.Fit(img, width, height, filter)
|
2020-01-06 14:32:15 +01:00
|
|
|
} else if method == ResampleFillCenter {
|
2020-05-29 18:04:30 +02:00
|
|
|
resImg = imaging.Fill(img, width, height, imaging.Center, filter)
|
2020-01-06 14:32:15 +01:00
|
|
|
} else if method == ResampleFillTopLeft {
|
2020-05-29 18:04:30 +02:00
|
|
|
resImg = imaging.Fill(img, width, height, imaging.TopLeft, filter)
|
2020-01-06 14:32:15 +01:00
|
|
|
} else if method == ResampleFillBottomRight {
|
2020-05-29 18:04:30 +02:00
|
|
|
resImg = imaging.Fill(img, width, height, imaging.BottomRight, filter)
|
2020-01-06 14:32:15 +01:00
|
|
|
} else if method == ResampleResize {
|
2020-05-29 18:04:30 +02:00
|
|
|
resImg = imaging.Resize(img, width, height, filter)
|
2020-01-06 14:32:15 +01:00
|
|
|
}
|
|
|
|
|
2020-05-29 18:04:30 +02:00
|
|
|
return resImg
|
2020-01-06 14:32:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func Postfix(width, height int, opts ...ResampleOption) (result string) {
|
|
|
|
method, _, format := ResampleOptions(opts...)
|
|
|
|
|
|
|
|
result = fmt.Sprintf("%dx%d_%s.%s", width, height, ResampleMethods[method], format)
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
func Filename(hash string, thumbPath string, width, height int, opts ...ResampleOption) (filename string, err error) {
|
2020-05-05 15:42:54 +02:00
|
|
|
if InvalidSize(width) {
|
2020-05-03 18:00:50 +02:00
|
|
|
return "", fmt.Errorf("resample: width exceeds limit (%d)", width)
|
2020-01-06 14:32:15 +01:00
|
|
|
}
|
|
|
|
|
2020-05-05 15:42:54 +02:00
|
|
|
if InvalidSize(height) {
|
2020-05-03 18:00:50 +02:00
|
|
|
return "", fmt.Errorf("resample: height exceeds limit (%d)", height)
|
2020-01-06 14:32:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(hash) < 4 {
|
2020-05-03 18:00:50 +02:00
|
|
|
return "", fmt.Errorf("resample: file hash is empty or too short (%s)", txt.Quote(hash))
|
2020-01-06 14:32:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(thumbPath) == 0 {
|
2020-05-07 12:49:06 +02:00
|
|
|
return "", errors.New("resample: folder is empty")
|
2020-01-06 14:32:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
postfix := Postfix(width, height, opts...)
|
|
|
|
p := path.Join(thumbPath, hash[0:1], hash[1:2], hash[2:3])
|
|
|
|
|
|
|
|
if err := os.MkdirAll(p, os.ModePerm); err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
filename = fmt.Sprintf("%s/%s_%s", p, hash, postfix)
|
|
|
|
|
|
|
|
return filename, nil
|
|
|
|
}
|
|
|
|
|
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 {
|
2020-05-03 18:00:50 +02:00
|
|
|
return "", fmt.Errorf("resample: file hash is empty or too short (%s)", txt.Quote(hash))
|
2020-01-06 14:32:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(imageFilename) < 4 {
|
2020-05-03 18:00:50 +02:00
|
|
|
return "", fmt.Errorf("resample: image filename is empty or too short (%s)", txt.Quote(imageFilename))
|
2020-01-06 14:32:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
fileName, err = Filename(hash, thumbPath, width, height, opts...)
|
|
|
|
|
|
|
|
if err != nil {
|
2020-05-05 15:42:54 +02:00
|
|
|
log.Error(err)
|
2020-01-06 14:32:15 +01:00
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2020-01-12 14:00:56 +01:00
|
|
|
if fs.FileExists(fileName) {
|
2020-01-06 14:32:15 +01:00
|
|
|
return fileName, nil
|
|
|
|
}
|
|
|
|
|
2020-05-05 15:42:54 +02:00
|
|
|
return "", ErrThumbNotCached
|
|
|
|
}
|
|
|
|
|
|
|
|
func FromFile(imageFilename, hash, thumbPath string, width, height int, opts ...ResampleOption) (fileName string, err error) {
|
|
|
|
if fileName, err := FromCache(imageFilename, hash, thumbPath, width, height, opts...); err == nil {
|
|
|
|
return fileName, err
|
|
|
|
} else if err != ErrThumbNotCached {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
fileName, err = Filename(hash, thumbPath, width, height, opts...)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Error(err)
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2020-01-06 14:32:15 +01:00
|
|
|
img, err := imaging.Open(imageFilename, imaging.AutoOrientation(true))
|
|
|
|
|
|
|
|
if err != nil {
|
2020-07-20 11:29:38 +02:00
|
|
|
log.Errorf("resample: %s in %s", err, txt.Quote(filepath.Base(imageFilename)))
|
2020-01-06 14:32:15 +01:00
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2020-05-29 18:04:30 +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
|
|
|
|
}
|
|
|
|
|
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) {
|
2020-05-03 18:00:50 +02:00
|
|
|
return img, fmt.Errorf("resample: 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) {
|
2020-05-03 18:00:50 +02:00
|
|
|
return img, fmt.Errorf("resample: height has an invalid value (%d)", height)
|
2020-01-06 14:32:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
result = Resample(img, width, height, opts...)
|
|
|
|
|
|
|
|
var saveOption imaging.EncodeOption
|
|
|
|
|
2020-12-12 17:20:31 +01:00
|
|
|
if filepath.Ext(fileName) == "."+string(fs.FormatPng) {
|
2020-01-06 14:32:15 +01:00
|
|
|
saveOption = imaging.PNGCompressionLevel(png.DefaultCompression)
|
|
|
|
} else if width <= 150 && height <= 150 {
|
|
|
|
saveOption = imaging.JPEGQuality(JpegQualitySmall)
|
|
|
|
} else {
|
|
|
|
saveOption = imaging.JPEGQuality(JpegQuality)
|
|
|
|
}
|
|
|
|
|
2020-05-29 18:04:30 +02:00
|
|
|
err = imaging.Save(result, fileName, saveOption)
|
2020-01-06 14:32:15 +01:00
|
|
|
|
|
|
|
if err != nil {
|
2020-07-20 11:29:38 +02:00
|
|
|
log.Errorf("resample: failed to save %s", txt.Quote(filepath.Base(fileName)))
|
2020-01-06 14:32:15 +01:00
|
|
|
return result, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|