Metadata: Skip brute-force search if no Exif headers were found #2196

This commit is contained in:
Michael Mayer 2022-03-28 15:57:29 +02:00
parent 50ae86aeb5
commit 39b0530313
15 changed files with 161 additions and 104 deletions

View file

@ -50,6 +50,9 @@ fmt: fmt-js fmt-go
clean-local: clean-local-config clean-local-cache clean-local: clean-local-config clean-local-cache
upgrade: dep-upgrade-js dep-upgrade upgrade: dep-upgrade-js dep-upgrade
devtools: install-go dep-npm devtools: install-go dep-npm
.SILENT: help;
help:
@echo "For build instructions, visit <https://docs.photoprism.app/developer-guide/>."
fix-permissions: fix-permissions:
$(info Updating filesystem permissions...) $(info Updating filesystem permissions...)
@if [ $(UID) != 0 ]; then\ @if [ $(UID) != 0 ]; then\
@ -405,7 +408,11 @@ fmt-go:
goimports -w pkg internal cmd goimports -w pkg internal cmd
tidy: tidy:
go mod tidy -go=1.16 && go mod tidy -go=1.17 go mod tidy -go=1.16 && go mod tidy -go=1.17
.PHONY: all build dev dep-npm dep dep-go dep-js dep-list dep-tensorflow dep-upgrade dep-upgrade-js test test-js test-go \ .PHONY: all build dev dep-npm dep dep-go dep-js dep-list dep-tensorflow dep-upgrade dep-upgrade-js test test-js test-go \
install generate fmt fmt-go fmt-js upgrade start stop terminal root-terminal packer-digitalocean acceptance clean tidy \ install generate fmt fmt-go fmt-js upgrade start stop terminal root-terminal packer-digitalocean acceptance clean tidy \
docker-develop docker-preview docker-preview-all docker-preview-arm docker-release docker-release-all docker-release-arm \ docker-develop docker-preview docker-preview-all docker-preview-arm docker-release docker-release-all docker-release-arm \
install-go install-darktable install-tensorflow devtools tar.gz fix-permissions rootshell; install-go install-darktable install-tensorflow devtools tar.gz fix-permissions rootshell help \
docker-local docker-local-all docker-local-bookworm docker-local-bullseye docker-local-buster docker-local-impish \
docker-local-develop docker-local-develop-all docker-local-develop-bookworm docker-local-develop-bullseye \
docker-local-develop-buster docker-local-develop-impish;

View file

@ -63,11 +63,10 @@ func configAction(ctx *cli.Context) error {
fmt.Printf("%-25s %d\n", "auto-index", conf.AutoIndex()/time.Second) fmt.Printf("%-25s %d\n", "auto-index", conf.AutoIndex()/time.Second)
fmt.Printf("%-25s %d\n", "auto-import", conf.AutoImport()/time.Second) fmt.Printf("%-25s %d\n", "auto-import", conf.AutoImport()/time.Second)
// Features. // Feature Flags.
fmt.Printf("%-25s %t\n", "disable-backups", conf.DisableBackups()) fmt.Printf("%-25s %t\n", "disable-backups", conf.DisableBackups())
fmt.Printf("%-25s %t\n", "disable-settings", conf.DisableSettings()) fmt.Printf("%-25s %t\n", "disable-settings", conf.DisableSettings())
fmt.Printf("%-25s %t\n", "disable-places", conf.DisablePlaces()) fmt.Printf("%-25s %t\n", "disable-places", conf.DisablePlaces())
fmt.Printf("%-25s %t\n", "disable-exiftool", conf.DisableExifTool())
fmt.Printf("%-25s %t\n", "disable-tensorflow", conf.DisableTensorFlow()) fmt.Printf("%-25s %t\n", "disable-tensorflow", conf.DisableTensorFlow())
fmt.Printf("%-25s %t\n", "disable-faces", conf.DisableFaces()) fmt.Printf("%-25s %t\n", "disable-faces", conf.DisableFaces())
fmt.Printf("%-25s %t\n", "disable-classification", conf.DisableClassification()) fmt.Printf("%-25s %t\n", "disable-classification", conf.DisableClassification())
@ -76,12 +75,17 @@ func configAction(ctx *cli.Context) error {
fmt.Printf("%-25s %t\n", "disable-sips", conf.DisableSips()) fmt.Printf("%-25s %t\n", "disable-sips", conf.DisableSips())
fmt.Printf("%-25s %t\n", "disable-heifconvert", conf.DisableHeifConvert()) fmt.Printf("%-25s %t\n", "disable-heifconvert", conf.DisableHeifConvert())
fmt.Printf("%-25s %t\n", "disable-ffmpeg", conf.DisableFFmpeg()) fmt.Printf("%-25s %t\n", "disable-ffmpeg", conf.DisableFFmpeg())
fmt.Printf("%-25s %t\n", "disable-exiftool", conf.DisableExifTool())
// Format Flags.
fmt.Printf("%-25s %t\n", "exif-bruteforce", conf.ExifBruteForce())
fmt.Printf("%-25s %t\n", "raw-presets", conf.RawPresets())
// TensorFlow. // TensorFlow.
fmt.Printf("%-25s %s\n", "tensorflow-version", conf.TensorFlowVersion())
fmt.Printf("%-25s %s\n", "tensorflow-model-path", conf.TensorFlowModelPath())
fmt.Printf("%-25s %t\n", "detect-nsfw", conf.DetectNSFW()) fmt.Printf("%-25s %t\n", "detect-nsfw", conf.DetectNSFW())
fmt.Printf("%-25s %t\n", "upload-nsfw", conf.UploadNSFW()) fmt.Printf("%-25s %t\n", "upload-nsfw", conf.UploadNSFW())
fmt.Printf("%-25s %s\n", "tensorflow-version", conf.TensorFlowVersion())
fmt.Printf("%-25s %s\n", "tensorflow-model-path", conf.TensorFlowModelPath())
// UI Defaults. // UI Defaults.
fmt.Printf("%-25s %s\n", "default-locale", conf.DefaultLocale()) fmt.Printf("%-25s %s\n", "default-locale", conf.DefaultLocale())
@ -110,7 +114,7 @@ func configAction(ctx *cli.Context) error {
fmt.Printf("%-25s %s\n", "api-uri", conf.ApiUri()) fmt.Printf("%-25s %s\n", "api-uri", conf.ApiUri())
fmt.Printf("%-25s %s\n", "base-uri", conf.BaseUri("/")) fmt.Printf("%-25s %s\n", "base-uri", conf.BaseUri("/"))
// Web Server.. // Web Server.
fmt.Printf("%-25s %s\n", "http-host", conf.HttpHost()) fmt.Printf("%-25s %s\n", "http-host", conf.HttpHost())
fmt.Printf("%-25s %d\n", "http-port", conf.HttpPort()) fmt.Printf("%-25s %d\n", "http-port", conf.HttpPort())
fmt.Printf("%-25s %s\n", "http-mode", conf.HttpMode()) fmt.Printf("%-25s %s\n", "http-mode", conf.HttpMode())
@ -127,7 +131,6 @@ func configAction(ctx *cli.Context) error {
fmt.Printf("%-25s %d\n", "database-conns-idle", conf.DatabaseConnsIdle()) fmt.Printf("%-25s %d\n", "database-conns-idle", conf.DatabaseConnsIdle())
// External Tools. // External Tools.
fmt.Printf("%-25s %t\n", "raw-presets", conf.RawPresets())
fmt.Printf("%-25s %s\n", "darktable-bin", conf.DarktableBin()) fmt.Printf("%-25s %s\n", "darktable-bin", conf.DarktableBin())
fmt.Printf("%-25s %s\n", "darktable-cache-path", conf.DarktableCachePath()) fmt.Printf("%-25s %s\n", "darktable-cache-path", conf.DarktableCachePath())
fmt.Printf("%-25s %s\n", "darktable-config-path", conf.DarktableConfigPath()) fmt.Printf("%-25s %s\n", "darktable-config-path", conf.DarktableConfigPath())

View file

@ -148,13 +148,6 @@ func TestConfig_ImportPath(t *testing.T) {
assert.True(t, strings.HasSuffix(result, "/storage/testdata/import")) assert.True(t, strings.HasSuffix(result, "/storage/testdata/import"))
} }
func TestConfig_ExifToolBin(t *testing.T) {
c := NewConfig(CliTestContext())
bin := c.ExifToolBin()
assert.Equal(t, "/usr/bin/exiftool", bin)
}
func TestConfig_CachePath(t *testing.T) { func TestConfig_CachePath(t *testing.T) {
c := NewConfig(CliTestContext()) c := NewConfig(CliTestContext())

View file

@ -170,16 +170,6 @@ var GlobalFlags = []cli.Flag{
Usage: "disable creating YAML metadata files", Usage: "disable creating YAML metadata files",
EnvVar: "PHOTOPRISM_DISABLE_BACKUPS", EnvVar: "PHOTOPRISM_DISABLE_BACKUPS",
}, },
cli.BoolFlag{
Name: "disable-exiftool",
Usage: "disable creating JSON metadata sidecar files with ExifTool",
EnvVar: "PHOTOPRISM_DISABLE_EXIFTOOL",
},
cli.BoolFlag{
Name: "disable-ffmpeg",
Usage: "disable video transcoding and thumbnail extraction with FFmpeg",
EnvVar: "PHOTOPRISM_DISABLE_FFMPEG",
},
cli.BoolFlag{ cli.BoolFlag{
Name: "disable-darktable", Name: "disable-darktable",
Usage: "disable converting RAW files with Darktable", Usage: "disable converting RAW files with Darktable",
@ -215,6 +205,26 @@ var GlobalFlags = []cli.Flag{
Usage: "disable image classification", Usage: "disable image classification",
EnvVar: "PHOTOPRISM_DISABLE_CLASSIFICATION", EnvVar: "PHOTOPRISM_DISABLE_CLASSIFICATION",
}, },
cli.BoolFlag{
Name: "disable-ffmpeg",
Usage: "disable video transcoding and thumbnail extraction with FFmpeg",
EnvVar: "PHOTOPRISM_DISABLE_FFMPEG",
},
cli.BoolFlag{
Name: "disable-exiftool",
Usage: "disable creating JSON metadata sidecar files with ExifTool",
EnvVar: "PHOTOPRISM_DISABLE_EXIFTOOL",
},
cli.BoolFlag{
Name: "exif-bruteforce",
Usage: "always perform a brute-force search if no Exif headers were found",
EnvVar: "PHOTOPRISM_EXIF_BRUTEFORCE",
},
cli.BoolFlag{
Name: "raw-presets",
Usage: "enable RAW file converter presets (may reduce performance)",
EnvVar: "PHOTOPRISM_RAW_PRESETS",
},
cli.BoolFlag{ cli.BoolFlag{
Name: "detect-nsfw", Name: "detect-nsfw",
Usage: "flag photos as private that may be offensive (requires TensorFlow)", Usage: "flag photos as private that may be offensive (requires TensorFlow)",
@ -368,11 +378,6 @@ var GlobalFlags = []cli.Flag{
Usage: "maximum `NUMBER` of idle database connections", Usage: "maximum `NUMBER` of idle database connections",
EnvVar: "PHOTOPRISM_DATABASE_CONNS_IDLE", EnvVar: "PHOTOPRISM_DATABASE_CONNS_IDLE",
}, },
cli.BoolFlag{
Name: "raw-presets",
Usage: "enable RAW file converter presets (may reduce performance)",
EnvVar: "PHOTOPRISM_RAW_PRESETS",
},
cli.StringFlag{ cli.StringFlag{
Name: "darktable-bin", Name: "darktable-bin",
Usage: "Darktable CLI `COMMAND` for RAW image conversion", Usage: "Darktable CLI `COMMAND` for RAW image conversion",

View file

@ -216,21 +216,6 @@ func (c *Config) ImportPath() string {
return fs.Abs(c.options.ImportPath) return fs.Abs(c.options.ImportPath)
} }
// ExifToolBin returns the exiftool executable file name.
func (c *Config) ExifToolBin() string {
return findExecutable(c.options.ExifToolBin, "exiftool")
}
// ExifToolJson tests if creating JSON metadata sidecar files with Exiftool is enabled.
func (c *Config) ExifToolJson() bool {
return !c.DisableExifTool()
}
// BackupYaml tests if creating YAML files is enabled.
func (c *Config) BackupYaml() bool {
return !c.DisableBackups()
}
// SidecarPath returns the storage path for generated sidecar files (relative or absolute). // SidecarPath returns the storage path for generated sidecar files (relative or absolute).
func (c *Config) SidecarPath() string { func (c *Config) SidecarPath() string {
if c.options.SidecarPath == "" { if c.options.SidecarPath == "" {

View file

@ -11,30 +11,6 @@ func TestConfig_FindExecutable(t *testing.T) {
assert.Equal(t, "", findExecutable("yyy", "xxx")) assert.Equal(t, "", findExecutable("yyy", "xxx"))
} }
func TestConfig_SidecarJson(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, true, c.ExifToolJson())
assert.Equal(t, c.DisableExifTool(), !c.ExifToolJson())
c.options.DisableExifTool = true
assert.Equal(t, false, c.ExifToolJson())
assert.Equal(t, c.DisableExifTool(), !c.ExifToolJson())
}
func TestConfig_SidecarYaml(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, true, c.BackupYaml())
assert.Equal(t, c.DisableBackups(), !c.BackupYaml())
c.options.DisableBackups = true
assert.Equal(t, false, c.BackupYaml())
assert.Equal(t, c.DisableBackups(), !c.BackupYaml())
}
func TestConfig_SidecarPath(t *testing.T) { func TestConfig_SidecarPath(t *testing.T) {
c := NewConfig(CliTestContext()) c := NewConfig(CliTestContext())

View file

@ -0,0 +1,21 @@
package config
// ExifBruteForce checks if a brute-force search should be performed when no Exif headers were found.
func (c *Config) ExifBruteForce() bool {
return c.options.ExifBruteForce || !c.ExifToolJson()
}
// ExifToolBin returns the exiftool executable file name.
func (c *Config) ExifToolBin() string {
return findExecutable(c.options.ExifToolBin, "exiftool")
}
// ExifToolJson tests if creating JSON metadata sidecar files with Exiftool is enabled.
func (c *Config) ExifToolJson() bool {
return !c.DisableExifTool()
}
// BackupYaml tests if creating YAML files is enabled.
func (c *Config) BackupYaml() bool {
return !c.DisableBackups()
}

View file

@ -0,0 +1,43 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestConfig_ExifBruteForce(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, false, c.ExifBruteForce())
}
func TestConfig_ExifToolBin(t *testing.T) {
c := NewConfig(CliTestContext())
bin := c.ExifToolBin()
assert.Equal(t, "/usr/bin/exiftool", bin)
}
func TestConfig_ExifToolJson(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, true, c.ExifToolJson())
assert.Equal(t, c.DisableExifTool(), !c.ExifToolJson())
c.options.DisableExifTool = true
assert.Equal(t, false, c.ExifToolJson())
assert.Equal(t, c.DisableExifTool(), !c.ExifToolJson())
}
func TestConfig_SidecarYaml(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, true, c.BackupYaml())
assert.Equal(t, c.DisableBackups(), !c.BackupYaml())
c.options.DisableBackups = true
assert.Equal(t, false, c.BackupYaml())
assert.Equal(t, c.DisableBackups(), !c.BackupYaml())
}

View file

@ -65,8 +65,6 @@ type Options struct {
DisableBackups bool `yaml:"DisableBackups" json:"DisableBackups" flag:"disable-backups"` DisableBackups bool `yaml:"DisableBackups" json:"DisableBackups" flag:"disable-backups"`
DisableSettings bool `yaml:"DisableSettings" json:"-" flag:"disable-settings"` DisableSettings bool `yaml:"DisableSettings" json:"-" flag:"disable-settings"`
DisablePlaces bool `yaml:"DisablePlaces" json:"DisablePlaces" flag:"disable-places"` DisablePlaces bool `yaml:"DisablePlaces" json:"DisablePlaces" flag:"disable-places"`
DisableExifTool bool `yaml:"DisableExifTool" json:"DisableExifTool" flag:"disable-exiftool"`
DisableFFmpeg bool `yaml:"DisableFFmpeg" json:"DisableFFmpeg" flag:"disable-ffmpeg"`
DisableDarktable bool `yaml:"DisableDarktable" json:"DisableDarktable" flag:"disable-darktable"` DisableDarktable bool `yaml:"DisableDarktable" json:"DisableDarktable" flag:"disable-darktable"`
DisableRawtherapee bool `yaml:"DisableRawtherapee" json:"DisableRawtherapee" flag:"disable-rawtherapee"` DisableRawtherapee bool `yaml:"DisableRawtherapee" json:"DisableRawtherapee" flag:"disable-rawtherapee"`
DisableSips bool `yaml:"DisableSips" json:"DisableSips" flag:"disable-sips"` DisableSips bool `yaml:"DisableSips" json:"DisableSips" flag:"disable-sips"`
@ -74,6 +72,10 @@ type Options struct {
DisableTensorFlow bool `yaml:"DisableTensorFlow" json:"DisableTensorFlow" flag:"disable-tensorflow"` DisableTensorFlow bool `yaml:"DisableTensorFlow" json:"DisableTensorFlow" flag:"disable-tensorflow"`
DisableFaces bool `yaml:"DisableFaces" json:"DisableFaces" flag:"disable-faces"` DisableFaces bool `yaml:"DisableFaces" json:"DisableFaces" flag:"disable-faces"`
DisableClassification bool `yaml:"DisableClassification" json:"DisableClassification" flag:"disable-classification"` DisableClassification bool `yaml:"DisableClassification" json:"DisableClassification" flag:"disable-classification"`
DisableFFmpeg bool `yaml:"DisableFFmpeg" json:"DisableFFmpeg" flag:"disable-ffmpeg"`
DisableExifTool bool `yaml:"DisableExifTool" json:"DisableExifTool" flag:"disable-exiftool"`
ExifBruteForce bool `yaml:"ExifBruteForce" json:"ExifBruteForce" flag:"exif-bruteforce"`
RawPresets bool `yaml:"RawPresets" json:"RawPresets" flag:"raw-presets"`
DetectNSFW bool `yaml:"DetectNSFW" json:"DetectNSFW" flag:"detect-nsfw"` DetectNSFW bool `yaml:"DetectNSFW" json:"DetectNSFW" flag:"detect-nsfw"`
UploadNSFW bool `yaml:"UploadNSFW" json:"-" flag:"upload-nsfw"` UploadNSFW bool `yaml:"UploadNSFW" json:"-" flag:"upload-nsfw"`
DefaultTheme string `yaml:"DefaultTheme" json:"DefaultTheme" flag:"default-theme"` DefaultTheme string `yaml:"DefaultTheme" json:"DefaultTheme" flag:"default-theme"`
@ -102,7 +104,6 @@ type Options struct {
HttpPort int `yaml:"HttpPort" json:"-" flag:"http-port"` HttpPort int `yaml:"HttpPort" json:"-" flag:"http-port"`
HttpMode string `yaml:"HttpMode" json:"-" flag:"http-mode"` HttpMode string `yaml:"HttpMode" json:"-" flag:"http-mode"`
HttpCompression string `yaml:"HttpCompression" json:"-" flag:"http-compression"` HttpCompression string `yaml:"HttpCompression" json:"-" flag:"http-compression"`
RawPresets bool `yaml:"RawPresets" json:"RawPresets" flag:"raw-presets"`
DarktableBin string `yaml:"DarktableBin" json:"-" flag:"darktable-bin"` DarktableBin string `yaml:"DarktableBin" json:"-" flag:"darktable-bin"`
DarktableBlacklist string `yaml:"DarktableBlacklist" json:"-" flag:"darktable-blacklist"` DarktableBlacklist string `yaml:"DarktableBlacklist" json:"-" flag:"darktable-blacklist"`
RawtherapeeBin string `yaml:"RawtherapeeBin" json:"-" flag:"rawtherapee-bin"` RawtherapeeBin string `yaml:"RawtherapeeBin" json:"-" flag:"rawtherapee-bin"`

View file

@ -63,6 +63,7 @@ func NewTestOptions() *Options {
ReadOnly: false, ReadOnly: false,
DetectNSFW: true, DetectNSFW: true,
UploadNSFW: false, UploadNSFW: false,
ExifBruteForce: false,
AssetsPath: assetsPath, AssetsPath: assetsPath,
AutoIndex: -1, AutoIndex: -1,
AutoImport: 7200, AutoImport: 7200,

View file

@ -12,7 +12,7 @@ const (
ImageTypeHDR = 3 // see https://exiftool.org/TagNames/Apple.html ImageTypeHDR = 3 // see https://exiftool.org/TagNames/Apple.html
) )
// Data represents image meta data. // Data represents image metadata.
type Data struct { type Data struct {
FileName string `meta:"FileName"` FileName string `meta:"FileName"`
DocumentID string `meta:"BurstUUID,MediaGroupUUID,ImageUniqueID,OriginalDocumentID,DocumentID"` DocumentID string `meta:"BurstUUID,MediaGroupUUID,ImageUniqueID,OriginalDocumentID,DocumentID"`

View file

@ -34,15 +34,15 @@ func init() {
} }
} }
// Exif parses an image file for Exif meta data and returns as Data struct. // Exif parses an image file for Exif metadata and returns as Data struct.
func Exif(fileName string, fileType fs.FileFormat) (data Data, err error) { func Exif(fileName string, fileType fs.FileFormat, bruteForce bool) (data Data, err error) {
err = data.Exif(fileName, fileType) err = data.Exif(fileName, fileType, bruteForce)
return data, err return data, err
} }
// Exif parses an image file for Exif meta data and returns as Data struct. // Exif parses an image file for Exif metadata and returns as Data struct.
func (data *Data) Exif(fileName string, fileType fs.FileFormat) (err error) { func (data *Data) Exif(fileName string, fileType fs.FileFormat, bruteForce bool) (err error) {
exifMutex.Lock() exifMutex.Lock()
defer exifMutex.Unlock() defer exifMutex.Unlock()
@ -53,7 +53,7 @@ func (data *Data) Exif(fileName string, fileType fs.FileFormat) (err error) {
}() }()
// Extract raw Exif block. // Extract raw Exif block.
rawExif, err := RawExif(fileName, fileType) rawExif, err := RawExif(fileName, fileType, bruteForce)
if err != nil { if err != nil {
return err return err

View file

@ -15,7 +15,7 @@ import (
"github.com/photoprism/photoprism/pkg/sanitize" "github.com/photoprism/photoprism/pkg/sanitize"
) )
func RawExif(fileName string, fileType fs.FileFormat) (rawExif []byte, err error) { func RawExif(fileName string, fileType fs.FileFormat, bruteForce bool) (rawExif []byte, err error) {
defer func() { defer func() {
if e := recover(); e != nil { if e := recover(); e != nil {
err = fmt.Errorf("metadata: %s in %s (raw exif panic)\nstack: %s", e, sanitize.Log(filepath.Base(fileName)), debug.Stack()) err = fmt.Errorf("metadata: %s in %s (raw exif panic)\nstack: %s", e, sanitize.Log(filepath.Base(fileName)), debug.Stack())
@ -25,8 +25,10 @@ func RawExif(fileName string, fileType fs.FileFormat) (rawExif []byte, err error
// Extract raw Exif block. // Extract raw Exif block.
var parsed bool var parsed bool
// Sanitized and shortened file name for logs.
logName := sanitize.Log(filepath.Base(fileName)) logName := sanitize.Log(filepath.Base(fileName))
// Try Exif parser for specific media file format first.
if fileType == fs.FormatJpeg { if fileType == fs.FormatJpeg {
jpegMp := jpegstructure.NewJpegMediaParser() jpegMp := jpegstructure.NewJpegMediaParser()
@ -38,7 +40,7 @@ func RawExif(fileName string, fileType fs.FileFormat) (rawExif []byte, err error
_, rawExif, err = sl.Exif() _, rawExif, err = sl.Exif()
if err != nil { if err != nil {
if strings.HasPrefix(err.Error(), "no exif header") { if !bruteForce || strings.HasPrefix(err.Error(), "no exif header") {
return rawExif, fmt.Errorf("metadata: found no exif header in %s (parse jpeg)", logName) return rawExif, fmt.Errorf("metadata: found no exif header in %s (parse jpeg)", logName)
} else if strings.HasPrefix(err.Error(), "no exif data") { } else if strings.HasPrefix(err.Error(), "no exif data") {
log.Debugf("metadata: failed parsing %s, starting brute-force search (parse jpeg)", logName) log.Debugf("metadata: failed parsing %s, starting brute-force search (parse jpeg)", logName)
@ -109,9 +111,13 @@ func RawExif(fileName string, fileType fs.FileFormat) (rawExif []byte, err error
parsed = true parsed = true
} }
} }
} else {
log.Infof("metadata: no file format parser for %s, performing brute-force search", logName)
bruteForce = true
} }
if !parsed { // Start brute-force search for Exif data?
if !parsed && bruteForce {
rawExif, err = exif.SearchFileAndExtractExif(fileName) rawExif, err = exif.SearchFileAndExtractExif(fileName)
if err != nil { if err != nil {

View file

@ -9,7 +9,7 @@ import (
func TestExif(t *testing.T) { func TestExif(t *testing.T) {
t.Run("photoshop.jpg", func(t *testing.T) { t.Run("photoshop.jpg", func(t *testing.T) {
data, err := Exif("testdata/photoshop.jpg", fs.FormatJpeg) data, err := Exif("testdata/photoshop.jpg", fs.FormatJpeg, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -40,7 +40,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("ladybug.jpg", func(t *testing.T) { t.Run("ladybug.jpg", func(t *testing.T) {
data, err := Exif("testdata/ladybug.jpg", fs.FormatJpeg) data, err := Exif("testdata/ladybug.jpg", fs.FormatJpeg, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -72,7 +72,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("gopro_hd2.jpg", func(t *testing.T) { t.Run("gopro_hd2.jpg", func(t *testing.T) {
data, err := Exif("testdata/gopro_hd2.jpg", fs.FormatJpeg) data, err := Exif("testdata/gopro_hd2.jpg", fs.FormatJpeg, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -102,7 +102,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("tweethog.png", func(t *testing.T) { t.Run("tweethog.png", func(t *testing.T) {
_, err := Exif("testdata/tweethog.png", fs.FormatPng) _, err := Exif("testdata/tweethog.png", fs.FormatPng, true)
if err == nil { if err == nil {
t.Fatal("err should NOT be nil") t.Fatal("err should NOT be nil")
@ -112,7 +112,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("iphone_7.heic", func(t *testing.T) { t.Run("iphone_7.heic", func(t *testing.T) {
data, err := Exif("testdata/iphone_7.heic", fs.FormatHEIF) data, err := Exif("testdata/iphone_7.heic", fs.FormatHEIF, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -133,7 +133,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("gps-2000.jpg", func(t *testing.T) { t.Run("gps-2000.jpg", func(t *testing.T) {
data, err := Exif("testdata/gps-2000.jpg", fs.FormatJpeg) data, err := Exif("testdata/gps-2000.jpg", fs.FormatJpeg, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -161,7 +161,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("image-2011.jpg", func(t *testing.T) { t.Run("image-2011.jpg", func(t *testing.T) {
data, err := Exif("testdata/image-2011.jpg", fs.FormatJpeg) data, err := Exif("testdata/image-2011.jpg", fs.FormatJpeg, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -196,7 +196,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("ship.jpg", func(t *testing.T) { t.Run("ship.jpg", func(t *testing.T) {
data, err := Exif("testdata/ship.jpg", fs.FormatJpeg) data, err := Exif("testdata/ship.jpg", fs.FormatJpeg, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -217,7 +217,17 @@ func TestExif(t *testing.T) {
}) })
t.Run("no-exif-data.jpg", func(t *testing.T) { t.Run("no-exif-data.jpg", func(t *testing.T) {
_, err := Exif("testdata/no-exif-data.jpg", fs.FormatJpeg) _, err := Exif("testdata/no-exif-data.jpg", fs.FormatJpeg, false)
if err == nil {
t.Fatal("err should NOT be nil")
}
assert.Equal(t, "metadata: found no exif header in no-exif-data.jpg (parse jpeg)", err.Error())
})
t.Run("no-exif-data.jpg/BruteForce", func(t *testing.T) {
_, err := Exif("testdata/no-exif-data.jpg", fs.FormatJpeg, true)
if err == nil { if err == nil {
t.Fatal("err should NOT be nil") t.Fatal("err should NOT be nil")
@ -227,7 +237,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("screenshot.png", func(t *testing.T) { t.Run("screenshot.png", func(t *testing.T) {
data, err := Exif("testdata/screenshot.png", fs.FormatPng) data, err := Exif("testdata/screenshot.png", fs.FormatPng, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -238,7 +248,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("orientation.jpg", func(t *testing.T) { t.Run("orientation.jpg", func(t *testing.T) {
data, err := Exif("testdata/orientation.jpg", fs.FormatJpeg) data, err := Exif("testdata/orientation.jpg", fs.FormatJpeg, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -266,13 +276,19 @@ func TestExif(t *testing.T) {
}) })
t.Run("gopher-preview.jpg", func(t *testing.T) { t.Run("gopher-preview.jpg", func(t *testing.T) {
_, err := Exif("testdata/gopher-preview.jpg", fs.FormatJpeg) _, err := Exif("testdata/gopher-preview.jpg", fs.FormatJpeg, false)
assert.EqualError(t, err, "metadata: found no exif header in gopher-preview.jpg (parse jpeg)")
})
t.Run("gopher-preview.jpg/BruteForce", func(t *testing.T) {
_, err := Exif("testdata/gopher-preview.jpg", fs.FormatJpeg, true)
assert.EqualError(t, err, "metadata: found no exif header in gopher-preview.jpg (search and extract)") assert.EqualError(t, err, "metadata: found no exif header in gopher-preview.jpg (search and extract)")
}) })
t.Run("huawei-gps-error.jpg", func(t *testing.T) { t.Run("huawei-gps-error.jpg", func(t *testing.T) {
data, err := Exif("testdata/huawei-gps-error.jpg", fs.FormatJpeg) data, err := Exif("testdata/huawei-gps-error.jpg", fs.FormatJpeg, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -293,7 +309,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("panorama360.jpg", func(t *testing.T) { t.Run("panorama360.jpg", func(t *testing.T) {
data, err := Exif("testdata/panorama360.jpg", fs.FormatJpeg) data, err := Exif("testdata/panorama360.jpg", fs.FormatJpeg, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -325,7 +341,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("exif-example.tiff", func(t *testing.T) { t.Run("exif-example.tiff", func(t *testing.T) {
data, err := Exif("testdata/exif-example.tiff", fs.FormatTiff) data, err := Exif("testdata/exif-example.tiff", fs.FormatTiff, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -357,7 +373,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("out-of-range-500.jpg", func(t *testing.T) { t.Run("out-of-range-500.jpg", func(t *testing.T) {
data, err := Exif("testdata/out-of-range-500.jpg", fs.FormatJpeg) data, err := Exif("testdata/out-of-range-500.jpg", fs.FormatJpeg, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -389,7 +405,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("digikam.jpg", func(t *testing.T) { t.Run("digikam.jpg", func(t *testing.T) {
data, err := Exif("testdata/digikam.jpg", fs.FormatJpeg) data, err := Exif("testdata/digikam.jpg", fs.FormatJpeg, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -424,7 +440,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("notebook.jpg", func(t *testing.T) { t.Run("notebook.jpg", func(t *testing.T) {
data, err := Exif("testdata/notebook.jpg", fs.FormatJpeg) data, err := Exif("testdata/notebook.jpg", fs.FormatJpeg, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -445,7 +461,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("snow.jpg", func(t *testing.T) { t.Run("snow.jpg", func(t *testing.T) {
data, err := Exif("testdata/snow.jpg", fs.FormatJpeg) data, err := Exif("testdata/snow.jpg", fs.FormatJpeg, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -466,7 +482,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("keywords.jpg", func(t *testing.T) { t.Run("keywords.jpg", func(t *testing.T) {
data, err := Exif("testdata/keywords.jpg", fs.FormatJpeg) data, err := Exif("testdata/keywords.jpg", fs.FormatJpeg, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -486,7 +502,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("Iceland-P3.jpg", func(t *testing.T) { t.Run("Iceland-P3.jpg", func(t *testing.T) {
data, err := Exif("testdata/Iceland-P3.jpg", fs.FormatJpeg) data, err := Exif("testdata/Iceland-P3.jpg", fs.FormatJpeg, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -518,7 +534,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("Iceland-sRGB.jpg", func(t *testing.T) { t.Run("Iceland-sRGB.jpg", func(t *testing.T) {
data, err := Exif("testdata/Iceland-sRGB.jpg", fs.FormatJpeg) data, err := Exif("testdata/Iceland-sRGB.jpg", fs.FormatJpeg, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

View file

@ -72,7 +72,7 @@ func (m *MediaFile) MetaData() (result meta.Data) {
var err error var err error
if m.ExifSupported() { if m.ExifSupported() {
err = m.metaData.Exif(m.FileName(), m.FileType()) err = m.metaData.Exif(m.FileName(), m.FileType(), Config().ExifBruteForce())
} else { } else {
err = fmt.Errorf("exif not supported") err = fmt.Errorf("exif not supported")
} }