photoprism/internal/api/thumbnails.go
Michael Mayer 604849e92c Search: Include RAW files in results by default #2040
With these changes the size and type of the RAW file as well as other
details can be displayed in the Cards View. This also improves the
indexing of camera and lens metadata.

Signed-off-by: Michael Mayer <michael@photoprism.app>
2023-10-06 02:22:48 +02:00

231 lines
6.3 KiB
Go

package api
import (
"net/http"
"path/filepath"
"time"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/crop"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
)
// GetThumb returns a thumbnail image matching the file hash, crop area, and type.
//
// GET /api/v1/t/:thumb/:token/:size
//
// Parameters:
//
// thumb: string sha1 file hash plus optional crop area
// token: string url security token, see config
// size: string thumb type, see thumb.Sizes
func GetThumb(router *gin.RouterGroup) {
router.GET("/t/:thumb/:token/:size", func(c *gin.Context) {
if InvalidPreviewToken(c) {
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
return
}
logPrefix := "thumb"
start := time.Now()
conf := get.Config()
download := c.Query("download") != ""
fileHash, cropArea := crop.ParseThumb(clean.Token(c.Param("thumb")))
// Is cropped thumbnail?
if cropArea != "" {
cropName := crop.Name(clean.Token(c.Param("size")))
cropSize, ok := crop.Sizes[cropName]
if !ok {
log.Errorf("%s: invalid size %s", logPrefix, clean.Log(string(cropName)))
c.Data(http.StatusOK, "image/svg+xml", photoIconSvg)
return
}
fileName, err := crop.FromRequest(fileHash, cropArea, cropSize, conf.ThumbCachePath())
if err != nil {
log.Warnf("%s: %s", logPrefix, err)
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
return
} else if fileName == "" {
log.Errorf("%s: empty file name - you may have found a bug", logPrefix)
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
return
}
// Add HTTP cache header.
AddImmutableCacheHeader(c)
if download {
c.FileAttachment(fileName, cropName.Jpeg())
} else {
c.File(fileName)
}
return
}
sizeName := thumb.Name(clean.Token(c.Param("size")))
size, ok := thumb.Sizes[sizeName]
if !ok {
log.Errorf("%s: invalid size %s", logPrefix, clean.Log(sizeName.String()))
c.Data(http.StatusOK, "image/svg+xml", photoIconSvg)
return
}
if size.Uncached() && !conf.ThumbUncached() {
sizeName, size = thumb.Find(conf.ThumbSizePrecached())
if sizeName == "" {
log.Errorf("%s: invalid size %d", logPrefix, conf.ThumbSizePrecached())
c.Data(http.StatusOK, "image/svg+xml", photoIconSvg)
return
}
}
cache := get.ThumbCache()
cacheKey := CacheKey("thumbs", fileHash, string(sizeName))
if cacheData, ok := cache.Get(cacheKey); ok {
log.Tracef("api-v1: cache hit for %s [%s]", cacheKey, time.Since(start))
cached := cacheData.(ThumbCache)
if !fs.FileExists(cached.FileName) {
log.Errorf("%s: %s not found", logPrefix, fileHash)
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
return
}
// Add HTTP cache header.
AddImmutableCacheHeader(c)
if download {
c.FileAttachment(cached.FileName, cached.ShareName)
} else {
c.File(cached.FileName)
}
return
}
// Return existing thumbs straight away.
if !download {
if fileName, err := size.ResolvedName(fileHash, conf.ThumbCachePath()); err == nil {
// Add HTTP cache header.
AddImmutableCacheHeader(c)
// Return requested content.
c.File(fileName)
return
}
}
// Query index for file infos.
f, err := query.FileByHash(fileHash)
if err != nil {
c.Data(http.StatusOK, "image/svg+xml", photoIconSvg)
return
}
// Find supported preview image if media file is not a JPEG or PNG.
if f.NoJPEG() && f.NoPNG() {
if f, err = query.FileByPhotoUID(f.PhotoUID); err != nil {
c.Data(http.StatusOK, "image/svg+xml", fileIconSvg)
return
}
}
// Return SVG icon as placeholder if file has errors.
if f.FileError != "" {
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
return
}
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if fileName, err = fs.Resolve(fileName); err != nil {
log.Errorf("%s: file %s is missing", logPrefix, clean.Log(f.FileName))
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore.
logError(logPrefix, f.Update("FileMissing", true))
if f.AllFilesMissing() {
log.Infof("%s: deleting photo, all files missing for %s", logPrefix, clean.Log(f.FileName))
if _, err := f.RelatedPhoto().Delete(false); err != nil {
log.Errorf("%s: %s while deleting %s", logPrefix, err, clean.Log(f.FileName))
}
}
return
}
// Choose the smallest fitting size if the original image is smaller.
if size.Fit && f.Bounds().In(size.Bounds()) {
size = thumb.FitBounds(f.Bounds())
log.Tracef("%s: smallest fitting size for %s is %s (width %d, height %d)", logPrefix, clean.Log(f.FileName), size.Name, size.Width, size.Height)
}
// Use original file if thumb size exceeds limit, see https://github.com/photoprism/photoprism/issues/157
if size.ExceedsLimit() && !download {
log.Debugf("%s: using original, size exceeds limit (width %d, height %d)", logPrefix, size.Width, size.Height)
// Add HTTP cache header.
AddImmutableCacheHeader(c)
// Return requested content.
c.File(fileName)
return
}
// thumbName is the thumbnail filename.
var thumbName string
// Try to find or create thumbnail image.
if conf.ThumbUncached() || size.Uncached() {
thumbName, err = size.FromFile(fileName, f.FileHash, conf.ThumbCachePath(), f.FileOrientation)
} else {
thumbName, err = size.FromCache(fileName, f.FileHash, conf.ThumbCachePath())
}
// Failed?
if err != nil {
log.Errorf("%s: %s", logPrefix, err)
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
return
} else if thumbName == "" {
log.Errorf("%s: %s has empty thumb name - you may have found a bug", logPrefix, filepath.Base(fileName))
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
return
}
// Cache thumbnail filename to reduce the number of index queries.
cache.SetDefault(cacheKey, ThumbCache{thumbName, f.ShareBase(0)})
log.Debugf("cached %s [%s]", cacheKey, time.Since(start))
// Add HTTP cache header.
AddImmutableCacheHeader(c)
// Return requested content.
if download {
c.FileAttachment(thumbName, f.DownloadName(DownloadName(c), 0))
} else {
c.File(thumbName)
}
})
}