mirror of
				https://github.com/photoprism/photoprism.git
				synced 2025-10-31 12:16:39 +08:00 
			
		
		
		
	People: Improve thumb size config and flag descriptions #22
This commit is contained in:
		| @@ -57,8 +57,8 @@ func RemoveFromFolderCache(rootName string) { | |||||||
| func RemoveFromAlbumCoverCache(uid string) { | func RemoveFromAlbumCoverCache(uid string) { | ||||||
| 	cache := service.CoverCache() | 	cache := service.CoverCache() | ||||||
|  |  | ||||||
| 	for typeName := range thumb.Sizes { | 	for thumbName := range thumb.Sizes { | ||||||
| 		cacheKey := CacheKey(albumCover, uid, typeName) | 		cacheKey := CacheKey(albumCover, uid, string(thumbName)) | ||||||
|  |  | ||||||
| 		cache.Delete(cacheKey) | 		cache.Delete(cacheKey) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -20,14 +20,16 @@ const ( | |||||||
| 	labelCover = "label-cover" | 	labelCover = "label-cover" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // GET /api/v1/albums/:uid/t/:token/:type | // AlbumCover returns an album cover image. | ||||||
|  | // | ||||||
|  | // GET /api/v1/albums/:uid/t/:token/:size | ||||||
| // | // | ||||||
| // Parameters: | // Parameters: | ||||||
| //   uid: string album uid | //   uid: string album uid | ||||||
| //   token: string security token (see config) | //   token: string security token (see config) | ||||||
| //   type: string thumb type, see photoprism.ThumbnailTypes | //   size: string thumb type, see photoprism.ThumbnailTypes | ||||||
| func AlbumCover(router *gin.RouterGroup) { | func AlbumCover(router *gin.RouterGroup) { | ||||||
| 	router.GET("/albums/:uid/t/:token/:type", func(c *gin.Context) { | 	router.GET("/albums/:uid/t/:token/:size", func(c *gin.Context) { | ||||||
| 		if InvalidPreviewToken(c) { | 		if InvalidPreviewToken(c) { | ||||||
| 			c.Data(http.StatusForbidden, "image/svg+xml", albumIconSvg) | 			c.Data(http.StatusForbidden, "image/svg+xml", albumIconSvg) | ||||||
| 			return | 			return | ||||||
| @@ -35,19 +37,19 @@ func AlbumCover(router *gin.RouterGroup) { | |||||||
|  |  | ||||||
| 		start := time.Now() | 		start := time.Now() | ||||||
| 		conf := service.Config() | 		conf := service.Config() | ||||||
| 		typeName := c.Param("type") | 		thumbName := thumb.Name(c.Param("size")) | ||||||
| 		uid := c.Param("uid") | 		uid := c.Param("uid") | ||||||
|  |  | ||||||
| 		size, ok := thumb.Sizes[typeName] | 		size, ok := thumb.Sizes[thumbName] | ||||||
|  |  | ||||||
| 		if !ok { | 		if !ok { | ||||||
| 			log.Errorf("%s: invalid type %s", albumCover, typeName) | 			log.Errorf("%s: invalid size %s", albumCover, thumbName) | ||||||
| 			c.Data(http.StatusOK, "image/svg+xml", albumIconSvg) | 			c.Data(http.StatusOK, "image/svg+xml", albumIconSvg) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		cache := service.CoverCache() | 		cache := service.CoverCache() | ||||||
| 		cacheKey := CacheKey(albumCover, uid, typeName) | 		cacheKey := CacheKey(albumCover, uid, string(thumbName)) | ||||||
|  |  | ||||||
| 		if cacheData, ok := cache.Get(cacheKey); ok { | 		if cacheData, ok := cache.Get(cacheKey); ok { | ||||||
| 			log.Debugf("api: cache hit for %s [%s]", cacheKey, time.Since(start)) | 			log.Debugf("api: cache hit for %s [%s]", cacheKey, time.Since(start)) | ||||||
| @@ -130,14 +132,16 @@ func AlbumCover(router *gin.RouterGroup) { | |||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| // GET /api/v1/labels/:uid/t/:token/:type | // LabelCover returns a label cover image. | ||||||
|  | // | ||||||
|  | // GET /api/v1/labels/:uid/t/:token/:size | ||||||
| // | // | ||||||
| // Parameters: | // Parameters: | ||||||
| //   uid: string label uid | //   uid: string label uid | ||||||
| //   token: string security token (see config) | //   token: string security token (see config) | ||||||
| //   type: string thumb type, see photoprism.ThumbnailTypes | //   size: string thumb type, see photoprism.ThumbnailTypes | ||||||
| func LabelCover(router *gin.RouterGroup) { | func LabelCover(router *gin.RouterGroup) { | ||||||
| 	router.GET("/labels/:uid/t/:token/:type", func(c *gin.Context) { | 	router.GET("/labels/:uid/t/:token/:size", func(c *gin.Context) { | ||||||
| 		if InvalidPreviewToken(c) { | 		if InvalidPreviewToken(c) { | ||||||
| 			c.Data(http.StatusForbidden, "image/svg+xml", labelIconSvg) | 			c.Data(http.StatusForbidden, "image/svg+xml", labelIconSvg) | ||||||
| 			return | 			return | ||||||
| @@ -145,19 +149,19 @@ func LabelCover(router *gin.RouterGroup) { | |||||||
|  |  | ||||||
| 		start := time.Now() | 		start := time.Now() | ||||||
| 		conf := service.Config() | 		conf := service.Config() | ||||||
| 		typeName := c.Param("type") | 		thumbName := thumb.Name(c.Param("size")) | ||||||
| 		uid := c.Param("uid") | 		uid := c.Param("uid") | ||||||
|  |  | ||||||
| 		size, ok := thumb.Sizes[typeName] | 		size, ok := thumb.Sizes[thumbName] | ||||||
|  |  | ||||||
| 		if !ok { | 		if !ok { | ||||||
| 			log.Errorf("%s: invalid type %s", labelCover, txt.Quote(typeName)) | 			log.Errorf("%s: invalid size %s", labelCover, thumbName) | ||||||
| 			c.Data(http.StatusOK, "image/svg+xml", labelIconSvg) | 			c.Data(http.StatusOK, "image/svg+xml", labelIconSvg) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		cache := service.CoverCache() | 		cache := service.CoverCache() | ||||||
| 		cacheKey := CacheKey(labelCover, uid, typeName) | 		cacheKey := CacheKey(labelCover, uid, string(thumbName)) | ||||||
|  |  | ||||||
| 		if cacheData, ok := cache.Get(cacheKey); ok { | 		if cacheData, ok := cache.Get(cacheKey); ok { | ||||||
| 			log.Debugf("api: cache hit for %s [%s]", cacheKey, time.Since(start)) | 			log.Debugf("api: cache hit for %s [%s]", cacheKey, time.Since(start)) | ||||||
|   | |||||||
| @@ -18,14 +18,16 @@ const ( | |||||||
| 	folderCover = "folder-cover" | 	folderCover = "folder-cover" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // GET /api/v1/folders/t/:hash/:token/:type | // FolderCover returns a folder cover image. | ||||||
|  | // | ||||||
|  | // GET /api/v1/folders/t/:hash/:token/:size | ||||||
| // | // | ||||||
| // Parameters: | // Parameters: | ||||||
| //   uid: string folder uid | //   uid: string folder uid | ||||||
| //   token: string url security token, see config | //   token: string url security token, see config | ||||||
| //   type: string thumb type, see thumb.Sizes | //   size: string thumb type, see thumb.Sizes | ||||||
| func GetFolderCover(router *gin.RouterGroup) { | func FolderCover(router *gin.RouterGroup) { | ||||||
| 	router.GET("/folders/t/:uid/:token/:type", func(c *gin.Context) { | 	router.GET("/folders/t/:uid/:token/:size", func(c *gin.Context) { | ||||||
| 		if InvalidPreviewToken(c) { | 		if InvalidPreviewToken(c) { | ||||||
| 			c.Data(http.StatusForbidden, "image/svg+xml", folderIconSvg) | 			c.Data(http.StatusForbidden, "image/svg+xml", folderIconSvg) | ||||||
| 			return | 			return | ||||||
| @@ -34,21 +36,21 @@ func GetFolderCover(router *gin.RouterGroup) { | |||||||
| 		start := time.Now() | 		start := time.Now() | ||||||
| 		conf := service.Config() | 		conf := service.Config() | ||||||
| 		uid := c.Param("uid") | 		uid := c.Param("uid") | ||||||
| 		typeName := c.Param("type") | 		thumbName := thumb.Name(c.Param("size")) | ||||||
| 		download := c.Query("download") != "" | 		download := c.Query("download") != "" | ||||||
|  |  | ||||||
| 		size, ok := thumb.Sizes[typeName] | 		size, ok := thumb.Sizes[thumbName] | ||||||
|  |  | ||||||
| 		if !ok { | 		if !ok { | ||||||
| 			log.Errorf("folder: invalid thumb type %s", txt.Quote(typeName)) | 			log.Errorf("%s: invalid size %s", folderCover, thumbName) | ||||||
| 			c.Data(http.StatusOK, "image/svg+xml", folderIconSvg) | 			c.Data(http.StatusOK, "image/svg+xml", folderIconSvg) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if size.Uncached() && !conf.ThumbUncached() { | 		if size.Uncached() && !conf.ThumbUncached() { | ||||||
| 			typeName, size = thumb.Find(conf.ThumbSizePrecached()) | 			thumbName, size = thumb.Find(conf.ThumbSizePrecached()) | ||||||
|  |  | ||||||
| 			if typeName == "" { | 			if thumbName == "" { | ||||||
| 				log.Errorf("folder: invalid thumb size %d", conf.ThumbSizePrecached()) | 				log.Errorf("folder: invalid thumb size %d", conf.ThumbSizePrecached()) | ||||||
| 				c.Data(http.StatusOK, "image/svg+xml", folderIconSvg) | 				c.Data(http.StatusOK, "image/svg+xml", folderIconSvg) | ||||||
| 				return | 				return | ||||||
| @@ -56,7 +58,7 @@ func GetFolderCover(router *gin.RouterGroup) { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		cache := service.CoverCache() | 		cache := service.CoverCache() | ||||||
| 		cacheKey := CacheKey(folderCover, uid, typeName) | 		cacheKey := CacheKey(folderCover, uid, string(thumbName)) | ||||||
|  |  | ||||||
| 		if cacheData, ok := cache.Get(cacheKey); ok { | 		if cacheData, ok := cache.Get(cacheKey); ok { | ||||||
| 			log.Debugf("api: cache hit for %s [%s]", cacheKey, time.Since(start)) | 			log.Debugf("api: cache hit for %s [%s]", cacheKey, time.Since(start)) | ||||||
|   | |||||||
| @@ -10,28 +10,28 @@ import ( | |||||||
| func TestGetFolderCover(t *testing.T) { | func TestGetFolderCover(t *testing.T) { | ||||||
| 	t.Run("no cover yet", func(t *testing.T) { | 	t.Run("no cover yet", func(t *testing.T) { | ||||||
| 		app, router, conf := NewApiTest() | 		app, router, conf := NewApiTest() | ||||||
| 		GetFolderCover(router) | 		FolderCover(router) | ||||||
| 		r := PerformRequest(app, "GET", "/api/v1/folders/t/dqo63pn35k2d495z/"+conf.PreviewToken()+"/tile_500") | 		r := PerformRequest(app, "GET", "/api/v1/folders/t/dqo63pn35k2d495z/"+conf.PreviewToken()+"/tile_500") | ||||||
| 		assert.Equal(t, http.StatusOK, r.Code) | 		assert.Equal(t, http.StatusOK, r.Code) | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	t.Run("invalid thumb type", func(t *testing.T) { | 	t.Run("invalid thumb type", func(t *testing.T) { | ||||||
| 		app, router, conf := NewApiTest() | 		app, router, conf := NewApiTest() | ||||||
| 		GetFolderCover(router) | 		FolderCover(router) | ||||||
| 		r := PerformRequest(app, "GET", "/api/v1/folders/t/dqo63pn35k2d495z/"+conf.PreviewToken()+"/xxx") | 		r := PerformRequest(app, "GET", "/api/v1/folders/t/dqo63pn35k2d495z/"+conf.PreviewToken()+"/xxx") | ||||||
| 		assert.Equal(t, http.StatusOK, r.Code) | 		assert.Equal(t, http.StatusOK, r.Code) | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	t.Run("invalid token", func(t *testing.T) { | 	t.Run("invalid token", func(t *testing.T) { | ||||||
| 		app, router, _ := NewApiTest() | 		app, router, _ := NewApiTest() | ||||||
| 		GetFolderCover(router) | 		FolderCover(router) | ||||||
| 		r := PerformRequest(app, "GET", "/api/v1/folders/t/dqo63pn35k2d495z/xxx/tile_500") | 		r := PerformRequest(app, "GET", "/api/v1/folders/t/dqo63pn35k2d495z/xxx/tile_500") | ||||||
| 		assert.Equal(t, http.StatusForbidden, r.Code) | 		assert.Equal(t, http.StatusForbidden, r.Code) | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	t.Run("could not find original", func(t *testing.T) { | 	t.Run("could not find original", func(t *testing.T) { | ||||||
| 		app, router, conf := NewApiTest() | 		app, router, conf := NewApiTest() | ||||||
| 		GetFolderCover(router) | 		FolderCover(router) | ||||||
| 		r := PerformRequest(app, "GET", "/api/v1/folders/t/dqo63pn2f87f02oi/"+conf.PreviewToken()+"/fit_7680") | 		r := PerformRequest(app, "GET", "/api/v1/folders/t/dqo63pn2f87f02oi/"+conf.PreviewToken()+"/fit_7680") | ||||||
| 		assert.Equal(t, http.StatusOK, r.Code) | 		assert.Equal(t, http.StatusOK, r.Code) | ||||||
| 	}) | 	}) | ||||||
|   | |||||||
| @@ -18,14 +18,14 @@ import ( | |||||||
|  |  | ||||||
| // GetThumb returns a thumbnail image matching the hash and type. | // GetThumb returns a thumbnail image matching the hash and type. | ||||||
| // | // | ||||||
| // GET /api/v1/t/:hash/:token/:type | // GET /api/v1/t/:hash/:token/:size | ||||||
| // | // | ||||||
| // Parameters: | // Parameters: | ||||||
| //   hash: string sha1 file hash | //   hash: string sha1 file hash | ||||||
| //   token: string url security token, see config | //   token: string url security token, see config | ||||||
| //   type: string thumb type, see thumb.Sizes | //   size: string thumb type, see thumb.Sizes | ||||||
| func GetThumb(router *gin.RouterGroup) { | func GetThumb(router *gin.RouterGroup) { | ||||||
| 	router.GET("/t/:hash/:token/:type", func(c *gin.Context) { | 	router.GET("/t/:hash/:token/:size", func(c *gin.Context) { | ||||||
| 		if InvalidPreviewToken(c) { | 		if InvalidPreviewToken(c) { | ||||||
| 			c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg) | 			c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg) | ||||||
| 			return | 			return | ||||||
| @@ -34,21 +34,21 @@ func GetThumb(router *gin.RouterGroup) { | |||||||
| 		start := time.Now() | 		start := time.Now() | ||||||
| 		conf := service.Config() | 		conf := service.Config() | ||||||
| 		fileHash := c.Param("hash") | 		fileHash := c.Param("hash") | ||||||
| 		typeName := c.Param("type") | 		thumbName := thumb.Name(c.Param("size")) | ||||||
| 		download := c.Query("download") != "" | 		download := c.Query("download") != "" | ||||||
|  |  | ||||||
| 		size, ok := thumb.Sizes[typeName] | 		size, ok := thumb.Sizes[thumbName] | ||||||
|  |  | ||||||
| 		if !ok { | 		if !ok { | ||||||
| 			log.Errorf("thumbs: invalid type %s", txt.Quote(typeName)) | 			log.Errorf("thumbs: invalid size %s", thumbName) | ||||||
| 			c.Data(http.StatusOK, "image/svg+xml", photoIconSvg) | 			c.Data(http.StatusOK, "image/svg+xml", photoIconSvg) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if size.Uncached() && !conf.ThumbUncached() { | 		if size.Uncached() && !conf.ThumbUncached() { | ||||||
| 			typeName, size = thumb.Find(conf.ThumbSizePrecached()) | 			thumbName, size = thumb.Find(conf.ThumbSizePrecached()) | ||||||
|  |  | ||||||
| 			if typeName == "" { | 			if thumbName == "" { | ||||||
| 				log.Errorf("thumbs: invalid size %d", conf.ThumbSizePrecached()) | 				log.Errorf("thumbs: invalid size %d", conf.ThumbSizePrecached()) | ||||||
| 				c.Data(http.StatusOK, "image/svg+xml", photoIconSvg) | 				c.Data(http.StatusOK, "image/svg+xml", photoIconSvg) | ||||||
| 				return | 				return | ||||||
| @@ -56,7 +56,7 @@ func GetThumb(router *gin.RouterGroup) { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		cache := service.ThumbCache() | 		cache := service.ThumbCache() | ||||||
| 		cacheKey := CacheKey("thumbs", fileHash, typeName) | 		cacheKey := CacheKey("thumbs", fileHash, string(thumbName)) | ||||||
|  |  | ||||||
| 		if cacheData, ok := cache.Get(cacheKey); ok { | 		if cacheData, ok := cache.Get(cacheKey); ok { | ||||||
| 			log.Debugf("api: cache hit for %s [%s]", cacheKey, time.Since(start)) | 			log.Debugf("api: cache hit for %s [%s]", cacheKey, time.Since(start)) | ||||||
| @@ -173,15 +173,15 @@ func GetThumb(router *gin.RouterGroup) { | |||||||
|  |  | ||||||
| // GetThumbCrop returns a cropped thumbnail image matching the hash and type. | // GetThumbCrop returns a cropped thumbnail image matching the hash and type. | ||||||
| // | // | ||||||
| // GET /api/v1/t/:hash/:token/:type/:area | // GET /api/v1/t/:hash/:token/:size/:area | ||||||
| // | // | ||||||
| // Parameters: | // Parameters: | ||||||
| //   hash: string sha1 file hash | //   hash: string sha1 file hash | ||||||
| //   token: string url security token, see config | //   token: string url security token, see config | ||||||
| //   type: string thumb type, see thumb.Sizes | //   size: string thumb type, see thumb.Sizes | ||||||
| //   area: string image area identifier, e.g. 022004010015 | //   area: string image area identifier, e.g. 022004010015 | ||||||
| func GetThumbCrop(router *gin.RouterGroup) { | func GetThumbCrop(router *gin.RouterGroup) { | ||||||
| 	router.GET("/t/:hash/:token/:type/:area", func(c *gin.Context) { | 	router.GET("/t/:hash/:token/:size/:area", func(c *gin.Context) { | ||||||
| 		if InvalidPreviewToken(c) { | 		if InvalidPreviewToken(c) { | ||||||
| 			c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg) | 			c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg) | ||||||
| 			return | 			return | ||||||
| @@ -189,18 +189,18 @@ func GetThumbCrop(router *gin.RouterGroup) { | |||||||
|  |  | ||||||
| 		conf := service.Config() | 		conf := service.Config() | ||||||
| 		fileHash := c.Param("hash") | 		fileHash := c.Param("hash") | ||||||
| 		typeName := c.Param("type") | 		thumbName := thumb.Name(c.Param("size")) | ||||||
| 		cropArea := c.Param("area") | 		cropArea := c.Param("area") | ||||||
| 		download := c.Query("download") != "" | 		download := c.Query("download") != "" | ||||||
|  |  | ||||||
| 		size, ok := thumb.Sizes[typeName] | 		size, ok := thumb.Sizes[thumbName] | ||||||
|  |  | ||||||
| 		if !ok || len(size.Options) < 1 { | 		if !ok || len(size.Options) < 1 { | ||||||
| 			log.Errorf("thumbs: invalid type %s", txt.Quote(typeName)) | 			log.Errorf("thumbs: invalid size %s", thumbName) | ||||||
| 			c.Data(http.StatusOK, "image/svg+xml", photoIconSvg) | 			c.Data(http.StatusOK, "image/svg+xml", photoIconSvg) | ||||||
| 			return | 			return | ||||||
| 		} else if size.Options[0] != thumb.ResampleCrop { | 		} else if size.Options[0] != thumb.ResampleCrop { | ||||||
| 			log.Errorf("thumbs: invalid crop %s", txt.Quote(typeName)) | 			log.Errorf("thumbs: invalid size %s", thumbName) | ||||||
| 			c.Data(http.StatusOK, "image/svg+xml", photoIconSvg) | 			c.Data(http.StatusOK, "image/svg+xml", photoIconSvg) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| @@ -220,7 +220,7 @@ func GetThumbCrop(router *gin.RouterGroup) { | |||||||
| 		AddThumbCacheHeader(c) | 		AddThumbCacheHeader(c) | ||||||
|  |  | ||||||
| 		if download { | 		if download { | ||||||
| 			c.FileAttachment(fileName, typeName+fs.JpegExt) | 			c.FileAttachment(fileName, thumbName.Jpeg()) | ||||||
| 		} else { | 		} else { | ||||||
| 			c.File(fileName) | 			c.File(fileName) | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -21,6 +21,8 @@ import ( | |||||||
| 	"github.com/photoprism/photoprism/pkg/txt" | 	"github.com/photoprism/photoprism/pkg/txt" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | // SharePreview returns a link share preview image. | ||||||
|  | // | ||||||
| // GET /s/:token/:uid/preview | // GET /s/:token/:uid/preview | ||||||
| // TODO: Proof of concept, needs refactoring. | // TODO: Proof of concept, needs refactoring. | ||||||
| func SharePreview(router *gin.RouterGroup) { | func SharePreview(router *gin.RouterGroup) { | ||||||
| @@ -88,7 +90,7 @@ func SharePreview(router *gin.RouterGroup) { | |||||||
| 			return | 			return | ||||||
| 		} else if count < 12 { | 		} else if count < 12 { | ||||||
| 			f := p[0] | 			f := p[0] | ||||||
| 			size, _ := thumb.Sizes["fit_720"] | 			size, _ := thumb.Sizes[thumb.Fit720] | ||||||
|  |  | ||||||
| 			fileName := photoprism.FileName(f.FileRoot, f.FileName) | 			fileName := photoprism.FileName(f.FileRoot, f.FileName) | ||||||
|  |  | ||||||
| @@ -117,7 +119,7 @@ func SharePreview(router *gin.RouterGroup) { | |||||||
| 		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}) | ||||||
| 		size, _ := thumb.Sizes["tile_224"] | 		size, _ := thumb.Sizes[thumb.Tile224] | ||||||
|  |  | ||||||
| 		for _, f := range p { | 		for _, f := range p { | ||||||
| 			fileName := photoprism.FileName(f.FileRoot, f.FileName) | 			fileName := photoprism.FileName(f.FileRoot, f.FileName) | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ import ( | |||||||
| var ResampleCommand = cli.Command{ | var ResampleCommand = cli.Command{ | ||||||
| 	Name:    "resample", | 	Name:    "resample", | ||||||
| 	Aliases: []string{"thumbs"}, | 	Aliases: []string{"thumbs"}, | ||||||
| 	Usage:   "Pre-caches thumbnails to reduce memory and cpu usage", | 	Usage:   "Pre-caches thumbnail images for improved performance", | ||||||
| 	Flags: []cli.Flag{ | 	Flags: []cli.Flag{ | ||||||
| 		cli.BoolFlag{ | 		cli.BoolFlag{ | ||||||
| 			Name:  "force, f", | 			Name:  "force, f", | ||||||
|   | |||||||
| @@ -77,7 +77,7 @@ func init() { | |||||||
| 		t := thumb.Sizes[name] | 		t := thumb.Sizes[name] | ||||||
|  |  | ||||||
| 		if t.Public { | 		if t.Public { | ||||||
| 			Thumbs = append(Thumbs, ThumbSize{Size: name, Use: t.Use, Width: t.Width, Height: t.Height}) | 			Thumbs = append(Thumbs, ThumbSize{Size: string(name), Use: t.Use, Width: t.Width, Height: t.Height}) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -380,25 +380,25 @@ var GlobalFlags = []cli.Flag{ | |||||||
| 		EnvVar: "PHOTOPRISM_PREVIEW_TOKEN", | 		EnvVar: "PHOTOPRISM_PREVIEW_TOKEN", | ||||||
| 	}, | 	}, | ||||||
| 	cli.StringFlag{ | 	cli.StringFlag{ | ||||||
| 		Name:   "thumb-filter, f", | 		Name:   "thumb-filter", | ||||||
| 		Usage:  "downscaling filter `NAME` (best to worst: blackman, lanczos, cubic, linear)", | 		Usage:  "image downscaling `FILTER` (best to worst: blackman, lanczos, cubic, linear)", | ||||||
| 		Value:  "lanczos", | 		Value:  "lanczos", | ||||||
| 		EnvVar: "PHOTOPRISM_THUMB_FILTER", | 		EnvVar: "PHOTOPRISM_THUMB_FILTER", | ||||||
| 	}, | 	}, | ||||||
| 	cli.IntFlag{ | 	cli.IntFlag{ | ||||||
| 		Name:   "thumb-size, s", | 		Name:   "thumb-size, s", | ||||||
| 		Usage:  "pre-cached thumbnail size in `PIXELS` (720-7680)", | 		Usage:  "max pre-cached thumbnail size in `PIXELS` (720-7680)", | ||||||
| 		Value:  2048, | 		Value:  2048, | ||||||
| 		EnvVar: "PHOTOPRISM_THUMB_SIZE", | 		EnvVar: "PHOTOPRISM_THUMB_SIZE", | ||||||
| 	}, | 	}, | ||||||
| 	cli.BoolFlag{ | 	cli.BoolFlag{ | ||||||
| 		Name:   "thumb-uncached, u", | 		Name:   "thumb-uncached, u", | ||||||
| 		Usage:  "enable dynamic thumbnail rendering (high memory and cpu usage)", | 		Usage:  "enable on-demand thumbnail generation (high memory and cpu usage)", | ||||||
| 		EnvVar: "PHOTOPRISM_THUMB_UNCACHED", | 		EnvVar: "PHOTOPRISM_THUMB_UNCACHED", | ||||||
| 	}, | 	}, | ||||||
| 	cli.IntFlag{ | 	cli.IntFlag{ | ||||||
| 		Name:   "thumb-size-uncached, x", | 		Name:   "thumb-size-uncached, x", | ||||||
| 		Usage:  "dynamic rendering size limit in `PIXELS` (720-7680)", | 		Usage:  "on-demand thumbnail generation size limit in `PIXELS` (720-7680)", | ||||||
| 		Value:  7680, | 		Value:  7680, | ||||||
| 		EnvVar: "PHOTOPRISM_THUMB_SIZE_UNCACHED", | 		EnvVar: "PHOTOPRISM_THUMB_SIZE_UNCACHED", | ||||||
| 	}, | 	}, | ||||||
|   | |||||||
| @@ -6,6 +6,8 @@ import ( | |||||||
| 	"math" | 	"math" | ||||||
|  |  | ||||||
| 	"github.com/lucasb-eyer/go-colorful" | 	"github.com/lucasb-eyer/go-colorful" | ||||||
|  |  | ||||||
|  | 	"github.com/photoprism/photoprism/internal/thumb" | ||||||
| 	"github.com/photoprism/photoprism/pkg/colors" | 	"github.com/photoprism/photoprism/pkg/colors" | ||||||
| 	"github.com/photoprism/photoprism/pkg/txt" | 	"github.com/photoprism/photoprism/pkg/txt" | ||||||
| ) | ) | ||||||
| @@ -16,7 +18,7 @@ func (m *MediaFile) Colors(thumbPath string) (perception colors.ColorPerception, | |||||||
| 		return perception, fmt.Errorf("%s is not a jpeg", txt.Quote(m.BaseName())) | 		return perception, fmt.Errorf("%s is not a jpeg", txt.Quote(m.BaseName())) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	img, err := m.Resample(thumbPath, "colors") | 	img, err := m.Resample(thumbPath, thumb.Colors) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Debugf("colors: %s in %s (resample)", err, txt.Quote(m.BaseName())) | 		log.Debugf("colors: %s in %s (resample)", err, txt.Quote(m.BaseName())) | ||||||
|   | |||||||
| @@ -5,6 +5,8 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/photoprism/photoprism/internal/classify" | 	"github.com/photoprism/photoprism/internal/classify" | ||||||
|  | 	"github.com/photoprism/photoprism/internal/thumb" | ||||||
|  |  | ||||||
| 	"github.com/photoprism/photoprism/pkg/txt" | 	"github.com/photoprism/photoprism/pkg/txt" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -12,18 +14,18 @@ import ( | |||||||
| func (ind *Index) classifyImage(jpeg *MediaFile) (results classify.Labels) { | func (ind *Index) classifyImage(jpeg *MediaFile) (results classify.Labels) { | ||||||
| 	start := time.Now() | 	start := time.Now() | ||||||
|  |  | ||||||
| 	var thumbs []string | 	var sizes []thumb.Name | ||||||
|  |  | ||||||
| 	if jpeg.AspectRatio() == 1 { | 	if jpeg.AspectRatio() == 1 { | ||||||
| 		thumbs = []string{"tile_224"} | 		sizes = []thumb.Name{thumb.Tile224} | ||||||
| 	} else { | 	} else { | ||||||
| 		thumbs = []string{"tile_224", "left_224", "right_224"} | 		sizes = []thumb.Name{thumb.Tile224, thumb.Left224, thumb.Right224} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var labels classify.Labels | 	var labels classify.Labels | ||||||
|  |  | ||||||
| 	for _, thumb := range thumbs { | 	for _, size := range sizes { | ||||||
| 		filename, err := jpeg.Thumbnail(Config().ThumbPath(), thumb) | 		filename, err := jpeg.Thumbnail(Config().ThumbPath(), size) | ||||||
|  |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Debugf("%s in %s", err, txt.Quote(jpeg.BaseName())) | 			log.Debugf("%s in %s", err, txt.Quote(jpeg.BaseName())) | ||||||
|   | |||||||
| @@ -4,6 +4,8 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/photoprism/photoprism/internal/face" | 	"github.com/photoprism/photoprism/internal/face" | ||||||
|  | 	"github.com/photoprism/photoprism/internal/thumb" | ||||||
|  |  | ||||||
| 	"github.com/photoprism/photoprism/pkg/txt" | 	"github.com/photoprism/photoprism/pkg/txt" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -14,15 +16,15 @@ func (ind *Index) detectFaces(jpeg *MediaFile) face.Faces { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var minSize int | 	var minSize int | ||||||
| 	var thumbSize string | 	var thumbSize thumb.Name | ||||||
|  |  | ||||||
| 	// Select best thumbnail depending on configured size. | 	// Select best thumbnail depending on configured size. | ||||||
| 	if Config().ThumbSizePrecached() < 1280 { | 	if Config().ThumbSizePrecached() < 1280 { | ||||||
| 		minSize = 30 | 		minSize = 30 | ||||||
| 		thumbSize = "fit_720" | 		thumbSize = thumb.Fit720 | ||||||
| 	} else { | 	} else { | ||||||
| 		minSize = 40 | 		minSize = 40 | ||||||
| 		thumbSize = "fit_1280" | 		thumbSize = thumb.Fit1280 | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	thumbName, err := jpeg.Thumbnail(Config().ThumbPath(), thumbSize) | 	thumbName, err := jpeg.Thumbnail(Config().ThumbPath(), thumbSize) | ||||||
|   | |||||||
| @@ -15,6 +15,8 @@ import ( | |||||||
| 	"github.com/photoprism/photoprism/internal/meta" | 	"github.com/photoprism/photoprism/internal/meta" | ||||||
| 	"github.com/photoprism/photoprism/internal/nsfw" | 	"github.com/photoprism/photoprism/internal/nsfw" | ||||||
| 	"github.com/photoprism/photoprism/internal/query" | 	"github.com/photoprism/photoprism/internal/query" | ||||||
|  | 	"github.com/photoprism/photoprism/internal/thumb" | ||||||
|  |  | ||||||
| 	"github.com/photoprism/photoprism/pkg/fs" | 	"github.com/photoprism/photoprism/pkg/fs" | ||||||
| 	"github.com/photoprism/photoprism/pkg/txt" | 	"github.com/photoprism/photoprism/pkg/txt" | ||||||
| ) | ) | ||||||
| @@ -751,7 +753,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( | |||||||
|  |  | ||||||
| // NSFW returns true if media file might be offensive and detection is enabled. | // NSFW returns true if media file might be offensive and detection is enabled. | ||||||
| func (ind *Index) NSFW(jpeg *MediaFile) bool { | func (ind *Index) NSFW(jpeg *MediaFile) bool { | ||||||
| 	filename, err := jpeg.Thumbnail(Config().ThumbPath(), "fit_720") | 	filename, err := jpeg.Thumbnail(Config().ThumbPath(), thumb.Fit720) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error(err) | 		log.Error(err) | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ import ( | |||||||
|  |  | ||||||
| 	"github.com/disintegration/imaging" | 	"github.com/disintegration/imaging" | ||||||
| 	"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/meta" | 	"github.com/photoprism/photoprism/internal/meta" | ||||||
| 	"github.com/photoprism/photoprism/internal/thumb" | 	"github.com/photoprism/photoprism/internal/thumb" | ||||||
| @@ -887,12 +888,12 @@ func (m *MediaFile) Orientation() int { | |||||||
| } | } | ||||||
|  |  | ||||||
| // 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, sizeName thumb.Name) (filename string, err error) { | ||||||
| 	size, ok := thumb.Sizes[typeName] | 	size, ok := thumb.Sizes[sizeName] | ||||||
|  |  | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		log.Errorf("media: invalid type %s", typeName) | 		log.Errorf("media: invalid type %s", sizeName) | ||||||
| 		return "", fmt.Errorf("media: invalid type %s", typeName) | 		return "", fmt.Errorf("media: invalid type %s", sizeName) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	thumbnail, err := thumb.FromFile(m.FileName(), m.Hash(), path, size.Width, size.Height, m.Orientation(), size.Options...) | 	thumbnail, err := thumb.FromFile(m.FileName(), m.Hash(), path, size.Width, size.Height, m.Orientation(), size.Options...) | ||||||
| @@ -907,8 +908,8 @@ func (m *MediaFile) Thumbnail(path string, typeName string) (filename string, er | |||||||
| } | } | ||||||
|  |  | ||||||
| // Resample returns a resampled image of the file. | // Resample returns a resampled image of the file. | ||||||
| func (m *MediaFile) Resample(path string, typeName string) (img image.Image, err error) { | func (m *MediaFile) Resample(path string, sizeName thumb.Name) (img image.Image, err error) { | ||||||
| 	filename, err := m.Thumbnail(path, typeName) | 	filename, err := m.Thumbnail(path, sizeName) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| @@ -937,7 +938,7 @@ func (m *MediaFile) ResampleDefault(thumbPath string, force bool) (err error) { | |||||||
|  |  | ||||||
| 	var originalImg image.Image | 	var originalImg image.Image | ||||||
| 	var sourceImg image.Image | 	var sourceImg image.Image | ||||||
| 	var sourceImgType string | 	var sourceName thumb.Name | ||||||
|  |  | ||||||
| 	for _, name := range thumb.DefaultSizes { | 	for _, name := range thumb.DefaultSizes { | ||||||
| 		size := thumb.Sizes[name] | 		size := thumb.Sizes[name] | ||||||
| @@ -948,7 +949,7 @@ func (m *MediaFile) ResampleDefault(thumbPath string, force bool) (err error) { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if fileName, err := thumb.FileName(hash, thumbPath, size.Width, size.Height, size.Options...); err != nil { | 		if fileName, err := thumb.FileName(hash, thumbPath, size.Width, size.Height, size.Options...); err != nil { | ||||||
| 			log.Errorf("media: failed creating %s (%s)", txt.Quote(name), err) | 			log.Errorf("media: failed creating %s (%s)", txt.Quote(string(name)), err) | ||||||
|  |  | ||||||
| 			return err | 			return err | ||||||
| 		} else { | 		} else { | ||||||
| @@ -970,18 +971,18 @@ func (m *MediaFile) ResampleDefault(thumbPath string, force bool) (err error) { | |||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if size.Source != "" { | 			if size.Source != "" { | ||||||
| 				if size.Source == sourceImgType && sourceImg != nil { | 				if size.Source == sourceName && sourceImg != nil { | ||||||
| 					_, err = thumb.Create(sourceImg, fileName, size.Width, size.Height, size.Options...) | 					_, err = thumb.Create(sourceImg, fileName, size.Width, size.Height, size.Options...) | ||||||
| 				} else { | 				} else { | ||||||
| 					_, err = thumb.Create(originalImg, fileName, size.Width, size.Height, size.Options...) | 					_, err = thumb.Create(originalImg, fileName, size.Width, size.Height, size.Options...) | ||||||
| 				} | 				} | ||||||
| 			} else { | 			} else { | ||||||
| 				sourceImg, err = thumb.Create(originalImg, fileName, size.Width, size.Height, size.Options...) | 				sourceImg, err = thumb.Create(originalImg, fileName, size.Width, size.Height, size.Options...) | ||||||
| 				sourceImgType = name | 				sourceName = name | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				log.Errorf("media: failed creating %s (%s)", txt.Quote(name), err) | 				log.Errorf("media: failed creating %s (%s)", txt.Quote(string(name)), err) | ||||||
| 				return err | 				return err | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ import ( | |||||||
| 	"github.com/photoprism/photoprism/internal/entity" | 	"github.com/photoprism/photoprism/internal/entity" | ||||||
| 	"github.com/photoprism/photoprism/internal/thumb" | 	"github.com/photoprism/photoprism/internal/thumb" | ||||||
| 	"github.com/photoprism/photoprism/pkg/fs" | 	"github.com/photoprism/photoprism/pkg/fs" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -1822,7 +1823,7 @@ func TestMediaFile_Resample(t *testing.T) { | |||||||
| 			t.Fatal(err) | 			t.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		thumbnail, err := image.Resample(thumbsPath, "tile_500") | 		thumbnail, err := image.Resample(thumbsPath, thumb.Tile500) | ||||||
|  |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			t.Fatal(err) | 			t.Fatal(err) | ||||||
| @@ -1873,7 +1874,7 @@ func TestMediaFile_RenderDefaultThumbs(t *testing.T) { | |||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	thumbFilename, err := thumb.FileName(m.Hash(), thumbsPath, thumb.Sizes["tile_50"].Width, thumb.Sizes["tile_50"].Height, thumb.Sizes["tile_50"].Options...) | 	thumbFilename, err := thumb.FileName(m.Hash(), thumbsPath, thumb.Sizes[thumb.Tile50].Width, thumb.Sizes[thumb.Tile50].Height, thumb.Sizes[thumb.Tile50].Options...) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
|   | |||||||
| @@ -87,23 +87,6 @@ func registerRoutes(router *gin.Engine, conf *config.Config) { | |||||||
| 		api.PhotoPrimary(v1) | 		api.PhotoPrimary(v1) | ||||||
| 		api.PhotoUnstack(v1) | 		api.PhotoUnstack(v1) | ||||||
|  |  | ||||||
| 		api.GetSubjects(v1) |  | ||||||
| 		api.GetSubject(v1) |  | ||||||
|  |  | ||||||
| 		api.GetLabels(v1) |  | ||||||
| 		api.UpdateLabel(v1) |  | ||||||
| 		api.GetLabelLinks(v1) |  | ||||||
| 		api.CreateLabelLink(v1) |  | ||||||
| 		api.UpdateLabelLink(v1) |  | ||||||
| 		api.DeleteLabelLink(v1) |  | ||||||
| 		api.LikeLabel(v1) |  | ||||||
| 		api.DislikeLabel(v1) |  | ||||||
| 		api.LabelCover(v1) |  | ||||||
|  |  | ||||||
| 		api.GetFoldersOriginals(v1) |  | ||||||
| 		api.GetFoldersImport(v1) |  | ||||||
| 		api.GetFolderCover(v1) |  | ||||||
|  |  | ||||||
| 		api.Upload(v1) | 		api.Upload(v1) | ||||||
| 		api.StartImport(v1) | 		api.StartImport(v1) | ||||||
| 		api.CancelImport(v1) | 		api.CancelImport(v1) | ||||||
| @@ -118,6 +101,24 @@ func registerRoutes(router *gin.Engine, conf *config.Config) { | |||||||
| 		api.BatchAlbumsDelete(v1) | 		api.BatchAlbumsDelete(v1) | ||||||
| 		api.BatchLabelsDelete(v1) | 		api.BatchLabelsDelete(v1) | ||||||
|  |  | ||||||
|  | 		api.GetSubjects(v1) | ||||||
|  | 		api.GetSubject(v1) | ||||||
|  |  | ||||||
|  | 		api.LabelCover(v1) | ||||||
|  | 		api.GetLabels(v1) | ||||||
|  | 		api.UpdateLabel(v1) | ||||||
|  | 		api.GetLabelLinks(v1) | ||||||
|  | 		api.CreateLabelLink(v1) | ||||||
|  | 		api.UpdateLabelLink(v1) | ||||||
|  | 		api.DeleteLabelLink(v1) | ||||||
|  | 		api.LikeLabel(v1) | ||||||
|  | 		api.DislikeLabel(v1) | ||||||
|  |  | ||||||
|  | 		api.FolderCover(v1) | ||||||
|  | 		api.GetFoldersOriginals(v1) | ||||||
|  | 		api.GetFoldersImport(v1) | ||||||
|  |  | ||||||
|  | 		api.AlbumCover(v1) | ||||||
| 		api.GetAlbum(v1) | 		api.GetAlbum(v1) | ||||||
| 		api.CreateAlbum(v1) | 		api.CreateAlbum(v1) | ||||||
| 		api.UpdateAlbum(v1) | 		api.UpdateAlbum(v1) | ||||||
| @@ -130,7 +131,6 @@ func registerRoutes(router *gin.Engine, conf *config.Config) { | |||||||
| 		api.DeleteAlbumLink(v1) | 		api.DeleteAlbumLink(v1) | ||||||
| 		api.LikeAlbum(v1) | 		api.LikeAlbum(v1) | ||||||
| 		api.DislikeAlbum(v1) | 		api.DislikeAlbum(v1) | ||||||
| 		api.AlbumCover(v1) |  | ||||||
| 		api.CloneAlbums(v1) | 		api.CloneAlbums(v1) | ||||||
| 		api.AddPhotosToAlbum(v1) | 		api.AddPhotosToAlbum(v1) | ||||||
| 		api.RemovePhotosFromAlbum(v1) | 		api.RemovePhotosFromAlbum(v1) | ||||||
|   | |||||||
| @@ -15,57 +15,6 @@ import ( | |||||||
| 	"github.com/disintegration/imaging" | 	"github.com/disintegration/imaging" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // ResampleOptions extracts filter, format, and method from resample options. |  | ||||||
| func ResampleOptions(opts ...ResampleOption) (method ResampleOption, filter imaging.ResampleFilter, format fs.FileFormat) { |  | ||||||
| 	method = ResampleFit |  | ||||||
| 	filter = imaging.Lanczos |  | ||||||
| 	format = fs.FormatJpeg |  | ||||||
|  |  | ||||||
| 	for _, option := range opts { |  | ||||||
| 		switch option { |  | ||||||
| 		case ResamplePng: |  | ||||||
| 			format = fs.FormatPng |  | ||||||
| 		case ResampleNearestNeighbor: |  | ||||||
| 			filter = imaging.NearestNeighbor |  | ||||||
| 		case ResampleDefault: |  | ||||||
| 			filter = Filter.Imaging() |  | ||||||
| 		case ResampleFillTopLeft: |  | ||||||
| 			method = ResampleFillTopLeft |  | ||||||
| 		case ResampleFillCenter: |  | ||||||
| 			method = ResampleFillCenter |  | ||||||
| 		case ResampleFillBottomRight: |  | ||||||
| 			method = ResampleFillBottomRight |  | ||||||
| 		case ResampleFit: |  | ||||||
| 			method = ResampleFit |  | ||||||
| 		case ResampleResize: |  | ||||||
| 			method = ResampleResize |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return method, filter, format |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Resample downscales an image and returns it. |  | ||||||
| func Resample(img image.Image, width, height int, opts ...ResampleOption) image.Image { |  | ||||||
| 	var resImg image.Image |  | ||||||
|  |  | ||||||
| 	method, filter, _ := ResampleOptions(opts...) |  | ||||||
|  |  | ||||||
| 	if method == ResampleFit { |  | ||||||
| 		resImg = imaging.Fit(img, width, height, filter) |  | ||||||
| 	} else if method == ResampleFillCenter { |  | ||||||
| 		resImg = imaging.Fill(img, width, height, imaging.Center, filter) |  | ||||||
| 	} else if method == ResampleFillTopLeft { |  | ||||||
| 		resImg = imaging.Fill(img, width, height, imaging.TopLeft, filter) |  | ||||||
| 	} else if method == ResampleFillBottomRight { |  | ||||||
| 		resImg = imaging.Fill(img, width, height, imaging.BottomRight, filter) |  | ||||||
| 	} else if method == ResampleResize { |  | ||||||
| 		resImg = imaging.Resize(img, width, height, filter) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return resImg |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Suffix returns the thumb cache file suffix. | // Suffix returns the thumb cache file suffix. | ||||||
| func Suffix(width, height int, opts ...ResampleOption) (result string) { | func Suffix(width, height int, opts ...ResampleOption) (result string) { | ||||||
| 	method, _, format := ResampleOptions(opts...) | 	method, _, format := ResampleOptions(opts...) | ||||||
|   | |||||||
| @@ -35,7 +35,7 @@ func TestResampleOptions(t *testing.T) { | |||||||
|  |  | ||||||
| func TestResample(t *testing.T) { | func TestResample(t *testing.T) { | ||||||
| 	t.Run("tile50 options", func(t *testing.T) { | 	t.Run("tile50 options", func(t *testing.T) { | ||||||
| 		tile50 := Sizes["tile_50"] | 		tile50 := Sizes[Tile50] | ||||||
|  |  | ||||||
| 		src := "testdata/example.jpg" | 		src := "testdata/example.jpg" | ||||||
|  |  | ||||||
| @@ -60,7 +60,7 @@ func TestResample(t *testing.T) { | |||||||
| 		assert.Equal(t, 50, boundsNew.Max.Y) | 		assert.Equal(t, 50, boundsNew.Max.Y) | ||||||
| 	}) | 	}) | ||||||
| 	t.Run("left_224 options", func(t *testing.T) { | 	t.Run("left_224 options", func(t *testing.T) { | ||||||
| 		left224 := Sizes["left_224"] | 		left224 := Sizes[Left224] | ||||||
|  |  | ||||||
| 		src := "testdata/example.jpg" | 		src := "testdata/example.jpg" | ||||||
|  |  | ||||||
| @@ -85,7 +85,7 @@ func TestResample(t *testing.T) { | |||||||
| 		assert.Equal(t, 224, boundsNew.Max.Y) | 		assert.Equal(t, 224, boundsNew.Max.Y) | ||||||
| 	}) | 	}) | ||||||
| 	t.Run("right_224 options", func(t *testing.T) { | 	t.Run("right_224 options", func(t *testing.T) { | ||||||
| 		right224 := Sizes["right_224"] | 		right224 := Sizes[Right224] | ||||||
|  |  | ||||||
| 		src := "testdata/example.jpg" | 		src := "testdata/example.jpg" | ||||||
|  |  | ||||||
| @@ -110,7 +110,7 @@ func TestResample(t *testing.T) { | |||||||
| 		assert.Equal(t, 224, boundsNew.Max.Y) | 		assert.Equal(t, 224, boundsNew.Max.Y) | ||||||
| 	}) | 	}) | ||||||
| 	t.Run("fit_1280 options", func(t *testing.T) { | 	t.Run("fit_1280 options", func(t *testing.T) { | ||||||
| 		fit1280 := Sizes["fit_1280"] | 		fit1280 := Sizes[Fit1280] | ||||||
|  |  | ||||||
| 		src := "testdata/example.jpg" | 		src := "testdata/example.jpg" | ||||||
|  |  | ||||||
| @@ -137,7 +137,7 @@ func TestResample(t *testing.T) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func TestSuffix(t *testing.T) { | func TestSuffix(t *testing.T) { | ||||||
| 	tile50 := Sizes["tile_50"] | 	tile50 := Sizes[Tile50] | ||||||
|  |  | ||||||
| 	result := Suffix(tile50.Width, tile50.Height, tile50.Options...) | 	result := Suffix(tile50.Width, tile50.Height, tile50.Options...) | ||||||
|  |  | ||||||
| @@ -146,7 +146,7 @@ func TestSuffix(t *testing.T) { | |||||||
|  |  | ||||||
| func TestFileName(t *testing.T) { | func TestFileName(t *testing.T) { | ||||||
| 	t.Run("colors", func(t *testing.T) { | 	t.Run("colors", func(t *testing.T) { | ||||||
| 		colorThumb := Sizes["colors"] | 		colorThumb := Sizes[Colors] | ||||||
|  |  | ||||||
| 		result, err := FileName("123456789098765432", "testdata", colorThumb.Width, colorThumb.Height, colorThumb.Options...) | 		result, err := FileName("123456789098765432", "testdata", colorThumb.Width, colorThumb.Height, colorThumb.Options...) | ||||||
|  |  | ||||||
| @@ -158,7 +158,7 @@ func TestFileName(t *testing.T) { | |||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	t.Run("fit_720", func(t *testing.T) { | 	t.Run("fit_720", func(t *testing.T) { | ||||||
| 		fit720 := Sizes["fit_720"] | 		fit720 := Sizes[Fit720] | ||||||
|  |  | ||||||
| 		result, err := FileName("123456789098765432", "testdata", fit720.Width, fit720.Height, fit720.Options...) | 		result, err := FileName("123456789098765432", "testdata", fit720.Width, fit720.Height, fit720.Options...) | ||||||
|  |  | ||||||
| @@ -169,7 +169,7 @@ func TestFileName(t *testing.T) { | |||||||
| 		assert.Equal(t, "testdata/1/2/3/123456789098765432_720x720_fit.jpg", result) | 		assert.Equal(t, "testdata/1/2/3/123456789098765432_720x720_fit.jpg", result) | ||||||
| 	}) | 	}) | ||||||
| 	t.Run("invalid width", func(t *testing.T) { | 	t.Run("invalid width", func(t *testing.T) { | ||||||
| 		colorThumb := Sizes["colors"] | 		colorThumb := Sizes[Colors] | ||||||
|  |  | ||||||
| 		result, err := FileName("123456789098765432", "testdata", -2, colorThumb.Height, colorThumb.Options...) | 		result, err := FileName("123456789098765432", "testdata", -2, colorThumb.Height, colorThumb.Options...) | ||||||
|  |  | ||||||
| @@ -180,7 +180,7 @@ func TestFileName(t *testing.T) { | |||||||
| 		assert.Empty(t, result) | 		assert.Empty(t, result) | ||||||
| 	}) | 	}) | ||||||
| 	t.Run("invalid height", func(t *testing.T) { | 	t.Run("invalid height", func(t *testing.T) { | ||||||
| 		colorThumb := Sizes["colors"] | 		colorThumb := Sizes[Colors] | ||||||
|  |  | ||||||
| 		result, err := FileName("123456789098765432", "testdata", colorThumb.Width, -3, colorThumb.Options...) | 		result, err := FileName("123456789098765432", "testdata", colorThumb.Width, -3, colorThumb.Options...) | ||||||
|  |  | ||||||
| @@ -191,7 +191,7 @@ func TestFileName(t *testing.T) { | |||||||
| 		assert.Empty(t, result) | 		assert.Empty(t, result) | ||||||
| 	}) | 	}) | ||||||
| 	t.Run("invalid hash", func(t *testing.T) { | 	t.Run("invalid hash", func(t *testing.T) { | ||||||
| 		colorThumb := Sizes["colors"] | 		colorThumb := Sizes[Colors] | ||||||
|  |  | ||||||
| 		result, err := FileName("12", "testdata", colorThumb.Width, colorThumb.Height, colorThumb.Options...) | 		result, err := FileName("12", "testdata", colorThumb.Width, colorThumb.Height, colorThumb.Options...) | ||||||
|  |  | ||||||
| @@ -202,7 +202,7 @@ func TestFileName(t *testing.T) { | |||||||
| 		assert.Empty(t, result) | 		assert.Empty(t, result) | ||||||
| 	}) | 	}) | ||||||
| 	t.Run("invalid thumb path", func(t *testing.T) { | 	t.Run("invalid thumb path", func(t *testing.T) { | ||||||
| 		colorThumb := Sizes["colors"] | 		colorThumb := Sizes[Colors] | ||||||
|  |  | ||||||
| 		result, err := FileName("123456789098765432", "", colorThumb.Width, colorThumb.Height, colorThumb.Options...) | 		result, err := FileName("123456789098765432", "", colorThumb.Width, colorThumb.Height, colorThumb.Options...) | ||||||
|  |  | ||||||
| @@ -216,7 +216,7 @@ func TestFileName(t *testing.T) { | |||||||
|  |  | ||||||
| func TestFromFile(t *testing.T) { | func TestFromFile(t *testing.T) { | ||||||
| 	t.Run("colors", func(t *testing.T) { | 	t.Run("colors", func(t *testing.T) { | ||||||
| 		colorThumb := Sizes["colors"] | 		colorThumb := Sizes[Colors] | ||||||
| 		src := "testdata/example.gif" | 		src := "testdata/example.gif" | ||||||
| 		dst := "testdata/1/2/3/123456789098765432_3x3_resize.png" | 		dst := "testdata/1/2/3/123456789098765432_3x3_resize.png" | ||||||
|  |  | ||||||
| @@ -234,7 +234,7 @@ func TestFromFile(t *testing.T) { | |||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	t.Run("orientation >1 ", func(t *testing.T) { | 	t.Run("orientation >1 ", func(t *testing.T) { | ||||||
| 		colorThumb := Sizes["colors"] | 		colorThumb := Sizes[Colors] | ||||||
| 		src := "testdata/example.gif" | 		src := "testdata/example.gif" | ||||||
| 		dst := "testdata/1/2/3/123456789098765432_3x3_resize.png" | 		dst := "testdata/1/2/3/123456789098765432_3x3_resize.png" | ||||||
|  |  | ||||||
| @@ -252,7 +252,7 @@ func TestFromFile(t *testing.T) { | |||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	t.Run("missing file", func(t *testing.T) { | 	t.Run("missing file", func(t *testing.T) { | ||||||
| 		colorThumb := Sizes["colors"] | 		colorThumb := Sizes[Colors] | ||||||
| 		src := "testdata/example.xxx" | 		src := "testdata/example.xxx" | ||||||
|  |  | ||||||
| 		assert.NoFileExists(t, src) | 		assert.NoFileExists(t, src) | ||||||
| @@ -263,7 +263,7 @@ func TestFromFile(t *testing.T) { | |||||||
| 		assert.Error(t, err) | 		assert.Error(t, err) | ||||||
| 	}) | 	}) | ||||||
| 	t.Run("empty filename", func(t *testing.T) { | 	t.Run("empty filename", func(t *testing.T) { | ||||||
| 		colorThumb := Sizes["colors"] | 		colorThumb := Sizes[Colors] | ||||||
|  |  | ||||||
| 		fileName, err := FromFile("", "193456789098765432", "testdata", colorThumb.Width, colorThumb.Height, OrientationNormal, colorThumb.Options...) | 		fileName, err := FromFile("", "193456789098765432", "testdata", colorThumb.Width, colorThumb.Height, OrientationNormal, colorThumb.Options...) | ||||||
|  |  | ||||||
| @@ -277,7 +277,7 @@ func TestFromFile(t *testing.T) { | |||||||
|  |  | ||||||
| func TestFromCache(t *testing.T) { | func TestFromCache(t *testing.T) { | ||||||
| 	t.Run("missing thumb", func(t *testing.T) { | 	t.Run("missing thumb", func(t *testing.T) { | ||||||
| 		tile50 := Sizes["tile_50"] | 		tile50 := Sizes[Tile50] | ||||||
| 		src := "testdata/example.jpg" | 		src := "testdata/example.jpg" | ||||||
|  |  | ||||||
| 		assert.FileExists(t, src) | 		assert.FileExists(t, src) | ||||||
| @@ -292,7 +292,7 @@ func TestFromCache(t *testing.T) { | |||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	t.Run("missing file", func(t *testing.T) { | 	t.Run("missing file", func(t *testing.T) { | ||||||
| 		tile50 := Sizes["tile_50"] | 		tile50 := Sizes[Tile50] | ||||||
| 		src := "testdata/example.xxx" | 		src := "testdata/example.xxx" | ||||||
|  |  | ||||||
| 		assert.NoFileExists(t, src) | 		assert.NoFileExists(t, src) | ||||||
| @@ -303,7 +303,7 @@ func TestFromCache(t *testing.T) { | |||||||
| 		assert.Error(t, err) | 		assert.Error(t, err) | ||||||
| 	}) | 	}) | ||||||
| 	t.Run("invalid hash", func(t *testing.T) { | 	t.Run("invalid hash", func(t *testing.T) { | ||||||
| 		tile50 := Sizes["tile_50"] | 		tile50 := Sizes[Tile50] | ||||||
| 		src := "testdata/example.jpg" | 		src := "testdata/example.jpg" | ||||||
|  |  | ||||||
| 		assert.FileExists(t, src) | 		assert.FileExists(t, src) | ||||||
| @@ -317,7 +317,7 @@ func TestFromCache(t *testing.T) { | |||||||
| 		assert.Empty(t, fileName) | 		assert.Empty(t, fileName) | ||||||
| 	}) | 	}) | ||||||
| 	t.Run("empty filename", func(t *testing.T) { | 	t.Run("empty filename", func(t *testing.T) { | ||||||
| 		tile50 := Sizes["tile_50"] | 		tile50 := Sizes[Tile50] | ||||||
|  |  | ||||||
| 		fileName, err := FromCache("", "193456789098765432", "testdata", tile50.Width, tile50.Height, tile50.Options...) | 		fileName, err := FromCache("", "193456789098765432", "testdata", tile50.Width, tile50.Height, tile50.Options...) | ||||||
|  |  | ||||||
| @@ -331,7 +331,7 @@ func TestFromCache(t *testing.T) { | |||||||
|  |  | ||||||
| func TestCreate(t *testing.T) { | func TestCreate(t *testing.T) { | ||||||
| 	t.Run("tile_500", func(t *testing.T) { | 	t.Run("tile_500", func(t *testing.T) { | ||||||
| 		tile500 := Sizes["tile_500"] | 		tile500 := Sizes[Tile500] | ||||||
| 		src := "testdata/example.jpg" | 		src := "testdata/example.jpg" | ||||||
| 		dst := "testdata/example.tile_500.jpg" | 		dst := "testdata/example.tile_500.jpg" | ||||||
|  |  | ||||||
| @@ -368,7 +368,7 @@ func TestCreate(t *testing.T) { | |||||||
| 		assert.Equal(t, 500, boundsNew.Max.Y) | 		assert.Equal(t, 500, boundsNew.Max.Y) | ||||||
| 	}) | 	}) | ||||||
| 	t.Run("width & height <= 150", func(t *testing.T) { | 	t.Run("width & height <= 150", func(t *testing.T) { | ||||||
| 		tile500 := Sizes["tile_500"] | 		tile500 := Sizes[Tile500] | ||||||
| 		src := "testdata/example.jpg" | 		src := "testdata/example.jpg" | ||||||
| 		dst := "testdata/example.tile_500.jpg" | 		dst := "testdata/example.tile_500.jpg" | ||||||
|  |  | ||||||
| @@ -405,7 +405,7 @@ func TestCreate(t *testing.T) { | |||||||
| 		assert.Equal(t, 150, boundsNew.Max.Y) | 		assert.Equal(t, 150, boundsNew.Max.Y) | ||||||
| 	}) | 	}) | ||||||
| 	t.Run("invalid width", func(t *testing.T) { | 	t.Run("invalid width", func(t *testing.T) { | ||||||
| 		tile500 := Sizes["tile_500"] | 		tile500 := Sizes[Tile500] | ||||||
| 		src := "testdata/example.jpg" | 		src := "testdata/example.jpg" | ||||||
| 		dst := "testdata/example.tile_500.jpg" | 		dst := "testdata/example.tile_500.jpg" | ||||||
|  |  | ||||||
| @@ -433,7 +433,7 @@ func TestCreate(t *testing.T) { | |||||||
| 		t.Log(resized) | 		t.Log(resized) | ||||||
| 	}) | 	}) | ||||||
| 	t.Run("invalid height", func(t *testing.T) { | 	t.Run("invalid height", func(t *testing.T) { | ||||||
| 		tile500 := Sizes["tile_500"] | 		tile500 := Sizes[Tile500] | ||||||
| 		src := "testdata/example.jpg" | 		src := "testdata/example.jpg" | ||||||
| 		dst := "testdata/example.tile_500.jpg" | 		dst := "testdata/example.tile_500.jpg" | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										31
									
								
								internal/thumb/names.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								internal/thumb/names.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | package thumb | ||||||
|  |  | ||||||
|  | import "github.com/photoprism/photoprism/pkg/fs" | ||||||
|  |  | ||||||
|  | // Name represents a thumbnail size name. | ||||||
|  | type Name string | ||||||
|  |  | ||||||
|  | // Jpeg returns the thumbnail name with a jpeg file extension suffix as string. | ||||||
|  | func (n Name) Jpeg() string { | ||||||
|  | 	return string(n) + fs.JpegExt | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Names of thumbnail sizes. | ||||||
|  | const ( | ||||||
|  | 	Tile50   Name = "tile_50" | ||||||
|  | 	Tile100  Name = "tile_100" | ||||||
|  | 	Crop160  Name = "crop_160" | ||||||
|  | 	Tile224  Name = "tile_224" | ||||||
|  | 	Tile500  Name = "tile_500" | ||||||
|  | 	Colors   Name = "colors" | ||||||
|  | 	Left224  Name = "left_224" | ||||||
|  | 	Right224 Name = "right_224" | ||||||
|  | 	Fit720   Name = "fit_720" | ||||||
|  | 	Fit1280  Name = "fit_1280" | ||||||
|  | 	Fit1920  Name = "fit_1920" | ||||||
|  | 	Fit2048  Name = "fit_2048" | ||||||
|  | 	Fit2560  Name = "fit_2560" | ||||||
|  | 	Fit3840  Name = "fit_3840" | ||||||
|  | 	Fit4096  Name = "fit_4096" | ||||||
|  | 	Fit7680  Name = "fit_7680" | ||||||
|  | ) | ||||||
							
								
								
									
										28
									
								
								internal/thumb/resample.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								internal/thumb/resample.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | package thumb | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"image" | ||||||
|  |  | ||||||
|  | 	"github.com/disintegration/imaging" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Resample downscales an image and returns it. | ||||||
|  | func Resample(img image.Image, width, height int, opts ...ResampleOption) image.Image { | ||||||
|  | 	var resImg image.Image | ||||||
|  |  | ||||||
|  | 	method, filter, _ := ResampleOptions(opts...) | ||||||
|  |  | ||||||
|  | 	if method == ResampleFit { | ||||||
|  | 		resImg = imaging.Fit(img, width, height, filter) | ||||||
|  | 	} else if method == ResampleFillCenter { | ||||||
|  | 		resImg = imaging.Fill(img, width, height, imaging.Center, filter) | ||||||
|  | 	} else if method == ResampleFillTopLeft { | ||||||
|  | 		resImg = imaging.Fill(img, width, height, imaging.TopLeft, filter) | ||||||
|  | 	} else if method == ResampleFillBottomRight { | ||||||
|  | 		resImg = imaging.Fill(img, width, height, imaging.BottomRight, filter) | ||||||
|  | 	} else if method == ResampleResize { | ||||||
|  | 		resImg = imaging.Resize(img, width, height, filter) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return resImg | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								internal/thumb/resample_filters.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								internal/thumb/resample_filters.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | package thumb | ||||||
|  |  | ||||||
|  | import "github.com/disintegration/imaging" | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	ResampleBlackman ResampleFilter = "blackman" | ||||||
|  | 	ResampleLanczos  ResampleFilter = "lanczos" | ||||||
|  | 	ResampleCubic    ResampleFilter = "cubic" | ||||||
|  | 	ResampleLinear   ResampleFilter = "linear" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type ResampleFilter string | ||||||
|  |  | ||||||
|  | func (a ResampleFilter) Imaging() imaging.ResampleFilter { | ||||||
|  | 	switch a { | ||||||
|  | 	case ResampleBlackman: | ||||||
|  | 		return imaging.Blackman | ||||||
|  | 	case ResampleLanczos: | ||||||
|  | 		return imaging.Lanczos | ||||||
|  | 	case ResampleCubic: | ||||||
|  | 		return imaging.CatmullRom | ||||||
|  | 	case ResampleLinear: | ||||||
|  | 		return imaging.Linear | ||||||
|  | 	default: | ||||||
|  | 		return imaging.Lanczos | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										59
									
								
								internal/thumb/resample_options.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								internal/thumb/resample_options.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | package thumb | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/disintegration/imaging" | ||||||
|  | 	"github.com/photoprism/photoprism/pkg/fs" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type ResampleOption int | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	ResampleFillCenter ResampleOption = iota | ||||||
|  | 	ResampleFillTopLeft | ||||||
|  | 	ResampleFillBottomRight | ||||||
|  | 	ResampleFit | ||||||
|  | 	ResampleCrop | ||||||
|  | 	ResampleResize | ||||||
|  | 	ResampleNearestNeighbor | ||||||
|  | 	ResampleDefault | ||||||
|  | 	ResamplePng | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ResampleMethods = map[ResampleOption]string{ | ||||||
|  | 	ResampleFillCenter:      "center", | ||||||
|  | 	ResampleFillTopLeft:     "left", | ||||||
|  | 	ResampleFillBottomRight: "right", | ||||||
|  | 	ResampleFit:             "fit", | ||||||
|  | 	ResampleCrop:            "crop", | ||||||
|  | 	ResampleResize:          "resize", | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ResampleOptions extracts filter, format, and method from resample options. | ||||||
|  | func ResampleOptions(opts ...ResampleOption) (method ResampleOption, filter imaging.ResampleFilter, format fs.FileFormat) { | ||||||
|  | 	method = ResampleFit | ||||||
|  | 	filter = imaging.Lanczos | ||||||
|  | 	format = fs.FormatJpeg | ||||||
|  |  | ||||||
|  | 	for _, option := range opts { | ||||||
|  | 		switch option { | ||||||
|  | 		case ResamplePng: | ||||||
|  | 			format = fs.FormatPng | ||||||
|  | 		case ResampleNearestNeighbor: | ||||||
|  | 			filter = imaging.NearestNeighbor | ||||||
|  | 		case ResampleDefault: | ||||||
|  | 			filter = Filter.Imaging() | ||||||
|  | 		case ResampleFillTopLeft: | ||||||
|  | 			method = ResampleFillTopLeft | ||||||
|  | 		case ResampleFillCenter: | ||||||
|  | 			method = ResampleFillCenter | ||||||
|  | 		case ResampleFillBottomRight: | ||||||
|  | 			method = ResampleFillBottomRight | ||||||
|  | 		case ResampleFit: | ||||||
|  | 			method = ResampleFit | ||||||
|  | 		case ResampleResize: | ||||||
|  | 			method = ResampleResize | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return method, filter, format | ||||||
|  | } | ||||||
| @@ -1,7 +1,5 @@ | |||||||
| package thumb | package thumb | ||||||
|  |  | ||||||
| import "github.com/disintegration/imaging" |  | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	SizePrecached    = 2048 | 	SizePrecached    = 2048 | ||||||
| 	SizeUncached     = 7680 | 	SizeUncached     = 7680 | ||||||
| @@ -22,102 +20,57 @@ func InvalidSize(size int) bool { | |||||||
| 	return size < 0 || size > MaxSize() | 	return size < 0 || size > MaxSize() | ||||||
| } | } | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	ResampleBlackman ResampleFilter = "blackman" |  | ||||||
| 	ResampleLanczos  ResampleFilter = "lanczos" |  | ||||||
| 	ResampleCubic    ResampleFilter = "cubic" |  | ||||||
| 	ResampleLinear   ResampleFilter = "linear" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type ResampleFilter string |  | ||||||
|  |  | ||||||
| func (a ResampleFilter) Imaging() imaging.ResampleFilter { |  | ||||||
| 	switch a { |  | ||||||
| 	case ResampleBlackman: |  | ||||||
| 		return imaging.Blackman |  | ||||||
| 	case ResampleLanczos: |  | ||||||
| 		return imaging.Lanczos |  | ||||||
| 	case ResampleCubic: |  | ||||||
| 		return imaging.CatmullRom |  | ||||||
| 	case ResampleLinear: |  | ||||||
| 		return imaging.Linear |  | ||||||
| 	default: |  | ||||||
| 		return imaging.Lanczos |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	ResampleFillCenter ResampleOption = iota |  | ||||||
| 	ResampleFillTopLeft |  | ||||||
| 	ResampleFillBottomRight |  | ||||||
| 	ResampleFit |  | ||||||
| 	ResampleCrop |  | ||||||
| 	ResampleResize |  | ||||||
| 	ResampleNearestNeighbor |  | ||||||
| 	ResampleDefault |  | ||||||
| 	ResamplePng |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type ResampleOption int |  | ||||||
|  |  | ||||||
| var ResampleMethods = map[ResampleOption]string{ |  | ||||||
| 	ResampleFillCenter:      "center", |  | ||||||
| 	ResampleFillTopLeft:     "left", |  | ||||||
| 	ResampleFillBottomRight: "right", |  | ||||||
| 	ResampleFit:             "fit", |  | ||||||
| 	ResampleCrop:            "crop", |  | ||||||
| 	ResampleResize:          "resize", |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type Size struct { | type Size struct { | ||||||
| 	Use     string           `json:"use"` | 	Use     string           `json:"use"` | ||||||
| 	Source  string           `json:"-"` | 	Source  Name             `json:"-"` | ||||||
| 	Width   int              `json:"w"` | 	Width   int              `json:"w"` | ||||||
| 	Height  int              `json:"h"` | 	Height  int              `json:"h"` | ||||||
| 	Public  bool             `json:"-"` | 	Public  bool             `json:"-"` | ||||||
| 	Options []ResampleOption `json:"-"` | 	Options []ResampleOption `json:"-"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type SizeMap map[string]Size | type SizeMap map[Name]Size | ||||||
|  |  | ||||||
|  | // Sizes contains the properties of all thumbnail sizes. | ||||||
| var Sizes = SizeMap{ | var Sizes = SizeMap{ | ||||||
| 	"tile_50":   {"Lists", "tile_500", 50, 50, false, []ResampleOption{ResampleFillCenter, ResampleDefault}}, | 	Tile50:   {"Lists", Tile500, 50, 50, false, []ResampleOption{ResampleFillCenter, ResampleDefault}}, | ||||||
| 	"tile_100":  {"Maps", "tile_500", 100, 100, false, []ResampleOption{ResampleFillCenter, ResampleDefault}}, | 	Tile100:  {"Maps", Tile500, 100, 100, false, []ResampleOption{ResampleFillCenter, ResampleDefault}}, | ||||||
| 	"crop_160":  {"FaceNet", "", 160, 160, false, []ResampleOption{ResampleCrop, ResampleDefault}}, | 	Crop160:  {"FaceNet", "", 160, 160, false, []ResampleOption{ResampleCrop, ResampleDefault}}, | ||||||
| 	"tile_224":  {"TensorFlow, Mosaic", "tile_500", 224, 224, false, []ResampleOption{ResampleFillCenter, ResampleDefault}}, | 	Tile224:  {"TensorFlow, Mosaic", Tile500, 224, 224, false, []ResampleOption{ResampleFillCenter, ResampleDefault}}, | ||||||
| 	"tile_500":  {"Tiles", "", 500, 500, false, []ResampleOption{ResampleFillCenter, ResampleDefault}}, | 	Tile500:  {"Tiles", "", 500, 500, false, []ResampleOption{ResampleFillCenter, ResampleDefault}}, | ||||||
| 	"colors":    {"Color Detection", "fit_720", 3, 3, false, []ResampleOption{ResampleResize, ResampleNearestNeighbor, ResamplePng}}, | 	Colors:   {"Color Detection", Fit720, 3, 3, false, []ResampleOption{ResampleResize, ResampleNearestNeighbor, ResamplePng}}, | ||||||
| 	"left_224":  {"TensorFlow", "fit_720", 224, 224, false, []ResampleOption{ResampleFillTopLeft, ResampleDefault}}, | 	Left224:  {"TensorFlow", Fit720, 224, 224, false, []ResampleOption{ResampleFillTopLeft, ResampleDefault}}, | ||||||
| 	"right_224": {"TensorFlow", "fit_720", 224, 224, false, []ResampleOption{ResampleFillBottomRight, ResampleDefault}}, | 	Right224: {"TensorFlow", Fit720, 224, 224, false, []ResampleOption{ResampleFillBottomRight, ResampleDefault}}, | ||||||
| 	"fit_720":   {"Mobile, TV", "", 720, 720, true, []ResampleOption{ResampleFit, ResampleDefault}}, | 	Fit720:   {"Mobile, TV", "", 720, 720, true, []ResampleOption{ResampleFit, ResampleDefault}}, | ||||||
| 	"fit_1280":  {"Mobile, HD Ready TV", "fit_2048", 1280, 1024, true, []ResampleOption{ResampleFit, ResampleDefault}}, | 	Fit1280:  {"Mobile, HD Ready TV", Fit2048, 1280, 1024, true, []ResampleOption{ResampleFit, ResampleDefault}}, | ||||||
| 	"fit_1920":  {"Mobile, Full HD TV", "fit_2048", 1920, 1200, true, []ResampleOption{ResampleFit, ResampleDefault}}, | 	Fit1920:  {"Mobile, Full HD TV", Fit2048, 1920, 1200, true, []ResampleOption{ResampleFit, ResampleDefault}}, | ||||||
| 	"fit_2048":  {"Tablets, Cinema 2K", "", 2048, 2048, true, []ResampleOption{ResampleFit, ResampleDefault}}, | 	Fit2048:  {"Tablets, Cinema 2K", "", 2048, 2048, true, []ResampleOption{ResampleFit, ResampleDefault}}, | ||||||
| 	"fit_2560":  {"Quad HD, Retina Display", "", 2560, 1600, true, []ResampleOption{ResampleFit, ResampleDefault}}, | 	Fit2560:  {"Quad HD, Retina Display", "", 2560, 1600, true, []ResampleOption{ResampleFit, ResampleDefault}}, | ||||||
| 	"fit_3840":  {"Ultra HD", "", 3840, 2400, false, []ResampleOption{ResampleFit, ResampleDefault}}, // Deprecated in favor of fit_4096 | 	Fit3840:  {"Ultra HD", "", 3840, 2400, false, []ResampleOption{ResampleFit, ResampleDefault}}, // Deprecated in favor of fit_4096 | ||||||
| 	"fit_4096":  {"Ultra HD, Retina 4K", "", 4096, 4096, true, []ResampleOption{ResampleFit, ResampleDefault}}, | 	Fit4096:  {"Ultra HD, Retina 4K", "", 4096, 4096, true, []ResampleOption{ResampleFit, ResampleDefault}}, | ||||||
| 	"fit_7680":  {"8K Ultra HD 2, Retina 6K", "", 7680, 4320, true, []ResampleOption{ResampleFit, ResampleDefault}}, | 	Fit7680:  {"8K Ultra HD 2, Retina 6K", "", 7680, 4320, true, []ResampleOption{ResampleFit, ResampleDefault}}, | ||||||
| } | } | ||||||
|  |  | ||||||
| var DefaultSizes = []string{ | // DefaultSizes contains all default size names. | ||||||
| 	"fit_7680", | var DefaultSizes = []Name{ | ||||||
| 	"fit_4096", | 	Fit7680, | ||||||
| 	"fit_2560", | 	Fit4096, | ||||||
| 	"fit_2048", | 	Fit2560, | ||||||
| 	"fit_1920", | 	Fit2048, | ||||||
| 	"fit_1280", | 	Fit1920, | ||||||
| 	"fit_720", | 	Fit1280, | ||||||
| 	"right_224", | 	Fit720, | ||||||
| 	"left_224", | 	Right224, | ||||||
| 	"colors", | 	Left224, | ||||||
| 	"tile_500", | 	Colors, | ||||||
| 	"tile_224", | 	Tile500, | ||||||
| 	"tile_100", | 	Tile224, | ||||||
| 	"tile_50", | 	Tile100, | ||||||
|  | 	Tile50, | ||||||
| } | } | ||||||
|  |  | ||||||
| // Find returns the largest default thumbnail type for the given size limit. | // Find returns the largest default thumbnail type for the given size limit. | ||||||
| func Find(limit int) (name string, result Size) { | func Find(limit int) (name Name, size Size) { | ||||||
| 	for _, name = range DefaultSizes { | 	for _, name = range DefaultSizes { | ||||||
| 		t := Sizes[name] | 		t := Sizes[name] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,13 +10,13 @@ func TestSize_ExceedsLimit(t *testing.T) { | |||||||
| 	SizePrecached = 1024 | 	SizePrecached = 1024 | ||||||
| 	SizeUncached = 2048 | 	SizeUncached = 2048 | ||||||
|  |  | ||||||
| 	fit4096 := Sizes["fit_4096"] | 	fit4096 := Sizes[Fit4096] | ||||||
| 	assert.True(t, fit4096.ExceedsLimit()) | 	assert.True(t, fit4096.ExceedsLimit()) | ||||||
|  |  | ||||||
| 	fit2048 := Sizes["fit_2048"] | 	fit2048 := Sizes[Fit2048] | ||||||
| 	assert.False(t, fit2048.ExceedsLimit()) | 	assert.False(t, fit2048.ExceedsLimit()) | ||||||
|  |  | ||||||
| 	tile500 := Sizes["tile_500"] | 	tile500 := Sizes[Tile500] | ||||||
| 	assert.False(t, tile500.ExceedsLimit()) | 	assert.False(t, tile500.ExceedsLimit()) | ||||||
|  |  | ||||||
| 	SizePrecached = 2048 | 	SizePrecached = 2048 | ||||||
| @@ -27,13 +27,13 @@ func TestSize_Uncached(t *testing.T) { | |||||||
| 	SizePrecached = 1024 | 	SizePrecached = 1024 | ||||||
| 	SizeUncached = 2048 | 	SizeUncached = 2048 | ||||||
|  |  | ||||||
| 	fit4096 := Sizes["fit_4096"] | 	fit4096 := Sizes[Fit4096] | ||||||
| 	assert.True(t, fit4096.Uncached()) | 	assert.True(t, fit4096.Uncached()) | ||||||
|  |  | ||||||
| 	fit2048 := Sizes["fit_2048"] | 	fit2048 := Sizes[Fit2048] | ||||||
| 	assert.True(t, fit2048.Uncached()) | 	assert.True(t, fit2048.Uncached()) | ||||||
|  |  | ||||||
| 	tile500 := Sizes["tile_500"] | 	tile500 := Sizes[Tile500] | ||||||
| 	assert.False(t, tile500.Uncached()) | 	assert.False(t, tile500.Uncached()) | ||||||
|  |  | ||||||
| 	SizePrecached = 2048 | 	SizePrecached = 2048 | ||||||
| @@ -57,16 +57,16 @@ func TestResampleFilter_Imaging(t *testing.T) { | |||||||
|  |  | ||||||
| func TestFind(t *testing.T) { | func TestFind(t *testing.T) { | ||||||
| 	t.Run("2048", func(t *testing.T) { | 	t.Run("2048", func(t *testing.T) { | ||||||
| 		tName, tType := Find(2048) | 		name, size := Find(2048) | ||||||
| 		assert.Equal(t, "fit_2048", tName) | 		assert.Equal(t, Fit2048, name) | ||||||
| 		assert.Equal(t, 2048, tType.Width) | 		assert.Equal(t, 2048, size.Width) | ||||||
| 		assert.Equal(t, 2048, tType.Height) | 		assert.Equal(t, 2048, size.Height) | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	t.Run("2000", func(t *testing.T) { | 	t.Run("2000", func(t *testing.T) { | ||||||
| 		tName, tType := Find(2000) | 		name, size := Find(2000) | ||||||
| 		assert.Equal(t, "fit_1920", tName) | 		assert.Equal(t, Fit1920, name) | ||||||
| 		assert.Equal(t, 1920, tType.Width) | 		assert.Equal(t, 1920, size.Width) | ||||||
| 		assert.Equal(t, 1200, tType.Height) | 		assert.Equal(t, 1200, size.Height) | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -97,7 +97,7 @@ func (worker *Share) Start() (err error) { | |||||||
| 			srcFileName := photoprism.FileName(file.File.FileRoot, file.File.FileName) | 			srcFileName := photoprism.FileName(file.File.FileRoot, file.File.FileName) | ||||||
|  |  | ||||||
| 			if a.ShareSize != "" { | 			if a.ShareSize != "" { | ||||||
| 				size, ok := thumb.Sizes[a.ShareSize] | 				size, ok := thumb.Sizes[thumb.Name(a.ShareSize)] | ||||||
|  |  | ||||||
| 				if !ok { | 				if !ok { | ||||||
| 					log.Errorf("share: invalid size %s", a.ShareSize) | 					log.Errorf("share: invalid size %s", a.ShareSize) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Michael Mayer
					Michael Mayer