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/event"
"github.com/photoprism/photoprism/internal/file"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/photoprism"
"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/binding"
@ -339,7 +341,7 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
}
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)
zipFileName := path.Join(zipPath, zipBaseName)
@ -362,21 +364,21 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
zipWriter := zip.NewWriter(newZipFile)
defer zipWriter.Close()
for _, file := range p {
fileName := path.Join(conf.OriginalsPath(), file.FileName)
fileAlias := file.DownloadFileName()
for _, f := range p {
fileName := path.Join(conf.OriginalsPath(), f.FileName)
fileAlias := f.DownloadFileName()
if util.Exists(fileName) {
if file.Exists(fileName) {
if err := addFileToZip(zipWriter, fileName, fileAlias); err != nil {
log.Error(err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": util.UcFirst("failed to create zip file")})
return
}
log.Infof("album: added \"%s\" as \"%s\"", file.FileName, fileAlias)
log.Infof("album: added \"%s\" as \"%s\"", f.FileName, fileAlias)
} else {
log.Warnf("album: \"%s\" is missing", file.FileName)
file.FileMissing = true
conf.Db().Save(&file)
log.Warnf("album: \"%s\" is missing", f.FileName)
f.FileMissing = true
conf.Db().Save(&f)
}
}
@ -385,7 +387,7 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
zipWriter.Close()
newZipFile.Close()
if !util.Exists(zipFileName) {
if !file.Exists(zipFileName) {
log.Errorf("could not find zip file: %s", zipFileName)
c.Data(404, "image/svg+xml", photoIconSvg)
return
@ -411,7 +413,7 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
typeName := c.Param("type")
uuid := c.Param("uuid")
thumbType, ok := photoprism.ThumbnailTypes[typeName]
thumbType, ok := thumb.Types[typeName]
if !ok {
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())
file, err := q.FindAlbumThumbByUUID(uuid)
f, err := q.FindAlbumThumbByUUID(uuid)
if err != nil {
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
}
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)
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore
file.FileMissing = true
conf.Db().Save(&file)
f.FileMissing = true
conf.Db().Save(&f)
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") != "" {
downloadFileName := file.DownloadFileName()
downloadFileName := f.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
func BatchAlbumsDelete(router *gin.RouterGroup, conf *config.Config) {
router.POST("/batch/albums/delete", func(c *gin.Context) {

View file

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

View file

@ -8,11 +8,10 @@ import (
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/util"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/file"
"github.com/photoprism/photoprism/internal/photoprism"
)
@ -61,7 +60,7 @@ func StartImport(router *gin.RouterGroup, conf *config.Config) {
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 {
log.Errorf("import: could not deleted empty directory \"%s\": %s", path, err)
} else {

View file

@ -55,11 +55,6 @@ func StartIndexing(router *gin.RouterGroup, conf *config.Config) {
return
}
// Set thumbnails JPEG quality and size
photoprism.JpegQuality = conf.ThumbQuality()
photoprism.MaxThumbWidth = conf.ThumbSize()
photoprism.MaxThumbHeight = conf.ThumbSize()
path := conf.OriginalsPath()
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/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/file"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/internal/util"
)
@ -123,7 +124,7 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
labelUUID := c.Param("uuid")
start := time.Now()
thumbType, ok := photoprism.ThumbnailTypes[typeName]
thumbType, ok := thumb.Types[typeName]
if !ok {
log.Errorf("invalid type: %s", typeName)
@ -142,7 +143,7 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
return
}
file, err := q.FindLabelThumbByUUID(labelUUID)
f, err := q.FindLabelThumbByUUID(labelUUID)
if err != nil {
log.Errorf(err.Error())
@ -150,19 +151,19 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
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)
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore
file.FileMissing = true
conf.Db().Save(&file)
f.FileMissing = true
conf.Db().Save(&f)
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)
if err != nil {

View file

@ -7,6 +7,7 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/file"
"github.com/photoprism/photoprism/internal/query"
"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) {
router.GET("/photos/:uuid/download", func(c *gin.Context) {
q := query.New(conf.OriginalsPath(), conf.Db())
file, err := q.FindFileByPhotoUUID(c.Param("uuid"))
f, err := q.FindFileByPhotoUUID(c.Param("uuid"))
if err != nil {
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})
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"))
c.Data(404, "image/svg+xml", photoIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore
file.FileMissing = true
conf.Db().Save(&file)
f.FileMissing = true
conf.Db().Save(&f)
return
}
downloadFileName := file.DownloadFileName()
downloadFileName := f.DownloadFileName()
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))

View file

@ -5,12 +5,11 @@ import (
"net/http"
"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/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
@ -23,7 +22,7 @@ func GetThumbnail(router *gin.RouterGroup, conf *config.Config) {
fileHash := c.Param("hash")
typeName := c.Param("type")
thumbType, ok := photoprism.ThumbnailTypes[typeName]
thumbType, ok := thumb.Types[typeName]
if !ok {
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())
file, err := q.FindFileByHash(fileHash)
f, err := q.FindFileByHash(fileHash)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()})
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)
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore
file.FileMissing = true
conf.Db().Save(&file)
f.FileMissing = true
conf.Db().Save(&f)
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") != "" {
downloadFileName := file.DownloadFileName()
downloadFileName := f.DownloadFileName()
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))
}

View file

@ -12,10 +12,10 @@ import (
"github.com/disintegration/imaging"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/file"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/util"
"github.com/photoprism/photoprism/internal/thumb"
)
// 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])
if util.Exists(previewFilename) {
if file.Exists(previewFilename) {
c.File(previewFilename)
return
}
@ -60,22 +60,22 @@ func GetPreview(router *gin.RouterGroup, conf *config.Config) {
y := 0
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 {
fileName := path.Join(conf.OriginalsPath(), file.FileName)
for _, f := range p {
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)
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore
file.FileMissing = true
conf.Db().Save(&file)
f.FileMissing = true
conf.Db().Save(&f)
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 {
log.Error(err)

View file

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

View file

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

View file

@ -10,8 +10,8 @@ import (
"time"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/file"
"github.com/photoprism/photoprism/internal/server"
"github.com/photoprism/photoprism/internal/util"
"github.com/sevlyar/go-daemon"
"github.com/urfave/cli"
)
@ -96,7 +96,7 @@ func startAction(ctx *cli.Context) error {
}
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())
}

View file

@ -32,10 +32,6 @@ func thumbnailsAction(ctx *cli.Context) error {
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 {
log.Error(err)
return err

View file

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

View file

@ -10,6 +10,7 @@ import (
_ "github.com/jinzhu/gorm/dialects/sqlite"
gc "github.com/patrickmn/go-cache"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -22,6 +23,15 @@ type Config struct {
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) {
log.SetFormatter(&logrus.TextFormatter{
DisableColors: false,
@ -44,6 +54,10 @@ func NewConfig(ctx *cli.Context) *Config {
log.SetLevel(c.LogLevel())
thumb.JpegQuality = c.ThumbQuality()
thumb.MaxWidth = c.ThumbSize()
thumb.MaxHeight = c.ThumbSize()
return c
}
@ -180,17 +194,32 @@ func (c *Config) Workers() int {
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 {
if c.config.ThumbQuality > 100 {
return 100
}
if c.config.ThumbQuality < 25 {
return 25
}
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 {
if c.config.ThumbSize > 16384 {
return 16384
}
if c.config.ThumbSize < 720 {
return 720
}
return c.config.ThumbSize
}
// GeoCodingApi returns the preferred geo coding api (none, osm or places).
func (c *Config) GeoCodingApi() string {
switch c.config.GeoCodingApi {

View file

@ -3,7 +3,7 @@ package config
import (
"testing"
"github.com/photoprism/photoprism/internal/util"
"github.com/photoprism/photoprism/internal/file"
"github.com/stretchr/testify/assert"
)
@ -17,7 +17,7 @@ func TestNewConfig(t *testing.T) {
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.ReadOnly())
}

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ import (
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"github.com/photoprism/photoprism/internal/util"
"github.com/photoprism/photoprism/internal/file"
"github.com/urfave/cli"
"gopkg.in/yaml.v2"
)
@ -87,7 +87,7 @@ func NewParams(ctx *cli.Context) *Params {
c.Name = ctx.App.Name
c.Copyright = ctx.App.Copyright
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 {
log.Debug(err)
@ -103,21 +103,21 @@ func NewParams(ctx *cli.Context) *Params {
}
func (c *Params) expandFilenames() {
c.ConfigPath = util.ExpandedFilename(c.ConfigPath)
c.ResourcesPath = util.ExpandedFilename(c.ResourcesPath)
c.AssetsPath = util.ExpandedFilename(c.AssetsPath)
c.CachePath = util.ExpandedFilename(c.CachePath)
c.OriginalsPath = util.ExpandedFilename(c.OriginalsPath)
c.ImportPath = util.ExpandedFilename(c.ImportPath)
c.ExportPath = util.ExpandedFilename(c.ExportPath)
c.SqlServerPath = util.ExpandedFilename(c.SqlServerPath)
c.PIDFilename = util.ExpandedFilename(c.PIDFilename)
c.LogFilename = util.ExpandedFilename(c.LogFilename)
c.ConfigPath = file.ExpandFilename(c.ConfigPath)
c.ResourcesPath = file.ExpandFilename(c.ResourcesPath)
c.AssetsPath = file.ExpandFilename(c.AssetsPath)
c.CachePath = file.ExpandFilename(c.CachePath)
c.OriginalsPath = file.ExpandFilename(c.OriginalsPath)
c.ImportPath = file.ExpandFilename(c.ImportPath)
c.ExportPath = file.ExpandFilename(c.ExportPath)
c.SqlServerPath = file.ExpandFilename(c.SqlServerPath)
c.PIDFilename = file.ExpandFilename(c.PIDFilename)
c.LogFilename = file.ExpandFilename(c.LogFilename)
}
// SetValuesFromFile uses a yaml config file to initiate the configuration entity.
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))
}

View file

@ -3,7 +3,7 @@ package config
import (
"testing"
"github.com/photoprism/photoprism/internal/util"
"github.com/photoprism/photoprism/internal/file"
"github.com/stretchr/testify/assert"
)
@ -17,7 +17,7 @@ func TestNewParams(t *testing.T) {
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.ReadOnly)
}

View file

@ -5,7 +5,7 @@ import (
"io/ioutil"
"os"
"github.com/photoprism/photoprism/internal/util"
"github.com/photoprism/photoprism/internal/file"
"gopkg.in/yaml.v2"
)
@ -20,7 +20,7 @@ func NewSettings() *Settings {
// SetValuesFromFile uses a yaml config file to initiate the configuration entity.
func (s *Settings) SetValuesFromFile(fileName string) error {
if !util.Exists(fileName) {
if !file.Exists(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.
func (s *Settings) WriteValuesToFile(fileName string) error {
if !util.Exists(fileName) {
if !file.Exists(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/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/urfave/cli"
)
@ -29,7 +30,7 @@ func testDataPath(assetsPath string) string {
}
func NewTestParams() *Params {
assetsPath := util.ExpandedFilename("../../assets")
assetsPath := file.ExpandFilename("../../assets")
testDataPath := testDataPath(assetsPath)
@ -52,7 +53,7 @@ func NewTestParams() *Params {
}
func NewTestParamsError() *Params {
assetsPath := util.ExpandedFilename("../..")
assetsPath := file.ExpandFilename("../..")
testDataPath := testDataPath("../../assets")
@ -93,6 +94,10 @@ func NewTestConfig() *Config {
c.ImportSQL(c.ExamplesPath() + "/fixtures.sql")
thumb.JpegQuality = c.ThumbQuality()
thumb.MaxWidth = c.ThumbSize()
thumb.MaxHeight = c.ThumbSize()
return c
}
@ -140,8 +145,8 @@ func (c *Config) RemoveTestData(t *testing.T) {
}
func (c *Config) DownloadTestData(t *testing.T) {
if util.Exists(TestDataZip) {
hash := util.Hash(TestDataZip)
if file.Exists(TestDataZip) {
hash := file.Hash(TestDataZip)
if hash != TestDataHash {
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)
if err := util.Download(TestDataZip, TestDataURL); err != nil {
if err := file.Download(TestDataZip, TestDataURL); err != nil {
fmt.Printf("Download failed: %s\n", err.Error())
}
}
}
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())
}
}

View file

@ -4,7 +4,7 @@ import (
"testing"
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/util"
"github.com/photoprism/photoprism/internal/file"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli"
)
@ -26,7 +26,7 @@ func TestNewTestParams(t *testing.T) {
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)
}
@ -43,7 +43,7 @@ func TestNewTestParamsError(t *testing.T) {
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.False(t, c.Debug)
}

View file

@ -16,7 +16,7 @@ import (
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/util"
"github.com/photoprism/photoprism/internal/rnd"
)
var log = event.Log
@ -32,7 +32,7 @@ func ID(prefix rune) string {
result := make([]byte, 0, 17)
result = append(result, byte(prefix))
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)
}

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 (
"archive/zip"
@ -9,8 +16,12 @@ import (
"os/user"
"path/filepath"
"strings"
"github.com/photoprism/photoprism/internal/event"
)
var log = event.Log
// Returns true if file exists
func Exists(filename string) bool {
info, err := os.Stat(filename)
@ -30,7 +41,7 @@ func Overwrite(fileName string, data []byte) bool {
}
// Returns full path; ~ replaced with actual home directory
func ExpandedFilename(filename string) string {
func ExpandFilename(filename string) string {
if filename == "" {
return ""
}
@ -50,32 +61,6 @@ func ExpandedFilename(filename string) string {
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
// 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) {
@ -151,7 +136,7 @@ func Download(filepath string, url string) error {
return nil
}
func DirectoryIsEmpty(path string) bool {
func IsEmpty(path string) bool {
f, err := os.Open(path)
if err != nil {

View file

@ -1,4 +1,4 @@
package util
package file
import (
"os"
@ -29,19 +29,19 @@ func TestOverwrite(t *testing.T) {
func TestExpandedFilename(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.IsType(t, "", filename)
})
t.Run("empty filename", func(t *testing.T) {
filename := ExpandedFilename("")
filename := ExpandFilename("")
assert.Equal(t, "", filename)
assert.IsType(t, "", filename)
})
t.Run("~ in filename", func(t *testing.T) {
usr, _ := user.Current()
expected := usr.HomeDir + "/test.jpg"
filename := ExpandedFilename("~/test.jpg")
filename := ExpandFilename("~/test.jpg")
assert.Equal(t, expected, filename)
assert.IsType(t, "", filename)
})
@ -49,14 +49,14 @@ func TestExpandedFilename(t *testing.T) {
func TestDirectoryIsEmpty(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) {
assert.Equal(t, false, DirectoryIsEmpty("./xxx"))
assert.Equal(t, false, IsEmpty("./xxx"))
})
t.Run("empty path", func(t *testing.T) {
os.Mkdir("./testdata/emptyDir", 0777)
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 (
"crypto/sha1"

View file

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

View file

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

View file

@ -1,4 +1,4 @@
package util
package file
import (
"testing"
@ -8,12 +8,12 @@ import (
func TestMimeType(t *testing.T) {
t.Run("jpg", func(t *testing.T) {
filename := ExpandedFilename("./testdata/test.jpg")
filename := ExpandFilename("./testdata/test.jpg")
mimeType := MimeType(filename)
assert.Equal(t, "image/jpeg", mimeType)
})
t.Run("not existing filename", func(t *testing.T) {
filename := ExpandedFilename("./testdata/xxx.jpg")
filename := ExpandFilename("./testdata/xxx.jpg")
mimeType := MimeType(filename)
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 (
"archive/zip"
"io"
"os"
"strings"
)
// ZipFiles compresses one or many files into a single zip archive file.
// Param 1: filename is the output zip file's name.
// 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)
if err != nil {
return err
@ -21,7 +22,7 @@ func ZipFiles(filename string, files []string) error {
// Add files to zip
for _, file := range files {
if err = AddFileToZip(zipWriter, file); err != nil {
if err = AddToZip(zipWriter, file); err != nil {
return err
}
}
@ -29,7 +30,7 @@ func ZipFiles(filename string, files []string) error {
return nil
}
func AddFileToZip(zipWriter *zip.Writer, filename string) error {
func AddToZip(zipWriter *zip.Writer, filename string) error {
fileToZip, err := os.Open(filename)
if err != nil {
@ -59,3 +60,29 @@ func AddFileToZip(zipWriter *zip.Writer, filename string) error {
_, err = io.Copy(writer, fileToZip)
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"
"sync"
"github.com/photoprism/photoprism/internal/util"
"github.com/photoprism/photoprism/internal/file"
tf "github.com/tensorflow/tensorflow/tensorflow/go"
"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.
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)
}

View file

@ -5,7 +5,7 @@ import (
"testing"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/util"
"github.com/photoprism/photoprism/internal/file"
"github.com/stretchr/testify/assert"
)
@ -30,7 +30,7 @@ func TestConvert_ToJpeg(t *testing.T) {
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)
@ -66,7 +66,7 @@ func TestConvert_ToJpeg(t *testing.T) {
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)
@ -92,7 +92,7 @@ func TestConvert_Path(t *testing.T) {
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)
@ -108,15 +108,15 @@ func TestConvert_Path(t *testing.T) {
existingJpegFilename := conf.ImportPath() + "/raw/IMG_2567.jpg"
oldHash := util.Hash(existingJpegFilename)
oldHash := file.Hash(existingJpegFilename)
os.Remove(existingJpegFilename)
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")
}

View file

@ -12,8 +12,7 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/util"
"github.com/photoprism/photoprism/internal/file"
)
// 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 {
// Remove empty directories from import path
for _, directory := range directories {
if util.DirectoryIsEmpty(directory) {
if file.IsEmpty(directory) {
if err := os.Remove(directory); err != nil {
log.Errorf("import: could not deleted empty directory \"%s\" (%s)", directory, err)
} else {
@ -202,8 +201,8 @@ func (imp *Import) DestinationFilename(mainFile *MediaFile, mediaFile *MediaFile
result := pathName + string(os.PathSeparator) + fileName + fileExtension
for util.Exists(result) {
if mediaFile.Hash() == util.Hash(result) {
for file.Exists(result) {
if mediaFile.Hash() == file.Hash(result) {
return result, fmt.Errorf("file already exists: %s", result)
}

View file

@ -13,7 +13,7 @@ import (
"github.com/djherbis/times"
"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.
@ -22,7 +22,7 @@ type MediaFile struct {
dateCreated time.Time
timeZone string
hash string
fileType FileType
fileType file.Type
mimeType string
perceptualHash string
width int
@ -33,13 +33,13 @@ type MediaFile struct {
// NewMediaFile returns a new MediaFile.
func NewMediaFile(filename string) (*MediaFile, error) {
if !util.Exists(filename) {
if !file.Exists(filename) {
return nil, fmt.Errorf("file does not exist: %s", filename)
}
instance := &MediaFile{
filename: filename,
fileType: FileTypeOther,
fileType: file.TypeOther,
}
return instance, nil
@ -231,7 +231,7 @@ func (m *MediaFile) CanonicalNameFromFileWithDirectory() string {
// Hash return a sha1 hash of a MediaFile based on the filename.
func (m *MediaFile) Hash() string {
if len(m.hash) == 0 {
m.hash = util.Hash(m.Filename())
m.hash = file.Hash(m.Filename())
}
return m.hash
@ -242,7 +242,7 @@ func (m *MediaFile) EditedFilename() string {
basename := filepath.Base(m.filename)
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
}
}
@ -381,7 +381,7 @@ func (m *MediaFile) MimeType() string {
return m.mimeType
}
m.mimeType = util.MimeType(m.Filename())
m.mimeType = file.MimeType(m.Filename())
return m.mimeType
}
@ -397,7 +397,7 @@ func (m *MediaFile) openFile() (*os.File, error) {
// Exists checks if a media file exists by filename.
func (m *MediaFile) Exists() bool {
return util.Exists(m.Filename())
return file.Exists(m.Filename())
}
// Remove a media file.
@ -480,13 +480,13 @@ func (m *MediaFile) IsJpeg() bool {
}
// Type returns the type of the media file.
func (m *MediaFile) Type() FileType {
return FileExtensions[m.Extension()]
func (m *MediaFile) Type() file.Type {
return file.Ext[m.Extension()]
}
// HasType returns true if this media file is of a given type.
func (m *MediaFile) HasType(t FileType) bool {
if t == FileTypeJpeg {
func (m *MediaFile) HasType(t file.Type) bool {
if t == file.TypeJpeg {
return m.IsJpeg()
}
@ -495,28 +495,28 @@ func (m *MediaFile) HasType(t FileType) bool {
// IsRaw returns true if this media file a RAW file.
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.
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).
func (m *MediaFile) IsSidecar() bool {
switch m.Type() {
case FileTypeXMP:
case file.TypeXMP:
return true
case FileTypeAAE:
case file.TypeAAE:
return true
case FileTypeXML:
case file.TypeXML:
return true
case FileTypeYaml:
case file.TypeYaml:
return true
case FileTypeText:
case file.TypeText:
return true
case FileTypeMarkdown:
case file.TypeMarkdown:
return true
default:
return false
@ -526,7 +526,7 @@ func (m *MediaFile) IsSidecar() bool {
// IsVideo returns true if this media file is a video file.
func (m *MediaFile) IsVideo() bool {
switch m.Type() {
case FileTypeMovie:
case file.TypeMovie:
return true
}
@ -544,9 +544,9 @@ func (m *MediaFile) Jpeg() (*MediaFile, error) {
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)
}

View file

@ -4,7 +4,7 @@ import (
"os"
"testing"
"github.com/photoprism/photoprism/internal/util"
"github.com/photoprism/photoprism/internal/file"
"github.com/photoprism/photoprism/internal/config"
"github.com/stretchr/testify/assert"
@ -514,7 +514,7 @@ func TestMediaFile_Move(t *testing.T) {
f, err := NewMediaFile(conf.ExamplesPath() + "/table_white.jpg")
assert.Nil(t, err)
f.Copy(origName)
assert.True(t, util.Exists(origName))
assert.True(t, file.Exists(origName))
m, err := NewMediaFile(origName)
assert.Nil(t, err)
@ -523,7 +523,7 @@ func TestMediaFile_Move(t *testing.T) {
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())
}
@ -539,7 +539,7 @@ func TestMediaFile_Copy(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/table_white.jpg")
assert.Nil(t, err)
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) {

View file

@ -16,7 +16,7 @@ import (
"github.com/disintegration/imaging"
"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"
"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))
if !util.Exists(fileName) {
if !file.Exists(fileName) {
e := fmt.Errorf("tensorflow: label rules file not found in \"%s\"", filepath.Base(fileName))
log.Error(e.Error())
return e

View file

@ -3,85 +3,19 @@ package photoprism
import (
"fmt"
"image"
"image/png"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/file"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/disintegration/imaging"
"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.
func CreateThumbnailsFromOriginals(originalsPath string, thumbnailsPath string, force bool) 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.
func (m *MediaFile) Thumbnail(path string, typeName string) (filename string, err error) {
thumbType, ok := ThumbnailTypes[typeName]
thumbType, ok := thumb.Types[typeName]
if !ok {
log.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 {
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))
}
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) {
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 sourceImgType string
for _, name := range DefaultThumbnails {
thumbType := ThumbnailTypes[name]
for _, name := range thumb.DefaultTypes {
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)
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)
return err
} else {
if !force && util.Exists(fileName) {
if !force && file.Exists(fileName) {
continue
}
if thumbType.Source != "" {
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 {
_, err = CreateThumbnail(img, fileName, thumbType.Width, thumbType.Height, thumbType.Options...)
_, err = thumb.Create(img, fileName, thumbType.Width, thumbType.Height, thumbType.Options...)
}
} 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
}

View file

@ -7,6 +7,7 @@ import (
"github.com/disintegration/imaging"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/nsfw"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/internal/config"
"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()
thumbsPath := conf.CachePath() + "/_tmp"
@ -129,25 +130,25 @@ func TestThumbnails_ThumbnailFilename(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.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) {
_, 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())
})
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())
})
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())
})
t.Run("empty thumbpath", func(t *testing.T) {
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())
})
}
@ -169,9 +170,9 @@ func TestThumbnails_ThumbnailFromFile(t *testing.T) {
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.FileExists(t, thumb)
assert.FileExists(t, thumbnail)
})
t.Run("hash too short", func(t *testing.T) {
@ -180,7 +181,7 @@ func TestThumbnails_ThumbnailFromFile(t *testing.T) {
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())
})
t.Run("filename too short", func(t *testing.T) {
@ -189,7 +190,7 @@ func TestThumbnails_ThumbnailFromFile(t *testing.T) {
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())
})
}
@ -210,7 +211,7 @@ func TestThumbnails_CreateThumbnail(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 {
t.Error(err)
@ -222,13 +223,13 @@ func TestThumbnails_CreateThumbnail(t *testing.T) {
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.NotNil(t, thumb)
assert.NotNil(t, thumbnail)
bounds := thumb.Bounds()
bounds := thumbnail.Bounds()
assert.Equal(t, 150, bounds.Dx())
assert.Equal(t, 99, bounds.Dy())
@ -236,7 +237,7 @@ func TestThumbnails_CreateThumbnail(t *testing.T) {
assert.FileExists(t, expectedFilename)
})
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 {
t.Error(err)
@ -248,7 +249,7 @@ func TestThumbnails_CreateThumbnail(t *testing.T) {
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())
bounds := thumbnail.Bounds()
@ -256,7 +257,7 @@ func TestThumbnails_CreateThumbnail(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 {
t.Error(err)
@ -268,7 +269,7 @@ func TestThumbnails_CreateThumbnail(t *testing.T) {
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())
bounds := thumbnail.Bounds()
@ -294,7 +295,7 @@ func TestThumbnails_CreateDefaultThumbnails(t *testing.T) {
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)

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 (
"crypto/rand"
@ -9,7 +9,7 @@ import (
uuid "github.com/satori/go.uuid"
)
func RandomToken(size uint) string {
func Token(size uint) string {
if size > 10 || size < 1 {
log.Fatalf("size out of range: %d", size)
}
@ -32,14 +32,14 @@ func RandomToken(size uint) string {
return string(result[:size])
}
func RandomPassword() string {
return RandomToken(8)
func Password() string {
return Token(8)
}
func ID() string {
result := make([]byte, 0, 16)
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)
}

View file

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