Backend: Refactor thumbnail package #157

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-01-06 14:32:15 +01:00
parent 17f6cd9593
commit e43983d579
48 changed files with 657 additions and 484 deletions

View file

@ -12,9 +12,11 @@ import (
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/file"
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query" "github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/rnd"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
@ -339,7 +341,7 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
} }
zipPath := path.Join(conf.ExportPath(), "album") zipPath := path.Join(conf.ExportPath(), "album")
zipToken := util.RandomToken(3) zipToken := rnd.Token(3)
zipBaseName := fmt.Sprintf("%s-%s.zip", strings.Title(a.AlbumSlug), zipToken) zipBaseName := fmt.Sprintf("%s-%s.zip", strings.Title(a.AlbumSlug), zipToken)
zipFileName := path.Join(zipPath, zipBaseName) zipFileName := path.Join(zipPath, zipBaseName)
@ -362,21 +364,21 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
zipWriter := zip.NewWriter(newZipFile) zipWriter := zip.NewWriter(newZipFile)
defer zipWriter.Close() defer zipWriter.Close()
for _, file := range p { for _, f := range p {
fileName := path.Join(conf.OriginalsPath(), file.FileName) fileName := path.Join(conf.OriginalsPath(), f.FileName)
fileAlias := file.DownloadFileName() fileAlias := f.DownloadFileName()
if util.Exists(fileName) { if file.Exists(fileName) {
if err := addFileToZip(zipWriter, fileName, fileAlias); err != nil { if err := addFileToZip(zipWriter, fileName, fileAlias); err != nil {
log.Error(err) log.Error(err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": util.UcFirst("failed to create zip file")}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": util.UcFirst("failed to create zip file")})
return return
} }
log.Infof("album: added \"%s\" as \"%s\"", file.FileName, fileAlias) log.Infof("album: added \"%s\" as \"%s\"", f.FileName, fileAlias)
} else { } else {
log.Warnf("album: \"%s\" is missing", file.FileName) log.Warnf("album: \"%s\" is missing", f.FileName)
file.FileMissing = true f.FileMissing = true
conf.Db().Save(&file) conf.Db().Save(&f)
} }
} }
@ -385,7 +387,7 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
zipWriter.Close() zipWriter.Close()
newZipFile.Close() newZipFile.Close()
if !util.Exists(zipFileName) { if !file.Exists(zipFileName) {
log.Errorf("could not find zip file: %s", zipFileName) log.Errorf("could not find zip file: %s", zipFileName)
c.Data(404, "image/svg+xml", photoIconSvg) c.Data(404, "image/svg+xml", photoIconSvg)
return return
@ -411,7 +413,7 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
typeName := c.Param("type") typeName := c.Param("type")
uuid := c.Param("uuid") uuid := c.Param("uuid")
thumbType, ok := photoprism.ThumbnailTypes[typeName] thumbType, ok := thumb.Types[typeName]
if !ok { if !ok {
log.Errorf("invalid type: %s", typeName) log.Errorf("invalid type: %s", typeName)
@ -421,7 +423,7 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
q := query.New(conf.OriginalsPath(), conf.Db()) q := query.New(conf.OriginalsPath(), conf.Db())
file, err := q.FindAlbumThumbByUUID(uuid) f, err := q.FindAlbumThumbByUUID(uuid)
if err != nil { if err != nil {
log.Debugf("album has no photos yet, using generic thumb image: %s", uuid) log.Debugf("album has no photos yet, using generic thumb image: %s", uuid)
@ -429,21 +431,21 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
return return
} }
fileName := path.Join(conf.OriginalsPath(), file.FileName) fileName := path.Join(conf.OriginalsPath(), f.FileName)
if !util.Exists(fileName) { if !file.Exists(fileName) {
log.Errorf("could not find original for thumbnail: %s", fileName) log.Errorf("could not find original for thumbnail: %s", fileName)
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg) c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore // Set missing flag so that the file doesn't show up in search results anymore
file.FileMissing = true f.FileMissing = true
conf.Db().Save(&file) conf.Db().Save(&f)
return return
} }
if thumbnail, err := photoprism.ThumbnailFromFile(fileName, file.FileHash, conf.ThumbnailsPath(), thumbType.Width, thumbType.Height, thumbType.Options...); err == nil { if thumbnail, err := thumb.FromFile(fileName, f.FileHash, conf.ThumbnailsPath(), thumbType.Width, thumbType.Height, thumbType.Options...); err == nil {
if c.Query("download") != "" { if c.Query("download") != "" {
downloadFileName := file.DownloadFileName() downloadFileName := f.DownloadFileName()
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName)) c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))
} }

View file

@ -90,7 +90,6 @@ func BatchPhotosRestore(router *gin.RouterGroup, conf *config.Config) {
}) })
} }
// POST /api/v1/batch/albums/delete // POST /api/v1/batch/albums/delete
func BatchAlbumsDelete(router *gin.RouterGroup, conf *config.Config) { func BatchAlbumsDelete(router *gin.RouterGroup, conf *config.Config) {
router.POST("/batch/albums/delete", func(c *gin.Context) { router.POST("/batch/albums/delete", func(c *gin.Context) {

View file

@ -5,8 +5,8 @@ import (
"path" "path"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/file"
"github.com/photoprism/photoprism/internal/query" "github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/util"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -24,26 +24,26 @@ func GetDownload(router *gin.RouterGroup, conf *config.Config) {
fileHash := c.Param("hash") fileHash := c.Param("hash")
q := query.New(conf.OriginalsPath(), conf.Db()) q := query.New(conf.OriginalsPath(), conf.Db())
file, err := q.FindFileByHash(fileHash) f, err := q.FindFileByHash(fileHash)
if err != nil { if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()}) c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})
return return
} }
fileName := path.Join(conf.OriginalsPath(), file.FileName) fileName := path.Join(conf.OriginalsPath(), f.FileName)
if !util.Exists(fileName) { if !file.Exists(fileName) {
log.Errorf("could not find original: %s", fileHash) log.Errorf("could not find original: %s", fileHash)
c.Data(404, "image/svg+xml", photoIconSvg) c.Data(404, "image/svg+xml", photoIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore // Set missing flag so that the file doesn't show up in search results anymore
file.FileMissing = true f.FileMissing = true
conf.Db().Save(&file) conf.Db().Save(&f)
return return
} }
downloadFileName := file.DownloadFileName() downloadFileName := f.DownloadFileName()
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName)) c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))

View file

@ -8,11 +8,10 @@ import (
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/file"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/photoprism" "github.com/photoprism/photoprism/internal/photoprism"
) )
@ -61,7 +60,7 @@ func StartImport(router *gin.RouterGroup, conf *config.Config) {
imp.Start(path) imp.Start(path)
if subPath != "" && path != conf.ImportPath() && util.DirectoryIsEmpty(path) { if subPath != "" && path != conf.ImportPath() && file.IsEmpty(path) {
if err := os.Remove(path); err != nil { if err := os.Remove(path); err != nil {
log.Errorf("import: could not deleted empty directory \"%s\": %s", path, err) log.Errorf("import: could not deleted empty directory \"%s\": %s", path, err)
} else { } else {

View file

@ -55,11 +55,6 @@ func StartIndexing(router *gin.RouterGroup, conf *config.Config) {
return return
} }
// Set thumbnails JPEG quality and size
photoprism.JpegQuality = conf.ThumbQuality()
photoprism.MaxThumbWidth = conf.ThumbSize()
photoprism.MaxThumbHeight = conf.ThumbSize()
path := conf.OriginalsPath() path := conf.OriginalsPath()
event.Info(fmt.Sprintf("indexing photos in \"%s\"", filepath.Base(path))) event.Info(fmt.Sprintf("indexing photos in \"%s\"", filepath.Base(path)))

View file

@ -12,9 +12,10 @@ import (
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/file"
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query" "github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/util"
) )
@ -123,7 +124,7 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
labelUUID := c.Param("uuid") labelUUID := c.Param("uuid")
start := time.Now() start := time.Now()
thumbType, ok := photoprism.ThumbnailTypes[typeName] thumbType, ok := thumb.Types[typeName]
if !ok { if !ok {
log.Errorf("invalid type: %s", typeName) log.Errorf("invalid type: %s", typeName)
@ -142,7 +143,7 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
return return
} }
file, err := q.FindLabelThumbByUUID(labelUUID) f, err := q.FindLabelThumbByUUID(labelUUID)
if err != nil { if err != nil {
log.Errorf(err.Error()) log.Errorf(err.Error())
@ -150,19 +151,19 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
return return
} }
fileName := path.Join(conf.OriginalsPath(), file.FileName) fileName := path.Join(conf.OriginalsPath(), f.FileName)
if !util.Exists(fileName) { if !file.Exists(fileName) {
log.Errorf("could not find original for thumbnail: %s", fileName) log.Errorf("could not find original for thumbnail: %s", fileName)
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg) c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore // Set missing flag so that the file doesn't show up in search results anymore
file.FileMissing = true f.FileMissing = true
conf.Db().Save(&file) conf.Db().Save(&f)
return return
} }
if thumbnail, err := photoprism.ThumbnailFromFile(fileName, file.FileHash, conf.ThumbnailsPath(), thumbType.Width, thumbType.Height, thumbType.Options...); err == nil { if thumbnail, err := thumb.FromFile(fileName, f.FileHash, conf.ThumbnailsPath(), thumbType.Width, thumbType.Height, thumbType.Options...); err == nil {
thumbData, err := ioutil.ReadFile(thumbnail) thumbData, err := ioutil.ReadFile(thumbnail)
if err != nil { if err != nil {

View file

@ -7,6 +7,7 @@ import (
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/file"
"github.com/photoprism/photoprism/internal/query" "github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/util"
@ -73,26 +74,26 @@ func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
func GetPhotoDownload(router *gin.RouterGroup, conf *config.Config) { func GetPhotoDownload(router *gin.RouterGroup, conf *config.Config) {
router.GET("/photos/:uuid/download", func(c *gin.Context) { router.GET("/photos/:uuid/download", func(c *gin.Context) {
q := query.New(conf.OriginalsPath(), conf.Db()) q := query.New(conf.OriginalsPath(), conf.Db())
file, err := q.FindFileByPhotoUUID(c.Param("uuid")) f, err := q.FindFileByPhotoUUID(c.Param("uuid"))
if err != nil { if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()}) c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})
return return
} }
fileName := path.Join(conf.OriginalsPath(), file.FileName) fileName := path.Join(conf.OriginalsPath(), f.FileName)
if !util.Exists(fileName) { if !file.Exists(fileName) {
log.Errorf("could not find original: %s", c.Param("uuid")) log.Errorf("could not find original: %s", c.Param("uuid"))
c.Data(404, "image/svg+xml", photoIconSvg) c.Data(404, "image/svg+xml", photoIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore // Set missing flag so that the file doesn't show up in search results anymore
file.FileMissing = true f.FileMissing = true
conf.Db().Save(&file) conf.Db().Save(&f)
return return
} }
downloadFileName := file.DownloadFileName() downloadFileName := f.DownloadFileName()
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName)) c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))

View file

@ -5,12 +5,11 @@ import (
"net/http" "net/http"
"path" "path"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/util"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/photoprism" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/file"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/thumb"
) )
// GET /api/v1/thumbnails/:hash/:type // GET /api/v1/thumbnails/:hash/:type
@ -23,7 +22,7 @@ func GetThumbnail(router *gin.RouterGroup, conf *config.Config) {
fileHash := c.Param("hash") fileHash := c.Param("hash")
typeName := c.Param("type") typeName := c.Param("type")
thumbType, ok := photoprism.ThumbnailTypes[typeName] thumbType, ok := thumb.Types[typeName]
if !ok { if !ok {
log.Errorf("invalid type: %s", typeName) log.Errorf("invalid type: %s", typeName)
@ -32,28 +31,28 @@ func GetThumbnail(router *gin.RouterGroup, conf *config.Config) {
} }
q := query.New(conf.OriginalsPath(), conf.Db()) q := query.New(conf.OriginalsPath(), conf.Db())
file, err := q.FindFileByHash(fileHash) f, err := q.FindFileByHash(fileHash)
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()}) c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()})
return return
} }
fileName := path.Join(conf.OriginalsPath(), file.FileName) fileName := path.Join(conf.OriginalsPath(), f.FileName)
if !util.Exists(fileName) { if !file.Exists(fileName) {
log.Errorf("could not find original for thumbnail: %s", fileName) log.Errorf("could not find original for thumbnail: %s", fileName)
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg) c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore // Set missing flag so that the file doesn't show up in search results anymore
file.FileMissing = true f.FileMissing = true
conf.Db().Save(&file) conf.Db().Save(&f)
return return
} }
if thumbnail, err := photoprism.ThumbnailFromFile(fileName, file.FileHash, conf.ThumbnailsPath(), thumbType.Width, thumbType.Height, thumbType.Options...); err == nil { if thumbnail, err := thumb.FromFile(fileName, f.FileHash, conf.ThumbnailsPath(), thumbType.Width, thumbType.Height, thumbType.Options...); err == nil {
if c.Query("download") != "" { if c.Query("download") != "" {
downloadFileName := file.DownloadFileName() downloadFileName := f.DownloadFileName()
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName)) c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))
} }

View file

@ -12,10 +12,10 @@ import (
"github.com/disintegration/imaging" "github.com/disintegration/imaging"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/file"
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query" "github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/thumb"
) )
// GET /api/v1/preview // GET /api/v1/preview
@ -33,7 +33,7 @@ func GetPreview(router *gin.RouterGroup, conf *config.Config) {
previewFilename := fmt.Sprintf("%s/%s.jpg", thumbPath, t[6:8]) previewFilename := fmt.Sprintf("%s/%s.jpg", thumbPath, t[6:8])
if util.Exists(previewFilename) { if file.Exists(previewFilename) {
c.File(previewFilename) c.File(previewFilename)
return return
} }
@ -60,22 +60,22 @@ func GetPreview(router *gin.RouterGroup, conf *config.Config) {
y := 0 y := 0
preview := imaging.New(width, height, color.NRGBA{255, 255, 255, 255}) preview := imaging.New(width, height, color.NRGBA{255, 255, 255, 255})
thumbType, _ := photoprism.ThumbnailTypes["tile_224"] thumbType, _ := thumb.Types["tile_224"]
for _, file := range p { for _, f := range p {
fileName := path.Join(conf.OriginalsPath(), file.FileName) fileName := path.Join(conf.OriginalsPath(), f.FileName)
if !util.Exists(fileName) { if !file.Exists(fileName) {
log.Errorf("could not find original for thumbnail: %s", fileName) log.Errorf("could not find original for thumbnail: %s", fileName)
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg) c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore // Set missing flag so that the file doesn't show up in search results anymore
file.FileMissing = true f.FileMissing = true
conf.Db().Save(&file) conf.Db().Save(&f)
return return
} }
thumbnail, err := photoprism.ThumbnailFromFile(fileName, file.FileHash, conf.ThumbnailsPath(), thumbType.Width, thumbType.Height, thumbType.Options...) thumbnail, err := thumb.FromFile(fileName, f.FileHash, conf.ThumbnailsPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
if err != nil { if err != nil {
log.Error(err) log.Error(err)

View file

@ -11,8 +11,10 @@ import (
"time" "time"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/file"
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/query" "github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/rnd"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/util"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -44,7 +46,7 @@ func CreateZip(router *gin.RouterGroup, conf *config.Config) {
} }
zipPath := path.Join(conf.ExportPath(), "zip") zipPath := path.Join(conf.ExportPath(), "zip")
zipToken := util.RandomToken(3) zipToken := rnd.Token(3)
zipYear := time.Now().Format("January-2006") zipYear := time.Now().Format("January-2006")
zipBaseName := fmt.Sprintf("Photos-%s-%s.zip", zipYear, zipToken) zipBaseName := fmt.Sprintf("Photos-%s-%s.zip", zipYear, zipToken)
zipFileName := path.Join(zipPath, zipBaseName) zipFileName := path.Join(zipPath, zipBaseName)
@ -68,21 +70,21 @@ func CreateZip(router *gin.RouterGroup, conf *config.Config) {
zipWriter := zip.NewWriter(newZipFile) zipWriter := zip.NewWriter(newZipFile)
defer zipWriter.Close() defer zipWriter.Close()
for _, file := range files { for _, f := range files {
fileName := path.Join(conf.OriginalsPath(), file.FileName) fileName := path.Join(conf.OriginalsPath(), f.FileName)
fileAlias := file.DownloadFileName() fileAlias := f.DownloadFileName()
if util.Exists(fileName) { if file.Exists(fileName) {
if err := addFileToZip(zipWriter, fileName, fileAlias); err != nil { if err := addFileToZip(zipWriter, fileName, fileAlias); err != nil {
log.Error(err) log.Error(err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": util.UcFirst("failed to create zip file")}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": util.UcFirst("failed to create zip file")})
return return
} }
log.Infof("zip: added \"%s\" as \"%s\"", file.FileName, fileAlias) log.Infof("zip: added \"%s\" as \"%s\"", f.FileName, fileAlias)
} else { } else {
log.Warnf("zip: \"%s\" is missing", file.FileName) log.Warnf("zip: \"%s\" is missing", f.FileName)
file.FileMissing = true f.FileMissing = true
conf.Db().Save(&file) conf.Db().Save(&f)
} }
} }
@ -103,7 +105,7 @@ func DownloadZip(router *gin.RouterGroup, conf *config.Config) {
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", zipBaseName)) c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", zipBaseName))
if !util.Exists(zipFileName) { if !file.Exists(zipFileName) {
log.Errorf("could not find zip file: %s", zipFileName) log.Errorf("could not find zip file: %s", zipFileName)
c.Data(404, "image/svg+xml", photoIconSvg) c.Data(404, "image/svg+xml", photoIconSvg)
return return

View file

@ -12,14 +12,14 @@ import (
"syscall" "syscall"
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/file"
"github.com/sevlyar/go-daemon" "github.com/sevlyar/go-daemon"
) )
var log = event.Log var log = event.Log
func childAlreadyRunning(filePath string) (pid int, running bool) { func childAlreadyRunning(filePath string) (pid int, running bool) {
if !util.Exists(filePath) { if !file.Exists(filePath) {
return pid, false return pid, false
} }

View file

@ -10,8 +10,8 @@ import (
"time" "time"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/file"
"github.com/photoprism/photoprism/internal/server" "github.com/photoprism/photoprism/internal/server"
"github.com/photoprism/photoprism/internal/util"
"github.com/sevlyar/go-daemon" "github.com/sevlyar/go-daemon"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -96,7 +96,7 @@ func startAction(ctx *cli.Context) error {
} }
if child != nil { if child != nil {
if !util.Overwrite(conf.PIDFilename(), []byte(strconv.Itoa(child.Pid))) { if !file.Overwrite(conf.PIDFilename(), []byte(strconv.Itoa(child.Pid))) {
log.Fatalf("failed writing process id to \"%s\"", conf.PIDFilename()) log.Fatalf("failed writing process id to \"%s\"", conf.PIDFilename())
} }

View file

@ -32,10 +32,6 @@ func thumbnailsAction(ctx *cli.Context) error {
log.Infof("creating thumbnails in \"%s\"", conf.ThumbnailsPath()) log.Infof("creating thumbnails in \"%s\"", conf.ThumbnailsPath())
photoprism.JpegQuality = conf.ThumbQuality()
photoprism.MaxThumbWidth = conf.ThumbSize()
photoprism.MaxThumbHeight = conf.ThumbSize()
if err := photoprism.CreateThumbnailsFromOriginals(conf.OriginalsPath(), conf.ThumbnailsPath(), ctx.Bool("force")); err != nil { if err := photoprism.CreateThumbnailsFromOriginals(conf.OriginalsPath(), conf.ThumbnailsPath(), ctx.Bool("force")); err != nil {
log.Error(err) log.Error(err)
return err return err

View file

@ -6,7 +6,7 @@ import (
"github.com/photoprism/photoprism/internal/colors" "github.com/photoprism/photoprism/internal/colors"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/file"
) )
// HTTP client / Web UI config values // HTTP client / Web UI config values
@ -118,8 +118,8 @@ func (c *Config) ClientConfig() ClientConfig {
categories[i].Title = strings.Title(l.LabelName) categories[i].Title = strings.Title(l.LabelName)
} }
jsHash := util.Hash(c.HttpStaticBuildPath() + "/app.js") jsHash := file.Hash(c.HttpStaticBuildPath() + "/app.js")
cssHash := util.Hash(c.HttpStaticBuildPath() + "/app.css") cssHash := file.Hash(c.HttpStaticBuildPath() + "/app.css")
// Feature Flags // Feature Flags
var flags []string var flags []string

View file

@ -10,6 +10,7 @@ import (
_ "github.com/jinzhu/gorm/dialects/sqlite" _ "github.com/jinzhu/gorm/dialects/sqlite"
gc "github.com/patrickmn/go-cache" gc "github.com/patrickmn/go-cache"
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -22,6 +23,15 @@ type Config struct {
config *Params config *Params
} }
func init() {
for name, t := range thumb.Types {
if t.Public {
thumbnail := Thumbnail{Name: name, Width: t.Width, Height: t.Height}
Thumbnails = append(Thumbnails, thumbnail)
}
}
}
func initLogger(debug bool) { func initLogger(debug bool) {
log.SetFormatter(&logrus.TextFormatter{ log.SetFormatter(&logrus.TextFormatter{
DisableColors: false, DisableColors: false,
@ -44,6 +54,10 @@ func NewConfig(ctx *cli.Context) *Config {
log.SetLevel(c.LogLevel()) log.SetLevel(c.LogLevel())
thumb.JpegQuality = c.ThumbQuality()
thumb.MaxWidth = c.ThumbSize()
thumb.MaxHeight = c.ThumbSize()
return c return c
} }
@ -180,17 +194,32 @@ func (c *Config) Workers() int {
return runtime.NumCPU() return runtime.NumCPU()
} }
// ThumbQuality returns the thumbnail jpeg quality setting (0-100). // ThumbQuality returns the thumbnail jpeg quality setting (25-100).
func (c *Config) ThumbQuality() int { func (c *Config) ThumbQuality() int {
if c.config.ThumbQuality > 100 {
return 100
}
if c.config.ThumbQuality < 25 {
return 25
}
return c.config.ThumbQuality return c.config.ThumbQuality
} }
// ThumbSize returns the thumbnail size limit in pixels. // ThumbSize returns the thumbnail size limit in pixels (720-16384).
func (c *Config) ThumbSize() int { func (c *Config) ThumbSize() int {
if c.config.ThumbSize > 16384 {
return 16384
}
if c.config.ThumbSize < 720 {
return 720
}
return c.config.ThumbSize return c.config.ThumbSize
} }
// GeoCodingApi returns the preferred geo coding api (none, osm or places). // GeoCodingApi returns the preferred geo coding api (none, osm or places).
func (c *Config) GeoCodingApi() string { func (c *Config) GeoCodingApi() string {
switch c.config.GeoCodingApi { switch c.config.GeoCodingApi {

View file

@ -3,7 +3,7 @@ package config
import ( import (
"testing" "testing"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/file"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -17,7 +17,7 @@ func TestNewConfig(t *testing.T) {
assert.IsType(t, new(Config), c) assert.IsType(t, new(Config), c)
assert.Equal(t, util.ExpandedFilename("../../assets"), c.AssetsPath()) assert.Equal(t, file.ExpandFilename("../../assets"), c.AssetsPath())
assert.False(t, c.Debug()) assert.False(t, c.Debug())
assert.False(t, c.ReadOnly()) assert.False(t, c.ReadOnly())
} }

View file

@ -138,6 +138,9 @@ func (c *Config) connectToDatabase(ctx context.Context) error {
} }
} }
db.LogMode(false)
db.SetLogger(log)
c.db = db c.db = db
return err return err
} }
@ -185,7 +188,7 @@ func (c *Config) ImportSQL(filename string) {
continue continue
} }
var result struct {} var result struct{}
err := q.Raw(stmt).Scan(&result).Error err := q.Raw(stmt).Scan(&result).Error

View file

@ -5,7 +5,7 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/file"
) )
func findExecutable(configBin, defaultBin string) (result string) { func findExecutable(configBin, defaultBin string) (result string) {
@ -19,7 +19,7 @@ func findExecutable(configBin, defaultBin string) (result string) {
result = path result = path
} }
if !util.Exists(result) { if !file.Exists(result) {
result = "" result = ""
} }

View file

@ -227,13 +227,13 @@ var GlobalFlags = []cli.Flag{
}, },
cli.IntFlag{ cli.IntFlag{
Name: "thumb-quality, q", Name: "thumb-quality, q",
Usage: "jpeg quality of thumbnails (0-100)", Usage: "jpeg quality of thumbnails (25-100)",
Value: 95, Value: 95,
EnvVar: "PHOTOPRISM_THUMB_QUALITY", EnvVar: "PHOTOPRISM_THUMB_QUALITY",
}, },
cli.IntFlag{ cli.IntFlag{
Name: "thumb-size", Name: "thumb-size",
Usage: "max thumbnail size in pixels", Usage: "max thumbnail size in pixels (720-16384)",
Value: 8192, Value: 8192,
EnvVar: "PHOTOPRISM_THUMB_SIZE", EnvVar: "PHOTOPRISM_THUMB_SIZE",
}, },

View file

@ -8,7 +8,7 @@ import (
_ "github.com/jinzhu/gorm/dialects/mysql" _ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/sqlite" _ "github.com/jinzhu/gorm/dialects/sqlite"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/file"
"github.com/urfave/cli" "github.com/urfave/cli"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@ -87,7 +87,7 @@ func NewParams(ctx *cli.Context) *Params {
c.Name = ctx.App.Name c.Name = ctx.App.Name
c.Copyright = ctx.App.Copyright c.Copyright = ctx.App.Copyright
c.Version = ctx.App.Version c.Version = ctx.App.Version
c.ConfigFile = util.ExpandedFilename(ctx.GlobalString("config-file")) c.ConfigFile = file.ExpandFilename(ctx.GlobalString("config-file"))
if err := c.SetValuesFromFile(c.ConfigFile); err != nil { if err := c.SetValuesFromFile(c.ConfigFile); err != nil {
log.Debug(err) log.Debug(err)
@ -103,21 +103,21 @@ func NewParams(ctx *cli.Context) *Params {
} }
func (c *Params) expandFilenames() { func (c *Params) expandFilenames() {
c.ConfigPath = util.ExpandedFilename(c.ConfigPath) c.ConfigPath = file.ExpandFilename(c.ConfigPath)
c.ResourcesPath = util.ExpandedFilename(c.ResourcesPath) c.ResourcesPath = file.ExpandFilename(c.ResourcesPath)
c.AssetsPath = util.ExpandedFilename(c.AssetsPath) c.AssetsPath = file.ExpandFilename(c.AssetsPath)
c.CachePath = util.ExpandedFilename(c.CachePath) c.CachePath = file.ExpandFilename(c.CachePath)
c.OriginalsPath = util.ExpandedFilename(c.OriginalsPath) c.OriginalsPath = file.ExpandFilename(c.OriginalsPath)
c.ImportPath = util.ExpandedFilename(c.ImportPath) c.ImportPath = file.ExpandFilename(c.ImportPath)
c.ExportPath = util.ExpandedFilename(c.ExportPath) c.ExportPath = file.ExpandFilename(c.ExportPath)
c.SqlServerPath = util.ExpandedFilename(c.SqlServerPath) c.SqlServerPath = file.ExpandFilename(c.SqlServerPath)
c.PIDFilename = util.ExpandedFilename(c.PIDFilename) c.PIDFilename = file.ExpandFilename(c.PIDFilename)
c.LogFilename = util.ExpandedFilename(c.LogFilename) c.LogFilename = file.ExpandFilename(c.LogFilename)
} }
// SetValuesFromFile uses a yaml config file to initiate the configuration entity. // SetValuesFromFile uses a yaml config file to initiate the configuration entity.
func (c *Params) SetValuesFromFile(fileName string) error { func (c *Params) SetValuesFromFile(fileName string) error {
if !util.Exists(fileName) { if !file.Exists(fileName) {
return errors.New(fmt.Sprintf("config file not found: \"%s\"", fileName)) return errors.New(fmt.Sprintf("config file not found: \"%s\"", fileName))
} }

View file

@ -3,7 +3,7 @@ package config
import ( import (
"testing" "testing"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/file"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -17,7 +17,7 @@ func TestNewParams(t *testing.T) {
assert.IsType(t, new(Params), c) assert.IsType(t, new(Params), c)
assert.Equal(t, util.ExpandedFilename("../../assets"), c.AssetsPath) assert.Equal(t, file.ExpandFilename("../../assets"), c.AssetsPath)
assert.False(t, c.Debug) assert.False(t, c.Debug)
assert.False(t, c.ReadOnly) assert.False(t, c.ReadOnly)
} }

View file

@ -5,7 +5,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/file"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@ -20,7 +20,7 @@ func NewSettings() *Settings {
// SetValuesFromFile uses a yaml config file to initiate the configuration entity. // SetValuesFromFile uses a yaml config file to initiate the configuration entity.
func (s *Settings) SetValuesFromFile(fileName string) error { func (s *Settings) SetValuesFromFile(fileName string) error {
if !util.Exists(fileName) { if !file.Exists(fileName) {
return fmt.Errorf("settings file not found: \"%s\"", fileName) return fmt.Errorf("settings file not found: \"%s\"", fileName)
} }
@ -35,7 +35,7 @@ func (s *Settings) SetValuesFromFile(fileName string) error {
// WriteValuesToFile uses a yaml config file to initiate the configuration entity. // WriteValuesToFile uses a yaml config file to initiate the configuration entity.
func (s *Settings) WriteValuesToFile(fileName string) error { func (s *Settings) WriteValuesToFile(fileName string) error {
if !util.Exists(fileName) { if !file.Exists(fileName) {
return fmt.Errorf("settings file not found: \"%s\"", fileName) return fmt.Errorf("settings file not found: \"%s\"", fileName)
} }

View file

@ -10,7 +10,8 @@ import (
_ "github.com/jinzhu/gorm/dialects/mysql" _ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/sqlite" _ "github.com/jinzhu/gorm/dialects/sqlite"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/file"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -29,7 +30,7 @@ func testDataPath(assetsPath string) string {
} }
func NewTestParams() *Params { func NewTestParams() *Params {
assetsPath := util.ExpandedFilename("../../assets") assetsPath := file.ExpandFilename("../../assets")
testDataPath := testDataPath(assetsPath) testDataPath := testDataPath(assetsPath)
@ -52,7 +53,7 @@ func NewTestParams() *Params {
} }
func NewTestParamsError() *Params { func NewTestParamsError() *Params {
assetsPath := util.ExpandedFilename("../..") assetsPath := file.ExpandFilename("../..")
testDataPath := testDataPath("../../assets") testDataPath := testDataPath("../../assets")
@ -93,6 +94,10 @@ func NewTestConfig() *Config {
c.ImportSQL(c.ExamplesPath() + "/fixtures.sql") c.ImportSQL(c.ExamplesPath() + "/fixtures.sql")
thumb.JpegQuality = c.ThumbQuality()
thumb.MaxWidth = c.ThumbSize()
thumb.MaxHeight = c.ThumbSize()
return c return c
} }
@ -140,8 +145,8 @@ func (c *Config) RemoveTestData(t *testing.T) {
} }
func (c *Config) DownloadTestData(t *testing.T) { func (c *Config) DownloadTestData(t *testing.T) {
if util.Exists(TestDataZip) { if file.Exists(TestDataZip) {
hash := util.Hash(TestDataZip) hash := file.Hash(TestDataZip)
if hash != TestDataHash { if hash != TestDataHash {
os.Remove(TestDataZip) os.Remove(TestDataZip)
@ -149,17 +154,17 @@ func (c *Config) DownloadTestData(t *testing.T) {
} }
} }
if !util.Exists(TestDataZip) { if !file.Exists(TestDataZip) {
fmt.Printf("downloading latest test data zip file from %s\n", TestDataURL) fmt.Printf("downloading latest test data zip file from %s\n", TestDataURL)
if err := util.Download(TestDataZip, TestDataURL); err != nil { if err := file.Download(TestDataZip, TestDataURL); err != nil {
fmt.Printf("Download failed: %s\n", err.Error()) fmt.Printf("Download failed: %s\n", err.Error())
} }
} }
} }
func (c *Config) UnzipTestData(t *testing.T) { func (c *Config) UnzipTestData(t *testing.T) {
if _, err := util.Unzip(TestDataZip, testDataPath(c.AssetsPath())); err != nil { if _, err := file.Unzip(TestDataZip, testDataPath(c.AssetsPath())); err != nil {
t.Logf("could not unzip test data: %s\n", err.Error()) t.Logf("could not unzip test data: %s\n", err.Error())
} }
} }

View file

@ -4,7 +4,7 @@ import (
"testing" "testing"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/file"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -26,7 +26,7 @@ func TestNewTestParams(t *testing.T) {
assert.IsType(t, new(Params), c) assert.IsType(t, new(Params), c)
assert.Equal(t, util.ExpandedFilename("../../assets"), c.AssetsPath) assert.Equal(t, file.ExpandFilename("../../assets"), c.AssetsPath)
assert.False(t, c.Debug) assert.False(t, c.Debug)
} }
@ -43,7 +43,7 @@ func TestNewTestParamsError(t *testing.T) {
assert.IsType(t, new(Params), c) assert.IsType(t, new(Params), c)
assert.Equal(t, util.ExpandedFilename("../.."), c.AssetsPath) assert.Equal(t, file.ExpandFilename("../.."), c.AssetsPath)
assert.Equal(t, "../../assets/testdata/cache", c.CachePath) assert.Equal(t, "../../assets/testdata/cache", c.CachePath)
assert.False(t, c.Debug) assert.False(t, c.Debug)
} }

View file

@ -16,7 +16,7 @@ import (
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/rnd"
) )
var log = event.Log var log = event.Log
@ -32,7 +32,7 @@ func ID(prefix rune) string {
result := make([]byte, 0, 17) result := make([]byte, 0, 17)
result = append(result, byte(prefix)) result = append(result, byte(prefix))
result = append(result, strconv.FormatInt(time.Now().UTC().Unix(), 36)[0:6]...) result = append(result, strconv.FormatInt(time.Now().UTC().Unix(), 36)[0:6]...)
result = append(result, util.RandomToken(10)...) result = append(result, rnd.Token(10)...)
return string(result) return string(result)
} }

View file

@ -1,4 +1,11 @@
package util /*
This package encapsulates file related constants and functions.
Additional information can be found in our Developer Guide:
https://github.com/photoprism/photoprism/wiki
*/
package file
import ( import (
"archive/zip" "archive/zip"
@ -9,8 +16,12 @@ import (
"os/user" "os/user"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/photoprism/photoprism/internal/event"
) )
var log = event.Log
// Returns true if file exists // Returns true if file exists
func Exists(filename string) bool { func Exists(filename string) bool {
info, err := os.Stat(filename) info, err := os.Stat(filename)
@ -30,7 +41,7 @@ func Overwrite(fileName string, data []byte) bool {
} }
// Returns full path; ~ replaced with actual home directory // Returns full path; ~ replaced with actual home directory
func ExpandedFilename(filename string) string { func ExpandFilename(filename string) string {
if filename == "" { if filename == "" {
return "" return ""
} }
@ -50,32 +61,6 @@ func ExpandedFilename(filename string) string {
return result return result
} }
// Extract Zip file in destination directory
func Unzip(src, dest string) (fileNames []string, err error) {
r, err := zip.OpenReader(src)
if err != nil {
return fileNames, err
}
defer r.Close()
for _, f := range r.File {
// Skip directories like __OSX
if strings.HasPrefix(f.Name, "__") {
continue
}
fn, err := copyToFile(f, dest)
if err != nil {
return fileNames, err
}
fileNames = append(fileNames, fn)
}
return fileNames, nil
}
// copyToFile copies the zip file to destination // copyToFile copies the zip file to destination
// if the zip file is a directory, a directory is created at the destination. // if the zip file is a directory, a directory is created at the destination.
func copyToFile(f *zip.File, dest string) (fileName string, err error) { func copyToFile(f *zip.File, dest string) (fileName string, err error) {
@ -151,7 +136,7 @@ func Download(filepath string, url string) error {
return nil return nil
} }
func DirectoryIsEmpty(path string) bool { func IsEmpty(path string) bool {
f, err := os.Open(path) f, err := os.Open(path)
if err != nil { if err != nil {

View file

@ -1,4 +1,4 @@
package util package file
import ( import (
"os" "os"
@ -29,19 +29,19 @@ func TestOverwrite(t *testing.T) {
func TestExpandedFilename(t *testing.T) { func TestExpandedFilename(t *testing.T) {
t.Run("test.jpg", func(t *testing.T) { t.Run("test.jpg", func(t *testing.T) {
filename := ExpandedFilename("./testdata/test.jpg") filename := ExpandFilename("./testdata/test.jpg")
assert.Contains(t, filename, "/testdata/test.jpg") assert.Contains(t, filename, "/testdata/test.jpg")
assert.IsType(t, "", filename) assert.IsType(t, "", filename)
}) })
t.Run("empty filename", func(t *testing.T) { t.Run("empty filename", func(t *testing.T) {
filename := ExpandedFilename("") filename := ExpandFilename("")
assert.Equal(t, "", filename) assert.Equal(t, "", filename)
assert.IsType(t, "", filename) assert.IsType(t, "", filename)
}) })
t.Run("~ in filename", func(t *testing.T) { t.Run("~ in filename", func(t *testing.T) {
usr, _ := user.Current() usr, _ := user.Current()
expected := usr.HomeDir + "/test.jpg" expected := usr.HomeDir + "/test.jpg"
filename := ExpandedFilename("~/test.jpg") filename := ExpandFilename("~/test.jpg")
assert.Equal(t, expected, filename) assert.Equal(t, expected, filename)
assert.IsType(t, "", filename) assert.IsType(t, "", filename)
}) })
@ -49,14 +49,14 @@ func TestExpandedFilename(t *testing.T) {
func TestDirectoryIsEmpty(t *testing.T) { func TestDirectoryIsEmpty(t *testing.T) {
t.Run("not empty path", func(t *testing.T) { t.Run("not empty path", func(t *testing.T) {
assert.Equal(t, false, DirectoryIsEmpty("./testdata")) assert.Equal(t, false, IsEmpty("./testdata"))
}) })
t.Run("not existing path", func(t *testing.T) { t.Run("not existing path", func(t *testing.T) {
assert.Equal(t, false, DirectoryIsEmpty("./xxx")) assert.Equal(t, false, IsEmpty("./xxx"))
}) })
t.Run("empty path", func(t *testing.T) { t.Run("empty path", func(t *testing.T) {
os.Mkdir("./testdata/emptyDir", 0777) os.Mkdir("./testdata/emptyDir", 0777)
defer os.RemoveAll("./testdata/emptyDir") defer os.RemoveAll("./testdata/emptyDir")
assert.Equal(t, true, DirectoryIsEmpty("./testdata/emptyDir")) assert.Equal(t, true, IsEmpty("./testdata/emptyDir"))
}) })
} }

View file

@ -1,4 +1,4 @@
package util package file
import ( import (
"crypto/sha1" "crypto/sha1"

View file

@ -1,4 +1,4 @@
package util package file
import ( import (
"testing" "testing"

View file

@ -1,4 +1,4 @@
package util package file
import ( import (
"net/http" "net/http"

View file

@ -1,4 +1,4 @@
package util package file
import ( import (
"testing" "testing"
@ -8,12 +8,12 @@ import (
func TestMimeType(t *testing.T) { func TestMimeType(t *testing.T) {
t.Run("jpg", func(t *testing.T) { t.Run("jpg", func(t *testing.T) {
filename := ExpandedFilename("./testdata/test.jpg") filename := ExpandFilename("./testdata/test.jpg")
mimeType := MimeType(filename) mimeType := MimeType(filename)
assert.Equal(t, "image/jpeg", mimeType) assert.Equal(t, "image/jpeg", mimeType)
}) })
t.Run("not existing filename", func(t *testing.T) { t.Run("not existing filename", func(t *testing.T) {
filename := ExpandedFilename("./testdata/xxx.jpg") filename := ExpandFilename("./testdata/xxx.jpg")
mimeType := MimeType(filename) mimeType := MimeType(filename)
assert.Equal(t, "", mimeType) assert.Equal(t, "", mimeType)
}) })

BIN
internal/file/testdata/test.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

97
internal/file/types.go Normal file
View file

@ -0,0 +1,97 @@
package file
import (
_ "image/gif" // Import for image.
_ "image/jpeg"
_ "image/png"
)
type Type string
const (
// JPEG image file.
TypeJpeg Type = "jpg"
// PNG image file.
TypePng Type = "png"
// RAW image file.
TypeRaw Type = "raw"
// High Efficiency Image File Format.
TypeHEIF Type = "heif" // High Efficiency Image File Format
// Movie file.
TypeMovie Type = "mov"
// Adobe XMP sidecar file (XML).
TypeXMP Type = "xmp"
// Apple sidecar file (XML).
TypeAAE Type = "aae"
// XML metadata / config / sidecar file.
TypeXML Type = "xml"
// YAML metadata / config / sidecar file.
TypeYaml Type = "yml"
// Text config / sidecar file.
TypeText Type = "txt"
// Markdown text sidecar file.
TypeMarkdown Type = "md"
// Unknown file format.
TypeOther Type = "unknown"
)
const (
// MimeTypeJpeg is jpeg image type
MimeTypeJpeg = "image/jpeg"
)
// Ext lists all the available and supported image file formats.
var Ext = map[string]Type{
".crw": TypeRaw,
".cr2": TypeRaw,
".nef": TypeRaw,
".arw": TypeRaw,
".dng": TypeRaw,
".mov": TypeMovie,
".avi": TypeMovie,
".yml": TypeYaml,
".jpg": TypeJpeg,
".thm": TypeJpeg,
".jpeg": TypeJpeg,
".xmp": TypeXMP,
".aae": TypeAAE,
".heif": TypeHEIF,
".heic": TypeHEIF,
".3fr": TypeRaw,
".ari": TypeRaw,
".bay": TypeRaw,
".cr3": TypeRaw,
".cap": TypeRaw,
".data": TypeRaw,
".dcs": TypeRaw,
".dcr": TypeRaw,
".drf": TypeRaw,
".eip": TypeRaw,
".erf": TypeRaw,
".fff": TypeRaw,
".gpr": TypeRaw,
".iiq": TypeRaw,
".k25": TypeRaw,
".kdc": TypeRaw,
".mdc": TypeRaw,
".mef": TypeRaw,
".mos": TypeRaw,
".mrw": TypeRaw,
".nrw": TypeRaw,
".obm": TypeRaw,
".orf": TypeRaw,
".pef": TypeRaw,
".ptx": TypeRaw,
".pxn": TypeRaw,
".r3d": TypeRaw,
".raf": TypeRaw,
".raw": TypeRaw,
".rwl": TypeRaw,
".rw2": TypeRaw,
".rwz": TypeRaw,
".sr2": TypeRaw,
".srf": TypeRaw,
".srw": TypeRaw,
".tif": TypeRaw,
".x3f": TypeRaw,
}

View file

@ -1,15 +1,16 @@
package util package file
import ( import (
"archive/zip" "archive/zip"
"io" "io"
"os" "os"
"strings"
) )
// ZipFiles compresses one or many files into a single zip archive file. // ZipFiles compresses one or many files into a single zip archive file.
// Param 1: filename is the output zip file's name. // Param 1: filename is the output zip file's name.
// Param 2: files is a list of files to add to the zip. // Param 2: files is a list of files to add to the zip.
func ZipFiles(filename string, files []string) error { func Zip(filename string, files []string) error {
newZipFile, err := os.Create(filename) newZipFile, err := os.Create(filename)
if err != nil { if err != nil {
return err return err
@ -21,7 +22,7 @@ func ZipFiles(filename string, files []string) error {
// Add files to zip // Add files to zip
for _, file := range files { for _, file := range files {
if err = AddFileToZip(zipWriter, file); err != nil { if err = AddToZip(zipWriter, file); err != nil {
return err return err
} }
} }
@ -29,7 +30,7 @@ func ZipFiles(filename string, files []string) error {
return nil return nil
} }
func AddFileToZip(zipWriter *zip.Writer, filename string) error { func AddToZip(zipWriter *zip.Writer, filename string) error {
fileToZip, err := os.Open(filename) fileToZip, err := os.Open(filename)
if err != nil { if err != nil {
@ -59,3 +60,29 @@ func AddFileToZip(zipWriter *zip.Writer, filename string) error {
_, err = io.Copy(writer, fileToZip) _, err = io.Copy(writer, fileToZip)
return err return err
} }
// Extract Zip file in destination directory
func Unzip(src, dest string) (fileNames []string, err error) {
r, err := zip.OpenReader(src)
if err != nil {
return fileNames, err
}
defer r.Close()
for _, f := range r.File {
// Skip directories like __OSX
if strings.HasPrefix(f.Name, "__") {
continue
}
fn, err := copyToFile(f, dest)
if err != nil {
return fileNames, err
}
fileNames = append(fileNames, fn)
}
return fileNames, nil
}

View file

@ -9,7 +9,7 @@ import (
"path/filepath" "path/filepath"
"sync" "sync"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/file"
tf "github.com/tensorflow/tensorflow/tensorflow/go" tf "github.com/tensorflow/tensorflow/tensorflow/go"
"github.com/tensorflow/tensorflow/tensorflow/go/op" "github.com/tensorflow/tensorflow/tensorflow/go/op"
) )
@ -30,7 +30,7 @@ func NewDetector(modelPath string) *Detector {
// LabelsFromFile returns matching labels for a jpeg media file. // LabelsFromFile returns matching labels for a jpeg media file.
func (t *Detector) LabelsFromFile(filename string) (result Labels, err error) { func (t *Detector) LabelsFromFile(filename string) (result Labels, err error) {
if util.MimeType(filename) != "image/jpeg" { if file.MimeType(filename) != "image/jpeg" {
return result, fmt.Errorf("nsfw: \"%s\" is not a jpeg file", filename) return result, fmt.Errorf("nsfw: \"%s\" is not a jpeg file", filename)
} }

View file

@ -5,7 +5,7 @@ import (
"testing" "testing"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/file"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -30,7 +30,7 @@ func TestConvert_ToJpeg(t *testing.T) {
jpegFilename := conf.ImportPath() + "/fern_green.jpg" jpegFilename := conf.ImportPath() + "/fern_green.jpg"
assert.Truef(t, util.Exists(jpegFilename), "file does not exist: %s", jpegFilename) assert.Truef(t, file.Exists(jpegFilename), "file does not exist: %s", jpegFilename)
t.Logf("Testing RAW to JPEG convert with %s", jpegFilename) t.Logf("Testing RAW to JPEG convert with %s", jpegFilename)
@ -66,7 +66,7 @@ func TestConvert_ToJpeg(t *testing.T) {
imageRaw, _ := convert.ToJpeg(rawMediaFile) imageRaw, _ := convert.ToJpeg(rawMediaFile)
assert.True(t, util.Exists(conf.ImportPath()+"/raw/IMG_2567.jpg"), "Jpeg file was not found - is Darktable installed?") assert.True(t, file.Exists(conf.ImportPath()+"/raw/IMG_2567.jpg"), "Jpeg file was not found - is Darktable installed?")
assert.NotEqual(t, rawFilename, imageRaw.filename) assert.NotEqual(t, rawFilename, imageRaw.filename)
@ -92,7 +92,7 @@ func TestConvert_Path(t *testing.T) {
jpegFilename := conf.ImportPath() + "/raw/canon_eos_6d.jpg" jpegFilename := conf.ImportPath() + "/raw/canon_eos_6d.jpg"
assert.True(t, util.Exists(jpegFilename), "Jpeg file was not found - is Darktable installed?") assert.True(t, file.Exists(jpegFilename), "Jpeg file was not found - is Darktable installed?")
image, err := NewMediaFile(jpegFilename) image, err := NewMediaFile(jpegFilename)
@ -108,15 +108,15 @@ func TestConvert_Path(t *testing.T) {
existingJpegFilename := conf.ImportPath() + "/raw/IMG_2567.jpg" existingJpegFilename := conf.ImportPath() + "/raw/IMG_2567.jpg"
oldHash := util.Hash(existingJpegFilename) oldHash := file.Hash(existingJpegFilename)
os.Remove(existingJpegFilename) os.Remove(existingJpegFilename)
convert.Path(conf.ImportPath()) convert.Path(conf.ImportPath())
newHash := util.Hash(existingJpegFilename) newHash := file.Hash(existingJpegFilename)
assert.True(t, util.Exists(existingJpegFilename), "Jpeg file was not found - is Darktable installed?") assert.True(t, file.Exists(existingJpegFilename), "Jpeg file was not found - is Darktable installed?")
assert.NotEqual(t, oldHash, newHash, "Fingerprint of old and new JPEG file must not be the same") assert.NotEqual(t, oldHash, newHash, "Fingerprint of old and new JPEG file must not be the same")
} }

View file

@ -12,8 +12,7 @@ import (
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/file"
"github.com/photoprism/photoprism/internal/util"
) )
// Import represents an importer that can copy/move MediaFiles to the originals directory. // Import represents an importer that can copy/move MediaFiles to the originals directory.
@ -164,7 +163,7 @@ func (imp *Import) Start(importPath string) {
if imp.removeEmptyDirectories { if imp.removeEmptyDirectories {
// Remove empty directories from import path // Remove empty directories from import path
for _, directory := range directories { for _, directory := range directories {
if util.DirectoryIsEmpty(directory) { if file.IsEmpty(directory) {
if err := os.Remove(directory); err != nil { if err := os.Remove(directory); err != nil {
log.Errorf("import: could not deleted empty directory \"%s\" (%s)", directory, err) log.Errorf("import: could not deleted empty directory \"%s\" (%s)", directory, err)
} else { } else {
@ -202,8 +201,8 @@ func (imp *Import) DestinationFilename(mainFile *MediaFile, mediaFile *MediaFile
result := pathName + string(os.PathSeparator) + fileName + fileExtension result := pathName + string(os.PathSeparator) + fileName + fileExtension
for util.Exists(result) { for file.Exists(result) {
if mediaFile.Hash() == util.Hash(result) { if mediaFile.Hash() == file.Hash(result) {
return result, fmt.Errorf("file already exists: %s", result) return result, fmt.Errorf("file already exists: %s", result)
} }

View file

@ -13,7 +13,7 @@ import (
"github.com/djherbis/times" "github.com/djherbis/times"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/file"
) )
// MediaFile represents a single photo, video or sidecar file. // MediaFile represents a single photo, video or sidecar file.
@ -22,7 +22,7 @@ type MediaFile struct {
dateCreated time.Time dateCreated time.Time
timeZone string timeZone string
hash string hash string
fileType FileType fileType file.Type
mimeType string mimeType string
perceptualHash string perceptualHash string
width int width int
@ -33,13 +33,13 @@ type MediaFile struct {
// NewMediaFile returns a new MediaFile. // NewMediaFile returns a new MediaFile.
func NewMediaFile(filename string) (*MediaFile, error) { func NewMediaFile(filename string) (*MediaFile, error) {
if !util.Exists(filename) { if !file.Exists(filename) {
return nil, fmt.Errorf("file does not exist: %s", filename) return nil, fmt.Errorf("file does not exist: %s", filename)
} }
instance := &MediaFile{ instance := &MediaFile{
filename: filename, filename: filename,
fileType: FileTypeOther, fileType: file.TypeOther,
} }
return instance, nil return instance, nil
@ -231,7 +231,7 @@ func (m *MediaFile) CanonicalNameFromFileWithDirectory() string {
// Hash return a sha1 hash of a MediaFile based on the filename. // Hash return a sha1 hash of a MediaFile based on the filename.
func (m *MediaFile) Hash() string { func (m *MediaFile) Hash() string {
if len(m.hash) == 0 { if len(m.hash) == 0 {
m.hash = util.Hash(m.Filename()) m.hash = file.Hash(m.Filename())
} }
return m.hash return m.hash
@ -242,7 +242,7 @@ func (m *MediaFile) EditedFilename() string {
basename := filepath.Base(m.filename) basename := filepath.Base(m.filename)
if strings.ToUpper(basename[:4]) == "IMG_" && strings.ToUpper(basename[:5]) != "IMG_E" { if strings.ToUpper(basename[:4]) == "IMG_" && strings.ToUpper(basename[:5]) != "IMG_E" {
if filename := filepath.Dir(m.filename) + string(os.PathSeparator) + basename[:4] + "E" + basename[4:]; util.Exists(filename) { if filename := filepath.Dir(m.filename) + string(os.PathSeparator) + basename[:4] + "E" + basename[4:]; file.Exists(filename) {
return filename return filename
} }
} }
@ -381,7 +381,7 @@ func (m *MediaFile) MimeType() string {
return m.mimeType return m.mimeType
} }
m.mimeType = util.MimeType(m.Filename()) m.mimeType = file.MimeType(m.Filename())
return m.mimeType return m.mimeType
} }
@ -397,7 +397,7 @@ func (m *MediaFile) openFile() (*os.File, error) {
// Exists checks if a media file exists by filename. // Exists checks if a media file exists by filename.
func (m *MediaFile) Exists() bool { func (m *MediaFile) Exists() bool {
return util.Exists(m.Filename()) return file.Exists(m.Filename())
} }
// Remove a media file. // Remove a media file.
@ -480,13 +480,13 @@ func (m *MediaFile) IsJpeg() bool {
} }
// Type returns the type of the media file. // Type returns the type of the media file.
func (m *MediaFile) Type() FileType { func (m *MediaFile) Type() file.Type {
return FileExtensions[m.Extension()] return file.Ext[m.Extension()]
} }
// HasType returns true if this media file is of a given type. // HasType returns true if this media file is of a given type.
func (m *MediaFile) HasType(t FileType) bool { func (m *MediaFile) HasType(t file.Type) bool {
if t == FileTypeJpeg { if t == file.TypeJpeg {
return m.IsJpeg() return m.IsJpeg()
} }
@ -495,28 +495,28 @@ func (m *MediaFile) HasType(t FileType) bool {
// IsRaw returns true if this media file a RAW file. // IsRaw returns true if this media file a RAW file.
func (m *MediaFile) IsRaw() bool { func (m *MediaFile) IsRaw() bool {
return m.HasType(FileTypeRaw) return m.HasType(file.TypeRaw)
} }
// IsHEIF returns true if this media file is a High Efficiency Image File Format file. // IsHEIF returns true if this media file is a High Efficiency Image File Format file.
func (m *MediaFile) IsHEIF() bool { func (m *MediaFile) IsHEIF() bool {
return m.HasType(FileTypeHEIF) return m.HasType(file.TypeHEIF)
} }
// IsSidecar returns true if this media file is a sidecar file (containing metadata). // IsSidecar returns true if this media file is a sidecar file (containing metadata).
func (m *MediaFile) IsSidecar() bool { func (m *MediaFile) IsSidecar() bool {
switch m.Type() { switch m.Type() {
case FileTypeXMP: case file.TypeXMP:
return true return true
case FileTypeAAE: case file.TypeAAE:
return true return true
case FileTypeXML: case file.TypeXML:
return true return true
case FileTypeYaml: case file.TypeYaml:
return true return true
case FileTypeText: case file.TypeText:
return true return true
case FileTypeMarkdown: case file.TypeMarkdown:
return true return true
default: default:
return false return false
@ -526,7 +526,7 @@ func (m *MediaFile) IsSidecar() bool {
// IsVideo returns true if this media file is a video file. // IsVideo returns true if this media file is a video file.
func (m *MediaFile) IsVideo() bool { func (m *MediaFile) IsVideo() bool {
switch m.Type() { switch m.Type() {
case FileTypeMovie: case file.TypeMovie:
return true return true
} }
@ -544,9 +544,9 @@ func (m *MediaFile) Jpeg() (*MediaFile, error) {
return m, nil return m, nil
} }
jpegFilename := fmt.Sprintf("%s.%s", m.DirectoryBasename(), FileTypeJpeg) jpegFilename := fmt.Sprintf("%s.%s", m.DirectoryBasename(), file.TypeJpeg)
if !util.Exists(jpegFilename) { if !file.Exists(jpegFilename) {
return nil, fmt.Errorf("jpeg file does not exist: %s", jpegFilename) return nil, fmt.Errorf("jpeg file does not exist: %s", jpegFilename)
} }

View file

@ -4,7 +4,7 @@ import (
"os" "os"
"testing" "testing"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/file"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -514,7 +514,7 @@ func TestMediaFile_Move(t *testing.T) {
f, err := NewMediaFile(conf.ExamplesPath() + "/table_white.jpg") f, err := NewMediaFile(conf.ExamplesPath() + "/table_white.jpg")
assert.Nil(t, err) assert.Nil(t, err)
f.Copy(origName) f.Copy(origName)
assert.True(t, util.Exists(origName)) assert.True(t, file.Exists(origName))
m, err := NewMediaFile(origName) m, err := NewMediaFile(origName)
assert.Nil(t, err) assert.Nil(t, err)
@ -523,7 +523,7 @@ func TestMediaFile_Move(t *testing.T) {
t.Errorf("failed to move: %s", err) t.Errorf("failed to move: %s", err)
} }
assert.True(t, util.Exists(destName)) assert.True(t, file.Exists(destName))
assert.Equal(t, destName, m.Filename()) assert.Equal(t, destName, m.Filename())
} }
@ -539,7 +539,7 @@ func TestMediaFile_Copy(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/table_white.jpg") mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/table_white.jpg")
assert.Nil(t, err) assert.Nil(t, err)
mediaFile.Copy(tmpPath + "table_whitecopy.jpg") mediaFile.Copy(tmpPath + "table_whitecopy.jpg")
assert.True(t, util.Exists(tmpPath+"table_whitecopy.jpg")) assert.True(t, file.Exists(tmpPath+"table_whitecopy.jpg"))
} }
func TestMediaFile_Extension(t *testing.T) { func TestMediaFile_Extension(t *testing.T) {

View file

@ -16,7 +16,7 @@ import (
"github.com/disintegration/imaging" "github.com/disintegration/imaging"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/file"
tf "github.com/tensorflow/tensorflow/tensorflow/go" tf "github.com/tensorflow/tensorflow/tensorflow/go"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@ -69,7 +69,7 @@ func (t *TensorFlow) loadLabelRules() (err error) {
log.Debugf("tensorflow: loading label rules from \"%s\"", filepath.Base(fileName)) log.Debugf("tensorflow: loading label rules from \"%s\"", filepath.Base(fileName))
if !util.Exists(fileName) { if !file.Exists(fileName) {
e := fmt.Errorf("tensorflow: label rules file not found in \"%s\"", filepath.Base(fileName)) e := fmt.Errorf("tensorflow: label rules file not found in \"%s\"", filepath.Base(fileName))
log.Error(e.Error()) log.Error(e.Error())
return e return e

View file

@ -3,85 +3,19 @@ package photoprism
import ( import (
"fmt" "fmt"
"image" "image"
"image/png"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/file"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/disintegration/imaging" "github.com/disintegration/imaging"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/util"
) )
var (
MaxThumbWidth = 8192
MaxThumbHeight = 8192
JpegQuality = 95
JpegQualitySmall = 80
)
const (
ResampleFillCenter ResampleOption = iota
ResampleFillTopLeft
ResampleFillBottomRight
ResampleFit
ResampleResize
ResampleNearestNeighbor
ResampleLanczos
ResamplePng
)
type ResampleOption int
var ResampleMethods = map[ResampleOption]string{
ResampleFillCenter: "center",
ResampleFillTopLeft: "left",
ResampleFillBottomRight: "right",
ResampleFit: "fit",
ResampleResize: "resize",
}
type ThumbnailType struct {
Source string
Width int
Height int
Public bool
Options []ResampleOption
}
var ThumbnailTypes = map[string]ThumbnailType{
"tile_50": {"tile_500", 50, 50, false, []ResampleOption{ResampleFillCenter, ResampleLanczos}},
"tile_100": {"tile_500", 100, 100, false, []ResampleOption{ResampleFillCenter, ResampleLanczos}},
"tile_224": {"tile_500", 224, 224, false, []ResampleOption{ResampleFillCenter, ResampleLanczos}},
"tile_500": {"", 500, 500, false, []ResampleOption{ResampleFillCenter, ResampleLanczos}},
"colors": {"fit_720", 3, 3, false, []ResampleOption{ResampleResize, ResampleNearestNeighbor, ResamplePng}},
"left_224": {"fit_720", 224, 224, false, []ResampleOption{ResampleFillTopLeft, ResampleLanczos}},
"right_224": {"fit_720", 224, 224, false, []ResampleOption{ResampleFillBottomRight, ResampleLanczos}},
"fit_720": {"", 720, 720, true, []ResampleOption{ResampleFit, ResampleLanczos}},
"fit_1280": {"fit_2048", 1280, 1024, true, []ResampleOption{ResampleFit, ResampleLanczos}},
"fit_1920": {"fit_2048", 1920, 1200, true, []ResampleOption{ResampleFit, ResampleLanczos}},
"fit_2048": {"", 2048, 2048, true, []ResampleOption{ResampleFit, ResampleLanczos}},
"fit_2560": {"", 2560, 1600, true, []ResampleOption{ResampleFit, ResampleLanczos}},
"fit_3840": {"", 3840, 2400, true, []ResampleOption{ResampleFit, ResampleLanczos}},
}
var DefaultThumbnails = []string{
"fit_3840", "fit_2560", "fit_2048", "fit_1920", "fit_1280", "fit_720", "right_224", "left_224", "colors", "tile_500", "tile_224", "tile_100", "tile_50",
}
func init() {
for name, t := range ThumbnailTypes {
if t.Public {
thumb := config.Thumbnail{Name: name, Width: t.Width, Height: t.Height}
config.Thumbnails = append(config.Thumbnails, thumb)
}
}
}
// CreateThumbnailsFromOriginals creates default thumbnails for all originals. // CreateThumbnailsFromOriginals creates default thumbnails for all originals.
func CreateThumbnailsFromOriginals(originalsPath string, thumbnailsPath string, force bool) error { func CreateThumbnailsFromOriginals(originalsPath string, thumbnailsPath string, force bool) error {
err := filepath.Walk(originalsPath, func(filename string, fileInfo os.FileInfo, err error) error { err := filepath.Walk(originalsPath, func(filename string, fileInfo os.FileInfo, err error) error {
@ -120,14 +54,14 @@ func CreateThumbnailsFromOriginals(originalsPath string, thumbnailsPath string,
// Thumbnail returns a thumbnail filename. // Thumbnail returns a thumbnail filename.
func (m *MediaFile) Thumbnail(path string, typeName string) (filename string, err error) { func (m *MediaFile) Thumbnail(path string, typeName string) (filename string, err error) {
thumbType, ok := ThumbnailTypes[typeName] thumbType, ok := thumb.Types[typeName]
if !ok { if !ok {
log.Errorf("invalid type: %s", typeName) log.Errorf("invalid type: %s", typeName)
return "", fmt.Errorf("invalid type: %s", typeName) return "", fmt.Errorf("invalid type: %s", typeName)
} }
thumbnail, err := ThumbnailFromFile(m.Filename(), m.Hash(), path, thumbType.Width, thumbType.Height, thumbType.Options...) thumbnail, err := thumb.FromFile(m.Filename(), m.Hash(), path, thumbType.Width, thumbType.Height, thumbType.Options...)
if err != nil { if err != nil {
log.Errorf("could not create thumbnail: %s", err) log.Errorf("could not create thumbnail: %s", err)
@ -148,157 +82,6 @@ func (m *MediaFile) Resample(path string, typeName string) (img image.Image, err
return imaging.Open(filename, imaging.AutoOrientation(true)) return imaging.Open(filename, imaging.AutoOrientation(true))
} }
func ResampleOptions(opts ...ResampleOption) (method ResampleOption, filter imaging.ResampleFilter, format FileType) {
method = ResampleFit
filter = imaging.Lanczos
format = FileTypeJpeg
for _, option := range opts {
switch option {
case ResamplePng:
format = FileTypePng
case ResampleNearestNeighbor:
filter = imaging.NearestNeighbor
case ResampleLanczos:
filter = imaging.Lanczos
case ResampleFillTopLeft:
method = ResampleFillTopLeft
case ResampleFillCenter:
method = ResampleFillCenter
case ResampleFillBottomRight:
method = ResampleFillBottomRight
case ResampleFit:
method = ResampleFit
case ResampleResize:
method = ResampleResize
default:
panic(fmt.Errorf("not a valid resample option: %d", option))
}
}
return method, filter, format
}
func Resample(img image.Image, width, height int, opts ...ResampleOption) (result image.Image) {
method, filter, _ := ResampleOptions(opts...)
if method == ResampleFit {
result = imaging.Fit(img, width, height, filter)
} else if method == ResampleFillCenter {
result = imaging.Fill(img, width, height, imaging.Center, filter)
} else if method == ResampleFillTopLeft {
result = imaging.Fill(img, width, height, imaging.TopLeft, filter)
} else if method == ResampleFillBottomRight {
result = imaging.Fill(img, width, height, imaging.BottomRight, filter)
} else if method == ResampleResize {
result = imaging.Resize(img, width, height, filter)
}
return result
}
func ThumbnailPostfix(width, height int, opts ...ResampleOption) (result string) {
method, _, format := ResampleOptions(opts...)
result = fmt.Sprintf("%dx%d_%s.%s", width, height, ResampleMethods[method], format)
return result
}
func ThumbnailFilename(hash string, thumbPath string, width, height int, opts ...ResampleOption) (filename string, err error) {
if width < 0 || width > MaxThumbWidth {
return "", fmt.Errorf("width has an invalid value: %d", width)
}
if height < 0 || height > MaxThumbHeight {
return "", fmt.Errorf("height has an invalid value: %d", height)
}
if len(hash) < 4 {
return "", fmt.Errorf("file hash is empty or too short: %s", hash)
}
if len(thumbPath) == 0 {
return "", fmt.Errorf("thumbnail path is empty: %s", thumbPath)
}
postfix := ThumbnailPostfix(width, height, opts...)
p := path.Join(thumbPath, hash[0:1], hash[1:2], hash[2:3])
if err := os.MkdirAll(p, os.ModePerm); err != nil {
return "", err
}
filename = fmt.Sprintf("%s/%s_%s", p, hash, postfix)
return filename, nil
}
func ThumbnailFromFile(imageFilename string, hash string, thumbPath string, width, height int, opts ...ResampleOption) (fileName string, err error) {
if len(hash) < 4 {
return "", fmt.Errorf("file hash is empty or too short: %s", hash)
}
if len(imageFilename) < 4 {
return "", fmt.Errorf("image filename is empty or too short: %s", imageFilename)
}
fileName, err = ThumbnailFilename(hash, thumbPath, width, height, opts...)
if err != nil {
log.Errorf("can't determine thumb filename: %s", err)
return "", err
}
if util.Exists(fileName) {
return fileName, nil
}
img, err := imaging.Open(imageFilename, imaging.AutoOrientation(true))
if err != nil {
log.Errorf("can't open original: %s", err)
return "", err
}
if _, err := CreateThumbnail(img, fileName, width, height, opts...); err != nil {
return "", err
}
return fileName, nil
}
func CreateThumbnail(img image.Image, fileName string, width, height int, opts ...ResampleOption) (result image.Image, err error) {
if width < 0 || width > MaxThumbWidth {
return img, fmt.Errorf("width has an invalid value: %d", width)
}
if height < 0 || height > MaxThumbHeight {
return img, fmt.Errorf("height has an invalid value: %d", height)
}
result = Resample(img, width, height, opts...)
var saveOption imaging.EncodeOption
if filepath.Ext(fileName) == "."+string(FileTypePng) {
saveOption = imaging.PNGCompressionLevel(png.DefaultCompression)
} else if width <= 150 && height <= 150 {
saveOption = imaging.JPEGQuality(JpegQualitySmall)
} else {
saveOption = imaging.JPEGQuality(JpegQuality)
}
err = imaging.Save(result, fileName, saveOption)
if err != nil {
log.Errorf("failed to save thumbnail: %v", err)
return result, err
}
return result, nil
}
func (m *MediaFile) CreateDefaultThumbnails(thumbPath string, force bool) (err error) { func (m *MediaFile) CreateDefaultThumbnails(thumbPath string, force bool) (err error) {
defer util.ProfileTime(time.Now(), fmt.Sprintf("creating thumbnails for \"%s\"", m.Filename())) defer util.ProfileTime(time.Now(), fmt.Sprintf("creating thumbnails for \"%s\"", m.Filename()))
@ -314,31 +97,31 @@ func (m *MediaFile) CreateDefaultThumbnails(thumbPath string, force bool) (err e
var sourceImg image.Image var sourceImg image.Image
var sourceImgType string var sourceImgType string
for _, name := range DefaultThumbnails { for _, name := range thumb.DefaultTypes {
thumbType := ThumbnailTypes[name] thumbType := thumb.Types[name]
if thumbType.Height > MaxThumbHeight || thumbType.Width > MaxThumbWidth { if thumbType.Height > thumb.MaxHeight || thumbType.Width > thumb.MaxWidth {
log.Debugf("thumbs: size exceeds limit (width %d, height %d)", thumbType.Width, thumbType.Height) log.Debugf("thumbs: size exceeds limit (width %d, height %d)", thumbType.Width, thumbType.Height)
continue continue
} }
if fileName, err := ThumbnailFilename(hash, thumbPath, thumbType.Width, thumbType.Height, thumbType.Options...); err != nil { if fileName, err := thumb.Filename(hash, thumbPath, thumbType.Width, thumbType.Height, thumbType.Options...); err != nil {
log.Errorf("could not create %s thumbnail: \"%s\"", name, err) log.Errorf("could not create %s thumbnail: \"%s\"", name, err)
return err return err
} else { } else {
if !force && util.Exists(fileName) { if !force && file.Exists(fileName) {
continue continue
} }
if thumbType.Source != "" { if thumbType.Source != "" {
if thumbType.Source == sourceImgType && sourceImg != nil { if thumbType.Source == sourceImgType && sourceImg != nil {
_, err = CreateThumbnail(sourceImg, fileName, thumbType.Width, thumbType.Height, thumbType.Options...) _, err = thumb.Create(sourceImg, fileName, thumbType.Width, thumbType.Height, thumbType.Options...)
} else { } else {
_, err = CreateThumbnail(img, fileName, thumbType.Width, thumbType.Height, thumbType.Options...) _, err = thumb.Create(img, fileName, thumbType.Width, thumbType.Height, thumbType.Options...)
} }
} else { } else {
sourceImg, err = CreateThumbnail(img, fileName, thumbType.Width, thumbType.Height, thumbType.Options...) sourceImg, err = thumb.Create(img, fileName, thumbType.Width, thumbType.Height, thumbType.Options...)
sourceImgType = name sourceImgType = name
} }

View file

@ -7,6 +7,7 @@ import (
"github.com/disintegration/imaging" "github.com/disintegration/imaging"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/nsfw" "github.com/photoprism/photoprism/internal/nsfw"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -117,7 +118,7 @@ func TestThumbnails_Resample(t *testing.T) {
} }
func TestThumbnails_ThumbnailFilename(t *testing.T) { func TestThumbnails_Filename(t *testing.T) {
conf := config.TestConfig() conf := config.TestConfig()
thumbsPath := conf.CachePath() + "/_tmp" thumbsPath := conf.CachePath() + "/_tmp"
@ -129,25 +130,25 @@ func TestThumbnails_ThumbnailFilename(t *testing.T) {
} }
t.Run("", func(t *testing.T) { t.Run("", func(t *testing.T) {
filename, err := ThumbnailFilename("99988", thumbsPath, 150, 150, ResampleFit, ResampleNearestNeighbor) filename, err := thumb.Filename("99988", thumbsPath, 150, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/testdata/cache/_tmp/9/9/9/99988_150x150_fit.jpg", filename) assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/testdata/cache/_tmp/9/9/9/99988_150x150_fit.jpg", filename)
}) })
t.Run("hash too short", func(t *testing.T) { t.Run("hash too short", func(t *testing.T) {
_, err := ThumbnailFilename("999", thumbsPath, 150, 150, ResampleFit, ResampleNearestNeighbor) _, err := thumb.Filename("999", thumbsPath, 150, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
assert.Equal(t, "file hash is empty or too short: 999", err.Error()) assert.Equal(t, "file hash is empty or too short: 999", err.Error())
}) })
t.Run("invalid width", func(t *testing.T) { t.Run("invalid width", func(t *testing.T) {
_, err := ThumbnailFilename("99988", thumbsPath, -4, 150, ResampleFit, ResampleNearestNeighbor) _, err := thumb.Filename("99988", thumbsPath, -4, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
assert.Equal(t, "width has an invalid value: -4", err.Error()) assert.Equal(t, "width has an invalid value: -4", err.Error())
}) })
t.Run("invalid height", func(t *testing.T) { t.Run("invalid height", func(t *testing.T) {
_, err := ThumbnailFilename("99988", thumbsPath, 200, -1, ResampleFit, ResampleNearestNeighbor) _, err := thumb.Filename("99988", thumbsPath, 200, -1, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
assert.Equal(t, "height has an invalid value: -1", err.Error()) assert.Equal(t, "height has an invalid value: -1", err.Error())
}) })
t.Run("empty thumbpath", func(t *testing.T) { t.Run("empty thumbpath", func(t *testing.T) {
path := "" path := ""
_, err := ThumbnailFilename("99988", path, 200, 150, ResampleFit, ResampleNearestNeighbor) _, err := thumb.Filename("99988", path, 200, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
assert.Equal(t, "thumbnail path is empty: ", err.Error()) assert.Equal(t, "thumbnail path is empty: ", err.Error())
}) })
} }
@ -169,9 +170,9 @@ func TestThumbnails_ThumbnailFromFile(t *testing.T) {
FileHash: "1234568889", FileHash: "1234568889",
} }
thumb, err := ThumbnailFromFile(fileModel.FileName, fileModel.FileHash, thumbsPath, 224, 224) thumbnail, err := thumb.FromFile(fileModel.FileName, fileModel.FileHash, thumbsPath, 224, 224)
assert.Nil(t, err) assert.Nil(t, err)
assert.FileExists(t, thumb) assert.FileExists(t, thumbnail)
}) })
t.Run("hash too short", func(t *testing.T) { t.Run("hash too short", func(t *testing.T) {
@ -180,7 +181,7 @@ func TestThumbnails_ThumbnailFromFile(t *testing.T) {
FileHash: "123", FileHash: "123",
} }
_, err := ThumbnailFromFile(fileModel.FileName, fileModel.FileHash, thumbsPath, 224, 224) _, err := thumb.FromFile(fileModel.FileName, fileModel.FileHash, thumbsPath, 224, 224)
assert.Equal(t, "file hash is empty or too short: 123", err.Error()) assert.Equal(t, "file hash is empty or too short: 123", err.Error())
}) })
t.Run("filename too short", func(t *testing.T) { t.Run("filename too short", func(t *testing.T) {
@ -189,7 +190,7 @@ func TestThumbnails_ThumbnailFromFile(t *testing.T) {
FileHash: "12367890", FileHash: "12367890",
} }
_, err := ThumbnailFromFile(fileModel.FileName, fileModel.FileHash, thumbsPath, 224, 224) _, err := thumb.FromFile(fileModel.FileName, fileModel.FileHash, thumbsPath, 224, 224)
assert.Equal(t, "image filename is empty or too short: xxx", err.Error()) assert.Equal(t, "image filename is empty or too short: xxx", err.Error())
}) })
} }
@ -210,7 +211,7 @@ func TestThumbnails_CreateThumbnail(t *testing.T) {
} }
t.Run("valid parameter", func(t *testing.T) { t.Run("valid parameter", func(t *testing.T) {
expectedFilename, err := ThumbnailFilename("12345", thumbsPath, 150, 150, ResampleFit, ResampleNearestNeighbor) expectedFilename, err := thumb.Filename("12345", thumbsPath, 150, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
@ -222,13 +223,13 @@ func TestThumbnails_CreateThumbnail(t *testing.T) {
t.Errorf("can't open original: %s", err) t.Errorf("can't open original: %s", err)
} }
thumb, err := CreateThumbnail(img, expectedFilename, 150, 150, ResampleFit, ResampleNearestNeighbor) thumbnail, err := thumb.Create(img, expectedFilename, 150, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
assert.Empty(t, err) assert.Empty(t, err)
assert.NotNil(t, thumb) assert.NotNil(t, thumbnail)
bounds := thumb.Bounds() bounds := thumbnail.Bounds()
assert.Equal(t, 150, bounds.Dx()) assert.Equal(t, 150, bounds.Dx())
assert.Equal(t, 99, bounds.Dy()) assert.Equal(t, 99, bounds.Dy())
@ -236,7 +237,7 @@ func TestThumbnails_CreateThumbnail(t *testing.T) {
assert.FileExists(t, expectedFilename) assert.FileExists(t, expectedFilename)
}) })
t.Run("invalid width", func(t *testing.T) { t.Run("invalid width", func(t *testing.T) {
expectedFilename, err := ThumbnailFilename("12345", thumbsPath, 150, 150, ResampleFit, ResampleNearestNeighbor) expectedFilename, err := thumb.Filename("12345", thumbsPath, 150, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
@ -248,7 +249,7 @@ func TestThumbnails_CreateThumbnail(t *testing.T) {
t.Errorf("can't open original: %s", err) t.Errorf("can't open original: %s", err)
} }
thumbnail, err := CreateThumbnail(img, expectedFilename, -1, 150, ResampleFit, ResampleNearestNeighbor) thumbnail, err := thumb.Create(img, expectedFilename, -1, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
assert.Equal(t, "width has an invalid value: -1", err.Error()) assert.Equal(t, "width has an invalid value: -1", err.Error())
bounds := thumbnail.Bounds() bounds := thumbnail.Bounds()
@ -256,7 +257,7 @@ func TestThumbnails_CreateThumbnail(t *testing.T) {
}) })
t.Run("invalid height", func(t *testing.T) { t.Run("invalid height", func(t *testing.T) {
expectedFilename, err := ThumbnailFilename("12345", thumbsPath, 150, 150, ResampleFit, ResampleNearestNeighbor) expectedFilename, err := thumb.Filename("12345", thumbsPath, 150, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
@ -268,7 +269,7 @@ func TestThumbnails_CreateThumbnail(t *testing.T) {
t.Errorf("can't open original: %s", err) t.Errorf("can't open original: %s", err)
} }
thumbnail, err := CreateThumbnail(img, expectedFilename, 150, -1, ResampleFit, ResampleNearestNeighbor) thumbnail, err := thumb.Create(img, expectedFilename, 150, -1, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
assert.Equal(t, "height has an invalid value: -1", err.Error()) assert.Equal(t, "height has an invalid value: -1", err.Error())
bounds := thumbnail.Bounds() bounds := thumbnail.Bounds()
@ -294,7 +295,7 @@ func TestThumbnails_CreateDefaultThumbnails(t *testing.T) {
assert.Empty(t, err) assert.Empty(t, err)
thumbFilename, err := ThumbnailFilename(m.Hash(), thumbsPath, ThumbnailTypes["tile_50"].Width, ThumbnailTypes["tile_50"].Height, ThumbnailTypes["tile_50"].Options...) thumbFilename, err := thumb.Filename(m.Hash(), thumbsPath, thumb.Types["tile_50"].Width, thumb.Types["tile_50"].Height, thumb.Types["tile_50"].Options...)
assert.Empty(t, err) assert.Empty(t, err)

14
internal/rnd/rnd.go Normal file
View file

@ -0,0 +1,14 @@
/*
Package config contains random token functions.
Additional information can be found in our Developer Guide:
https://github.com/photoprism/photoprism/wiki
*/
package rnd
import (
"github.com/photoprism/photoprism/internal/event"
)
var log = event.Log

View file

@ -1,4 +1,4 @@
package util package rnd
import ( import (
"crypto/rand" "crypto/rand"
@ -9,7 +9,7 @@ import (
uuid "github.com/satori/go.uuid" uuid "github.com/satori/go.uuid"
) )
func RandomToken(size uint) string { func Token(size uint) string {
if size > 10 || size < 1 { if size > 10 || size < 1 {
log.Fatalf("size out of range: %d", size) log.Fatalf("size out of range: %d", size)
} }
@ -32,14 +32,14 @@ func RandomToken(size uint) string {
return string(result[:size]) return string(result[:size])
} }
func RandomPassword() string { func Password() string {
return RandomToken(8) return Token(8)
} }
func ID() string { func ID() string {
result := make([]byte, 0, 16) result := make([]byte, 0, 16)
result = append(result, strconv.FormatInt(time.Now().UTC().Unix(), 36)[0:6]...) result = append(result, strconv.FormatInt(time.Now().UTC().Unix(), 36)[0:6]...)
result = append(result, RandomToken(10)...) result = append(result, Token(10)...)
return string(result) return string(result)
} }

View file

@ -1,4 +1,4 @@
package util package rnd
import ( import (
"testing" "testing"
@ -8,24 +8,24 @@ import (
func TestRandomToken(t *testing.T) { func TestRandomToken(t *testing.T) {
t.Run("size 4", func(t *testing.T) { t.Run("size 4", func(t *testing.T) {
token := RandomToken(4) token := Token(4)
assert.NotEmpty(t, token) assert.NotEmpty(t, token)
}) })
t.Run("size 8", func(t *testing.T) { t.Run("size 8", func(t *testing.T) {
token := RandomToken(9) token := Token(9)
assert.NotEmpty(t, token) assert.NotEmpty(t, token)
}) })
} }
func TestRandomPassword(t *testing.T) { func TestRandomPassword(t *testing.T) {
pw := RandomPassword() pw := Password()
t.Logf("password: %s", pw) t.Logf("password: %s", pw)
assert.Equal(t, 8, len(pw)) assert.Equal(t, 8, len(pw))
} }
func BenchmarkRandomPassword(b *testing.B) { func BenchmarkRandomPassword(b *testing.B) {
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
RandomPassword() Password()
} }
} }
@ -45,12 +45,12 @@ func BenchmarkUUID(b *testing.B) {
func BenchmarkRandomToken4(b *testing.B) { func BenchmarkRandomToken4(b *testing.B) {
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
RandomToken(4) Token(4)
} }
} }
func BenchmarkRandomToken3(b *testing.B) { func BenchmarkRandomToken3(b *testing.B) {
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
RandomToken(3) Token(3)
} }
} }

165
internal/thumb/create.go Normal file
View file

@ -0,0 +1,165 @@
package thumb
import (
"fmt"
"image"
"image/png"
"os"
"path"
"path/filepath"
"github.com/photoprism/photoprism/internal/file"
"github.com/disintegration/imaging"
)
func ResampleOptions(opts ...ResampleOption) (method ResampleOption, filter imaging.ResampleFilter, format file.Type) {
method = ResampleFit
filter = imaging.Lanczos
format = file.TypeJpeg
for _, option := range opts {
switch option {
case ResamplePng:
format = file.TypePng
case ResampleNearestNeighbor:
filter = imaging.NearestNeighbor
case ResampleLanczos:
filter = imaging.Lanczos
case ResampleFillTopLeft:
method = ResampleFillTopLeft
case ResampleFillCenter:
method = ResampleFillCenter
case ResampleFillBottomRight:
method = ResampleFillBottomRight
case ResampleFit:
method = ResampleFit
case ResampleResize:
method = ResampleResize
default:
panic(fmt.Errorf("not a valid resample option: %d", option))
}
}
return method, filter, format
}
func Resample(img image.Image, width, height int, opts ...ResampleOption) (result image.Image) {
method, filter, _ := ResampleOptions(opts...)
if method == ResampleFit {
result = imaging.Fit(img, width, height, filter)
} else if method == ResampleFillCenter {
result = imaging.Fill(img, width, height, imaging.Center, filter)
} else if method == ResampleFillTopLeft {
result = imaging.Fill(img, width, height, imaging.TopLeft, filter)
} else if method == ResampleFillBottomRight {
result = imaging.Fill(img, width, height, imaging.BottomRight, filter)
} else if method == ResampleResize {
result = imaging.Resize(img, width, height, filter)
}
return result
}
func Postfix(width, height int, opts ...ResampleOption) (result string) {
method, _, format := ResampleOptions(opts...)
result = fmt.Sprintf("%dx%d_%s.%s", width, height, ResampleMethods[method], format)
return result
}
func Filename(hash string, thumbPath string, width, height int, opts ...ResampleOption) (filename string, err error) {
if width < 0 || width > MaxWidth {
return "", fmt.Errorf("width has an invalid value: %d", width)
}
if height < 0 || height > MaxHeight {
return "", fmt.Errorf("height has an invalid value: %d", height)
}
if len(hash) < 4 {
return "", fmt.Errorf("file hash is empty or too short: %s", hash)
}
if len(thumbPath) == 0 {
return "", fmt.Errorf("thumbnail path is empty: %s", thumbPath)
}
postfix := Postfix(width, height, opts...)
p := path.Join(thumbPath, hash[0:1], hash[1:2], hash[2:3])
if err := os.MkdirAll(p, os.ModePerm); err != nil {
return "", err
}
filename = fmt.Sprintf("%s/%s_%s", p, hash, postfix)
return filename, nil
}
func FromFile(imageFilename string, hash string, thumbPath string, width, height int, opts ...ResampleOption) (fileName string, err error) {
if len(hash) < 4 {
return "", fmt.Errorf("file hash is empty or too short: %s", hash)
}
if len(imageFilename) < 4 {
return "", fmt.Errorf("image filename is empty or too short: %s", imageFilename)
}
fileName, err = Filename(hash, thumbPath, width, height, opts...)
if err != nil {
log.Errorf("can't determine thumb filename: %s", err)
return "", err
}
if file.Exists(fileName) {
return fileName, nil
}
img, err := imaging.Open(imageFilename, imaging.AutoOrientation(true))
if err != nil {
log.Errorf("can't open original: %s", err)
return "", err
}
if _, err := Create(img, fileName, width, height, opts...); err != nil {
return "", err
}
return fileName, nil
}
func Create(img image.Image, fileName string, width, height int, opts ...ResampleOption) (result image.Image, err error) {
if width < 0 || width > MaxWidth {
return img, fmt.Errorf("width has an invalid value: %d", width)
}
if height < 0 || height > MaxHeight {
return img, fmt.Errorf("height has an invalid value: %d", height)
}
result = Resample(img, width, height, opts...)
var saveOption imaging.EncodeOption
if filepath.Ext(fileName) == "."+string(file.TypePng) {
saveOption = imaging.PNGCompressionLevel(png.DefaultCompression)
} else if width <= 150 && height <= 150 {
saveOption = imaging.JPEGQuality(JpegQualitySmall)
} else {
saveOption = imaging.JPEGQuality(JpegQuality)
}
err = imaging.Save(result, fileName, saveOption)
if err != nil {
log.Errorf("failed to save thumbnail: %v", err)
return result, err
}
return result, nil
}

14
internal/thumb/thumb.go Normal file
View file

@ -0,0 +1,14 @@
/*
This package encapsulates JPEG thumbnail generation.
Additional information can be found in our Developer Guide:
https://github.com/photoprism/photoprism/wiki
*/
package thumb
import (
"github.com/photoprism/photoprism/internal/event"
)
var log = event.Log

57
internal/thumb/types.go Normal file
View file

@ -0,0 +1,57 @@
package thumb
var (
MaxWidth = 8192
MaxHeight = 8192
JpegQuality = 95
JpegQualitySmall = 80
)
const (
ResampleFillCenter ResampleOption = iota
ResampleFillTopLeft
ResampleFillBottomRight
ResampleFit
ResampleResize
ResampleNearestNeighbor
ResampleLanczos
ResamplePng
)
type ResampleOption int
var ResampleMethods = map[ResampleOption]string{
ResampleFillCenter: "center",
ResampleFillTopLeft: "left",
ResampleFillBottomRight: "right",
ResampleFit: "fit",
ResampleResize: "resize",
}
type Type struct {
Source string
Width int
Height int
Public bool
Options []ResampleOption
}
var Types = map[string]Type{
"tile_50": {"tile_500", 50, 50, false, []ResampleOption{ResampleFillCenter, ResampleLanczos}},
"tile_100": {"tile_500", 100, 100, false, []ResampleOption{ResampleFillCenter, ResampleLanczos}},
"tile_224": {"tile_500", 224, 224, false, []ResampleOption{ResampleFillCenter, ResampleLanczos}},
"tile_500": {"", 500, 500, false, []ResampleOption{ResampleFillCenter, ResampleLanczos}},
"colors": {"fit_720", 3, 3, false, []ResampleOption{ResampleResize, ResampleNearestNeighbor, ResamplePng}},
"left_224": {"fit_720", 224, 224, false, []ResampleOption{ResampleFillTopLeft, ResampleLanczos}},
"right_224": {"fit_720", 224, 224, false, []ResampleOption{ResampleFillBottomRight, ResampleLanczos}},
"fit_720": {"", 720, 720, true, []ResampleOption{ResampleFit, ResampleLanczos}},
"fit_1280": {"fit_2048", 1280, 1024, true, []ResampleOption{ResampleFit, ResampleLanczos}},
"fit_1920": {"fit_2048", 1920, 1200, true, []ResampleOption{ResampleFit, ResampleLanczos}},
"fit_2048": {"", 2048, 2048, true, []ResampleOption{ResampleFit, ResampleLanczos}},
"fit_2560": {"", 2560, 1600, true, []ResampleOption{ResampleFit, ResampleLanczos}},
"fit_3840": {"", 3840, 2400, true, []ResampleOption{ResampleFit, ResampleLanczos}},
}
var DefaultTypes = []string{
"fit_3840", "fit_2560", "fit_2048", "fit_1920", "fit_1280", "fit_720", "right_224", "left_224", "colors", "tile_500", "tile_224", "tile_100", "tile_50",
}