diff --git a/compose.yaml b/compose.yaml index 8f5bab659..c158a7d1b 100644 --- a/compose.yaml +++ b/compose.yaml @@ -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 diff --git a/internal/config/config.go b/internal/config/config.go index 370a275cb..975eb3939 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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() diff --git a/internal/config/extensions.go b/internal/config/extensions.go index edee8b211..3ab3e26e2 100644 --- a/internal/config/extensions.go +++ b/internal/config/extensions.go @@ -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 } diff --git a/internal/entity/search/viewer/result.go b/internal/entity/search/viewer/result.go index abf79cb50..b2ff42111 100644 --- a/internal/entity/search/viewer/result.go +++ b/internal/entity/search/viewer/result.go @@ -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"` } diff --git a/internal/thumb/crop/image.go b/internal/thumb/crop/image.go index fff80f210..c03c942db 100644 --- a/internal/thumb/crop/image.go +++ b/internal/thumb/crop/image.go @@ -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)) diff --git a/internal/thumb/fit.go b/internal/thumb/fit.go index a45b01df8..e7320fcc9 100644 --- a/internal/thumb/fit.go +++ b/internal/thumb/fit.go @@ -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()) } diff --git a/internal/thumb/fit_test.go b/internal/thumb/fit_test.go index 9612b0362..f6d86dcac 100644 --- a/internal/thumb/fit_test.go +++ b/internal/thumb/fit_test.go @@ -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) { diff --git a/internal/thumb/names.go b/internal/thumb/names.go index 832e819f6..265b09e5e 100644 --- a/internal/thumb/names.go +++ b/internal/thumb/names.go @@ -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, diff --git a/internal/thumb/size_test.go b/internal/thumb/size_test.go index 657a01105..262b9a77c 100644 --- a/internal/thumb/size_test.go +++ b/internal/thumb/size_test.go @@ -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)) }) } diff --git a/internal/thumb/sizes.go b/internal/thumb/sizes.go index a846028b8..be5acfdfa 100644 --- a/internal/thumb/sizes.go +++ b/internal/thumb/sizes.go @@ -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, } diff --git a/internal/thumb/thumb.go b/internal/thumb/thumb.go index 258e226de..79fa9b19d 100644 --- a/internal/thumb/thumb.go +++ b/internal/thumb/thumb.go @@ -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)} } diff --git a/internal/thumb/viewer.go b/internal/thumb/viewer.go index 355d59dc2..5f6f8b309 100644 --- a/internal/thumb/viewer.go +++ b/internal/thumb/viewer.go @@ -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))) } diff --git a/internal/thumb/viewer_test.go b/internal/thumb/viewer_test.go index 955b087de..087d313e7 100644 --- a/internal/thumb/viewer_test.go +++ b/internal/thumb/viewer_test.go @@ -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)