精简后的项目:仅保留 api/ui 源码、文档、构建依赖
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
package exif
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/log"
|
||||
"github.com/photoview/photoview/api/scanner/externaltools/exiftool"
|
||||
)
|
||||
|
||||
var globalExifParser *exiftool.Exiftool
|
||||
var globalInit sync.Once
|
||||
|
||||
func Initialize() (func(), error) {
|
||||
var err error
|
||||
globalInit.Do(func() {
|
||||
globalExifParser, err = exiftool.New()
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Info(nil, "Found exiftool.", "binary_path", globalExifParser.BinaryPath(), "version", globalExifParser.Version())
|
||||
|
||||
return func() {
|
||||
globalMu.Lock()
|
||||
defer globalMu.Unlock()
|
||||
|
||||
if globalExifParser == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := globalExifParser.Close(); err != nil {
|
||||
log.Error(nil, "Cleanup exiftool error", "error", err)
|
||||
return
|
||||
}
|
||||
globalExifParser = nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
var globalMu sync.Mutex
|
||||
|
||||
func Parse(filepath string) (*models.MediaEXIF, error) {
|
||||
globalMu.Lock()
|
||||
defer globalMu.Unlock()
|
||||
|
||||
if globalExifParser == nil {
|
||||
return nil, fmt.Errorf("no exif parser initialized")
|
||||
}
|
||||
|
||||
var values struct {
|
||||
exiftool.PhotoMeta
|
||||
exiftool.TimeAll
|
||||
exiftool.GPS
|
||||
}
|
||||
if err := globalExifParser.QueryJSONTagsByNumber(filepath, &values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
values.PhotoMeta.SanitizeFloats()
|
||||
|
||||
ret := models.MediaEXIF{
|
||||
Camera: values.Model,
|
||||
Maker: values.Make,
|
||||
Lens: values.LensModel,
|
||||
Iso: values.ISO,
|
||||
Flash: values.Flash,
|
||||
Orientation: values.Orientation,
|
||||
ExposureProgram: values.ExposureProgram,
|
||||
Exposure: values.ExposureTime,
|
||||
Aperture: values.Aperture,
|
||||
FocalLength: values.FocalLength,
|
||||
Description: values.ImageDescription,
|
||||
}
|
||||
|
||||
dateShot := values.TimeAll.TimeInLocal()
|
||||
if !dateShot.IsZero() {
|
||||
ret.DateShot = new(dateShot)
|
||||
}
|
||||
|
||||
offsetSec, ok := values.TimeAll.OffsetSecs(dateShot)
|
||||
if ok {
|
||||
ret.OffsetSecShot = &offsetSec
|
||||
}
|
||||
|
||||
if values.GPS.IsValid() {
|
||||
ret.GPSLatitude = values.GPS.GPSLatitude
|
||||
ret.GPSLongitude = values.GPS.GPSLongitude
|
||||
}
|
||||
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func MIMEType(filepath string) (string, error) {
|
||||
globalMu.Lock()
|
||||
defer globalMu.Unlock()
|
||||
|
||||
if globalExifParser == nil {
|
||||
return "", fmt.Errorf("no exif parser initialized")
|
||||
}
|
||||
|
||||
var mime exiftool.MIMEType
|
||||
if err := globalExifParser.QueryJSONTagsByNumber(filepath, &mime); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if mime.MIMEType == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return *mime.MIMEType, nil
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package exif
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
_ "github.com/photoview/photoview/api/test_utils/flags"
|
||||
)
|
||||
|
||||
func resetForTest() {
|
||||
globalMu.Lock()
|
||||
defer globalMu.Unlock()
|
||||
if globalExifParser != nil {
|
||||
_ = globalExifParser.Close()
|
||||
}
|
||||
globalExifParser = nil
|
||||
// Allow Initialize() to run again in subsequent tests
|
||||
globalInit = sync.Once{}
|
||||
}
|
||||
|
||||
func TestParseWithoutInit(t *testing.T) {
|
||||
resetForTest()
|
||||
if _, err := Parse("./test_data/bird.jpg"); err == nil {
|
||||
t.Fatalf("Parse() without Init() doesn't return an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
resetForTest()
|
||||
|
||||
cleanup, err := Initialize()
|
||||
if err != nil {
|
||||
t.Fatalf("Initialize() error: %v", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
filename := "./test_data/bird.jpg"
|
||||
|
||||
metadata, err := Parse(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("Parse() returns an error: %v", err)
|
||||
}
|
||||
|
||||
if metadata == nil {
|
||||
t.Errorf("Parse(%q) should not return nil", filename)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMIMEType(t *testing.T) {
|
||||
resetForTest()
|
||||
|
||||
cleanup, err := Initialize()
|
||||
if err != nil {
|
||||
t.Fatalf("Initialize() error: %v", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
filename := "./test_data/bird.jpg"
|
||||
|
||||
mime, err := MIMEType(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("MIMEType() returns an error: %v", err)
|
||||
}
|
||||
|
||||
if mime == "" {
|
||||
t.Errorf("MIMEType(%q) should not return an empty string", filename)
|
||||
}
|
||||
}
|
||||
|
||||
func fileModifyDateLiteralInUTC(t *testing.T, file string) time.Time {
|
||||
fstat, err := os.Stat(file)
|
||||
if err != nil {
|
||||
t.Fatalf("os.Stat(%q) error: %v", file, err)
|
||||
}
|
||||
|
||||
ret := fstat.ModTime().Truncate(time.Second)
|
||||
_, offset := ret.Zone()
|
||||
ret = ret.Add(time.Duration(offset) * time.Second).UTC()
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func mustParseNoTimeZone(t *testing.T, timeStr string) time.Time {
|
||||
layout := "2006:01:02 15:04:05.999"
|
||||
ret, err := time.ParseInLocation(layout, timeStr, time.UTC)
|
||||
if err != nil {
|
||||
t.Fatalf("time.Parse(%q) error: %v", timeStr, err)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func TestSamplesTime(t *testing.T) {
|
||||
resetForTest()
|
||||
|
||||
cleanup, err := Initialize()
|
||||
if err != nil {
|
||||
t.Fatalf("Initialize() error: %v", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
tests := []struct {
|
||||
file string
|
||||
wantTime time.Time
|
||||
wantOffsetSec *int
|
||||
}{
|
||||
{"./test_data/sample1.heif", fileModifyDateLiteralInUTC(t, "./test_data/sample1.heif"), nil},
|
||||
{"./test_data/sample1_nef.jpg", mustParseNoTimeZone(t, "2008:03:15 07:44:21.49"), new(-7 * 60 * 60)},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.file, func(t *testing.T) {
|
||||
metadata, err := Parse(tc.file)
|
||||
if err != nil {
|
||||
t.Fatalf("Parse(%q) returns an error: %v", tc.file, err)
|
||||
}
|
||||
|
||||
if metadata == nil {
|
||||
t.Fatalf("metadata == nil")
|
||||
}
|
||||
if metadata.DateShot == nil {
|
||||
t.Fatalf("metadata.DateShot == nil")
|
||||
}
|
||||
|
||||
if got := *metadata.DateShot; !got.Equal(tc.wantTime) {
|
||||
t.Errorf("metadata.DateShot = %v, want: %v", got, tc.wantTime)
|
||||
}
|
||||
|
||||
if tc.wantOffsetSec == nil {
|
||||
if got := metadata.OffsetSecShot; got != nil {
|
||||
t.Errorf("metadata.OffsetSecShot = %+v, want: nil", got)
|
||||
}
|
||||
} else {
|
||||
if got, want := *metadata.OffsetSecShot, *tc.wantOffsetSec; got != want {
|
||||
t.Errorf("metadata.OffsetSecShot = &(%v), want: &(%v)", got, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user