Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
2034110c5d
commit
1b89915cc4
4
Makefile
4
Makefile
@ -50,6 +50,7 @@ test-go: reset-sqlite run-test-go
|
||||
test-pkg: reset-sqlite run-test-pkg
|
||||
test-api: reset-sqlite run-test-api
|
||||
test-commands: reset-sqlite run-test-commands
|
||||
test-photoprism: reset-sqlite run-test-photoprism
|
||||
test-short: reset-sqlite run-test-short
|
||||
test-mariadb: reset-acceptance run-test-mariadb
|
||||
acceptance-run-chromium: storage/acceptance acceptance-auth-sqlite-restart acceptance-auth acceptance-auth-sqlite-stop acceptance-sqlite-restart acceptance acceptance-sqlite-stop
|
||||
@ -263,6 +264,9 @@ run-test-api:
|
||||
run-test-commands:
|
||||
$(info Running all CLI command tests...)
|
||||
$(GOTEST) -parallel 2 -count 1 -cpu 2 -tags slow -timeout 20m ./internal/commands/...
|
||||
run-test-photoprism:
|
||||
$(info Running all Go tests in "/internal/photoprism"...)
|
||||
$(GOTEST) -parallel 2 -count 1 -cpu 2 -tags slow -timeout 20m ./internal/photoprism/...
|
||||
test-parallel:
|
||||
$(info Running all Go tests in parallel mode...)
|
||||
$(GOTEST) -parallel 2 -count 1 -cpu 2 -tags slow -timeout 20m ./pkg/... ./internal/...
|
||||
|
11
go.mod
11
go.mod
@ -13,15 +13,14 @@ require (
|
||||
github.com/dsoprea/go-png-image-structure/v2 v2.0.0-20210512210324-29b889a6093d
|
||||
github.com/dsoprea/go-tiff-image-structure/v2 v2.0.0-20221003165014-8ecc4f52edca
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/esimov/pigo v1.4.5
|
||||
github.com/esimov/pigo v1.4.6
|
||||
github.com/gin-contrib/gzip v0.0.6
|
||||
github.com/gin-gonic/gin v1.8.1
|
||||
github.com/go-playground/validator/v10 v10.11.1 // indirect
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551
|
||||
github.com/google/open-location-code/go v0.0.0-20220922185916-75f4f40254f8
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/gosimple/slug v1.13.0
|
||||
github.com/h2non/filetype v1.1.3
|
||||
github.com/gosimple/slug v1.13.1
|
||||
github.com/jinzhu/gorm v1.9.16
|
||||
github.com/jinzhu/inflection v1.0.0
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
|
||||
@ -48,8 +47,8 @@ require (
|
||||
github.com/ulule/deepcopier v0.0.0-20200430083143-45decc6639b6
|
||||
github.com/urfave/cli v1.22.10
|
||||
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be
|
||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b
|
||||
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b
|
||||
golang.org/x/net v0.0.0-20221004154528-8021a29435af
|
||||
gonum.org/v1/gonum v0.12.0
|
||||
gopkg.in/photoprism/go-tz.v2 v2.1.1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
@ -79,6 +78,8 @@ require (
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
)
|
||||
|
||||
require github.com/gabriel-vasile/mimetype v1.4.1
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
|
||||
|
19
go.sum
19
go.sum
@ -153,11 +153,13 @@ github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||
github.com/esimov/pigo v1.4.5 h1:ySG0QqMh02VNALvHnx04L1ScRu66N6XA5vLLga8GiLg=
|
||||
github.com/esimov/pigo v1.4.5/go.mod h1:SGkOUpm4wlEmQQJKlaymAkThY8/8iP+XE0gFo7g8G6w=
|
||||
github.com/esimov/pigo v1.4.6 h1:wpB9FstbqeGP/CZP+nTR52tUJe7XErq8buG+k4xCXlw=
|
||||
github.com/esimov/pigo v1.4.6/go.mod h1:uqj9Y3+3IRYhFK071rxz1QYq0ePhA6+R9jrUZavi46M=
|
||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
|
||||
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
@ -299,13 +301,11 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gosimple/slug v1.13.0 h1:w4W2sU2a/JcAkI+LN316Cn/NE4CXopoXto9aloYTic0=
|
||||
github.com/gosimple/slug v1.13.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
|
||||
github.com/gosimple/slug v1.13.1 h1:bQ+kpX9Qa6tHRaK+fZR0A0M2Kd7Pa5eHPPsb1JpHD+Q=
|
||||
github.com/gosimple/slug v1.13.1/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
|
||||
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
|
||||
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
|
||||
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
@ -479,8 +479,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
|
||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0=
|
||||
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@ -583,8 +583,9 @@ golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b h1:6e93nYa3hNqAvLr0pD4PN1fFS+gKzp2zAXqrnTCstqU=
|
||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.0.0-20221004154528-8021a29435af h1:wv66FM3rLZGPdxpYL+ApnDe2HzHcTFta3z5nsc13wI4=
|
||||
golang.org/x/net v0.0.0-20221004154528-8021a29435af/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
@ -41,6 +41,11 @@ func (c *Config) DisableExifTool() bool {
|
||||
return c.options.DisableExifTool
|
||||
}
|
||||
|
||||
// ExifToolEnabled checks if the use of ExifTool is possible.
|
||||
func (c *Config) ExifToolEnabled() bool {
|
||||
return !c.DisableExifTool()
|
||||
}
|
||||
|
||||
// DisableTensorFlow checks if all features depending on TensorFlow should be disabled.
|
||||
func (c *Config) DisableTensorFlow() bool {
|
||||
if LowMem && !c.options.DisableTensorFlow {
|
||||
|
@ -59,6 +59,14 @@ func TestConfig_DisableExifTool(t *testing.T) {
|
||||
assert.True(t, c.DisableExifTool())
|
||||
}
|
||||
|
||||
func TestConfig_ExifToolEnabled(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
assert.True(t, c.ExifToolEnabled())
|
||||
|
||||
c.options.ExifToolBin = "XXX"
|
||||
assert.False(t, c.ExifToolEnabled())
|
||||
}
|
||||
|
||||
func TestConfig_DisableFaces(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
assert.False(t, c.DisableFaces())
|
||||
|
@ -550,7 +550,7 @@ var Flags = CliFlags{
|
||||
Flag: cli.StringFlag{
|
||||
Name: "darktable-blacklist",
|
||||
Usage: "do not use Darktable to convert files with these `EXTENSIONS`",
|
||||
Value: "dng",
|
||||
Value: "",
|
||||
EnvVar: "PHOTOPRISM_DARKTABLE_BLACKLIST",
|
||||
}},
|
||||
CliFlag{
|
||||
@ -578,7 +578,7 @@ var Flags = CliFlags{
|
||||
Flag: cli.StringFlag{
|
||||
Name: "rawtherapee-blacklist",
|
||||
Usage: "do not use RawTherapee to convert files with these `EXTENSIONS`",
|
||||
Value: "avif,avifs",
|
||||
Value: "dng",
|
||||
EnvVar: "PHOTOPRISM_RAWTHERAPEE_BLACKLIST",
|
||||
}},
|
||||
CliFlag{
|
||||
@ -598,7 +598,7 @@ var Flags = CliFlags{
|
||||
CliFlag{
|
||||
Flag: cli.StringFlag{
|
||||
Name: "heifconvert-bin",
|
||||
Usage: "HEIC/HEIF image conversion `COMMAND`",
|
||||
Usage: "HEIC/HEIF/AVIF image conversion `COMMAND`",
|
||||
Value: "heif-convert",
|
||||
EnvVar: "PHOTOPRISM_HEIFCONVERT_BIN",
|
||||
}},
|
||||
|
@ -99,7 +99,7 @@ func (c *Convert) Start(path string, force bool) (err error) {
|
||||
|
||||
f, err := NewMediaFile(fileName)
|
||||
|
||||
if err != nil || f.Empty() || !(f.IsRaw() || f.IsHEIF() || f.IsAVIF() || f.IsImageOther() || f.IsVideo()) {
|
||||
if err != nil || f.Empty() || !(f.IsRaw() || f.IsHEIC() || f.IsAVIF() || f.IsImageOther() || f.IsVideo()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -4,14 +4,16 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"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"
|
||||
)
|
||||
@ -32,6 +34,8 @@ func (c *Convert) ToJpeg(f *MediaFile, force bool) (*MediaFile, error) {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
jpegName := fs.ImageJPEG.FindFirst(f.FileName(), []string{c.conf.SidecarPath(), fs.HiddenPath}, c.conf.OriginalsPath(), false)
|
||||
|
||||
mediaFile, err := NewMediaFile(jpegName)
|
||||
@ -56,7 +60,7 @@ func (c *Convert) ToJpeg(f *MediaFile, force bool) (*MediaFile, error) {
|
||||
}
|
||||
|
||||
fileName := f.RelName(c.conf.OriginalsPath())
|
||||
xmpName := fs.XmpFile.Find(f.FileName(), false)
|
||||
xmpName := fs.SidecarXMP.Find(f.FileName(), false)
|
||||
|
||||
event.Publish("index.converting", event.Data{
|
||||
"fileType": f.FileType(),
|
||||
@ -81,10 +85,12 @@ func (c *Convert) ToJpeg(f *MediaFile, force bool) (*MediaFile, error) {
|
||||
return NewMediaFile(jpegName)
|
||||
}
|
||||
|
||||
cmd, useMutex, err := c.JpegConvertCommand(f, jpegName, xmpName)
|
||||
cmds, useMutex, err := c.JpegConvertCommands(f, jpegName, xmpName)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if len(cmds) == 0 {
|
||||
return nil, fmt.Errorf("file type %s not supported", f.FileType())
|
||||
}
|
||||
|
||||
if useMutex {
|
||||
@ -98,47 +104,78 @@ func (c *Convert) ToJpeg(f *MediaFile, force bool) (*MediaFile, error) {
|
||||
return NewMediaFile(jpegName)
|
||||
}
|
||||
|
||||
// Fetch command output.
|
||||
var out bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Env = []string{fmt.Sprintf("HOME=%s", c.conf.CmdCachePath())}
|
||||
for _, cmd := range cmds {
|
||||
// Fetch command output.
|
||||
var out bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Env = []string{fmt.Sprintf("HOME=%s", c.conf.CmdCachePath())}
|
||||
|
||||
log.Infof("convert: converting %s to %s (%s)", clean.Log(filepath.Base(fileName)), clean.Log(filepath.Base(jpegName)), filepath.Base(cmd.Path))
|
||||
log.Infof("convert: converting %s to %s (%s)", clean.Log(filepath.Base(fileName)), clean.Log(filepath.Base(jpegName)), filepath.Base(cmd.Path))
|
||||
|
||||
// Log exact command for debugging in trace mode.
|
||||
log.Trace(cmd.String())
|
||||
// Log exact command for debugging in trace mode.
|
||||
log.Trace(cmd.String())
|
||||
|
||||
// Run convert command.
|
||||
if err := cmd.Run(); err != nil {
|
||||
if stderr.String() != "" {
|
||||
return nil, errors.New(stderr.String())
|
||||
// Run convert command.
|
||||
if err = cmd.Run(); err != nil {
|
||||
if stderr.String() != "" {
|
||||
err = errors.New(stderr.String())
|
||||
}
|
||||
|
||||
log.Tracef("convert: %s (%s)", err, filepath.Base(cmd.Path))
|
||||
continue
|
||||
} else if fs.FileExistsNotEmpty(jpegName) {
|
||||
log.Infof("convert: %s created in %s (%s)", clean.Log(filepath.Base(jpegName)), time.Since(start), filepath.Base(cmd.Path))
|
||||
break
|
||||
} else if res := out.Bytes(); len(res) < 512 || !mimetype.Detect(res).Is(fs.MimeTypeJpeg) {
|
||||
continue
|
||||
} else if err = os.WriteFile(jpegName, res, os.ModePerm); err != nil {
|
||||
log.Tracef("convert: %s (%s)", err, filepath.Base(cmd.Path))
|
||||
continue
|
||||
} else {
|
||||
return nil, err
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("convert: %s created in %s (%s)", clean.Log(filepath.Base(jpegName)), time.Since(start), filepath.Base(cmd.Path))
|
||||
// Ok?
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewMediaFile(jpegName)
|
||||
}
|
||||
|
||||
// JpegConvertCommand returns the command for converting files to JPEG, depending on the format.
|
||||
func (c *Convert) JpegConvertCommand(f *MediaFile, jpegName string, xmpName string) (result *exec.Cmd, useMutex bool, err error) {
|
||||
// JpegConvertCommands returns the command for converting files to JPEG, depending on the format.
|
||||
func (c *Convert) JpegConvertCommands(f *MediaFile, jpegName string, xmpName string) (result []*exec.Cmd, useMutex bool, err error) {
|
||||
result = make([]*exec.Cmd, 0, 2)
|
||||
|
||||
if f == nil {
|
||||
return result, useMutex, fmt.Errorf("file is nil - possible bug")
|
||||
}
|
||||
|
||||
// Find conversion command depending on the file type and runtime environment.
|
||||
fileExt := f.Extension()
|
||||
maxSize := strconv.Itoa(c.conf.JpegSize())
|
||||
|
||||
// Select conversion command depending on the file type and runtime environment.
|
||||
if (f.IsRaw() || f.IsHEIF() || f.IsAVIF()) && c.conf.SipsEnabled() && c.sipsBlacklist.Ok(fileExt) {
|
||||
result = exec.Command(c.conf.SipsBin(), "-Z", maxSize, "-s", "format", "jpeg", "--out", jpegName, f.FileName())
|
||||
} else if f.IsRaw() && c.conf.RawEnabled() || f.IsAVIF() {
|
||||
if c.conf.DarktableEnabled() && c.darktableBlacklist.Ok(fileExt) {
|
||||
// Apple Scriptable image processing system: https://ss64.com/osx/sips.html
|
||||
if (f.IsRaw() || f.IsHEIC() || f.IsAVIF()) && c.conf.SipsEnabled() && c.sipsBlacklist.Allow(fileExt) {
|
||||
result = append(result, exec.Command(c.conf.SipsBin(), "-Z", maxSize, "-s", "format", "jpeg", "--out", jpegName, f.FileName()))
|
||||
}
|
||||
|
||||
// Use heif-convert for HEIC/HEIF and AVIF image files.
|
||||
if (f.IsHEIC() || f.IsAVIF()) && c.conf.HeifConvertEnabled() {
|
||||
result = append(result, exec.Command(c.conf.HeifConvertBin(), "-q", c.conf.JpegQuality().String(), f.FileName(), jpegName))
|
||||
}
|
||||
|
||||
// Video thumbnails can be created with FFmpeg.
|
||||
if f.IsVideo() && c.conf.FFmpegEnabled() {
|
||||
result = append(result, exec.Command(c.conf.FFmpegBin(), "-y", "-i", f.FileName(), "-ss", "00:00:00.001", "-vframes", "1", jpegName))
|
||||
}
|
||||
|
||||
// RAW files may be concerted with Darktable and Rawtherapee.
|
||||
if f.IsRaw() && c.conf.RawEnabled() {
|
||||
if c.conf.DarktableEnabled() && c.darktableBlacklist.Allow(fileExt) {
|
||||
var args []string
|
||||
|
||||
// Set RAW, XMP, and JPEG filenames.
|
||||
@ -168,28 +205,37 @@ func (c *Convert) JpegConvertCommand(f *MediaFile, jpegName string, xmpName stri
|
||||
args = append(args, "--cachedir", dir)
|
||||
}
|
||||
|
||||
result = exec.Command(c.conf.DarktableBin(), args...)
|
||||
} else if c.conf.RawtherapeeEnabled() && c.rawtherapeeBlacklist.Ok(fileExt) {
|
||||
result = append(result, exec.Command(c.conf.DarktableBin(), args...))
|
||||
}
|
||||
|
||||
if c.conf.RawtherapeeEnabled() && c.rawtherapeeBlacklist.Allow(fileExt) {
|
||||
jpegQuality := fmt.Sprintf("-j%d", c.conf.JpegQuality())
|
||||
profile := filepath.Join(conf.AssetsPath(), "profiles", "raw.pp3")
|
||||
|
||||
args := []string{"-o", jpegName, "-p", profile, "-s", "-d", jpegQuality, "-js3", "-b8", "-c", f.FileName()}
|
||||
|
||||
result = exec.Command(c.conf.RawtherapeeBin(), args...)
|
||||
} else {
|
||||
return nil, useMutex, fmt.Errorf("no suitable converter found")
|
||||
result = append(result, exec.Command(c.conf.RawtherapeeBin(), args...))
|
||||
}
|
||||
} else if f.IsVideo() && c.conf.FFmpegEnabled() {
|
||||
result = exec.Command(c.conf.FFmpegBin(), "-y", "-i", f.FileName(), "-ss", "00:00:00.001", "-vframes", "1", jpegName)
|
||||
} else if f.IsHEIF() && c.conf.HeifConvertEnabled() {
|
||||
result = exec.Command(c.conf.HeifConvertBin(), "-q", c.conf.JpegQuality().String(), f.FileName(), jpegName)
|
||||
} else {
|
||||
return nil, useMutex, fmt.Errorf("file type %s not supported", f.FileType())
|
||||
}
|
||||
|
||||
// Extract preview image from DNG files.
|
||||
if f.IsDNG() && c.conf.ExifToolEnabled() {
|
||||
// Example: exiftool -b -PreviewImage -w IMG_4691.DNG.jpg IMG_4691.DNG
|
||||
result = append(result, exec.Command(c.conf.ExifToolBin(), "-q", "-q", "-b", "-PreviewImage", f.FileName()))
|
||||
}
|
||||
|
||||
// No suitable converter found?
|
||||
if len(result) == 0 {
|
||||
return result, useMutex, fmt.Errorf("file type %s not supported", f.FileType())
|
||||
}
|
||||
|
||||
// Log convert command in trace mode only as it exposes server internals.
|
||||
if result != nil {
|
||||
log.Tracef("convert: %s", result.String())
|
||||
for i, cmd := range result {
|
||||
if i == 0 {
|
||||
log.Tracef("convert: %s", cmd.String())
|
||||
} else {
|
||||
log.Tracef("convert: %s (alternative)", cmd.String())
|
||||
}
|
||||
}
|
||||
|
||||
return result, useMutex, nil
|
||||
|
@ -245,7 +245,7 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
|
||||
photo.PhotoStack = entity.IsStackable
|
||||
}
|
||||
|
||||
if yamlName := fs.YamlFile.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), stripSequence); yamlName != "" {
|
||||
if yamlName := fs.SidecarYAML.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), stripSequence); yamlName != "" {
|
||||
if err := photo.LoadFromYaml(yamlName); err != nil {
|
||||
log.Errorf("index: %s in %s (restore from yaml)", err.Error(), logName)
|
||||
} else {
|
||||
@ -431,7 +431,7 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
|
||||
log.Warn(err.Error())
|
||||
file.FileError = err.Error()
|
||||
}
|
||||
case m.IsRaw(), m.IsHEIF(), m.IsImageOther():
|
||||
case m.IsRaw(), m.IsDNG(), m.IsHEIC(), m.IsAVIF(), m.IsImageOther():
|
||||
if metaData := m.MetaData(); metaData.Error == nil {
|
||||
// Update basic metadata.
|
||||
photo.SetTitle(metaData.Title, entity.SrcMeta)
|
||||
|
@ -3,12 +3,11 @@ package photoprism
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/classify"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/face"
|
||||
"github.com/photoprism/photoprism/internal/nsfw"
|
||||
)
|
||||
|
@ -352,7 +352,7 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e
|
||||
matches = append(matches, name)
|
||||
}
|
||||
|
||||
isHEIF := false
|
||||
isHEIC := false
|
||||
|
||||
for _, fileName := range matches {
|
||||
f, fileErr := NewMediaFile(fileName)
|
||||
@ -371,14 +371,16 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e
|
||||
result.Main = f
|
||||
} else if f.IsRaw() {
|
||||
result.Main = f
|
||||
} else if f.IsDNG() {
|
||||
result.Main = f
|
||||
} else if f.IsAVIF() {
|
||||
result.Main = f
|
||||
} else if f.IsHEIF() {
|
||||
isHEIF = true
|
||||
} else if f.IsHEIC() {
|
||||
isHEIC = true
|
||||
result.Main = f
|
||||
} else if f.IsImageOther() {
|
||||
result.Main = f
|
||||
} else if f.IsVideo() && !isHEIF {
|
||||
} else if f.IsVideo() && !isHEIC {
|
||||
result.Main = f
|
||||
} else if result.Main != nil && f.IsJpeg() {
|
||||
if result.Main.IsJpeg() && len(result.Main.FileName()) > len(f.FileName()) {
|
||||
@ -709,7 +711,7 @@ func (m *MediaFile) Extension() string {
|
||||
// IsJpeg return true if this media file is a JPEG image.
|
||||
func (m *MediaFile) IsJpeg() bool {
|
||||
// Don't import/use existing thumbnail files (we create our own)
|
||||
if m.Extension() == ".thm" {
|
||||
if m.Extension() == fs.ExtTHM {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -731,9 +733,14 @@ func (m *MediaFile) IsTiff() bool {
|
||||
return m.HasFileType(fs.ImageTIFF) && m.MimeType() == fs.MimeTypeTiff
|
||||
}
|
||||
|
||||
// IsHEIF returns true if this is a High Efficiency Image File Format image.
|
||||
func (m *MediaFile) IsHEIF() bool {
|
||||
return m.MimeType() == fs.MimeTypeHEIF
|
||||
// IsDNG returns true if this is a Adobe Digital Negative image.
|
||||
func (m *MediaFile) IsDNG() bool {
|
||||
return m.MimeType() == fs.MimeTypeDNG
|
||||
}
|
||||
|
||||
// IsHEIC returns true if this is a High Efficiency Image File Format image.
|
||||
func (m *MediaFile) IsHEIC() bool {
|
||||
return m.MimeType() == fs.MimeTypeHEIC
|
||||
}
|
||||
|
||||
// IsAVIF returns true if this is an AV1 Image File Format image.
|
||||
@ -768,7 +775,7 @@ func (m *MediaFile) IsAnimated() bool {
|
||||
|
||||
// IsJson return true if this media file is a json sidecar file.
|
||||
func (m *MediaFile) IsJson() bool {
|
||||
return m.HasFileType(fs.JsonFile)
|
||||
return m.HasFileType(fs.SidecarJSON)
|
||||
}
|
||||
|
||||
// FileType returns the file type (jpg, gif, tiff,...).
|
||||
@ -780,12 +787,14 @@ func (m *MediaFile) FileType() fs.Type {
|
||||
return fs.ImagePNG
|
||||
case m.IsGif():
|
||||
return fs.ImageGIF
|
||||
case m.IsAVIF():
|
||||
return fs.ImageAVIF
|
||||
case m.IsHEIF():
|
||||
return fs.ImageHEIF
|
||||
case m.IsBitmap():
|
||||
return fs.ImageBMP
|
||||
case m.IsDNG():
|
||||
return fs.ImageDNG
|
||||
case m.IsAVIF():
|
||||
return fs.ImageAVIF
|
||||
case m.IsHEIC():
|
||||
return fs.ImageHEIF
|
||||
default:
|
||||
return fs.FileType(m.fileName)
|
||||
}
|
||||
@ -807,12 +816,12 @@ func (m *MediaFile) HasFileType(fileType fs.Type) bool {
|
||||
|
||||
// IsRaw returns true if this is a RAW file.
|
||||
func (m *MediaFile) IsRaw() bool {
|
||||
return m.HasFileType(fs.RawImage)
|
||||
return m.HasFileType(fs.ImageRaw) || m.IsDNG()
|
||||
}
|
||||
|
||||
// IsXMP returns true if this is a XMP sidecar file.
|
||||
func (m *MediaFile) IsXMP() bool {
|
||||
return m.FileType() == fs.XmpFile
|
||||
return m.FileType() == fs.SidecarXMP
|
||||
}
|
||||
|
||||
// InOriginals checks if the file is stored in the 'originals' folder.
|
||||
@ -852,12 +861,12 @@ func (m *MediaFile) IsImageNative() bool {
|
||||
|
||||
// IsImage checks if the file is an image
|
||||
func (m *MediaFile) IsImage() bool {
|
||||
return m.IsImageNative() || m.IsRaw() || m.IsAVIF() || m.IsHEIF()
|
||||
return m.IsImageNative() || m.IsRaw() || m.IsDNG() || m.IsAVIF() || m.IsHEIC()
|
||||
}
|
||||
|
||||
// IsLive checks if the file is a live photo.
|
||||
func (m *MediaFile) IsLive() bool {
|
||||
if m.IsHEIF() {
|
||||
if m.IsHEIC() {
|
||||
return fs.VideoMOV.FindFirst(m.FileName(), []string{}, Config().OriginalsPath(), false) != ""
|
||||
}
|
||||
|
||||
@ -870,12 +879,12 @@ func (m *MediaFile) IsLive() bool {
|
||||
|
||||
// ExifSupported returns true if parsing exif metadata is supported for the media file type.
|
||||
func (m *MediaFile) ExifSupported() bool {
|
||||
return m.IsJpeg() || m.IsRaw() || m.IsHEIF() || m.IsPng() || m.IsTiff()
|
||||
return m.IsJpeg() || m.IsRaw() || m.IsHEIC() || m.IsPng() || m.IsTiff()
|
||||
}
|
||||
|
||||
// IsMedia returns true if this is a media file (photo or video, not sidecar or other).
|
||||
func (m *MediaFile) IsMedia() bool {
|
||||
return m.IsJpeg() || m.IsVideo() || m.IsRaw() || m.IsAVIF() || m.IsHEIF() || m.IsImageOther()
|
||||
return m.IsImageNative() || m.IsVideo() || m.IsRaw() || m.IsDNG() || m.IsAVIF() || m.IsHEIC()
|
||||
}
|
||||
|
||||
// Jpeg returns the JPEG version of the media file (if exists).
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/meta"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
@ -17,7 +16,7 @@ func (m *MediaFile) HasSidecarJson() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
return fs.JsonFile.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false) != ""
|
||||
return fs.SidecarJSON.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false) != ""
|
||||
}
|
||||
|
||||
// SidecarJsonName returns the corresponding JSON sidecar file name as used by Google Photos (and potentially other apps).
|
||||
@ -84,7 +83,7 @@ func (m *MediaFile) MetaData() (result meta.Data) {
|
||||
|
||||
// Parse regular JSON sidecar files ("img_1234.json")
|
||||
if !m.IsSidecar() {
|
||||
if jsonFiles := fs.JsonFile.FindAll(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false); len(jsonFiles) == 0 {
|
||||
if jsonFiles := fs.SidecarJSON.FindAll(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false); len(jsonFiles) == 0 {
|
||||
log.Tracef("metadata: found no additional sidecar file for %s", clean.Log(filepath.Base(m.FileName())))
|
||||
} else {
|
||||
for _, jsonFile := range jsonFiles {
|
||||
|
@ -870,8 +870,10 @@ func TestMediaFile_MimeType(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "image/tiff", mediaFile.MimeType())
|
||||
|
||||
assert.Equal(t, "image/dng", mediaFile.MimeType())
|
||||
assert.True(t, mediaFile.IsDNG())
|
||||
assert.True(t, mediaFile.IsRaw())
|
||||
})
|
||||
|
||||
t.Run("iphone_7.xmp", func(t *testing.T) {
|
||||
@ -879,7 +881,7 @@ func TestMediaFile_MimeType(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "", mediaFile.MimeType())
|
||||
assert.Equal(t, "text/plain", mediaFile.MimeType())
|
||||
})
|
||||
|
||||
t.Run("iphone_7.json", func(t *testing.T) {
|
||||
@ -887,14 +889,14 @@ func TestMediaFile_MimeType(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "", mediaFile.MimeType())
|
||||
assert.Equal(t, "application/json", mediaFile.MimeType())
|
||||
})
|
||||
t.Run("fox.profile0.8bpc.yuv420.avif", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/fox.profile0.8bpc.yuv420.avif")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "image/avif", mediaFile.MimeType())
|
||||
assert.Equal(t, fs.MimeTypeAVIF, mediaFile.MimeType())
|
||||
assert.True(t, mediaFile.IsAVIF())
|
||||
})
|
||||
t.Run("iphone_7.heic", func(t *testing.T) {
|
||||
@ -902,15 +904,15 @@ func TestMediaFile_MimeType(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "image/heif", mediaFile.MimeType())
|
||||
assert.True(t, mediaFile.IsHEIF())
|
||||
assert.Equal(t, fs.MimeTypeHEIC, mediaFile.MimeType())
|
||||
assert.True(t, mediaFile.IsHEIC())
|
||||
})
|
||||
t.Run("IMG_4120.AAE", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/IMG_4120.AAE")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "", mediaFile.MimeType())
|
||||
assert.Equal(t, fs.MimeTypeXML, mediaFile.MimeType())
|
||||
})
|
||||
|
||||
t.Run("earth.mov", func(t *testing.T) {
|
||||
@ -1130,28 +1132,28 @@ func TestMediaFile_IsHEIF(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, false, mediaFile.IsHEIF())
|
||||
assert.Equal(t, false, mediaFile.IsHEIC())
|
||||
})
|
||||
t.Run("iphone_7.heic", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, true, mediaFile.IsHEIF())
|
||||
assert.Equal(t, true, mediaFile.IsHEIC())
|
||||
})
|
||||
t.Run("canon_eos_6d.dng", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/canon_eos_6d.dng")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, false, mediaFile.IsHEIF())
|
||||
assert.Equal(t, false, mediaFile.IsHEIC())
|
||||
})
|
||||
t.Run("elephants.jpg", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/elephants.jpg")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, false, mediaFile.IsHEIF())
|
||||
assert.Equal(t, false, mediaFile.IsHEIC())
|
||||
})
|
||||
}
|
||||
|
||||
@ -1220,8 +1222,8 @@ func TestMediaFile_IsTiff(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, fs.JsonFile, mediaFile.FileType())
|
||||
assert.Equal(t, "", mediaFile.MimeType())
|
||||
assert.Equal(t, fs.SidecarJSON, mediaFile.FileType())
|
||||
assert.Equal(t, fs.MimeTypeJSON, mediaFile.MimeType())
|
||||
assert.Equal(t, false, mediaFile.IsTiff())
|
||||
})
|
||||
t.Run("purple.tiff", func(t *testing.T) {
|
||||
|
@ -11,7 +11,7 @@ func AccountUploads(a entity.Account, limit int) (results entity.Files, err erro
|
||||
Where("files.id NOT IN (SELECT file_id FROM files_sync WHERE file_id > 0 AND account_id = ?)", a.ID)
|
||||
|
||||
if !a.SyncRaw {
|
||||
s = s.Where("files.file_type <> ? OR files.file_type IS NULL", fs.RawImage)
|
||||
s = s.Where("files.file_type <> ? OR files.file_type IS NULL", fs.ImageRaw)
|
||||
}
|
||||
|
||||
s = s.Order("files.file_name ASC")
|
||||
|
Loading…
x
Reference in New Issue
Block a user