From 3c80d4a842f876801b90a6932605f7b04b0fb82d Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Sun, 17 Jun 2018 12:56:02 +0200 Subject: [PATCH] Implemented thumbnails command --- cmd/photoprism/photoprism.go | 37 ++++++++++- config.go | 9 +++ mediafile.go | 10 +-- thumbnails.go | 120 ++++++++++++++++++++++++++++++++--- thumbnails_test.go | 54 ++++++++++++++++ 5 files changed, 216 insertions(+), 14 deletions(-) create mode 100644 thumbnails_test.go diff --git a/cmd/photoprism/photoprism.go b/cmd/photoprism/photoprism.go index 10b2f7cfe..6a8438763 100644 --- a/cmd/photoprism/photoprism.go +++ b/cmd/photoprism/photoprism.go @@ -43,6 +43,8 @@ func main() { conf.SetValuesFromCliContext(context) + conf.CreateDirectories() + fmt.Printf("Importing photos from %s...\n", conf.ImportPath) importer := photoprism.NewImporter(conf.OriginalsPath) @@ -62,6 +64,8 @@ func main() { conf.SetValuesFromCliContext(context) + conf.CreateDirectories() + fmt.Printf("Converting RAW images in %s to JPEG...\n", conf.OriginalsPath) converter := photoprism.NewConverter(conf.DarktableCli) @@ -76,14 +80,43 @@ func main() { { Name: "thumbnails", Usage: "Create thumbnails", + Flags: []cli.Flag{ + cli.IntSliceFlag{ + Name: "size, s", + Usage: "Thumbnail size in pixels", + }, + cli.BoolFlag{ + Name: "default, d", + Usage: "Render default sizes: 320, 500, 640, 1280, 1920 and 2560px", + }, + cli.BoolFlag{ + Name: "square, q", + Usage: "Square aspect ratio", + }, + }, Action: func(context *cli.Context) error { conf.SetValuesFromFile(photoprism.GetExpandedFilename(context.GlobalString("config-file"))) conf.SetValuesFromCliContext(context) + conf.CreateDirectories() + fmt.Printf("Creating thumbnails in %s...\n", conf.ThumbnailsPath) - fmt.Println("[TODO]") + sizes := context.IntSlice("size") + + if context.Bool("default") { + sizes = []int{320, 500, 640, 1280, 1920, 2560} + } + + if len(sizes) == 0 { + fmt.Println("No sizes selected. Nothing to do.") + return nil + } + + for _, size := range sizes { + photoprism.CreateThumbnailsFromOriginals(conf.OriginalsPath, conf.ThumbnailsPath, size, context.Bool("square")) + } fmt.Println("Done.") @@ -115,6 +148,8 @@ func main() { conf.SetValuesFromCliContext(context) + conf.CreateDirectories() + fmt.Printf("Exporting photos to %s...\n", conf.ExportPath) fmt.Println("[TODO]") diff --git a/config.go b/config.go index ec4938f09..1e4c4bd67 100644 --- a/config.go +++ b/config.go @@ -3,6 +3,8 @@ package photoprism import ( "github.com/kylelemons/go-gypsy/yaml" "github.com/urfave/cli" + "os" + "path" ) type Config struct { @@ -73,3 +75,10 @@ func (c *Config) SetValuesFromCliContext(context *cli.Context) error { return nil } + +func (c *Config) CreateDirectories() { + os.MkdirAll(path.Dir(c.OriginalsPath), os.ModePerm) + os.MkdirAll(path.Dir(c.ThumbnailsPath), os.ModePerm) + os.MkdirAll(path.Dir(c.ImportPath), os.ModePerm) + os.MkdirAll(path.Dir(c.ExportPath), os.ModePerm) +} diff --git a/mediafile.go b/mediafile.go index b06765203..2cf7ab3a3 100644 --- a/mediafile.go +++ b/mediafile.go @@ -103,14 +103,9 @@ func (m *MediaFile) GetCameraModel() string { func (m *MediaFile) GetCanonicalName() string { dateCreated := m.GetDateCreated().UTC() - //cameraModel := strings.Replace(m.GetCameraModel(), " ", "_", -1) result := dateCreated.Format("20060102_150405_") + strings.ToUpper(m.GetHash()[:12]) - /* if cameraModel != "" { - result = result + "_" + cameraModel - } */ - return result } @@ -264,6 +259,11 @@ func (m *MediaFile) GetExtension() string { } func (m *MediaFile) IsJpeg() bool { + // Don't import/use existing thumbnail files (we create our own) + if m.GetExtension() == ".thm" { + return false + } + return m.GetMimeType() == MimeTypeJpeg } diff --git a/thumbnails.go b/thumbnails.go index b9877f3da..60ec19073 100644 --- a/thumbnails.go +++ b/thumbnails.go @@ -3,18 +3,122 @@ package photoprism import ( "github.com/disintegration/imaging" "log" + "os" + "fmt" + "path/filepath" + "strings" ) -func CreateThumbnail() { - src, err := imaging.Open("testdata/lena_512.png") +func CreateThumbnailsFromOriginals(originalsPath string, thumbnailsPath string, size int, square bool) { + err := filepath.Walk(originalsPath, func(filename string, fileInfo os.FileInfo, err error) error { + if err != nil || fileInfo.IsDir() || strings.HasPrefix(filepath.Base(filename), ".") { + return nil + } + + mediaFile := NewMediaFile(filename) + + if !mediaFile.Exists() || !mediaFile.IsJpeg() { + return nil + } + + if square { + log.Printf("Creating square %dpx thumbnail for %s", size, filename) + + if _, err := mediaFile.GetSquareThumbnail(thumbnailsPath, size); err != nil { + log.Print(err.Error()) + } + } else { + log.Printf("Creating %dpx thumbnail for %s", size, filename) + + if _, err := mediaFile.GetThumbnail(thumbnailsPath, size); err != nil { + log.Print(err.Error()) + } + } + + return nil + }) if err != nil { - log.Printf("Open failed: %s", err.Error()) + log.Print(err.Error()) + } +} + +func (m *MediaFile) GetThumbnail(path string, size int) (result *MediaFile, err error) { + canonicalName := m.GetCanonicalName() + dateCreated := m.GetDateCreated() + + thumbnailPath := fmt.Sprintf("%s/%dpx/%s", path, size, dateCreated.UTC().Format("2006/01")) + + os.MkdirAll(thumbnailPath, os.ModePerm) + + thumbnailFilename := fmt.Sprintf("%s/%s_%dpx.jpg", thumbnailPath, canonicalName, size) + + if fileExists(thumbnailFilename) { + return NewMediaFile(thumbnailFilename), nil } - // Crop the original image to 350x350px size using the center anchor. - src = imaging.CropAnchor(src, 350, 350, imaging.Center) - - // Resize the cropped image to width = 256px preserving the aspect ratio. - src = imaging.Resize(src, 256, 0, imaging.Lanczos) + return m.CreateThumbnail(thumbnailFilename, size) +} + +// Resize preserving the aspect ratio +func (m *MediaFile) CreateThumbnail(filename string, size int) (result *MediaFile, err error) { + image, err := imaging.Open(m.filename) + + if err != nil { + log.Printf("open failed: %s", err.Error()) + return nil, err + } + + image = imaging.Fit(image, size, size, imaging.Lanczos) + + err = imaging.Save(image, filename) + + if err != nil { + log.Fatalf("failed to save image: %v", err) + return nil, err + } + + result = NewMediaFile(filename) + + return result, nil +} + +func (m *MediaFile) GetSquareThumbnail(path string, size int) (result *MediaFile, err error) { + canonicalName := m.GetCanonicalName() + dateCreated := m.GetDateCreated() + + thumbnailPath := fmt.Sprintf("%s/square/%dpx/%s", path, size, dateCreated.UTC().Format("2006/01")) + + os.MkdirAll(thumbnailPath, os.ModePerm) + + thumbnailFilename := fmt.Sprintf("%s/%s_square_%dpx.jpg", thumbnailPath, canonicalName, size) + + if fileExists(thumbnailFilename) { + return NewMediaFile(thumbnailFilename), nil + } + + return m.CreateSquareThumbnail(thumbnailFilename, size) +} + +// Resize and crop to square format +func (m *MediaFile) CreateSquareThumbnail(filename string, size int) (result *MediaFile, err error) { + image, err := imaging.Open(m.filename) + + if err != nil { + log.Printf("open failed: %s", err.Error()) + return nil, err + } + + image = imaging.Fill(image, size, size, imaging.Center, imaging.Lanczos) + + err = imaging.Save(image, filename) + + if err != nil { + log.Fatalf("failed to save image: %v", err) + return nil, err + } + + result = NewMediaFile(filename) + + return result, nil } diff --git a/thumbnails_test.go b/thumbnails_test.go new file mode 100644 index 000000000..8c3efb30b --- /dev/null +++ b/thumbnails_test.go @@ -0,0 +1,54 @@ +package photoprism + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestMediaFile_GetThumbnail(t *testing.T) { + conf := NewTestConfig() + + conf.CreateDirectories() + + conf.InitializeTestData(t) + + image1 := NewMediaFile(conf.ImportPath + "/iphone/IMG_6788.JPG") + + thumbnail1, err := image1.GetThumbnail(conf.ThumbnailsPath, 350) + + assert.Empty(t, err) + + assert.IsType(t, &MediaFile{}, thumbnail1) +} + +func TestMediaFile_GetSquareThumbnail(t *testing.T) { + conf := NewTestConfig() + + conf.CreateDirectories() + + conf.InitializeTestData(t) + + image1 := NewMediaFile(conf.ImportPath + "/iphone/IMG_6788.JPG") + + thumbnail1, err := image1.GetSquareThumbnail(conf.ThumbnailsPath, 350) + + assert.Empty(t, err) + + assert.IsType(t, &MediaFile{}, thumbnail1) +} + +func TestCreateThumbnailsFromOriginals(t *testing.T) { + conf := NewTestConfig() + + conf.CreateDirectories() + + conf.InitializeTestData(t) + + importer := NewImporter(conf.OriginalsPath) + + importer.ImportPhotosFromDirectory(conf.ImportPath) + + CreateThumbnailsFromOriginals(conf.OriginalsPath, conf.ThumbnailsPath, 600, false) + + CreateThumbnailsFromOriginals(conf.OriginalsPath, conf.ThumbnailsPath, 300, true) +} \ No newline at end of file