Skip to content

Commit

Permalink
Config: Improve thumbnail generation option parsing and defaults #1474
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Mayer <michael@photoprism.app>
  • Loading branch information
lastzero committed May 18, 2024
1 parent c60c4c7 commit fd853e0
Show file tree
Hide file tree
Showing 23 changed files with 396 additions and 187 deletions.
7 changes: 1 addition & 6 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,8 @@ services:
PHOTOPRISM_DETECT_NSFW: "false" # automatically flags photos as private that MAY be offensive (requires TensorFlow)
PHOTOPRISM_UPLOAD_NSFW: "false" # allows uploads that MAY be offensive (no effect without TensorFlow)
PHOTOPRISM_THUMB_LIBRARY: "auto" # image processing library to be used for generating thumbnails (auto, imaging, vips)
PHOTOPRISM_THUMB_FILTER: "lanczos" # image downscaling filter, best to worst: lanczos, cubic, linear
PHOTOPRISM_THUMB_FILTER: "auto" # downscaling filter (imaging best to worst: blackman, lanczos, cubic, linear, nearest)
PHOTOPRISM_THUMB_UNCACHED: "true" # enables on-demand thumbnail rendering (high memory and cpu usage)
PHOTOPRISM_THUMB_SIZE: 2048 # pre-rendered thumbnail size limit (default 2048, min 720, max 7680)
# PHOTOPRISM_THUMB_SIZE: 4096 # Retina 4K, DCI 4K (requires more storage); 7680 for 8K Ultra HD
PHOTOPRISM_THUMB_SIZE_UNCACHED: 7680 # on-demand rendering size limit (default 7680, min 720, max 7680)
PHOTOPRISM_JPEG_SIZE: 7680 # size limit for converted image files in pixels (720-30000)
PHOTOPRISM_JPEG_QUALITY: 85 # a higher value increases the quality and file size of JPEG images and thumbnails (25-100)
TF_CPP_MIN_LOG_LEVEL: 0 # show TensorFlow log messages for development
## Video Transcoding (https://docs.photoprism.app/getting-started/advanced/transcoding/):
# PHOTOPRISM_FFMPEG_ENCODER: "software" # H.264/AVC encoder (software, intel, nvidia, apple, raspberry, or vaapi)
Expand Down
2 changes: 1 addition & 1 deletion internal/api/share_preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func SharePreview(router *gin.RouterGroup) {
preview = imaging.Resize(preview, 1200, 0, imaging.Lanczos)

// Save the resulting album preview as JPEG.
err = imaging.Save(preview, previewFilename, thumb.JpegQualitySmall.EncodeOption())
err = imaging.Save(preview, previewFilename, thumb.JpegQualitySmall().EncodeOption())

if err != nil {
log.Error(err)
Expand Down
8 changes: 4 additions & 4 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,11 @@ func (c *Config) Propagate() {

// Initialize the thumbnail generation package.
thumb.Library = c.ThumbLibrary()
thumb.StandardRGB = c.ThumbSRGB()
thumb.SizePrecached = c.ThumbSizePrecached()
thumb.SizeUncached = c.ThumbSizeUncached()
thumb.Color = c.ThumbColor()
thumb.Filter = c.ThumbFilter()
thumb.JpegQuality = c.JpegQuality()
thumb.SizeCached = c.ThumbSizePrecached()
thumb.SizeOnDemand = c.ThumbSizeUncached()
thumb.JpegQualityDefault = c.JpegQuality()
thumb.CachePublic = c.HttpCachePublic()

// Set cache expiration defaults.
Expand Down
43 changes: 9 additions & 34 deletions internal/config/config_resample.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package config

import (
"strings"

"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/clean"
)

// JpegSize returns the size limit for automatically converted files in `PIXELS` (720-30000).
Expand Down Expand Up @@ -35,46 +34,22 @@ func (c *Config) JpegQuality() thumb.Quality {

// ThumbLibrary returns the name of the image processing library to be used for generating thumbnails.
func (c *Config) ThumbLibrary() string {
switch strings.ToLower(c.options.ThumbLibrary) {
case thumb.LibVips:
return thumb.LibVips
default:
switch clean.TypeLowerUnderscore(c.options.ThumbLibrary) {
case thumb.LibImaging, "", "imagine", "internal":
return thumb.LibImaging
default:
return thumb.LibVips
}
}

// ThumbColor returns the color profile name for thumbnails.
func (c *Config) ThumbColor() string {
if c.options.ThumbColor == "auto" {
if c.ThumbLibrary() != thumb.LibVips {
return "srgb"
}

return c.options.ThumbColor
}

return strings.ToLower(c.options.ThumbColor)
}

// ThumbSRGB checks if colors should be normalized to standard RGB in thumbnails.
func (c *Config) ThumbSRGB() bool {
return c.ThumbColor() == "srgb"
// ThumbColor returns the color space for thumbnails.
func (c *Config) ThumbColor() thumb.ColorSpace {
return thumb.ParseColor(c.options.ThumbColor, c.ThumbLibrary())
}

// ThumbFilter returns the thumbnail resample filter (best to worst: blackman, lanczos, cubic or linear).
func (c *Config) ThumbFilter() thumb.ResampleFilter {
switch strings.ToLower(c.options.ThumbFilter) {
case "blackman":
return thumb.ResampleBlackman
case "lanczos":
return thumb.ResampleLanczos
case "cubic":
return thumb.ResampleCubic
case "linear":
return thumb.ResampleLinear
default:
return thumb.ResampleCubic
}
return thumb.ParseFilter(c.options.ThumbFilter, c.ThumbLibrary())
}

// ThumbUncached checks if on-demand thumbnail rendering is enabled (high memory and cpu usage).
Expand Down
44 changes: 24 additions & 20 deletions internal/config/config_resample_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,41 +20,45 @@ func TestConfig_ConvertSize(t *testing.T) {
func TestConfig_JpegQuality(t *testing.T) {
c := NewConfig(CliTestContext())

assert.Equal(t, thumb.QualityDefault, c.JpegQuality())
assert.Equal(t, thumb.QualityMedium, c.JpegQuality())
c.options.JpegQuality = "110"
assert.Equal(t, thumb.QualityDefault, c.JpegQuality())
assert.Equal(t, thumb.QualityMedium, c.JpegQuality())
c.options.JpegQuality = "98"
assert.Equal(t, thumb.Quality(98), c.JpegQuality())
c.options.JpegQuality = ""
assert.Equal(t, thumb.QualityDefault, c.JpegQuality())
assert.Equal(t, thumb.QualityMedium, c.JpegQuality())
c.options.JpegQuality = "best "
assert.Equal(t, thumb.QualityBest, c.JpegQuality())
assert.Equal(t, thumb.QualityMax, c.JpegQuality())
c.options.JpegQuality = "high"
assert.Equal(t, thumb.QualityHigh, c.JpegQuality())
c.options.JpegQuality = "med "
assert.Equal(t, thumb.QualityMedium, c.JpegQuality())
c.options.JpegQuality = "medium "
assert.Equal(t, thumb.QualityDefault, c.JpegQuality())
assert.Equal(t, thumb.QualityMedium, c.JpegQuality())
c.options.JpegQuality = "low "
assert.Equal(t, thumb.QualityLow, c.JpegQuality())
c.options.JpegQuality = "bad"
assert.Equal(t, thumb.QualityBad, c.JpegQuality())
c.options.JpegQuality = "worst "
assert.Equal(t, thumb.QualityWorst, c.JpegQuality())
c.options.JpegQuality = "max"
assert.Equal(t, thumb.QualityMax, c.JpegQuality())
c.options.JpegQuality = "min "
assert.Equal(t, thumb.QualityMin, c.JpegQuality())
c.options.JpegQuality = "default"
assert.Equal(t, thumb.QualityDefault, c.JpegQuality())
assert.Equal(t, thumb.QualityMedium, c.JpegQuality())
}

func TestConfig_ThumbFilter(t *testing.T) {
c := NewConfig(CliTestContext())

assert.Equal(t, thumb.ResampleFilter("cubic"), c.ThumbFilter())
assert.Equal(t, thumb.ResampleAuto, c.ThumbFilter())
c.options.ThumbFilter = "blackman"
assert.Equal(t, thumb.ResampleFilter("blackman"), c.ThumbFilter())
assert.Equal(t, thumb.ResampleBlackman, c.ThumbFilter())
c.options.ThumbFilter = "lanczos"
assert.Equal(t, thumb.ResampleFilter("lanczos"), c.ThumbFilter())
assert.Equal(t, thumb.ResampleLanczos, c.ThumbFilter())
c.options.ThumbFilter = "linear"
assert.Equal(t, thumb.ResampleFilter("linear"), c.ThumbFilter())
c.options.ThumbFilter = "cubic"
assert.Equal(t, thumb.ResampleFilter("cubic"), c.ThumbFilter())
assert.Equal(t, thumb.ResampleLinear, c.ThumbFilter())
c.options.ThumbFilter = "auto"
assert.Equal(t, thumb.ResampleAuto, c.ThumbFilter())
c.options.ThumbFilter = ""
assert.Equal(t, thumb.ResampleAuto, c.ThumbFilter())
}

func TestConfig_ThumbSizeUncached(t *testing.T) {
Expand All @@ -66,17 +70,17 @@ func TestConfig_ThumbSizeUncached(t *testing.T) {
func TestConfig_ThumbSize(t *testing.T) {
c := NewConfig(CliTestContext())

assert.Equal(t, int(720), c.ThumbSizePrecached())
assert.Equal(t, 720, c.ThumbSizePrecached())
c.options.ThumbSize = 7681
assert.Equal(t, int(7680), c.ThumbSizePrecached())
assert.Equal(t, 7680, c.ThumbSizePrecached())
}

func TestConfig_ThumbSizeUncached2(t *testing.T) {
c := NewConfig(CliTestContext())

assert.Equal(t, int(720), c.ThumbSizeUncached())
assert.Equal(t, 720, c.ThumbSizeUncached())
c.options.ThumbSizeUncached = 7681
assert.Equal(t, int(7680), c.ThumbSizeUncached())
assert.Equal(t, 7680, c.ThumbSizeUncached())
c.options.ThumbSizeUncached = 800
c.options.ThumbSize = 900
assert.Equal(t, int(900), c.ThumbSizeUncached())
Expand Down
16 changes: 8 additions & 8 deletions internal/config/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -760,26 +760,26 @@ var Flags = CliFlags{
}}, {
Flag: cli.StringFlag{
Name: "thumb-color",
Usage: "default color `PROFILE` for thumbnails (leave blank to disable normalization)",
Value: "auto",
Usage: "default color `PROFILE` for thumbnails (auto, preserve, srgb, none)",
Value: thumb.ColorAuto,
EnvVar: EnvVar("THUMB_COLOR"),
}}, {
Flag: cli.StringFlag{
Name: "thumb-filter, filter",
Usage: "image downscaling filter `NAME` (best to worst: lanczos, cubic, linear)",
Value: "lanczos",
Usage: "downscaling filter `NAME` (imaging best to worst: blackman, lanczos, cubic, linear, nearest)",
Value: thumb.ResampleAuto.String(),
EnvVar: EnvVar("THUMB_FILTER"),
}}, {
Flag: cli.IntFlag{
Name: "thumb-size",
Usage: "maximum size of thumbnails generated while indexing in `PIXELS` (720-7680)",
Value: 2048,
Usage: "maximum size of pre-generated thumbnails in `PIXELS` (720-7680)",
Value: thumb.SizeCached,
EnvVar: EnvVar("THUMB_SIZE"),
}}, {
Flag: cli.IntFlag{
Name: "thumb-size-uncached",
Usage: "maximum size of thumbnails generated on demand in `PIXELS` (720-7680)",
Value: 7680,
Value: thumb.SizeOnDemand,
EnvVar: EnvVar("THUMB_SIZE_UNCACHED"),
}}, {
Flag: cli.BoolFlag{
Expand All @@ -790,7 +790,7 @@ var Flags = CliFlags{
Flag: cli.StringFlag{
Name: "jpeg-quality, q",
Usage: "higher values increase the image `QUALITY` and file size (25-100)",
Value: thumb.JpegQuality.String(),
Value: thumb.JpegQualityDefault.String(),
EnvVar: EnvVar("JPEG_QUALITY"),
}}, {
Flag: cli.IntFlag{
Expand Down
2 changes: 1 addition & 1 deletion internal/config/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ func (c *Config) Report() (rows [][]string, cols []string) {
{"preview-token", c.PreviewToken()},
{"thumb-library", c.ThumbLibrary()},
{"thumb-color", c.ThumbColor()},
{"thumb-filter", string(c.ThumbFilter())},
{"thumb-filter", c.ThumbFilter().String()},
{"thumb-size", fmt.Sprintf("%d", c.ThumbSizePrecached())},
{"thumb-size-uncached", fmt.Sprintf("%d", c.ThumbSizeUncached())},
{"thumb-uncached", fmt.Sprintf("%t", c.ThumbUncached())},
Expand Down
6 changes: 3 additions & 3 deletions internal/config/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,10 @@ func NewTestConfig(pkg string) *Config {
c.RegisterDb()
c.InitTestDb()

thumb.SizePrecached = c.ThumbSizePrecached()
thumb.SizeUncached = c.ThumbSizeUncached()
thumb.SizeCached = c.ThumbSizePrecached()
thumb.SizeOnDemand = c.ThumbSizeUncached()
thumb.Filter = c.ThumbFilter()
thumb.JpegQuality = c.JpegQuality()
thumb.JpegQualityDefault = c.JpegQuality()

return c
}
Expand Down
30 changes: 30 additions & 0 deletions internal/thumb/color.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package thumb

import "github.com/photoprism/photoprism/pkg/clean"

type ColorSpace = string

// Supported thumbnail color profiles.
const (
ColorNone ColorSpace = "none"
ColorAuto ColorSpace = "auto"
ColorSRGB ColorSpace = "srgb"
ColorPreserve ColorSpace = "preserve"
)

// Color sets the default color profiles for thumbnails.
var Color = ColorAuto

// ParseColor returns a ColorSpace based on the config value string and image library.
func ParseColor(name string, lib Lib) ColorSpace {
if lib == LibVips {
return ColorPreserve
}

switch clean.TypeLowerUnderscore(name) {
case ColorNone, "":
return ColorNone
default:
return ColorSRGB
}
}
22 changes: 22 additions & 0 deletions internal/thumb/color_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package thumb

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestParseColor(t *testing.T) {
t.Run("Vips", func(t *testing.T) {
assert.Equal(t, ColorPreserve, ParseColor("", LibVips))
assert.Equal(t, ColorPreserve, ParseColor(ColorAuto, LibVips))
assert.Equal(t, ColorPreserve, ParseColor(ColorSRGB, LibVips))
assert.Equal(t, ColorPreserve, ParseColor(ColorNone, LibVips))
})
t.Run("Imaging", func(t *testing.T) {
assert.Equal(t, ColorNone, ParseColor("", LibImaging))
assert.Equal(t, ColorSRGB, ParseColor(ColorAuto, LibImaging))
assert.Equal(t, ColorSRGB, ParseColor(ColorSRGB, LibImaging))
assert.Equal(t, ColorNone, ParseColor(ColorNone, LibImaging))
})
}
4 changes: 1 addition & 3 deletions internal/thumb/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,8 @@ func Create(img image.Image, fileName string, width, height int, opts ...Resampl

if fs.FileType(fileName) == fs.ImagePNG {
quality = imaging.PNGCompressionLevel(png.DefaultCompression)
} else if width <= 150 && height <= 150 {
quality = JpegQualitySmall.EncodeOption()
} else {
quality = JpegQuality.EncodeOption()
quality = JpegQuality(width, height).EncodeOption()
}

err = imaging.Save(result, fileName, quality)
Expand Down
31 changes: 27 additions & 4 deletions internal/thumb/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ package thumb
import (
"github.com/davidbyttow/govips/v2/vips"
"github.com/disintegration/imaging"
"github.com/photoprism/photoprism/pkg/clean"
)

// ResampleFilter represents a downscaling filter.
type ResampleFilter string

// Supported downscaling filter types.
const (
ResampleAuto ResampleFilter = "auto"
ResampleBlackman ResampleFilter = "blackman"
ResampleLanczos ResampleFilter = "lanczos"
ResampleCubic ResampleFilter = "cubic"
Expand All @@ -17,15 +22,17 @@ const (
// Filter specifies the default downscaling filter.
var Filter = ResampleLanczos

// ResampleFilter represents a downscaling filter.
type ResampleFilter string
// String returns the downscaling filter name as string.
func (a ResampleFilter) String() string {
return string(a)
}

// Imaging returns the downscaling filter for use with the "imaging" library.
func (a ResampleFilter) Imaging() imaging.ResampleFilter {
switch a {
case ResampleBlackman:
return imaging.Blackman
case ResampleLanczos:
case ResampleLanczos, ResampleAuto:
return imaging.Lanczos
case ResampleCubic:
return imaging.CatmullRom
Expand All @@ -43,7 +50,7 @@ func (a ResampleFilter) Vips() vips.Kernel {
switch a {
case ResampleBlackman:
return vips.KernelLanczos3
case ResampleLanczos:
case ResampleLanczos, ResampleAuto:
return vips.KernelLanczos3
case ResampleCubic:
return vips.KernelCubic
Expand All @@ -55,3 +62,19 @@ func (a ResampleFilter) Vips() vips.Kernel {
return vips.KernelLanczos3
}
}

// ParseFilter returns a ResampleFilter based on the config value string and image library.
func ParseFilter(name string, lib Lib) ResampleFilter {
if lib == LibVips {
return ResampleAuto
}

filter := ResampleFilter(clean.TypeLowerUnderscore(name))

switch filter {
case ResampleBlackman, ResampleLanczos, ResampleCubic, ResampleLinear, ResampleNearest:
return filter
default:
return ResampleAuto
}
}

0 comments on commit fd853e0

Please sign in to comment.