Files
photoprism/internal/photoprism/convert_image.go
2025-03-27 16:42:24 +01:00

207 lines
6.5 KiB
Go

package photoprism
import (
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/gabriel-vasile/mimetype"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/media"
"github.com/photoprism/photoprism/pkg/media/http/header"
)
// ToImage converts a media file to a directly supported image file format.
func (w *Convert) ToImage(f *MediaFile, force bool) (result *MediaFile, err error) {
if f == nil {
return nil, fmt.Errorf("convert: no media file provided for processing - you may have found a bug")
}
if !f.Exists() {
return nil, fmt.Errorf("convert: %s not found", clean.Log(f.RootRelName()))
} else if f.Empty() {
return nil, fmt.Errorf("convert: %s is empty", clean.Log(f.RootRelName()))
} else if f.IsThumb() {
return nil, fmt.Errorf("convert: %s is a thumbnail image", clean.Log(f.RootRelName()))
}
if f.IsPreviewImage() {
return f, nil
}
imageName := fs.ImagePng.FindFirst(f.FileName(), []string{w.conf.SidecarPath(), fs.PPHiddenPathname}, w.conf.OriginalsPath(), false)
if imageName == "" {
imageName = fs.ImageJpeg.FindFirst(f.FileName(), []string{w.conf.SidecarPath(), fs.PPHiddenPathname}, w.conf.OriginalsPath(), false)
}
mediaFile, err := NewMediaFile(imageName)
// Replace existing sidecar if "force" is true.
if err == nil && mediaFile.IsPreviewImage() {
if force && mediaFile.InSidecar() {
if removeErr := mediaFile.Remove(); removeErr != nil {
return mediaFile, fmt.Errorf("convert: failed removing %s (%s)", clean.Log(mediaFile.RootRelName()), removeErr)
} else {
log.Infof("convert: replacing %s", clean.Log(mediaFile.RootRelName()))
}
} else {
return mediaFile, nil
}
} else if f.IsVector() {
if !w.conf.VectorEnabled() {
return nil, fmt.Errorf("convert: vector graphics support disabled (%s)", clean.Log(f.RootRelName()))
}
imageName, _ = fs.FileName(f.FileName(), w.conf.SidecarPath(), w.conf.OriginalsPath(), fs.ExtPng)
} else {
imageName, _ = fs.FileName(f.FileName(), w.conf.SidecarPath(), w.conf.OriginalsPath(), fs.ExtJpeg)
}
if !w.conf.SidecarWritable() {
return nil, fmt.Errorf("convert: disabled in read-only mode (%s)", clean.Log(f.RootRelName()))
}
fileName := f.RelName(w.conf.OriginalsPath())
fileOrientation := media.KeepOrientation
xmpName := fs.SidecarXMP.Find(f.FileName(), false)
// Publish file conversion event.
event.Publish("index.converting", event.Data{
"fileType": f.FileType(),
"fileName": fileName,
"baseName": filepath.Base(fileName),
"xmpName": filepath.Base(xmpName),
})
start := time.Now()
// PNG, GIF, BMP, TIFF, and WebP can be handled natively.
if f.IsImageOther() {
log.Infof("convert: converting %s to %s (%s)", clean.Log(filepath.Base(fileName)), clean.Log(filepath.Base(imageName)), f.FileType())
// Create PNG or JPEG image from source file.
switch fs.LowerExt(imageName) {
case fs.ExtPng:
_, err = thumb.Png(f.FileName(), imageName, f.Orientation())
case fs.ExtJpeg:
_, err = thumb.Jpeg(f.FileName(), imageName, f.Orientation())
default:
return nil, fmt.Errorf("convert: unsupported target format %s (%s)", fs.LowerExt(imageName), clean.Log(f.RootRelName()))
}
// Check result.
if err == nil {
log.Infof("convert: %s created in %s (%s)", clean.Log(filepath.Base(imageName)), time.Since(start), f.FileType())
return NewMediaFile(imageName)
} else if !f.IsTiff() && !f.IsWebp() {
// See https://github.com/photoprism/photoprism/issues/1612
// for TIFF file format compatibility.
return nil, err
}
}
// Run external commands for other formats.
var cmds ConvertCmds
var useMutex bool
var expectedMime string
switch fs.LowerExt(imageName) {
case fs.ExtPng:
cmds, useMutex, err = w.PngConvertCmds(f, imageName)
expectedMime = header.ContentTypePng
case fs.ExtJpeg:
cmds, useMutex, err = w.JpegConvertCmds(f, imageName, xmpName)
expectedMime = header.ContentTypeJpeg
default:
return nil, fmt.Errorf("convert: unsupported target format %s (%s)", fs.LowerExt(imageName), clean.Log(f.RootRelName()))
}
if err != nil {
return nil, err
} else if len(cmds) == 0 {
return nil, fmt.Errorf("file type %s not supported", f.FileType())
}
if useMutex {
// Make sure only one command is executed at a time.
// See https://photo.stackexchange.com/questions/105969/darktable-cli-fails-because-of-locked-database-file
w.cmdMutex.Lock()
defer w.cmdMutex.Unlock()
}
if fs.FileExistsNotEmpty(imageName) {
return NewMediaFile(imageName)
}
// Try compatible converters.
for _, c := range cmds {
// Fetch command output.
var out bytes.Buffer
var stderr bytes.Buffer
cmd := c.Cmd
cmd.Stdout = &out
cmd.Stderr = &stderr
cmd.Env = append(cmd.Env, []string{
fmt.Sprintf("HOME=%s", w.conf.CmdCachePath()),
fmt.Sprintf("LD_LIBRARY_PATH=%s", w.conf.CmdLibPath()),
}...)
log.Infof("convert: converting %s to %s (%s)", clean.Log(filepath.Base(fileName)), clean.Log(filepath.Base(imageName)), filepath.Base(cmd.Path))
// Log exact command for debugging in trace mode.
log.Trace(cmd.String())
// Run convert command.
if err = cmd.Run(); err != nil {
if errStr := strings.TrimSpace(stderr.String()); errStr != "" {
err = errors.New(errStr)
}
log.Tracef("convert: %s (%s)", strings.TrimSpace(err.Error()), filepath.Base(cmd.Path))
continue
} else if fs.FileExistsNotEmpty(imageName) {
log.Infof("convert: %s created in %s (%s)", clean.Log(filepath.Base(imageName)), time.Since(start), filepath.Base(cmd.Path))
fileOrientation = c.Orientation
break
} else if res := out.Bytes(); len(res) < 512 || !mimetype.Detect(res).Is(expectedMime) {
continue
} else if err = os.WriteFile(imageName, res, fs.ModeFile); err != nil {
log.Tracef("convert: %s (%s)", err, filepath.Base(cmd.Path))
continue
} else {
log.Infof("convert: %s created in %s (%s)", clean.Log(filepath.Base(imageName)), time.Since(start), filepath.Base(cmd.Path))
fileOrientation = c.Orientation
break
}
}
// Ok?
if err != nil {
return nil, err
}
// Create a MediaFile instance from the generated file.
if result, err = NewMediaFile(imageName); err != nil {
return result, err
}
// Change the Exif orientation of the generated file if required.
switch fileOrientation {
case media.ResetOrientation:
if err = result.ChangeOrientation(1); err != nil {
log.Warnf("convert: %s in %s (change orientation)", err, clean.Log(result.RootRelName()))
}
}
return result, nil
}