2e85b3cccd
Clustering and matching have been improved along the way. This opens the door for further optimizations while keeping the code readable.
188 lines
4.8 KiB
Go
188 lines
4.8 KiB
Go
package thumb
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"image/png"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
|
|
"github.com/photoprism/photoprism/pkg/fs"
|
|
"github.com/photoprism/photoprism/pkg/txt"
|
|
|
|
"github.com/disintegration/imaging"
|
|
)
|
|
|
|
func ResampleOptions(opts ...ResampleOption) (method ResampleOption, filter imaging.ResampleFilter, format fs.FileFormat) {
|
|
method = ResampleFit
|
|
filter = imaging.Lanczos
|
|
format = fs.FormatJpeg
|
|
|
|
for _, option := range opts {
|
|
switch option {
|
|
case ResamplePng:
|
|
format = fs.FormatPng
|
|
case ResampleNearestNeighbor:
|
|
filter = imaging.NearestNeighbor
|
|
case ResampleDefault:
|
|
filter = Filter.Imaging()
|
|
case ResampleFillTopLeft:
|
|
method = ResampleFillTopLeft
|
|
case ResampleFillCenter:
|
|
method = ResampleFillCenter
|
|
case ResampleFillBottomRight:
|
|
method = ResampleFillBottomRight
|
|
case ResampleFit:
|
|
method = ResampleFit
|
|
case ResampleResize:
|
|
method = ResampleResize
|
|
}
|
|
}
|
|
|
|
return method, filter, format
|
|
}
|
|
|
|
func Resample(img image.Image, width, height int, opts ...ResampleOption) image.Image {
|
|
var resImg image.Image
|
|
|
|
method, filter, _ := ResampleOptions(opts...)
|
|
|
|
if method == ResampleFit {
|
|
resImg = imaging.Fit(img, width, height, filter)
|
|
} else if method == ResampleFillCenter {
|
|
resImg = imaging.Fill(img, width, height, imaging.Center, filter)
|
|
} else if method == ResampleFillTopLeft {
|
|
resImg = imaging.Fill(img, width, height, imaging.TopLeft, filter)
|
|
} else if method == ResampleFillBottomRight {
|
|
resImg = imaging.Fill(img, width, height, imaging.BottomRight, filter)
|
|
} else if method == ResampleResize {
|
|
resImg = imaging.Resize(img, width, height, filter)
|
|
}
|
|
|
|
return resImg
|
|
}
|
|
|
|
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) {
|
|
if InvalidSize(width) {
|
|
return "", fmt.Errorf("resample: width exceeds limit (%d)", width)
|
|
}
|
|
|
|
if InvalidSize(height) {
|
|
return "", fmt.Errorf("resample: height exceeds limit (%d)", height)
|
|
}
|
|
|
|
if len(hash) < 4 {
|
|
return "", fmt.Errorf("resample: file hash is empty or too short (%s)", txt.Quote(hash))
|
|
}
|
|
|
|
if len(thumbPath) == 0 {
|
|
return "", errors.New("resample: folder is empty")
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func FromCache(imageFilename, hash, thumbPath string, width, height int, opts ...ResampleOption) (fileName string, err error) {
|
|
if len(hash) < 4 {
|
|
return "", fmt.Errorf("resample: invalid file hash %s", txt.Quote(hash))
|
|
}
|
|
|
|
if len(imageFilename) < 4 {
|
|
return "", fmt.Errorf("resample: invalid file name %s", txt.Quote(imageFilename))
|
|
}
|
|
|
|
fileName, err = Filename(hash, thumbPath, width, height, opts...)
|
|
|
|
if err != nil {
|
|
log.Error(err)
|
|
return "", err
|
|
}
|
|
|
|
if fs.FileExists(fileName) {
|
|
return fileName, nil
|
|
}
|
|
|
|
return "", ErrThumbNotCached
|
|
}
|
|
|
|
func FromFile(imageFilename, hash, thumbPath string, width, height, orientation 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
|
|
}
|
|
|
|
img, err := imaging.Open(imageFilename)
|
|
|
|
if err != nil {
|
|
log.Errorf("resample: %s in %s", err, txt.Quote(filepath.Base(imageFilename)))
|
|
return "", err
|
|
}
|
|
|
|
if orientation > 1 {
|
|
img = Rotate(img, orientation)
|
|
}
|
|
|
|
if _, err := Create(img, fileName, width, height, opts...); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return fileName, nil
|
|
}
|
|
|
|
func Create(img image.Image, fileName string, width, height int, opts ...ResampleOption) (result image.Image, err error) {
|
|
if InvalidSize(width) {
|
|
return img, fmt.Errorf("resample: width has an invalid value (%d)", width)
|
|
}
|
|
|
|
if InvalidSize(height) {
|
|
return img, fmt.Errorf("resample: height has an invalid value (%d)", height)
|
|
}
|
|
|
|
result = Resample(img, width, height, opts...)
|
|
|
|
var saveOption imaging.EncodeOption
|
|
|
|
if filepath.Ext(fileName) == "."+string(fs.FormatPng) {
|
|
saveOption = imaging.PNGCompressionLevel(png.DefaultCompression)
|
|
} else if width <= 150 && height <= 150 {
|
|
saveOption = imaging.JPEGQuality(JpegQualitySmall)
|
|
} else {
|
|
saveOption = imaging.JPEGQuality(JpegQuality)
|
|
}
|
|
|
|
err = imaging.Save(result, fileName, saveOption)
|
|
|
|
if err != nil {
|
|
log.Errorf("resample: failed to save %s", txt.Quote(filepath.Base(fileName)))
|
|
return result, err
|
|
}
|
|
|
|
return result, nil
|
|
}
|