photoprism/internal/photoprism/mediafile_thumbs.go
Michael Mayer e9d96e8849 Photos: Update file mod time when changing image orientation #464
Signed-off-by: Michael Mayer <michael@photoprism.app>
2023-03-22 17:41:07 +01:00

190 lines
4.9 KiB
Go

package photoprism
import (
"bytes"
"errors"
"fmt"
"image"
"os/exec"
"strconv"
"strings"
"time"
"github.com/disintegration/imaging"
"github.com/dustin/go-humanize/english"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/capture"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
)
// Bounds returns the media dimensions as image.Rectangle.
func (m *MediaFile) Bounds() image.Rectangle {
return image.Rectangle{Min: image.Point{}, Max: image.Point{X: m.Width(), Y: m.Height()}}
}
// Thumbnail returns a thumbnail filename.
func (m *MediaFile) Thumbnail(path string, sizeName thumb.Name) (filename string, err error) {
size, ok := thumb.Sizes[sizeName]
if !ok {
log.Errorf("media: invalid type %s", sizeName)
return "", fmt.Errorf("media: invalid type %s", sizeName)
}
// Choose the smallest fitting size if the original image is smaller.
if size.Fit && m.Bounds().In(size.Bounds()) {
size = thumb.FitBounds(m.Bounds())
log.Tracef("media: smallest fitting size for %s is %s (width %d, height %d)", clean.Log(m.RootRelName()), size.Name, size.Width, size.Height)
}
thumbName, err := size.FromFile(m.FileName(), m.Hash(), path, m.Orientation())
if err != nil {
err = fmt.Errorf("media: failed creating thumbnail for %s (%s)", clean.Log(m.BaseName()), err)
log.Debug(err)
return "", err
}
return thumbName, nil
}
// Resample returns a resampled image of the file.
func (m *MediaFile) Resample(path string, sizeName thumb.Name) (img image.Image, err error) {
thumbName, err := m.Thumbnail(path, sizeName)
if err != nil {
return nil, err
}
return imaging.Open(thumbName)
}
// CreateThumbnails creates the default thumbnail sizes if the media file
// is a JPEG and they don't exist yet (except force is true).
func (m *MediaFile) CreateThumbnails(thumbPath string, force bool) (err error) {
if !m.IsPreviewImage() {
// Skip.
return
}
count := 0
start := time.Now()
defer func() {
switch count {
case 0:
log.Debug(capture.Time(start, fmt.Sprintf("media: created no new thumbnails for %s", clean.Log(m.RootRelName()))))
default:
log.Info(capture.Time(start, fmt.Sprintf("media: created %s for %s", english.Plural(count, "thumbnail", "thumbnails"), clean.Log(m.RootRelName()))))
}
}()
hash := m.Hash()
var original image.Image
var srcImg image.Image
var srcName thumb.Name
for _, name := range thumb.Names {
var size thumb.Size
var fileName string
if size = thumb.Sizes[name]; size.Uncached() {
// Skip, exceeds pre-cached size limit.
continue
} else if fileName, err = size.FileName(hash, thumbPath); err != nil {
log.Errorf("media: failed creating %s (%s)", clean.Log(string(name)), err)
return err
} else if force || !fs.FileExists(fileName) {
// Open original if needed.
if original == nil {
img, err := thumb.Open(m.FileName(), m.Orientation())
// Try to fix broken JPEGs if possible, fail otherwise.
if err != nil {
if !strings.HasPrefix(err.Error(), "invalid JPEG format") {
log.Debugf("media: %s in %s", err.Error(), clean.Log(m.RootRelName()))
return err
}
if fixed, err := NewConvert(conf).FixJpeg(m, false); err != nil {
return err
} else if fixedImg, err := thumb.Open(fixed.FileName(), m.Orientation()); err != nil {
return err
} else {
img = fixedImg
}
}
original = img
log.Debugf("media: opened %s [%s]", clean.Log(m.RootRelName()), thumb.MemSize(original).String())
}
// Thumb size too large
// for the original image?
if size.Skip(original) {
continue
}
// Reuse existing thumb to improve performance
// and reduce server load?
if size.Source != "" {
if size.Source == srcName && srcImg != nil {
_, err = size.Create(srcImg, fileName)
} else {
_, err = size.Create(original, fileName)
}
} else {
srcImg, err = size.Create(original, fileName)
srcName = name
}
// Failed?
if err != nil {
log.Errorf("media: failed creating %s (%s)", name.String(), err)
return err
}
count++
}
}
return nil
}
// ChangeOrientation changes the file orientation.
func (m *MediaFile) ChangeOrientation(val int) (err error) {
if !m.IsPreviewImage() {
// Skip.
return fmt.Errorf("orientation can currently only be changed for jpeg and png files")
}
cnf := Config()
cmd := exec.Command(cnf.ExifToolBin(), "-overwrite_original", "-n", "-Orientation="+strconv.Itoa(val), m.FileName())
// Fetch command output.
var out bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr
cmd.Env = []string{fmt.Sprintf("HOME=%s", cnf.CmdCachePath())}
// Log exact command for debugging in trace mode.
log.Trace(cmd.String())
// Run exiftool command.
if err = cmd.Run(); err != nil {
if stderr.String() != "" {
return errors.New(stderr.String())
} else {
return err
}
}
return nil
}