Index: Improve file size/resolution checks, add WebP support #1017 #1226

Renames the config flag to from "megapixel-limit" to "resolution-limit".
Adds native support for the WebP image file format.
This commit is contained in:
Michael Mayer 2022-04-02 18:04:02 +02:00
parent 05a18bf6f2
commit a604e9a9c6
34 changed files with 833 additions and 485 deletions

View file

@ -49,12 +49,8 @@ func StartIndexing(router *gin.RouterGroup) {
ind := service.Index()
indOpt := photoprism.IndexOptions{
Rescan: f.Rescan,
Convert: conf.Settings().Index.Convert && conf.SidecarWritable(),
Path: filepath.Clean(f.Path),
Stack: true,
}
convert := conf.Settings().Index.Convert && conf.SidecarWritable()
indOpt := photoprism.NewIndexOptions(filepath.Clean(f.Path), f.Rescan, convert, true, false)
if len(indOpt.Path) > 1 {
event.InfoMsg(i18n.MsgIndexingFiles, sanitize.Log(indOpt.Path))

View file

@ -59,12 +59,8 @@ func Index() error {
ind := service.Index()
indOpt := photoprism.IndexOptions{
Rescan: false,
Convert: conf.Settings().Index.Convert && conf.SidecarWritable(),
Path: entity.RootPath,
Stack: true,
}
convert := conf.Settings().Index.Convert && conf.SidecarWritable()
indOpt := photoprism.NewIndexOptions(entity.RootPath, false, convert, true, false)
indexed := ind.Start(indOpt)

View file

@ -42,7 +42,7 @@ func configAction(ctx *cli.Context) error {
// Originals.
fmt.Printf("%-25s %s\n", "originals-path", conf.OriginalsPath())
fmt.Printf("%-25s %d\n", "originals-limit", conf.OriginalsLimit())
fmt.Printf("%-25s %d\n", "megapixel-limit", conf.MegapixelLimit())
fmt.Printf("%-25s %d\n", "resolution-limit", conf.ResolutionLimit())
// Other paths.
fmt.Printf("%-25s %s\n", "storage-path", conf.StoragePath())

View file

@ -251,13 +251,8 @@ func facesIndexAction(ctx *cli.Context) error {
var indexed fs.Done
if w := service.Index(); w != nil {
opt := photoprism.IndexOptions{
Path: subPath,
Rescan: true,
Convert: conf.Settings().Index.Convert && conf.SidecarWritable(),
Stack: true,
FacesOnly: true,
}
convert := conf.Settings().Index.Convert && conf.SidecarWritable()
opt := photoprism.NewIndexOptions(subPath, true, convert, true, true)
indexed = w.Start(opt)
}

View file

@ -69,12 +69,8 @@ func indexAction(ctx *cli.Context) error {
var indexed fs.Done
if w := service.Index(); w != nil {
opt := photoprism.IndexOptions{
Path: subPath,
Rescan: ctx.Bool("force"),
Convert: conf.Settings().Index.Convert && conf.SidecarWritable(),
Stack: true,
}
convert := conf.Settings().Index.Convert && conf.SidecarWritable()
opt := photoprism.NewIndexOptions(subPath, ctx.Bool("force"), convert, true, false)
indexed = w.Start(opt)
}

View file

@ -578,8 +578,8 @@ func (c *Config) GeoApi() string {
return "places"
}
// OriginalsLimit returns the maximum size of originals in megabytes.
func (c *Config) OriginalsLimit() int64 {
// OriginalsLimit returns the maximum size of originals in MB.
func (c *Config) OriginalsLimit() int {
if c.options.OriginalsLimit <= 0 || c.options.OriginalsLimit > 100000 {
return -1
}
@ -589,26 +589,24 @@ func (c *Config) OriginalsLimit() int64 {
// OriginalsLimitBytes returns the maximum size of originals in bytes.
func (c *Config) OriginalsLimitBytes() int64 {
if megabyte := c.OriginalsLimit(); megabyte < 1 {
if result := c.OriginalsLimit(); result <= 0 {
return -1
} else {
return megabyte * 1024 * 1024
return int64(result) * 1024 * 1024
}
}
// MegapixelLimit returns the maximum resolution of originals in megapixels (width x height).
func (c *Config) MegapixelLimit() int {
mp := c.options.MegapixelLimit
// ResolutionLimit returns the maximum resolution of originals in megapixels (width x height).
func (c *Config) ResolutionLimit() int {
result := c.options.ResolutionLimit
if mp < 0 {
if result <= 0 {
return -1
} else if c.options.MegapixelLimit > 900 {
mp = 900
} else if c.options.MegapixelLimit == 0 {
mp = 100
} else if result > 900 {
result = 900
}
return mp
return result
}
// UpdateHub updates backend api credentials for maps & places.

View file

@ -306,9 +306,9 @@ func TestConfig_GeoApi(t *testing.T) {
func TestConfig_OriginalsLimit(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, int64(-1), c.OriginalsLimit())
assert.Equal(t, -1, c.OriginalsLimit())
c.options.OriginalsLimit = 800
assert.Equal(t, int64(800), c.OriginalsLimit())
assert.Equal(t, 800, c.OriginalsLimit())
}
func TestConfig_OriginalsLimitBytes(t *testing.T) {
@ -319,16 +319,18 @@ func TestConfig_OriginalsLimitBytes(t *testing.T) {
assert.Equal(t, int64(838860800), c.OriginalsLimitBytes())
}
func TestConfig_MegapixelLimit(t *testing.T) {
func TestConfig_ResolutionLimit(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, 100, c.MegapixelLimit())
c.options.MegapixelLimit = 800
assert.Equal(t, 800, c.MegapixelLimit())
c.options.MegapixelLimit = 950
assert.Equal(t, 900, c.MegapixelLimit())
c.options.MegapixelLimit = -1
assert.Equal(t, -1, c.MegapixelLimit())
assert.Equal(t, -1, c.ResolutionLimit())
c.options.ResolutionLimit = 800
assert.Equal(t, 800, c.ResolutionLimit())
c.options.ResolutionLimit = 950
assert.Equal(t, 900, c.ResolutionLimit())
c.options.ResolutionLimit = 0
assert.Equal(t, -1, c.ResolutionLimit())
c.options.ResolutionLimit = -1
assert.Equal(t, -1, c.ResolutionLimit())
}
func TestConfig_BaseUri(t *testing.T) {

View file

@ -77,29 +77,29 @@ var GlobalFlags = []cli.Flag{
EnvVar: "PHOTOPRISM_CONFIG_FILE",
},
cli.StringFlag{
Name: "config-path",
Name: "config-path, conf",
Usage: "config `PATH` to be searched for additional configuration and settings files",
EnvVar: "PHOTOPRISM_CONFIG_PATH",
},
cli.StringFlag{
Name: "originals-path, o",
Name: "originals-path, media",
Usage: "storage `PATH` of your original media files (photos and videos)",
EnvVar: "PHOTOPRISM_ORIGINALS_PATH",
},
cli.IntFlag{
Name: "originals-limit",
Name: "originals-limit, max-mb",
Value: 1000,
Usage: "maximum size of media files in `MEGABYTES` (1-100000; -1 to disable)",
EnvVar: "PHOTOPRISM_ORIGINALS_LIMIT",
},
cli.IntFlag{
Name: "megapixel-limit",
Name: "resolution-limit, max-mp",
Value: 100,
Usage: "maximum resolution of media files in `MEGAPIXELS` (1-900; -1 to disable)",
EnvVar: "PHOTOPRISM_MEGAPIXEL_LIMIT",
EnvVar: "PHOTOPRISM_RESOLUTION_LIMIT",
},
cli.StringFlag{
Name: "storage-path, t",
Name: "storage-path, storage",
Usage: "writable storage `PATH` for cache, database, and sidecar files",
EnvVar: "PHOTOPRISM_STORAGE_PATH",
},
@ -124,12 +124,12 @@ var GlobalFlags = []cli.Flag{
EnvVar: "PHOTOPRISM_TEMP_PATH",
},
cli.StringFlag{
Name: "backup-path, b",
Name: "backup-path",
Usage: "custom backup `PATH` for index backup files (optional)",
EnvVar: "PHOTOPRISM_BACKUP_PATH",
},
cli.StringFlag{
Name: "assets-path, a",
Name: "assets-path",
Usage: "assets `PATH` containing static resources like icons, models, and translations",
EnvVar: "PHOTOPRISM_ASSETS_PATH",
},
@ -387,31 +387,31 @@ var GlobalFlags = []cli.Flag{
},
cli.StringFlag{
Name: "darktable-bin",
Usage: "Darktable CLI `COMMAND` for RAW image conversion",
Usage: "Darktable CLI `COMMAND` for RAW to JPEG conversion",
Value: "darktable-cli",
EnvVar: "PHOTOPRISM_DARKTABLE_BIN",
},
cli.StringFlag{
Name: "darktable-blacklist",
Usage: "file `EXTENSIONS` incompatible with Darktable",
Usage: "do not use Darktable to convert files with these `EXTENSIONS`",
Value: "dng,cr3",
EnvVar: "PHOTOPRISM_DARKTABLE_BLACKLIST",
},
cli.StringFlag{
Name: "rawtherapee-bin",
Usage: "RawTherapee CLI `COMMAND` for RAW image conversion",
Usage: "RawTherapee CLI `COMMAND` for RAW to JPEG conversion",
Value: "rawtherapee-cli",
EnvVar: "PHOTOPRISM_RAWTHERAPEE_BIN",
},
cli.StringFlag{
Name: "rawtherapee-blacklist",
Usage: "file `EXTENSIONS` incompatible with RawTherapee",
Usage: "do not use RawTherapee to convert files with these `EXTENSIONS`",
Value: "",
EnvVar: "PHOTOPRISM_RAWTHERAPEE_BLACKLIST",
},
cli.StringFlag{
Name: "sips-bin",
Usage: "Sips `COMMAND` for RAW image conversion (macOS only)",
Usage: "Sips `COMMAND` for RAW to JPEG conversion (macOS only)",
Value: "sips",
EnvVar: "PHOTOPRISM_SIPS_BIN",
},
@ -469,7 +469,7 @@ var GlobalFlags = []cli.Flag{
},
cli.StringFlag{
Name: "thumb-colorspace",
Usage: "convert Apple Display P3 colors in thumbnails to standard color space",
Usage: "convert Apple Display P3 colors in thumbnails to standard color space (\"\" to disable)",
Value: "sRGB",
EnvVar: "PHOTOPRISM_THUMB_COLORSPACE",
},

View file

@ -51,8 +51,8 @@ type Options struct {
ConfigPath string `yaml:"ConfigPath" json:"-" flag:"config-path"`
ConfigFile string `json:"-"`
OriginalsPath string `yaml:"OriginalsPath" json:"-" flag:"originals-path"`
OriginalsLimit int64 `yaml:"OriginalsLimit" json:"OriginalsLimit" flag:"originals-limit"`
MegapixelLimit int `yaml:"MegapixelLimit" json:"MegapixelLimit" flag:"megapixel-limit"`
OriginalsLimit int `yaml:"OriginalsLimit" json:"OriginalsLimit" flag:"originals-limit"`
ResolutionLimit int `yaml:"ResolutionLimit" json:"ResolutionLimit" flag:"resolution-limit"`
StoragePath string `yaml:"StoragePath" json:"-" flag:"storage-path"`
ImportPath string `yaml:"ImportPath" json:"-" flag:"import-path"`
CachePath string `yaml:"CachePath" json:"-" flag:"cache-path"`

View file

@ -75,30 +75,32 @@ func NewTestOptions(pkg string) *Options {
// Test config options.
c := &Options{
Name: "PhotoPrism",
Version: "0.0.0",
Copyright: "(c) 2018-2022 Michael Mayer",
Test: true,
Debug: true,
Public: true,
Experimental: true,
ReadOnly: false,
DetectNSFW: true,
UploadNSFW: false,
ExifBruteForce: false,
AssetsPath: assetsPath,
AutoIndex: -1,
AutoImport: 7200,
StoragePath: testDataPath,
CachePath: testDataPath + "/cache",
OriginalsPath: testDataPath + "/originals",
ImportPath: testDataPath + "/import",
TempPath: testDataPath + "/temp",
ConfigPath: testDataPath + "/config",
SidecarPath: testDataPath + "/sidecar",
DatabaseDriver: driver,
DatabaseDsn: dsn,
AdminPassword: "photoprism",
Name: "PhotoPrism",
Version: "0.0.0",
Copyright: "(c) 2018-2022 Michael Mayer",
Test: true,
Debug: true,
Public: true,
Experimental: true,
ReadOnly: false,
DetectNSFW: true,
UploadNSFW: false,
ExifBruteForce: false,
AssetsPath: assetsPath,
AutoIndex: -1,
AutoImport: 7200,
StoragePath: testDataPath,
CachePath: testDataPath + "/cache",
OriginalsPath: testDataPath + "/originals",
ImportPath: testDataPath + "/import",
TempPath: testDataPath + "/temp",
ConfigPath: testDataPath + "/config",
SidecarPath: testDataPath + "/sidecar",
DatabaseDriver: driver,
DatabaseDsn: dsn,
AdminPassword: "photoprism",
OriginalsLimit: 66,
ResolutionLimit: 33,
}
return c

View file

@ -100,12 +100,8 @@ func (imp *Import) Start(opt ImportOptions) fs.Done {
filesImported := 0
indexOpt := IndexOptions{
Path: "/",
Rescan: true,
Stack: true,
Convert: imp.conf.Settings().Index.Convert && imp.conf.SidecarWritable(),
}
convert := imp.conf.Settings().Index.Convert && imp.conf.SidecarWritable()
indexOpt := NewIndexOptions("/", true, convert, true, false)
ignore := fs.NewIgnoreList(fs.IgnoreFile, true, false)

View file

@ -23,17 +23,19 @@ type ImportJob struct {
func ImportWorker(jobs <-chan ImportJob) {
for job := range jobs {
var destMainFileName string
related := job.Related
o := job.IndexOpt
imp := job.Imp
opt := job.ImportOpt
indexOpt := job.IndexOpt
importPath := job.ImportOpt.Path
impOpt := job.ImportOpt
impPath := job.ImportOpt.Path
related := job.Related
if related.Main == nil {
log.Warnf("import: %s belongs to no supported media file", sanitize.Log(fs.RelName(job.FileName, importPath)))
log.Warnf("import: %s belongs to no supported media file", sanitize.Log(fs.RelName(job.FileName, impPath)))
continue
}
// Extract metadata to a JSON file with Exiftool.
if related.Main.NeedsExifToolJson() {
if jsonName, err := imp.convert.ToJson(related.Main); err != nil {
log.Debugf("import: %s in %s (extract metadata)", sanitize.Log(err.Error()), sanitize.Log(related.Main.BaseName()))
@ -44,7 +46,7 @@ func ImportWorker(jobs <-chan ImportJob) {
}
}
originalName := related.Main.RelName(importPath)
originalName := related.Main.RelName(impPath)
event.Publish("import.file", event.Data{
"fileName": originalName,
@ -52,7 +54,7 @@ func ImportWorker(jobs <-chan ImportJob) {
})
for _, f := range related.Files {
relFileName := f.RelName(importPath)
relFileName := f.RelName(impPath)
if destFileName, err := imp.DestinationFilename(related.Main, f); err == nil {
destDir := filepath.Dir(destFileName)
@ -78,7 +80,7 @@ func ImportWorker(jobs <-chan ImportJob) {
log.Infof("import: moving related %s file %s to %s", f.FileType(), sanitize.Log(relFileName), sanitize.Log(fs.RelName(destFileName, imp.originalsPath())))
}
if opt.Move {
if impOpt.Move {
if err := f.Move(destFileName); err != nil {
logRelName := sanitize.Log(fs.RelName(destMainFileName, imp.originalsPath()))
log.Debugf("import: %s", err.Error())
@ -99,12 +101,12 @@ func ImportWorker(jobs <-chan ImportJob) {
// Do nothing.
} else if file, err := entity.FirstFileByHash(fileHash); err != nil {
// Do nothing.
} else if err := entity.AddPhotoToAlbums(file.PhotoUID, opt.Albums); err != nil {
} else if err := entity.AddPhotoToAlbums(file.PhotoUID, impOpt.Albums); err != nil {
log.Warn(err)
}
// Remove duplicates to save storage.
if opt.RemoveExistingFiles {
if impOpt.RemoveExistingFiles {
if err := f.Remove(); err != nil {
log.Errorf("import: failed deleting %s (%s)", sanitize.Log(f.BaseName()), err.Error())
} else {
@ -122,71 +124,81 @@ func ImportWorker(jobs <-chan ImportJob) {
continue
}
// Extract metadata to a JSON file with Exiftool.
if f.NeedsExifToolJson() {
if jsonName, err := imp.convert.ToJson(f); err != nil {
log.Debugf("import: %s in %s (extract metadata)", sanitize.Log(err.Error()), sanitize.Log(f.BaseName()))
log.Debugf("import: %s in %s (extract metadata)", sanitize.Log(err.Error()), sanitize.Log(f.RootRelName()))
} else {
log.Debugf("import: created %s", filepath.Base(jsonName))
}
}
if indexOpt.Convert && f.IsMedia() && !f.HasJpeg() {
// Create JPEG sidecar for media files in other formats so that thumbnails can be created.
if o.Convert && f.IsMedia() && !f.HasJpeg() {
if jpegFile, err := imp.convert.ToJpeg(f); err != nil {
log.Errorf("import: %s in %s (convert to jpeg)", err.Error(), sanitize.Log(fs.RelName(destMainFileName, imp.originalsPath())))
log.Errorf("import: %s in %s (convert to jpeg)", err.Error(), sanitize.Log(f.RootRelName()))
continue
} else {
log.Debugf("import: created %s", sanitize.Log(jpegFile.BaseName()))
}
}
// Ensure that a JPEG and the configured default thumbnail sizes exist.
if jpg, err := f.Jpeg(); err != nil {
log.Error(err)
} else {
if err := jpg.ResampleDefault(imp.thumbPath(), false); err != nil {
log.Errorf("import: %s in %s (resample)", err.Error(), sanitize.Log(jpg.BaseName()))
continue
}
} else if exceeds, actual := jpg.ExceedsResolution(o.ResolutionLimit); exceeds {
log.Errorf("index: %s exceeds resolution limit (%d / %d MP)", sanitize.Log(f.RootRelName()), actual, o.ResolutionLimit)
continue
} else if err := jpg.CreateThumbnails(imp.thumbPath(), false); err != nil {
log.Errorf("import: failed creating thumbnails for %s (%s)", sanitize.Log(f.RootRelName()), err.Error())
continue
}
// Find related files.
related, err := f.RelatedFiles(imp.conf.Settings().StackSequences())
// Skip import if the finding related files results in an error.
if err != nil {
log.Errorf("import: %s in %s (find related files)", err.Error(), sanitize.Log(fs.RelName(destMainFileName, imp.originalsPath())))
continue
}
done := make(map[string]bool)
ind := imp.index
limitSize := ind.conf.OriginalsLimitBytes()
photoUID := ""
if related.Main != nil {
f := related.Main
// Enforce file size limit for originals.
if limitSize > 0 && f.FileSize() > limitSize {
log.Warnf("import: %s exceeds file size limit (%d / %d megabyte)", sanitize.Log(f.BaseName()), f.FileSize()/(1024*1024), limitSize/(1024*1024))
// Enforce file size and resolution limits.
if exceeds, actual := f.ExceedsFileSize(o.OriginalsLimit); exceeds {
log.Warnf("import: %s exceeds file size limit (%d / %d MB)", sanitize.Log(f.RootRelName()), actual, o.OriginalsLimit)
continue
} else if exceeds, actual = f.ExceedsResolution(o.ResolutionLimit); exceeds {
log.Warnf("import: %s exceeds resolution limit (%d / %d MP)", sanitize.Log(f.RootRelName()), actual, o.ResolutionLimit)
continue
}
res := ind.MediaFile(f, indexOpt, originalName, "")
// Index main MediaFile.
res := ind.MediaFile(f, o, originalName, "")
log.Infof("import: %s main %s file %s", res, f.FileType(), sanitize.Log(f.RelName(ind.originalsPath())))
// Log result.
log.Infof("import: %s main %s file %s", res, f.FileType(), sanitize.Log(f.RootRelName()))
done[f.FileName()] = true
if !res.Success() {
// Skip importing related files if the main file was not indexed successfully.
continue
} else if res.PhotoUID != "" {
photoUID = res.PhotoUID
if err := entity.AddPhotoToAlbums(photoUID, opt.Albums); err != nil {
// Add photo to album if a list of albums was provided when importing.
if err := entity.AddPhotoToAlbums(photoUID, impOpt.Albums); err != nil {
log.Warn(err)
}
}
} else {
log.Warnf("import: found no main file for %s, conversion to jpeg may have failed", fs.RelName(destMainFileName, imp.originalsPath()))
log.Warnf("import: found no main file for %s, conversion to jpeg may have failed", sanitize.Log(f.RootRelName()))
}
for _, f := range related.Files {
@ -200,30 +212,32 @@ func ImportWorker(jobs <-chan ImportJob) {
done[f.FileName()] = true
// Enforce file size limit for originals.
if limitSize > 0 && f.FileSize() > limitSize {
log.Warnf("import: %s exceeds file size limit (%d / %d megabyte)", sanitize.Log(f.BaseName()), f.FileSize()/(1024*1024), limitSize/(1024*1024))
continue
// Show warning if sidecar file exceeds size or resolution limit.
if exceeds, actual := f.ExceedsFileSize(o.OriginalsLimit); exceeds {
log.Warnf("import: sidecar file %s exceeds size limit (%d / %d MB)", sanitize.Log(f.RootRelName()), actual, o.OriginalsLimit)
} else if exceeds, actual = f.ExceedsResolution(o.ResolutionLimit); exceeds {
log.Warnf("import: sidecar file %s exceeds resolution limit (%d / %d MP)", sanitize.Log(f.RootRelName()), actual, o.ResolutionLimit)
}
// Extract metadata to a JSON file with Exiftool.
if f.NeedsExifToolJson() {
if jsonName, err := imp.convert.ToJson(f); err != nil {
log.Debugf("import: %s in %s (extract metadata)", sanitize.Log(err.Error()), sanitize.Log(f.BaseName()))
log.Debugf("import: %s in %s (extract metadata)", sanitize.Log(err.Error()), sanitize.Log(f.RootRelName()))
} else {
log.Debugf("import: created %s", filepath.Base(jsonName))
}
}
res := ind.MediaFile(f, indexOpt, "", photoUID)
// Index related MediaFile.
res := ind.MediaFile(f, o, "", photoUID)
if res.Indexed() && f.IsJpeg() {
if err := f.ResampleDefault(ind.thumbPath(), false); err != nil {
log.Errorf("import: failed creating thumbnails for %s (%s)", sanitize.Log(f.BaseName()), err.Error())
query.SetFileError(res.FileUID, err.Error())
}
// Save file error.
if fileUid, err := res.FileError(); err != nil {
query.SetFileError(fileUid, err.Error())
}
log.Infof("import: %s related %s file %s", res, f.FileType(), sanitize.Log(f.RelName(ind.originalsPath())))
// Log result.
log.Infof("import: %s related %s file %s", res, f.FileType(), sanitize.Log(f.RootRelName()))
}
}

View file

@ -71,7 +71,7 @@ func (ind *Index) Cancel() {
}
// Start indexes media files in the "originals" folder.
func (ind *Index) Start(opt IndexOptions) fs.Done {
func (ind *Index) Start(o IndexOptions) fs.Done {
defer func() {
if r := recover(); r != nil {
log.Errorf("index: %s (panic)\nstack: %s", r, debug.Stack())
@ -86,7 +86,7 @@ func (ind *Index) Start(opt IndexOptions) fs.Done {
}
originalsPath := ind.originalsPath()
optionsPath := filepath.Join(originalsPath, opt.Path)
optionsPath := filepath.Join(originalsPath, o.Path)
if !fs.PathExists(optionsPath) {
event.Error(fmt.Sprintf("index: %s does not exist", sanitize.Log(optionsPath)))
@ -186,7 +186,7 @@ func (ind *Index) Start(opt IndexOptions) fs.Done {
return nil
}
if ind.files.Indexed(relName, entity.RootOriginals, mf.modTime, opt.Rescan) {
if ind.files.Indexed(relName, entity.RootOriginals, mf.modTime, o.Rescan) {
return nil
}
@ -205,7 +205,7 @@ func (ind *Index) Start(opt IndexOptions) fs.Done {
continue
}
if f.FileSize() == 0 || ind.files.Indexed(f.RootRelName(), f.Root(), f.ModTime(), opt.Rescan) {
if f.FileSize() == 0 || ind.files.Indexed(f.RootRelName(), f.Root(), f.ModTime(), o.Rescan) {
done[f.FileName()] = fs.Found
continue
}
@ -227,7 +227,7 @@ func (ind *Index) Start(opt IndexOptions) fs.Done {
jobs <- IndexJob{
FileName: mf.FileName(),
Related: related,
IndexOpt: opt,
IndexOpt: o,
Ind: ind,
}

View file

@ -0,0 +1,78 @@
package photoprism
import (
"fmt"
"path/filepath"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/sanitize"
)
// IndexMain indexes the main file from a group of related files and returns the result.
func IndexMain(related *RelatedFiles, ind *Index, o IndexOptions) (result IndexResult) {
// Skip if main file is nil.
if related.Main == nil {
result.Err = fmt.Errorf("index: no main file for %s", sanitize.Log(related.String()))
result.Status = IndexFailed
return result
}
f := related.Main
// Enforce file size and resolution limits.
if exceeds, actual := f.ExceedsFileSize(o.OriginalsLimit); exceeds {
result.Err = fmt.Errorf("index: %s exceeds file size limit (%d / %d MB)", sanitize.Log(f.RootRelName()), actual, o.OriginalsLimit)
result.Status = IndexFailed
return result
} else if exceeds, actual = f.ExceedsResolution(o.ResolutionLimit); exceeds {
result.Err = fmt.Errorf("index: %s exceeds resolution limit (%d / %d MP)", sanitize.Log(f.RootRelName()), actual, o.ResolutionLimit)
result.Status = IndexFailed
return result
}
// Extract metadata to a JSON file with Exiftool.
if f.NeedsExifToolJson() {
if jsonName, err := ind.convert.ToJson(f); err != nil {
log.Debugf("index: %s in %s (extract metadata)", sanitize.Log(err.Error()), sanitize.Log(f.RootRelName()))
} else {
log.Debugf("index: created %s", filepath.Base(jsonName))
}
}
// Create JPEG sidecar for media files in other formats so that thumbnails can be created.
if o.Convert && f.IsMedia() && !f.HasJpeg() {
if jpg, err := ind.convert.ToJpeg(f); err != nil {
result.Err = fmt.Errorf("index: failed converting %s to jpeg (%s)", sanitize.Log(f.RootRelName()), err.Error())
result.Status = IndexFailed
return result
} else if exceeds, actual := jpg.ExceedsResolution(o.ResolutionLimit); exceeds {
result.Err = fmt.Errorf("index: %s exceeds resolution limit (%d / %d MP)", sanitize.Log(f.RootRelName()), actual, o.ResolutionLimit)
result.Status = IndexFailed
return result
} else {
log.Debugf("index: created %s", sanitize.Log(jpg.BaseName()))
if err := jpg.CreateThumbnails(ind.thumbPath(), false); err != nil {
result.Err = fmt.Errorf("index: failed creating thumbnails for %s (%s)", sanitize.Log(f.RootRelName()), err.Error())
result.Status = IndexFailed
return result
}
related.Files = append(related.Files, jpg)
}
}
// Index main MediaFile.
result = ind.MediaFile(f, o, "", "")
// Save file error.
if fileUid, err := result.FileError(); err != nil {
query.SetFileError(fileUid, err.Error())
}
// Log index result.
log.Infof("index: %s main %s file %s", result, f.FileType(), sanitize.Log(f.RootRelName()))
return result
}

View file

@ -130,13 +130,6 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID
}
}
// Skip media files that exceed the configured resolution limit, unless the file has already been indexed.
if !fileExists && m.ExceedsMegapixelLimit() {
log.Warnf("index: %s exceeds resolution limit (%d / %d megapixels)", logName, m.Megapixels(), Config().MegapixelLimit())
result.Status = IndexSkipped
return result
}
// Find existing photo if a photo uid was provided or file has not been indexed yet...
if !fileExists && photoUID != "" {
// Find existing photo by UID.
@ -242,6 +235,13 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID
log.Error(err)
}
// Create default thumbnails if needed.
if err := m.CreateThumbnails(ind.thumbPath(), false); err != nil {
result.Err = fmt.Errorf("index: failed creating thumbnails for %s (%s)", sanitize.Log(m.RootRelName()), err.Error())
result.Status = IndexFailed
return result
}
// Fetch photo details such as keywords, subject, and artist.
details := photo.GetDetails()

View file

@ -41,7 +41,9 @@ func TestIndex_MediaFile(t *testing.T) {
words := mediaFile.metaData.Keywords.String()
t.Logf("size in megapixel: %d", mediaFile.Megapixels())
t.Logf("megapixel limit exceeded: %t", mediaFile.ExceedsMegapixelLimit())
exceeds, actual := mediaFile.ExceedsResolution(conf.ResolutionLimit())
t.Logf("megapixel limit exceeded: %t, %d / %d MP", exceeds, actual, conf.ResolutionLimit())
assert.Contains(t, words, "marienkäfer")
assert.Contains(t, words, "burst")

View file

@ -1,59 +1,52 @@
package photoprism
// IndexOptions represents media file indexing options.
type IndexOptions struct {
Path string
Rescan bool
Convert bool
Stack bool
FacesOnly bool
Path string
Rescan bool
Convert bool
Stack bool
FacesOnly bool
OriginalsLimit int
ResolutionLimit int
}
// NewIndexOptions returns new index options instance.
func NewIndexOptions(path string, rescan, convert, stack, facesOnly bool) IndexOptions {
result := IndexOptions{
Path: path,
Rescan: rescan,
Convert: convert,
Stack: stack,
FacesOnly: facesOnly,
OriginalsLimit: Config().OriginalsLimit(),
ResolutionLimit: Config().ResolutionLimit(),
}
return result
}
// SkipUnchanged checks if unchanged media files should be skipped.
func (o *IndexOptions) SkipUnchanged() bool {
return !o.Rescan
}
// IndexOptionsAll returns new index options with all options set to true.
func IndexOptionsAll() IndexOptions {
result := IndexOptions{
Path: "/",
Rescan: true,
Convert: true,
Stack: true,
FacesOnly: false,
}
return result
return NewIndexOptions("/", true, true, true, false)
}
// IndexOptionsFacesOnly returns new index options for updating faces only.
func IndexOptionsFacesOnly() IndexOptions {
result := IndexOptions{
Path: "/",
Rescan: true,
Convert: true,
Stack: true,
FacesOnly: true,
}
return result
return NewIndexOptions("/", true, true, true, true)
}
// IndexOptionsSingle returns new index options for unstacked, single files.
func IndexOptionsSingle() IndexOptions {
result := IndexOptions{
Path: "/",
Rescan: true,
Convert: true,
Stack: false,
FacesOnly: false,
}
return result
return NewIndexOptions("/", true, true, false, false)
}
// IndexOptionsNone returns new index options with all options set to false.
func IndexOptionsNone() IndexOptions {
result := IndexOptions{}
return result
return NewIndexOptions("", false, false, false, false)
}

View file

@ -11,69 +11,8 @@ import (
"github.com/photoprism/photoprism/pkg/sanitize"
)
// IndexMain indexes the main file from a group of related files and returns the result.
func IndexMain(related *RelatedFiles, ind *Index, opt IndexOptions) (result IndexResult) {
// Skip if main file is nil.
if related.Main == nil {
result.Err = fmt.Errorf("index: no main file for %s", sanitize.Log(related.String()))
result.Status = IndexFailed
return result
}
f := related.Main
limitSize := ind.conf.OriginalsLimitBytes()
// Enforce file size limit for originals.
if limitSize > 0 && f.FileSize() > limitSize {
result.Err = fmt.Errorf("index: %s exceeds file size limit (%d / %d megabyte)", sanitize.Log(f.BaseName()), f.FileSize()/(1024*1024), limitSize/(1024*1024))
result.Status = IndexFailed
return result
}
if f.NeedsExifToolJson() {
if jsonName, err := ind.convert.ToJson(f); err != nil {
log.Debugf("index: %s in %s (extract metadata)", sanitize.Log(err.Error()), sanitize.Log(f.BaseName()))
} else {
log.Debugf("index: created %s", filepath.Base(jsonName))
}
}
if opt.Convert && f.IsMedia() && !f.HasJpeg() {
if jpegFile, err := ind.convert.ToJpeg(f); err != nil {
result.Err = fmt.Errorf("index: failed converting %s to jpeg (%s)", sanitize.Log(f.BaseName()), err.Error())
result.Status = IndexFailed
return result
} else {
log.Debugf("index: created %s", sanitize.Log(jpegFile.BaseName()))
if err := jpegFile.ResampleDefault(ind.thumbPath(), false); err != nil {
result.Err = fmt.Errorf("index: failed creating thumbnails for %s (%s)", sanitize.Log(f.BaseName()), err.Error())
result.Status = IndexFailed
return result
}
related.Files = append(related.Files, jpegFile)
}
}
result = ind.MediaFile(f, opt, "", "")
if result.Indexed() && f.IsJpeg() {
if err := f.ResampleDefault(ind.thumbPath(), false); err != nil {
log.Errorf("index: failed creating thumbnails for %s (%s)", sanitize.Log(f.BaseName()), err.Error())
query.SetFileError(result.FileUID, err.Error())
}
}
log.Infof("index: %s main %s file %s", result, f.FileType(), sanitize.Log(f.RelName(ind.originalsPath())))
return result
}
// IndexRelated indexes a group of related files and returns the result.
func IndexRelated(related RelatedFiles, ind *Index, opt IndexOptions) (result IndexResult) {
func IndexRelated(related RelatedFiles, ind *Index, o IndexOptions) (result IndexResult) {
// Skip if main file is nil.
if related.Main == nil {
result.Err = fmt.Errorf("index: no main file for %s", sanitize.Log(related.String()))
@ -82,12 +21,10 @@ func IndexRelated(related RelatedFiles, ind *Index, opt IndexOptions) (result In
}
done := make(map[string]bool)
sizeLimit := ind.conf.OriginalsLimitBytes()
result = IndexMain(&related, ind, opt)
result = IndexMain(&related, ind, o)
if result.Failed() {
log.Warn(result.Err)
log.Error(result.Err)
return result
} else if !result.Success() {
// Skip related files if indexing was not completely successful.
@ -121,50 +58,51 @@ func IndexRelated(related RelatedFiles, ind *Index, opt IndexOptions) (result In
done[f.FileName()] = true
// Enforce file size limit for originals.
if sizeLimit > 0 && f.FileSize() > sizeLimit {
log.Warnf("index: %s exceeds file size limit (%d / %d megabyte)", sanitize.Log(f.BaseName()), f.FileSize()/(1024*1024), sizeLimit/(1024*1024))
continue
// Show warning if sidecar file exceeds size or resolution limit.
if exceeds, actual := f.ExceedsFileSize(o.OriginalsLimit); exceeds {
log.Warnf("index: sidecar file %s exceeds size limit (%d / %d MB)", sanitize.Log(f.RootRelName()), actual, o.OriginalsLimit)
} else if exceeds, actual = f.ExceedsResolution(o.ResolutionLimit); exceeds {
log.Warnf("index: sidecar file %s exceeds resolution limit (%d / %d MP)", sanitize.Log(f.RootRelName()), actual, o.ResolutionLimit)
}
// Extract metadata to a JSON file with Exiftool.
if f.NeedsExifToolJson() {
if jsonName, err := ind.convert.ToJson(f); err != nil {
log.Debugf("index: %s in %s (extract metadata)", sanitize.Log(err.Error()), sanitize.Log(f.BaseName()))
log.Debugf("index: %s in %s (extract metadata)", sanitize.Log(err.Error()), sanitize.Log(f.RootRelName()))
} else {
log.Debugf("index: created %s", filepath.Base(jsonName))
}
}
if opt.Convert && f.IsMedia() && !f.HasJpeg() {
if jpegFile, err := ind.convert.ToJpeg(f); err != nil {
result.Err = fmt.Errorf("index: failed converting %s to jpeg (%s)", sanitize.Log(f.BaseName()), err.Error())
// Create JPEG sidecar for media files in other formats so that thumbnails can be created.
if o.Convert && f.IsMedia() && !f.HasJpeg() {
if jpg, err := ind.convert.ToJpeg(f); err != nil {
result.Err = fmt.Errorf("index: failed converting %s to jpeg (%s)", sanitize.Log(f.RootRelName()), err.Error())
result.Status = IndexFailed
return result
} else {
log.Debugf("index: created %s", sanitize.Log(jpegFile.BaseName()))
log.Debugf("index: created %s", sanitize.Log(jpg.BaseName()))
if err := jpegFile.ResampleDefault(ind.thumbPath(), false); err != nil {
result.Err = fmt.Errorf("index: failed creating thumbnails for %s (%s)", sanitize.Log(f.BaseName()), err.Error())
if err := jpg.CreateThumbnails(ind.thumbPath(), false); err != nil {
result.Err = fmt.Errorf("index: failed creating thumbnails for %s (%s)", sanitize.Log(f.RootRelName()), err.Error())
result.Status = IndexFailed
return result
}
related.Files = append(related.Files, jpegFile)
related.Files = append(related.Files, jpg)
}
}
res := ind.MediaFile(f, opt, "", result.PhotoUID)
// Index related MediaFile.
res := ind.MediaFile(f, o, "", result.PhotoUID)
if res.Indexed() && f.IsJpeg() {
if err := f.ResampleDefault(ind.thumbPath(), false); err != nil {
log.Errorf("index: failed creating thumbnails for %s (%s)", sanitize.Log(f.BaseName()), err.Error())
query.SetFileError(res.FileUID, err.Error())
}
// Save file error.
if fileUid, err := res.FileError(); err != nil {
query.SetFileError(fileUid, err.Error())
}
log.Infof("index: %s related %s file %s", res, f.FileType(), sanitize.Log(f.BaseName()))
// Log index result.
log.Infof("index: %s related %s file %s", res, f.FileType(), sanitize.Log(f.RootRelName()))
}
return result

View file

@ -124,6 +124,7 @@ func TestIndexRelated(t *testing.T) {
result := IndexRelated(related, ind, opt)
assert.Nil(t, result.Err)
assert.False(t, result.Failed())
assert.False(t, result.Stacked())
assert.True(t, result.Success())

View file

@ -12,6 +12,7 @@ const (
type IndexStatus string
// IndexResult represents a media file indexing result.
type IndexResult struct {
Status IndexStatus
Err error
@ -21,30 +22,46 @@ type IndexResult struct {
PhotoUID string
}
// String returns the indexing result as string.
func (r IndexResult) String() string {
return string(r.Status)
}
func (r IndexResult) Failed() bool {
return r.Err != nil
}
// Success checks whether a media file was successfully indexed or skipped.
func (r IndexResult) Success() bool {
return r.Err == nil && (r.FileID > 0 || r.Stacked() || r.Skipped() || r.Archived())
return !r.Failed() && (r.FileID > 0 || r.Stacked() || r.Skipped() || r.Archived())
}
// Failed checks if indexing has failed.
func (r IndexResult) Failed() bool {
return r.Err != nil && r.Status == IndexFailed
}
// Indexed checks whether a media file was successfully indexed.
func (r IndexResult) Indexed() bool {
return r.Status == IndexAdded || r.Status == IndexUpdated || r.Status == IndexStacked
}
// Stacked checks whether a media file was stacked while indexing.
func (r IndexResult) Stacked() bool {
return r.Status == IndexStacked
}
// Skipped checks whether a media file was skipped while indexing.
func (r IndexResult) Skipped() bool {
return r.Status == IndexSkipped
}
// Archived checks whether a media file was skipped because it is archived.
func (r IndexResult) Archived() bool {
return r.Status == IndexArchived
}
// FileError checks if there is a file error and returns it.
func (r IndexResult) FileError() (string, error) {
if r.Failed() && r.FileUID != "" {
return r.FileUID, r.Err
} else {
return "", nil
}
}

View file

@ -9,11 +9,20 @@ import (
"path"
"path/filepath"
"regexp"
"runtime/debug"
"sort"
"strings"
"sync"
"time"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
_ "golang.org/x/image/webp"
"github.com/disintegration/imaging"
"github.com/djherbis/times"
"github.com/dustin/go-humanize/english"
@ -22,6 +31,7 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/meta"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/capture"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/sanitize"
@ -47,8 +57,10 @@ type MediaFile struct {
width int
height int
metaData meta.Data
metaDataOnce sync.Once
metaOnce sync.Once
fileMutex sync.Mutex
location *entity.Cell
imageConfig *image.Config
}
// NewMediaFile returns a new media file.
@ -123,7 +135,7 @@ func (m *MediaFile) TakenAt() (time.Time, string) {
m.takenAt = data.TakenAt.UTC()
m.takenAtSrc = entity.SrcMeta
log.Infof("media: %s was taken at %s (%s)", filepath.Base(m.fileName), m.takenAt.String(), m.takenAtSrc)
log.Infof("media: %s was taken at %s (%s)", sanitize.Log(filepath.Base(m.fileName)), m.takenAt.String(), m.takenAtSrc)
return m.takenAt, m.takenAtSrc
}
@ -132,7 +144,7 @@ func (m *MediaFile) TakenAt() (time.Time, string) {
m.takenAt = nameTime
m.takenAtSrc = entity.SrcName
log.Infof("media: %s was taken at %s (%s)", filepath.Base(m.fileName), m.takenAt.String(), m.takenAtSrc)
log.Infof("media: %s was taken at %s (%s)", sanitize.Log(filepath.Base(m.fileName)), m.takenAt.String(), m.takenAtSrc)
return m.takenAt, m.takenAtSrc
}
@ -143,17 +155,17 @@ func (m *MediaFile) TakenAt() (time.Time, string) {
if err != nil {
log.Warnf("media: %s (file stat)", err.Error())
log.Infof("media: %s was taken at %s (now)", filepath.Base(m.fileName), m.takenAt.String())
log.Infof("media: %s was taken at %s (now)", sanitize.Log(filepath.Base(m.fileName)), m.takenAt.String())
return m.takenAt, m.takenAtSrc
}
if fileInfo.HasBirthTime() {
m.takenAt = fileInfo.BirthTime().UTC()
log.Infof("media: %s was taken at %s (file birth time)", filepath.Base(m.fileName), m.takenAt.String())
log.Infof("media: %s was taken at %s (file birth time)", sanitize.Log(filepath.Base(m.fileName)), m.takenAt.String())
} else {
m.takenAt = fileInfo.ModTime().UTC()
log.Infof("media: %s was taken at %s (file mod time)", filepath.Base(m.fileName), m.takenAt.String())
log.Infof("media: %s was taken at %s (file mod time)", sanitize.Log(filepath.Base(m.fileName)), m.takenAt.String())
}
return m.takenAt, m.takenAtSrc
@ -547,12 +559,15 @@ func (m *MediaFile) MimeType() string {
return m.mimeType
}
// openFile opens the file and returns the descriptor.
func (m *MediaFile) openFile() (*os.File, error) {
handle, err := os.Open(m.fileName)
if err != nil {
log.Error(err.Error())
return nil, err
}
return handle, nil
}
@ -609,6 +624,9 @@ func (m *MediaFile) Copy(dest string) error {
return err
}
m.fileMutex.Lock()
defer m.fileMutex.Unlock()
thisFile, err := m.openFile()
if err != nil {
@ -652,31 +670,36 @@ func (m *MediaFile) IsJpeg() bool {
return m.MimeType() == fs.MimeTypeJpeg
}
// IsPng returns true if this is a PNG file.
// IsPng returns true if this is a PNG image.
func (m *MediaFile) IsPng() bool {
return m.MimeType() == fs.MimeTypePng
}
// IsGif returns true if this is a GIF file.
// IsGif returns true if this is a GIF image.
func (m *MediaFile) IsGif() bool {
return m.MimeType() == fs.MimeTypeGif
}
// IsTiff returns true if this is a TIFF file.
// IsTiff returns true if this is a TIFF image.
func (m *MediaFile) IsTiff() bool {
return m.HasFileType(fs.FormatTiff) && m.MimeType() == fs.MimeTypeTiff
}
// IsHEIF returns true if this is a High Efficiency Image File Format file.
// IsHEIF returns true if this is a High Efficiency Image File Format image.
func (m *MediaFile) IsHEIF() bool {
return m.MimeType() == fs.MimeTypeHEIF
}
// IsBitmap returns true if this is a bitmap file.
// IsBitmap returns true if this is a bitmap image.
func (m *MediaFile) IsBitmap() bool {
return m.MimeType() == fs.MimeTypeBitmap
}
// IsWebP returns true if this is a WebP image file.
func (m *MediaFile) IsWebP() bool {
return m.MimeType() == fs.MimeTypeWebP
}
// IsVideo returns true if this is a video file.
func (m *MediaFile) IsVideo() bool {
return strings.HasPrefix(m.MimeType(), "video/") || m.MediaType() == fs.MediaVideo
@ -724,16 +747,6 @@ func (m *MediaFile) IsRaw() bool {
return m.HasFileType(fs.FormatRaw)
}
// IsImageOther returns true if this is a PNG, GIF, BMP or TIFF file.
func (m *MediaFile) IsImageOther() bool {
switch {
case m.IsPng(), m.IsGif(), m.IsTiff(), m.IsBitmap():
return true
default:
return false
}
}
// IsXMP returns true if this is a XMP sidecar file.
func (m *MediaFile) IsXMP() bool {
return m.FileType() == fs.FormatXMP
@ -749,9 +762,24 @@ func (m *MediaFile) IsPlayableVideo() bool {
return m.IsVideo() && (m.HasFileType(fs.FormatMp4) || m.HasFileType(fs.FormatAvc))
}
// IsImageOther returns true if this is a PNG, GIF, BMP, TIFF, or WebP file.
func (m *MediaFile) IsImageOther() bool {
switch {
case m.IsPng(), m.IsGif(), m.IsTiff(), m.IsBitmap(), m.IsWebP():
return true
default:
return false
}
}
// IsImageNative returns true if it is a natively supported image file.
func (m *MediaFile) IsImageNative() bool {
return m.IsJpeg() || m.IsImageOther()
}
// IsImage checks if the file is an image
func (m *MediaFile) IsImage() bool {
return m.IsJpeg() || m.IsRaw() || m.IsHEIF() || m.IsImageOther()
return m.IsImageNative() || m.IsRaw() || m.IsHEIF()
}
// IsLive checks if the file is a live photo.
@ -820,19 +848,17 @@ func (m *MediaFile) HasJpeg() bool {
func (m *MediaFile) decodeDimensions() error {
if !m.IsMedia() {
return fmt.Errorf("failed decoding dimensions for %s", sanitize.Log(m.BaseName()))
return fmt.Errorf("failed decoding dimensions of %s file", sanitize.Log(m.Extension()))
}
if m.IsJpeg() || m.IsPng() || m.IsGif() {
file, err := os.Open(m.FileName())
// Media dimensions already known?
if m.width > 0 && m.height > 0 {
return nil
}
if err != nil || file == nil {
return err
}
defer file.Close()
size, _, err := image.DecodeConfig(file)
// Extract the actual width and height from natively supported formats.
if m.IsImageNative() {
cfg, err := m.DecodeConfig()
if err != nil {
return err
@ -841,20 +867,63 @@ func (m *MediaFile) decodeDimensions() error {
orientation := m.Orientation()
if orientation > 4 && orientation <= 8 {
m.width = size.Height
m.height = size.Width
m.width = cfg.Height
m.height = cfg.Width
} else {
m.width = size.Width
m.height = size.Height
m.width = cfg.Width
m.height = cfg.Height
}
} else if data := m.MetaData(); data.Error == nil {
m.width = data.ActualWidth()
m.height = data.ActualHeight()
} else {
return data.Error
return nil
}
return nil
// Extract the width and height from metadata for other formats.
if data := m.MetaData(); data.Error != nil {
return data.Error
} else {
m.width = data.ActualWidth()
m.height = data.ActualHeight()
return nil
}
}
// DecodeConfig extracts the raw dimensions from the header of natively supported image file formats.
func (m *MediaFile) DecodeConfig() (_ *image.Config, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic %s while decoding %s dimensions\nstack: %s", r, sanitize.Log(m.Extension()), debug.Stack())
}
}()
if m.imageConfig != nil {
return m.imageConfig, nil
}
if !m.IsImageNative() {
return nil, fmt.Errorf("%s not supported natively", sanitize.Log(m.Extension()))
}
m.fileMutex.Lock()
defer m.fileMutex.Unlock()
file, err := os.Open(m.FileName())
if err != nil || file == nil {
return nil, err
}
defer file.Close()
cfg, _, err := image.DecodeConfig(file)
if err != nil {
return nil, err
}
m.imageConfig = &cfg
return m.imageConfig, nil
}
// Width return the width dimension of a MediaFile.
@ -887,6 +956,50 @@ func (m *MediaFile) Height() int {
return m.height
}
// Megapixels returns the resolution in megapixels if possible.
func (m *MediaFile) Megapixels() (resolution int) {
if !m.IsMedia() {
return 0
}
if cfg, err := m.DecodeConfig(); err == nil {
resolution = int(math.Round(float64(cfg.Width*cfg.Height) / 1000000))
}
if resolution <= 0 {
resolution = m.metaData.Megapixels()
}
return resolution
}
// ExceedsFileSize checks if the file exceeds the configured file size limit in MB.
func (m *MediaFile) ExceedsFileSize(limit int) (exceeds bool, actual int) {
const mega = 1048576
if limit <= 0 {
return false, actual
} else if size := m.FileSize(); size <= 0 {
return false, actual
} else {
actual = int(size / mega)
return size > int64(limit)*mega, actual
}
}
// ExceedsResolution checks if an image in a natively supported format exceeds the configured resolution limit in megapixels.
func (m *MediaFile) ExceedsResolution(limit int) (exceeds bool, actual int) {
if limit <= 0 {
return false, actual
} else if !m.IsImage() {
return false, actual
} else if actual = m.Megapixels(); actual <= 0 {
return false, actual
} else {
return actual > limit, actual
}
}
// AspectRatio returns the aspect ratio of a MediaFile.
func (m *MediaFile) AspectRatio() float32 {
width := float64(m.Width())
@ -906,36 +1019,6 @@ func (m *MediaFile) Portrait() bool {
return m.Width() < m.Height()
}
// Megapixels returns the resolution in megapixels if possible.
func (m *MediaFile) Megapixels() (resolution int) {
if !m.IsMedia() {
return 0
}
if m.IsJpeg() || m.IsPng() || m.IsGif() {
resolution = int(math.Round(float64(m.Width()*m.Height()) / 1000000))
}
if resolution <= 0 {
resolution = m.MetaData().Megapixels()
}
return resolution
}
// ExceedsMegapixelLimit checks if the media file exceeds the configured resolution limit in megapixels.
func (m *MediaFile) ExceedsMegapixelLimit() bool {
if !m.IsMedia() {
return false
} else if limit := Config().MegapixelLimit(); limit <= 0 {
return false
} else if mp := m.Megapixels(); mp <= 0 {
return false
} else {
return mp > limit
}
}
// Orientation returns the Exif orientation of the media file.
func (m *MediaFile) Orientation() int {
if data := m.MetaData(); data.Error == nil {
@ -976,8 +1059,14 @@ func (m *MediaFile) Resample(path string, sizeName thumb.Name) (img image.Image,
return imaging.Open(filename)
}
// ResampleDefault creates the configured default thumbnails at indexing time.
func (m *MediaFile) ResampleDefault(thumbPath string, force bool) (err error) {
// 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.IsJpeg() {
// Skip.
return
}
count := 0
start := time.Now()
@ -1125,6 +1214,9 @@ func (m *MediaFile) ColorProfile() string {
start := time.Now()
logName := sanitize.Log(m.BaseName())
m.fileMutex.Lock()
defer m.fileMutex.Unlock()
// Open file.
fileReader, err := os.Open(m.FileName())

View file

@ -5,8 +5,8 @@ import (
"path/filepath"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/meta"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/sanitize"
)
@ -68,7 +68,7 @@ func (m *MediaFile) ReadExifToolJson() error {
// MetaData returns exif meta data of a media file.
func (m *MediaFile) MetaData() (result meta.Data) {
m.metaDataOnce.Do(func() {
m.metaOnce.Do(func() {
var err error
if m.ExifSupported() {

View file

@ -1,6 +1,7 @@
package photoprism
import (
"image"
"os"
"path/filepath"
"strings"
@ -977,36 +978,30 @@ func TestMediaFile_Copy(t *testing.T) {
}
func TestMediaFile_Extension(t *testing.T) {
t.Run("/iphone_7.json", func(t *testing.T) {
conf := config.TestConfig()
conf := config.TestConfig()
t.Run("iphone_7.json", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.json")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, ".json", mediaFile.Extension())
})
t.Run("/iphone_7.heic", func(t *testing.T) {
conf := config.TestConfig()
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, ".heic", mediaFile.Extension())
})
t.Run("/canon_eos_6d.dng", func(t *testing.T) {
conf := config.TestConfig()
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, ".dng", mediaFile.Extension())
})
t.Run("/elephants.jpg", func(t *testing.T) {
conf := config.TestConfig()
t.Run("elephants.jpg", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/elephants.jpg")
if err != nil {
t.Fatal(err)
@ -1016,36 +1011,30 @@ func TestMediaFile_Extension(t *testing.T) {
}
func TestMediaFile_IsJpeg(t *testing.T) {
t.Run("/iphone_7.json", func(t *testing.T) {
conf := config.TestConfig()
conf := config.TestConfig()
t.Run("iphone_7.json", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.json")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, false, mediaFile.IsJpeg())
})
t.Run("/iphone_7.heic", func(t *testing.T) {
conf := config.TestConfig()
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, false, mediaFile.IsJpeg())
})
t.Run("/canon_eos_6d.dng", func(t *testing.T) {
conf := config.TestConfig()
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.IsJpeg())
})
t.Run("/elephants.jpg", func(t *testing.T) {
conf := config.TestConfig()
t.Run("elephants.jpg", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/elephants.jpg")
if err != nil {
t.Fatal(err)
@ -1055,27 +1044,23 @@ func TestMediaFile_IsJpeg(t *testing.T) {
}
func TestMediaFile_HasType(t *testing.T) {
t.Run("/iphone_7.heic", func(t *testing.T) {
conf := config.TestConfig()
conf := config.TestConfig()
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, false, mediaFile.HasFileType("jpg"))
})
t.Run("/iphone_7.heic", func(t *testing.T) {
conf := config.TestConfig()
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.HasFileType("heif"))
})
t.Run("/iphone_7.xmp", func(t *testing.T) {
conf := config.TestConfig()
t.Run("iphone_7.xmp", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.xmp")
if err != nil {
t.Fatal(err)
@ -1085,36 +1070,30 @@ func TestMediaFile_HasType(t *testing.T) {
}
func TestMediaFile_IsHEIF(t *testing.T) {
t.Run("/iphone_7.json", func(t *testing.T) {
conf := config.TestConfig()
conf := config.TestConfig()
t.Run("iphone_7.json", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.json")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, false, mediaFile.IsHEIF())
})
t.Run("/iphone_7.heic", func(t *testing.T) {
conf := config.TestConfig()
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())
})
t.Run("/canon_eos_6d.dng", func(t *testing.T) {
conf := config.TestConfig()
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())
})
t.Run("/elephants.jpg", func(t *testing.T) {
conf := config.TestConfig()
t.Run("elephants.jpg", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/elephants.jpg")
if err != nil {
t.Fatal(err)
@ -1124,27 +1103,23 @@ func TestMediaFile_IsHEIF(t *testing.T) {
}
func TestMediaFile_IsRaw(t *testing.T) {
t.Run("/iphone_7.json", func(t *testing.T) {
conf := config.TestConfig()
conf := config.TestConfig()
t.Run("iphone_7.json", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.json")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, false, mediaFile.IsRaw())
})
t.Run("/iphone_7.heic", func(t *testing.T) {
conf := config.TestConfig()
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, false, mediaFile.IsRaw())
})
t.Run("/canon_eos_6d.dng", func(t *testing.T) {
conf := config.TestConfig()
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)
@ -1152,9 +1127,7 @@ func TestMediaFile_IsRaw(t *testing.T) {
assert.Equal(t, true, mediaFile.IsRaw())
})
t.Run("/elephants.jpg", func(t *testing.T) {
conf := config.TestConfig()
t.Run("elephants.jpg", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/elephants.jpg")
if err != nil {
t.Fatal(err)
@ -1164,18 +1137,16 @@ func TestMediaFile_IsRaw(t *testing.T) {
}
func TestMediaFile_IsPng(t *testing.T) {
t.Run("/iphone_7.json", func(t *testing.T) {
conf := config.TestConfig()
conf := config.TestConfig()
t.Run("iphone_7.json", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.json")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, false, mediaFile.IsPng())
})
t.Run("/tweethog.png", func(t *testing.T) {
conf := config.TestConfig()
t.Run("tweethog.png", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/tweethog.png")
if err != nil {
@ -1189,9 +1160,9 @@ func TestMediaFile_IsPng(t *testing.T) {
}
func TestMediaFile_IsTiff(t *testing.T) {
t.Run("/iphone_7.json", func(t *testing.T) {
conf := config.TestConfig()
conf := config.TestConfig()
t.Run("iphone_7.json", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.json")
if err != nil {
t.Fatal(err)
@ -1200,9 +1171,7 @@ func TestMediaFile_IsTiff(t *testing.T) {
assert.Equal(t, "", mediaFile.MimeType())
assert.Equal(t, false, mediaFile.IsTiff())
})
t.Run("/purple.tiff", func(t *testing.T) {
conf := config.TestConfig()
t.Run("purple.tiff", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/purple.tiff")
if err != nil {
t.Fatal(err)
@ -1211,9 +1180,7 @@ func TestMediaFile_IsTiff(t *testing.T) {
assert.Equal(t, "image/tiff", mediaFile.MimeType())
assert.Equal(t, true, mediaFile.IsTiff())
})
t.Run("/example.tiff", func(t *testing.T) {
conf := config.TestConfig()
t.Run("example.tiff", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/example.tif")
if err != nil {
t.Fatal(err)
@ -1225,48 +1192,56 @@ func TestMediaFile_IsTiff(t *testing.T) {
}
func TestMediaFile_IsImageOther(t *testing.T) {
t.Run("/iphone_7.json", func(t *testing.T) {
conf := config.TestConfig()
conf := config.TestConfig()
t.Run("iphone_7.json", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.json")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, false, mediaFile.IsImageOther())
})
t.Run("/purple.tiff", func(t *testing.T) {
conf := config.TestConfig()
t.Run("purple.tiff", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/purple.tiff")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, true, mediaFile.IsImageOther())
})
t.Run("/tweethog.png", func(t *testing.T) {
conf := config.TestConfig()
t.Run("tweethog.png", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/tweethog.png")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, false, mediaFile.IsJpeg())
assert.Equal(t, false, mediaFile.IsGif())
assert.Equal(t, true, mediaFile.IsPng())
assert.Equal(t, false, mediaFile.IsBitmap())
assert.Equal(t, false, mediaFile.IsWebP())
assert.Equal(t, true, mediaFile.IsImage())
assert.Equal(t, true, mediaFile.IsImageNative())
assert.Equal(t, true, mediaFile.IsImageOther())
assert.Equal(t, false, mediaFile.IsVideo())
assert.Equal(t, false, mediaFile.IsPlayableVideo())
})
t.Run("/yellow_rose-small.bmp", func(t *testing.T) {
conf := config.TestConfig()
t.Run("yellow_rose-small.bmp", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/yellow_rose-small.bmp")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, fs.FormatBitmap, mediaFile.FileType())
assert.Equal(t, "image/bmp", mediaFile.MimeType())
assert.Equal(t, false, mediaFile.IsJpeg())
assert.Equal(t, false, mediaFile.IsGif())
assert.Equal(t, true, mediaFile.IsBitmap())
assert.Equal(t, false, mediaFile.IsWebP())
assert.Equal(t, true, mediaFile.IsImage())
assert.Equal(t, true, mediaFile.IsImageNative())
assert.Equal(t, true, mediaFile.IsImageOther())
assert.Equal(t, false, mediaFile.IsVideo())
assert.Equal(t, false, mediaFile.IsPlayableVideo())
})
t.Run("/preloader.gif", func(t *testing.T) {
conf := config.TestConfig()
t.Run("preloader.gif", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/preloader.gif")
if err != nil {
t.Fatal(err)
@ -1274,30 +1249,53 @@ func TestMediaFile_IsImageOther(t *testing.T) {
assert.Equal(t, fs.FormatGif, mediaFile.FileType())
assert.Equal(t, "image/gif", mediaFile.MimeType())
assert.Equal(t, false, mediaFile.IsJpeg())
assert.Equal(t, true, mediaFile.IsGif())
assert.Equal(t, false, mediaFile.IsBitmap())
assert.Equal(t, false, mediaFile.IsWebP())
assert.Equal(t, true, mediaFile.IsImage())
assert.Equal(t, true, mediaFile.IsImageNative())
assert.Equal(t, true, mediaFile.IsImageOther())
assert.Equal(t, false, mediaFile.IsVideo())
assert.Equal(t, false, mediaFile.IsPlayableVideo())
})
t.Run("norway-kjetil-moe.webp", func(t *testing.T) {
mediaFile, err := NewMediaFile("testdata/norway-kjetil-moe.webp")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, fs.FormatWebP, mediaFile.FileType())
assert.Equal(t, fs.MimeTypeWebP, mediaFile.MimeType())
assert.Equal(t, false, mediaFile.IsJpeg())
assert.Equal(t, false, mediaFile.IsGif())
assert.Equal(t, false, mediaFile.IsBitmap())
assert.Equal(t, true, mediaFile.IsWebP())
assert.Equal(t, true, mediaFile.IsImage())
assert.Equal(t, true, mediaFile.IsImageNative())
assert.Equal(t, true, mediaFile.IsImageOther())
assert.Equal(t, false, mediaFile.IsVideo())
assert.Equal(t, false, mediaFile.IsPlayableVideo())
})
}
func TestMediaFile_IsSidecar(t *testing.T) {
t.Run("/iphone_7.xmp", func(t *testing.T) {
conf := config.TestConfig()
conf := config.TestConfig()
t.Run("iphone_7.xmp", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.xmp")
assert.Nil(t, err)
assert.Equal(t, true, mediaFile.IsSidecar())
})
t.Run("/IMG_4120.AAE", func(t *testing.T) {
conf := config.TestConfig()
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, true, mediaFile.IsSidecar())
})
t.Run("/test.xml", func(t *testing.T) {
conf := config.TestConfig()
t.Run("test.xml", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/test.xml")
if err != nil {
@ -1305,9 +1303,7 @@ func TestMediaFile_IsSidecar(t *testing.T) {
}
assert.Equal(t, true, mediaFile.IsSidecar())
})
t.Run("/test.txt", func(t *testing.T) {
conf := config.TestConfig()
t.Run("test.txt", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/test.txt")
if err != nil {
@ -1315,9 +1311,7 @@ func TestMediaFile_IsSidecar(t *testing.T) {
}
assert.Equal(t, true, mediaFile.IsSidecar())
})
t.Run("/test.yml", func(t *testing.T) {
conf := config.TestConfig()
t.Run("test.yml", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/test.yml")
if err != nil {
@ -1325,9 +1319,7 @@ func TestMediaFile_IsSidecar(t *testing.T) {
}
assert.Equal(t, true, mediaFile.IsSidecar())
})
t.Run("/test.md", func(t *testing.T) {
conf := config.TestConfig()
t.Run("test.md", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/test.md")
if err != nil {
@ -1335,9 +1327,7 @@ func TestMediaFile_IsSidecar(t *testing.T) {
}
assert.Equal(t, true, mediaFile.IsSidecar())
})
t.Run("/canon_eos_6d.dng", func(t *testing.T) {
conf := config.TestConfig()
t.Run("canon_eos_6d.dng", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/canon_eos_6d.dng")
if err != nil {
@ -1536,7 +1526,7 @@ func TestMediaFile_decodeDimension(t *testing.T) {
decodeErr := mediaFile.decodeDimensions()
assert.EqualError(t, decodeErr, "failed decoding dimensions for Random.docx")
assert.EqualError(t, decodeErr, "failed decoding dimensions of .docx file")
})
t.Run("clock_purple.jpg", func(t *testing.T) {
@ -1685,6 +1675,242 @@ func TestMediaFile_Height(t *testing.T) {
})
}
func TestMediaFile_Megapixels(t *testing.T) {
conf := config.TestConfig()
t.Run("Random.docx", func(t *testing.T) {
if f, err := NewMediaFile(conf.ExamplesPath() + "/Random.docx"); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, 0, f.Megapixels())
}
})
t.Run("elephant_mono.jpg", func(t *testing.T) {
if f, err := NewMediaFile(conf.ExamplesPath() + "/elephant_mono.jpg"); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, 0, f.Megapixels())
}
})
t.Run("telegram_2020-01-30_09-57-18.jpg", func(t *testing.T) {
if f, err := NewMediaFile(conf.ExamplesPath() + "/telegram_2020-01-30_09-57-18.jpg"); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, 1, f.Megapixels())
}
})
t.Run("6720px_white.jpg", func(t *testing.T) {
if f, err := NewMediaFile(conf.ExamplesPath() + "/6720px_white.jpg"); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, 30, f.Megapixels())
}
})
t.Run("canon_eos_6d.dng", func(t *testing.T) {
if f, err := NewMediaFile(conf.ExamplesPath() + "/canon_eos_6d.dng"); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, 0, f.Megapixels())
}
})
t.Run("example.bmp", func(t *testing.T) {
if f, err := NewMediaFile(conf.ExamplesPath() + "/example.bmp"); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, 0, f.Megapixels())
}
})
t.Run("panorama360.jpg", func(t *testing.T) {
if f, err := NewMediaFile("testdata/panorama360.jpg"); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, 0, f.Megapixels())
}
})
t.Run("panorama360.json", func(t *testing.T) {
if f, err := NewMediaFile("testdata/panorama360.json"); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, 0, f.Megapixels())
}
})
t.Run("2018-04-12 19_24_49.gif", func(t *testing.T) {
if f, err := NewMediaFile("testdata/2018-04-12 19_24_49.gif"); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, 0, f.Megapixels())
}
})
t.Run("2018-04-12 19_24_49.mov", func(t *testing.T) {
if f, err := NewMediaFile("testdata/2018-04-12 19_24_49.mov"); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, 0, f.Megapixels())
}
})
t.Run("rotate/6.png", func(t *testing.T) {
if f, err := NewMediaFile("testdata/rotate/6.png"); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, 1, f.Megapixels())
}
})
t.Run("rotate/6.tiff", func(t *testing.T) {
if f, err := NewMediaFile("testdata/rotate/6.tiff"); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, 0, f.Megapixels())
}
})
t.Run("norway-kjetil-moe.webp", func(t *testing.T) {
if f, err := NewMediaFile("testdata/norway-kjetil-moe.webp"); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, 0, f.Megapixels())
}
})
}
func TestMediaFile_ExceedsFileSize(t *testing.T) {
t.Run("norway-kjetil-moe.webp", func(t *testing.T) {
if f, err := NewMediaFile("testdata/norway-kjetil-moe.webp"); err != nil {
t.Fatal(err)
} else {
result, actual := f.ExceedsFileSize(3)
assert.False(t, result)
assert.Equal(t, 0, actual)
}
})
t.Run("telegram_2020-01-30_09-57-18.jpg", func(t *testing.T) {
if f, err := NewMediaFile(conf.ExamplesPath() + "/telegram_2020-01-30_09-57-18.jpg"); err != nil {
t.Fatal(err)
} else {
result, actual := f.ExceedsFileSize(-1)
assert.False(t, result)
assert.Equal(t, 0, actual)
}
})
t.Run("6720px_white.jpg", func(t *testing.T) {
if f, err := NewMediaFile(conf.ExamplesPath() + "/6720px_white.jpg"); err != nil {
t.Fatal(err)
} else {
result, actual := f.ExceedsFileSize(0)
assert.False(t, result)
assert.Equal(t, 0, actual)
}
})
t.Run("canon_eos_6d.dng", func(t *testing.T) {
if f, err := NewMediaFile(conf.ExamplesPath() + "/canon_eos_6d.dng"); err != nil {
t.Fatal(err)
} else {
result, actual := f.ExceedsFileSize(10)
assert.False(t, result)
assert.Equal(t, 0, actual)
}
})
t.Run("example.bmp", func(t *testing.T) {
if f, err := NewMediaFile(conf.ExamplesPath() + "/example.bmp"); err != nil {
t.Fatal(err)
} else {
result, actual := f.ExceedsFileSize(10)
assert.False(t, result)
assert.Equal(t, 0, actual)
}
})
}
func TestMediaFile_DecodeConfig(t *testing.T) {
t.Run("6720px_white.jpg", func(t *testing.T) {
f, err := NewMediaFile(conf.ExamplesPath() + "/6720px_white.jpg")
if err != nil {
t.Fatal(err)
}
cfg1, err1 := f.DecodeConfig()
assert.Nil(t, err1)
assert.IsType(t, &image.Config{}, cfg1)
assert.Equal(t, 6720, cfg1.Width)
assert.Equal(t, 4480, cfg1.Height)
cfg2, err2 := f.DecodeConfig()
assert.Nil(t, err2)
assert.IsType(t, &image.Config{}, cfg2)
assert.Equal(t, 6720, cfg2.Width)
assert.Equal(t, 4480, cfg2.Height)
cfg3, err3 := f.DecodeConfig()
assert.Nil(t, err3)
assert.IsType(t, &image.Config{}, cfg3)
assert.Equal(t, 6720, cfg3.Width)
assert.Equal(t, 4480, cfg3.Height)
})
}
func TestMediaFile_ExceedsResolution(t *testing.T) {
t.Run("norway-kjetil-moe.webp", func(t *testing.T) {
if f, err := NewMediaFile("testdata/norway-kjetil-moe.webp"); err != nil {
t.Fatal(err)
} else {
result, actual := f.ExceedsResolution(3)
assert.False(t, result)
assert.Equal(t, 0, actual)
}
})
t.Run("telegram_2020-01-30_09-57-18.jpg", func(t *testing.T) {
if f, err := NewMediaFile(conf.ExamplesPath() + "/telegram_2020-01-30_09-57-18.jpg"); err != nil {
t.Fatal(err)
} else {
result, actual := f.ExceedsResolution(3)
assert.False(t, result)
assert.Equal(t, 1, actual)
}
})
t.Run("6720px_white.jpg", func(t *testing.T) {
f, err := NewMediaFile(conf.ExamplesPath() + "/6720px_white.jpg")
if err != nil {
t.Fatal(err)
}
exceeds3, actual3 := f.ExceedsResolution(3)
assert.True(t, exceeds3)
assert.Equal(t, 30, actual3)
exceeds30, actual30 := f.ExceedsResolution(30)
assert.False(t, exceeds30)
assert.Equal(t, 30, actual30)
exceeds33, actual33 := f.ExceedsResolution(33)
assert.False(t, exceeds33)
assert.Equal(t, 30, actual33)
})
t.Run("canon_eos_6d.dng", func(t *testing.T) {
if f, err := NewMediaFile(conf.ExamplesPath() + "/canon_eos_6d.dng"); err != nil {
t.Fatal(err)
} else {
result, actual := f.ExceedsResolution(3)
assert.False(t, result)
assert.Equal(t, 0, actual)
}
})
t.Run("example.bmp", func(t *testing.T) {
if f, err := NewMediaFile(conf.ExamplesPath() + "/example.bmp"); err != nil {
t.Fatal(err)
} else {
result, actual := f.ExceedsResolution(3)
assert.False(t, result)
assert.Equal(t, 0, actual)
}
})
}
func TestMediaFile_AspectRatio(t *testing.T) {
t.Run("iphone_7.heic", func(t *testing.T) {
conf := config.TestConfig()
@ -1867,7 +2093,7 @@ func TestMediaFile_RenderDefaultThumbs(t *testing.T) {
t.Fatal(err)
}
err = m.ResampleDefault(thumbsPath, true)
err = m.CreateThumbnails(thumbsPath, true)
if err != nil {
t.Fatal(err)
@ -1881,7 +2107,7 @@ func TestMediaFile_RenderDefaultThumbs(t *testing.T) {
assert.FileExists(t, thumbFilename)
err = m.ResampleDefault(thumbsPath, false)
err = m.CreateThumbnails(thumbsPath, false)
assert.Empty(t, err)
}

View file

@ -67,5 +67,5 @@ func (m RelatedFiles) MainLogName() string {
return ""
}
return sanitize.Log(m.Main.RelName(Config().OriginalsPath()))
return sanitize.Log(m.Main.RootRelName())
}

View file

@ -197,7 +197,7 @@ func TestRelatedFiles_MainLogName(t *testing.T) {
Files: MediaFiles{},
Main: mediaFile,
}
assert.Equal(t, conf.ExamplesPath()+"/telegram_2020-01-30_09-57-18.jpg", relatedFiles.MainLogName())
assert.Equal(t, "telegram_2020-01-30_09-57-18.jpg", relatedFiles.MainLogName())
})
t.Run("iPhone7", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/telegram_2020-01-30_09-57-18.jpg")
@ -216,6 +216,6 @@ func TestRelatedFiles_MainLogName(t *testing.T) {
Files: MediaFiles{mediaFile, mediaFile2, mediaFile3},
Main: mediaFile3,
}
assert.Equal(t, conf.ExamplesPath()+"/iphone_7.heic", relatedFiles.MainLogName())
assert.Equal(t, "iphone_7.heic", relatedFiles.MainLogName())
})
}

View file

@ -15,7 +15,7 @@ func ResampleWorker(jobs <-chan ResampleJob) {
continue
}
if err := mf.ResampleDefault(job.path, job.force); err != nil {
if err := mf.CreateThumbnails(job.path, job.force); err != nil {
log.Errorf("resample: %s", err)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -98,7 +98,7 @@ func FromFile(imageFilename, hash, thumbPath string, width, height, orientation
img, err := Open(imageFilename, orientation)
if err != nil {
log.Error(err)
log.Debugf("resample: %s in %s", err, sanitize.Log(filepath.Base(imageFilename)))
return "", err
}
@ -135,7 +135,7 @@ func Create(img image.Image, fileName string, width, height int, opts ...Resampl
err = imaging.Save(result, fileName, quality)
if err != nil {
log.Errorf("resample: failed to save %s", sanitize.Log(filepath.Base(fileName)))
log.Debugf("resample: failed to save %s", sanitize.Log(filepath.Base(fileName)))
return result, err
}

View file

@ -3,9 +3,6 @@ package thumb
import (
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"github.com/disintegration/imaging"
"github.com/photoprism/photoprism/pkg/fs"

View file

@ -3,9 +3,6 @@ package thumb
import (
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"os"
"path/filepath"

View file

@ -27,6 +27,14 @@ Additional information can be found in our Developer Guide:
package thumb
import (
_ "image/gif"
_ "image/jpeg"
_ "image/png"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
_ "golang.org/x/image/webp"
"github.com/photoprism/photoprism/internal/event"
)

View file

@ -18,22 +18,23 @@ const (
FormatTiff FileFormat = "tiff" // TIFF image file.
FormatBitmap FileFormat = "bmp" // BMP image file.
FormatRaw FileFormat = "raw" // RAW image file.
FormatMpo FileFormat = "mpo" // Stereoscopic Image that consists of two JPG images that are combined into one 3D image
FormatHEIF FileFormat = "heif" // High Efficiency Image File Format
FormatHEVC FileFormat = "hevc"
FormatMov FileFormat = "mov" // Video files.
FormatMp4 FileFormat = "mp4"
FormatMpo FileFormat = "mpo"
FormatAvc FileFormat = "avc"
FormatAvi FileFormat = "avi"
Format3gp FileFormat = "3gp"
Format3g2 FileFormat = "3g2"
FormatFlv FileFormat = "flv"
FormatMkv FileFormat = "mkv"
FormatMpg FileFormat = "mpg"
FormatMts FileFormat = "mts"
FormatOgv FileFormat = "ogv"
FormatWebm FileFormat = "webm"
FormatWMV FileFormat = "wmv"
FormatWebP FileFormat = "webp" // Google WebP Image
FormatWebM FileFormat = "webm" // Google WebM Video
FormatHEVC FileFormat = "hevc" // H.265, High Efficiency Video Coding (HEVC)
FormatAvc FileFormat = "avc" // H.264, Advanced Video Coding (AVC), MPEG-4 Part 10, used internally
FormatMov FileFormat = "mov" // QuickTime File Format, can contain AVC, HEVC,...
FormatMp4 FileFormat = "mp4" // Standard MPEG-4 Container based on QuickTime, can contain AVC, HEVC,...
FormatAvi FileFormat = "avi" // Microsoft Audio Video Interleave (AVI)
Format3gp FileFormat = "3gp" // Mobile Multimedia Container Format, MPEG-4 Part 12
Format3g2 FileFormat = "3g2" // Similar to 3GP, consumes less space & bandwidth
FormatFlv FileFormat = "flv" // Flash Video
FormatMkv FileFormat = "mkv" // Matroska Multimedia Container, free and open
FormatMpg FileFormat = "mpg" // Moving Picture Experts Group (MPEG)
FormatMts FileFormat = "mts" // AVCHD (Advanced Video Coding High Definition)
FormatOgv FileFormat = "ogv" // Ogg container format maintained by the Xiph.Org, free and open
FormatWMV FileFormat = "wmv" // Windows Media Video
FormatXMP FileFormat = "xmp" // Adobe XMP sidecar file (XML).
FormatAAE FileFormat = "aae" // Apple sidecar file (XML).
FormatXML FileFormat = "xml" // XML metadata / config / sidecar file.
@ -75,7 +76,8 @@ var FileExt = FileExtensions{
".mpo": FormatMpo,
".mts": FormatMts,
".ogv": FormatOgv,
".webm": FormatWebm,
".webp": FormatWebP,
".webm": FormatWebM,
".wmv": FormatWMV,
".yml": FormatYaml,
".yaml": FormatYaml,

View file

@ -17,10 +17,12 @@ var MediaTypes = map[FileFormat]MediaType{
FormatGif: MediaImage,
FormatTiff: MediaImage,
FormatBitmap: MediaImage,
FormatHEIF: MediaImage,
FormatMpo: MediaImage,
FormatAvi: MediaVideo,
FormatHEIF: MediaImage,
FormatHEVC: MediaVideo,
FormatWebP: MediaImage,
FormatWebM: MediaVideo,
FormatAvi: MediaVideo,
FormatAvc: MediaVideo,
FormatMp4: MediaVideo,
FormatMov: MediaVideo,
@ -31,7 +33,6 @@ var MediaTypes = map[FileFormat]MediaType{
FormatMpg: MediaVideo,
FormatMts: MediaVideo,
FormatOgv: MediaVideo,
FormatWebm: MediaVideo,
FormatWMV: MediaVideo,
FormatXMP: MediaSidecar,
FormatXML: MediaSidecar,

View file

@ -11,11 +11,12 @@ const (
MimeTypePng = "image/png"
MimeTypeGif = "image/gif"
MimeTypeBitmap = "image/bmp"
MimeTypeWebP = "image/webp"
MimeTypeTiff = "image/tiff"
MimeTypeHEIF = "image/heif"
)
// MimeType returns the mime type of a file, empty string if unknown.
// MimeType returns the mime type of a file, an empty string if it is unknown.
func MimeType(filename string) string {
handle, err := os.Open(filename)