Thumbs: Add "fit_5120" size suitable for Retina 5K displays #4810

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-02-25 11:21:38 +01:00
parent 220b914ae0
commit 78f5104020
13 changed files with 79 additions and 31 deletions

View File

@@ -91,6 +91,7 @@ services:
PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets"
PHOTOPRISM_STORAGE_PATH: "/go/src/github.com/photoprism/photoprism/storage"
PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals"
PHOTOPRISM_ORIGINALS_LIMIT: 128000 # sets originals file size limit to 128 GB
PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/storage/import"
PHOTOPRISM_DISABLE_CHOWN: "false" # disables updating storage permissions via chmod and chown on startup
PHOTOPRISM_DISABLE_BACKUPS: "false" # disables backing up albums and photo metadata to YAML files

View File

@@ -62,6 +62,7 @@ import (
// log points to the global logger.
var log = event.Log
var initThumbsMutex sync.Mutex
// Config holds database, cache and all parameters of photoprism
type Config struct {
@@ -86,11 +87,25 @@ func init() {
LowMem = TotalMem < MinMem
}
initThumbs()
}
func initThumbs() {
initThumbsMutex.Lock()
defer initThumbsMutex.Unlock()
maxSize := thumb.MaxSize()
Thumbs = ThumbSizes{}
// Init public thumb sizes for use in client apps.
for i := len(thumb.Names) - 1; i >= 0; i-- {
name := thumb.Names[i]
t := thumb.Sizes[name]
if t.Width > maxSize {
continue
}
if t.Public {
Thumbs = append(Thumbs, ThumbSize{Size: string(name), Usage: t.Usage, Width: t.Width, Height: t.Height})
}
@@ -254,6 +269,7 @@ func (c *Config) Propagate() {
thumb.SizeOnDemand = c.ThumbSizeUncached()
thumb.JpegQualityDefault = c.JpegQuality()
thumb.CachePublic = c.HttpCachePublic()
initThumbs()
// Set cache expiration defaults.
ttl.CacheDefault = c.HttpCacheMaxAge()

View File

@@ -17,16 +17,19 @@ var (
// Register registers a new package extension.
func Register(name string, initConfig func(c *Config) error, clientConfig func(c *Config, t ClientType) Map) {
extMutex.Lock()
defer extMutex.Unlock()
n, _ := extensions.Load().(Extensions)
extensions.Store(append(n, Extension{name: name, init: initConfig, clientValues: clientConfig}))
extMutex.Unlock()
}
// Ext returns all registered package extensions.
func Ext() (ext Extensions) {
extMutex.Lock()
defer extMutex.Unlock()
ext, _ = extensions.Load().(Extensions)
extMutex.Unlock()
return ext
}

View File

@@ -23,7 +23,7 @@ type Result struct {
Hash string `json:"Hash"`
Codec string `json:"Codec,omitempty"`
Mime string `json:"Mime,omitempty"`
Thumbs *thumb.Viewer `json:"Thumbs"`
Thumbs *thumb.Viewer `json:"Thumbs,omitempty"`
DownloadUrl string `json:"DownloadUrl,omitempty"`
}

View File

@@ -23,6 +23,7 @@ var thumbFileNames = []string{
"%s_1920x1200_fit.jpg",
"%s_2560x1600_fit.jpg",
"%s_4096x4096_fit.jpg",
"%s_5120x5120_fit.jpg",
"%s_7680x4320_fit.jpg",
}
@@ -33,6 +34,7 @@ var thumbFileSizes = []thumb.Size{
thumb.Sizes[thumb.Fit1920],
thumb.Sizes[thumb.Fit2560],
thumb.Sizes[thumb.Fit4096],
thumb.Sizes[thumb.Fit5120],
thumb.Sizes[thumb.Fit7680],
}
@@ -56,10 +58,10 @@ func ImageFromThumb(thumbName string, area Area, size Size, cache bool) (img ima
// Cached?
if !fs.FileExists(cropName) {
// Do nothing.
} else if img, err := imaging.Open(cropName); err != nil {
} else if cropImg, cropErr := imaging.Open(cropName); cropErr != nil {
log.Errorf("crop: failed loading %s", filepath.Base(cropName))
} else {
return img, nil
return cropImg, nil
}
// Open thumb image file.
@@ -70,21 +72,21 @@ func ImageFromThumb(thumbName string, area Area, size Size, cache bool) (img ima
}
// Get absolute crop coordinates and dimension.
min, max, dim := area.Bounds(img)
posMin, posMax, dim := area.Bounds(img)
if dim < size.Width {
log.Debugf("crop: %s is too small, upscaling %dpx to %dpx", filepath.Base(thumbName), dim, size.Width)
}
// Crop area from image.
img = imaging.Crop(img, image.Rect(min.X, min.Y, max.X, max.Y))
img = imaging.Crop(img, image.Rect(posMin.X, posMin.Y, posMax.X, posMax.Y))
// Resample crop area.
img = thumb.Resample(img, size.Width, size.Height, size.Options...)
// Cache crop image?
if cache {
if err := imaging.Save(img, cropName); err != nil {
if err = imaging.Save(img, cropName); err != nil {
log.Errorf("crop: failed caching %s", filepath.Base(cropName))
} else {
log.Debugf("crop: saved %s", filepath.Base(cropName))

View File

@@ -6,6 +6,7 @@ import "image"
// Best for the viewer as proportional resizing maintains the aspect ratio.
var FitSizes = SizeList{
Sizes[Fit7680],
Sizes[Fit5120],
Sizes[Fit4096],
Sizes[Fit2560],
Sizes[Fit1920],
@@ -13,7 +14,7 @@ var FitSizes = SizeList{
Sizes[Fit720],
}
// Fit returns the largest fitting thumbnail size.
// Fit returns the smallest fitting thumbnail size.
func Fit(w, h int) (size Size) {
j := len(FitSizes) - 1
@@ -26,7 +27,7 @@ func Fit(w, h int) (size Size) {
return FitSizes[0]
}
// FitBounds returns the largest thumbnail size fitting the rectangle.
// FitBounds returns the smallest thumbnail size fitting the rectangle.
func FitBounds(r image.Rectangle) (s Size) {
return Fit(r.Dx(), r.Dy())
}

View File

@@ -17,7 +17,9 @@ func TestFit(t *testing.T) {
assert.Equal(t, Sizes[Fit2560], Fit(1600, 1600))
assert.Equal(t, Sizes[Fit4096], Fit(1000, 3000))
assert.Equal(t, Sizes[Fit4096], Fit(2300, 2000))
assert.Equal(t, Sizes[Fit7680], Fit(5000, 5000))
assert.Equal(t, Sizes[Fit5120], Fit(5000, 2000))
assert.Equal(t, Sizes[Fit7680], Fit(6020, 2000))
assert.Equal(t, Sizes[Fit7680], Fit(8000, 8000))
}
func TestFitBounds(t *testing.T) {

View File

@@ -33,12 +33,14 @@ const (
Fit2560 Name = "fit_2560"
Fit3840 Name = "fit_3840"
Fit4096 Name = "fit_4096"
Fit5120 Name = "fit_5120"
Fit7680 Name = "fit_7680"
)
// Names contains all default size names.
var Names = []Name{
Fit7680,
Fit5120,
Fit4096,
Fit2560,
Fit1920,

View File

@@ -98,6 +98,19 @@ func TestSize_Skip(t *testing.T) {
t.Fatal(err)
}
assert.True(t, size.Skip(img))
})
t.Run("Fit5120", func(t *testing.T) {
size := Sizes[Fit5120]
assert.FileExists(t, src)
img, err := imaging.Open(src, imaging.AutoOrientation(true))
if err != nil {
t.Fatal(err)
}
assert.True(t, size.Skip(img))
})
}

View File

@@ -1,8 +1,9 @@
package thumb
// Default thumbnail size limits (cached and uncached).
var (
SizeCached = SizeFit1920.Width
SizeOnDemand = SizeFit7680.Width
SizeOnDemand = SizeFit5120.Width
)
// MaxSize returns the max supported size in pixels.
@@ -50,9 +51,10 @@ var (
SizeFit1600 = Size{Fit1600, Fit1920, "Social Media", 1600, 900, false, true, true, false, Options{ResampleFit, ResampleDefault}}
SizeFit1920 = Size{Fit1920, "", "Full HD", 1920, 1200, true, true, false, false, Options{ResampleFit, ResampleDefault}}
SizeFit2048 = Size{Fit2048, Fit4096, "DCI 2K, Tablets", 2048, 2048, false, true, true, false, Options{ResampleFit, ResampleDefault}}
SizeFit2560 = Size{Fit2560, Fit4096, "Quad HD, Notebooks", 2560, 1600, true, true, false, false, Options{ResampleFit, ResampleDefault}}
SizeFit2560 = Size{Fit2560, Fit4096, "Quad HD", 2560, 1600, true, true, false, false, Options{ResampleFit, ResampleDefault}}
SizeFit3840 = Size{Fit3840, Fit4096, "4K Ultra HD", 3840, 2400, false, true, true, false, Options{ResampleFit, ResampleDefault}}
SizeFit4096 = Size{Fit4096, "", "DCI 4K, Retina 4K", 4096, 4096, true, true, false, false, Options{ResampleFit, ResampleDefault}}
SizeFit5120 = Size{Fit5120, "", "Retina 5K", 5120, 5120, true, true, false, false, Options{ResampleFit, ResampleDefault}}
SizeFit7680 = Size{Fit7680, "", "8K Ultra HD 2", 7680, 4320, true, true, false, false, Options{ResampleFit, ResampleDefault}}
)
@@ -74,5 +76,6 @@ var Sizes = SizeMap{
Fit2560: SizeFit2560,
Fit3840: SizeFit3840, // Deprecated in favor of Fit4096
Fit4096: SizeFit4096,
Fit5120: SizeFit5120,
Fit7680: SizeFit7680,
}

View File

@@ -53,10 +53,10 @@ type Thumb struct {
}
// New creates a new photo thumbnail.
func New(w, h int, hash string, s Size, contentUri, previewToken string) Thumb {
func New(w, h int, hash string, s Size, contentUri, previewToken string) *Thumb {
if s.Width >= w && s.Height >= h {
// Smaller
return Thumb{W: w, H: h, Src: Url(hash, s.Name.String(), contentUri, previewToken)}
return &Thumb{W: w, H: h, Src: Url(hash, s.Name.String(), contentUri, previewToken)}
}
srcAspectRatio := float64(w) / float64(h)
@@ -66,11 +66,11 @@ func New(w, h int, hash string, s Size, contentUri, previewToken string) Thumb {
if srcAspectRatio > maxAspectRatio {
newW = s.Width
newH = int(math.Round(float64(newW) / srcAspectRatio))
newH = int(math.Ceil(float64(newW) / srcAspectRatio))
} else {
newH = s.Height
newW = int(math.Round(float64(newH) * srcAspectRatio))
newW = int(math.Ceil(float64(newH) * srcAspectRatio))
}
return Thumb{W: newW, H: newH, Src: Url(hash, s.Name.String(), contentUri, previewToken)}
return &Thumb{W: newW, H: newH, Src: Url(hash, s.Name.String(), contentUri, previewToken)}
}

View File

@@ -6,12 +6,13 @@ import (
// Viewer represents thumbnail URLs for the photo/video viewer.
type Viewer struct {
Fit720 Thumb `json:"fit_720"`
Fit1280 Thumb `json:"fit_1280"`
Fit1920 Thumb `json:"fit_1920"`
Fit2560 Thumb `json:"fit_2560"`
Fit4096 Thumb `json:"fit_4096"`
Fit7680 Thumb `json:"fit_7680"`
Fit720 *Thumb `json:"fit_720"`
Fit1280 *Thumb `json:"fit_1280"`
Fit1920 *Thumb `json:"fit_1920"`
Fit2560 *Thumb `json:"fit_2560"`
Fit4096 *Thumb `json:"fit_4096"`
Fit5120 *Thumb `json:"fit_5120"`
Fit7680 *Thumb `json:"fit_7680"`
}
// ViewerThumbs creates and returns a Viewer struct pointer with the required thumbnail URLs for the photo/video viewer.
@@ -21,8 +22,9 @@ func ViewerThumbs(fileWidth, fileHeight int, fileHash, contentUri, previewToken
// Get Viewer struct fields.
fields := reflect.ValueOf(thumbs).Elem()
// Remember the largest size needed, if any.
var maxSize Size
// Remember the maximum allowed size and the maximum actual size.
var maxSize = MaxSize()
var largestSize Size
// Iterate through all Viewer struct fields and set the best matching thumb size.
for i := 0; i < fields.NumField(); i++ {
@@ -43,13 +45,13 @@ func ViewerThumbs(fileWidth, fileHeight int, fileHash, contentUri, previewToken
}
// Remember this as the largest size needed if the original size is smaller than the thumb size.
if maxSize.Name == "" && s.Width >= fileWidth && s.Height >= fileHeight {
maxSize = s
if largestSize.Name == "" && (s.Width >= fileWidth && s.Height >= fileHeight || s.Width >= maxSize) {
largestSize = s
}
// Set the field value to the current size or the maximum size, if any.
if maxSize.Name != "" {
thumb.Set(reflect.ValueOf(New(fileWidth, fileHeight, fileHash, maxSize, contentUri, previewToken)))
if largestSize.Name != "" {
thumb.Set(reflect.ValueOf(New(fileWidth, fileHeight, fileHash, largestSize, contentUri, previewToken)))
} else {
thumb.Set(reflect.ValueOf(New(fileWidth, fileHeight, fileHash, s, contentUri, previewToken)))
}

View File

@@ -24,7 +24,10 @@ func TestViewerThumbs(t *testing.T) {
assert.Equal(t, "https://example.com/t/011df944f313a05f89d170a561fad09ce6cef44e/12345678/fit_2560", result.Fit2560.Src)
assert.Equal(t, 2000, result.Fit4096.W)
assert.Equal(t, 1500, result.Fit4096.H)
assert.Equal(t, "https://example.com/t/011df944f313a05f89d170a561fad09ce6cef44e/12345678/fit_2560", result.Fit4096.Src)
assert.Equal(t, "https://example.com/t/011df944f313a05f89d170a561fad09ce6cef44e/12345678/fit_2560", result.Fit2560.Src)
assert.Equal(t, 2000, result.Fit5120.W)
assert.Equal(t, 1500, result.Fit5120.H)
assert.Equal(t, "https://example.com/t/011df944f313a05f89d170a561fad09ce6cef44e/12345678/fit_2560", result.Fit5120.Src)
assert.Equal(t, 2000, result.Fit7680.W)
assert.Equal(t, 1500, result.Fit7680.H)
assert.Equal(t, "https://example.com/t/011df944f313a05f89d170a561fad09ce6cef44e/12345678/fit_2560", result.Fit7680.Src)