Config: Improve flag descriptions of JPEG and thumbnail parameters #2215

This commit is contained in:
Michael Mayer 2022-04-01 13:25:25 +02:00
parent 342904a4fa
commit 44efdd232a
10 changed files with 229 additions and 48 deletions

View file

@ -6,6 +6,7 @@ import (
"github.com/photoprism/photoprism/internal/face"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/thumb"
)
// GlobalFlags describes global command-line parameters and flags.
@ -456,37 +457,37 @@ var GlobalFlags = []cli.Flag{
},
cli.StringFlag{
Name: "thumb-filter",
Usage: "thumbnail downscaling `FILTER` (best to worst: blackman, lanczos, cubic, linear)",
Usage: "image downscaling `FILTER` (best to worst: blackman, lanczos, cubic, linear)",
Value: "lanczos",
EnvVar: "PHOTOPRISM_THUMB_FILTER",
},
cli.IntFlag{
Name: "thumb-size, s",
Usage: "maximum pre-cached thumbnail image size in `PIXELS` (720-7680)",
Value: 2048,
EnvVar: "PHOTOPRISM_THUMB_SIZE",
},
cli.BoolFlag{
Name: "thumb-uncached, u",
Usage: "enable on-demand thumbnail generation (high memory and cpu usage)",
Usage: "enable on-demand creation of missing thumbnails (high memory and cpu usage)",
EnvVar: "PHOTOPRISM_THUMB_UNCACHED",
},
cli.IntFlag{
Name: "thumb-size, s",
Usage: "maximum size of thumbnails created during indexing in `PIXELS` (720-7680)",
Value: 2048,
EnvVar: "PHOTOPRISM_THUMB_SIZE",
},
cli.IntFlag{
Name: "thumb-size-uncached, x",
Usage: "maximum size of on-demand generated thumbnails in `PIXELS` (720-7680)",
Usage: "maximum size of missing thumbnails created on demand in `PIXELS` (720-7680)",
Value: 7680,
EnvVar: "PHOTOPRISM_THUMB_SIZE_UNCACHED",
},
cli.IntFlag{
Name: "jpeg-size",
Usage: "maximum size of generated JPEG images in `PIXELS` (720-30000)",
Usage: "maximum size of created JPEG sidecar files in `PIXELS` (720-30000)",
Value: 7680,
EnvVar: "PHOTOPRISM_JPEG_SIZE",
},
cli.IntFlag{
cli.StringFlag{
Name: "jpeg-quality, q",
Usage: "`QUALITY` of generated JPEG images, a higher value reduces compression (25-100)",
Value: 85,
Usage: "`QUALITY` of created JPEG sidecars and thumbnails (25-100, best, high, default, low, worst)",
Value: thumb.JpegQuality.String(),
EnvVar: "PHOTOPRISM_JPEG_QUALITY",
},
cli.IntFlag{

View file

@ -125,7 +125,7 @@ type Options struct {
ThumbSize int `yaml:"ThumbSize" json:"ThumbSize" flag:"thumb-size"`
ThumbSizeUncached int `yaml:"ThumbSizeUncached" json:"ThumbSizeUncached" flag:"thumb-size-uncached"`
JpegSize int `yaml:"JpegSize" json:"JpegSize" flag:"jpeg-size"`
JpegQuality int `yaml:"JpegQuality" json:"JpegQuality" flag:"jpeg-quality"`
JpegQuality string `yaml:"JpegQuality" json:"JpegQuality" flag:"jpeg-quality"`
FaceSize int `yaml:"-" json:"-" flag:"face-size"`
FaceScore float64 `yaml:"-" json:"-" flag:"face-score"`
FaceOverlap int `yaml:"-" json:"-" flag:"face-overlap"`

View file

@ -17,17 +17,9 @@ func (c *Config) JpegSize() int {
return c.options.JpegSize
}
// JpegQuality returns the jpeg quality for resampling, use 95 for high-quality thumbs (25-100).
func (c *Config) JpegQuality() int {
if c.options.JpegQuality > 100 {
return 100
}
if c.options.JpegQuality < 25 {
return 25
}
return c.options.JpegQuality
// JpegQuality returns the jpeg image quality as thumb.Quality (25-100).
func (c *Config) JpegQuality() thumb.Quality {
return thumb.ParseQuality(c.options.JpegQuality)
}
// ThumbFilter returns the thumbnail resample filter (best to worst: blackman, lanczos, cubic or linear).

View file

@ -10,21 +10,37 @@ import (
func TestConfig_ConvertSize(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, int(720), c.JpegSize())
assert.Equal(t, 720, c.JpegSize())
c.options.JpegSize = 31000
assert.Equal(t, int(30000), c.JpegSize())
assert.Equal(t, 30000, c.JpegSize())
c.options.JpegSize = 800
assert.Equal(t, int(800), c.JpegSize())
assert.Equal(t, 800, c.JpegSize())
}
func TestConfig_JpegQuality(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, int(25), c.JpegQuality())
c.options.JpegQuality = 110
assert.Equal(t, int(100), c.JpegQuality())
c.options.JpegQuality = 98
assert.Equal(t, int(98), c.JpegQuality())
assert.Equal(t, thumb.QualityMedium, c.JpegQuality())
c.options.JpegQuality = "110"
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.QualityMedium, c.JpegQuality())
c.options.JpegQuality = "best "
assert.Equal(t, thumb.QualityBest, c.JpegQuality())
c.options.JpegQuality = "high"
assert.Equal(t, thumb.QualityHigh, c.JpegQuality())
c.options.JpegQuality = "medium "
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 = "default"
assert.Equal(t, thumb.QualityMedium, c.JpegQuality())
}
func TestConfig_ThumbFilter(t *testing.T) {

View file

@ -122,17 +122,17 @@ func Create(img image.Image, fileName string, width, height int, opts ...Resampl
result = Resample(img, width, height, opts...)
var saveOption imaging.EncodeOption
var quality imaging.EncodeOption
if filepath.Ext(fileName) == "."+string(fs.FormatPng) {
saveOption = imaging.PNGCompressionLevel(png.DefaultCompression)
quality = imaging.PNGCompressionLevel(png.DefaultCompression)
} else if width <= 150 && height <= 150 {
saveOption = imaging.JPEGQuality(JpegQualitySmall)
quality = JpegQualitySmall.EncodeOption()
} else {
saveOption = imaging.JPEGQuality(JpegQuality)
quality = JpegQuality.EncodeOption()
}
err = imaging.Save(result, fileName, saveOption)
err = imaging.Save(result, fileName, quality)
if err != nil {
log.Errorf("resample: failed to save %s", sanitize.Log(filepath.Base(fileName)))

View file

@ -20,9 +20,9 @@ func Jpeg(srcFilename, jpgFilename string, orientation int) (img image.Image, er
img = Rotate(img, orientation)
}
saveOption := imaging.JPEGQuality(JpegQuality)
quality := JpegQuality.EncodeOption()
if err = imaging.Save(img, jpgFilename, saveOption); err != nil {
if err = imaging.Save(img, jpgFilename, quality); err != nil {
log.Errorf("resample: failed to save %s", sanitize.Log(filepath.Base(jpgFilename)))
return img, err
}

89
internal/thumb/quality.go Normal file
View file

@ -0,0 +1,89 @@
package thumb
import (
"strconv"
"strings"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/disintegration/imaging"
)
// Quality represents a JPEG image quality.
type Quality int
// EncodeOption returns the quality as imaging.EncodeOption.
func (q Quality) EncodeOption() imaging.EncodeOption {
return imaging.JPEGQuality(int(q))
}
// String returns the quality as string.
func (q Quality) String() string {
return strconv.Itoa(int(q))
}
// Common Quality levels.
// see https://docs.photoprism.app/user-guide/settings/advanced/#jpeg-quality
const (
QualityBest Quality = 95
QualityHigh Quality = 92
QualityMedium Quality = 85
QualityLow Quality = 80
QualityBad Quality = 75
QualityWorst Quality = 70
)
// QualityLevels maps human-readable settings to a numeric Quality.
var QualityLevels = map[string]Quality{
"5": QualityBest,
"ultra": QualityBest,
"best": QualityBest,
"4": QualityHigh,
"excellent": QualityHigh,
"good": QualityHigh,
"high": QualityHigh,
"3": QualityMedium,
"": QualityMedium,
"ok": QualityMedium,
"default": QualityMedium,
"standard": QualityMedium,
"medium": QualityMedium,
"2": QualityLow,
"low": QualityLow,
"small": QualityLow,
"1": QualityBad,
"bad": QualityBad,
"0": QualityWorst,
"worst": QualityWorst,
"lowest": QualityWorst,
}
// Current Quality settings.
var (
JpegQuality = QualityMedium
JpegQualitySmall = QualityLow
)
// ParseQuality returns the matching quality based on a config value string.
func ParseQuality(s string) Quality {
// Default to medium if empty.
if s == "" {
return QualityMedium
}
// Try to parse as positive integer.
if i := txt.Int(s); i >= 25 && i <= 100 {
return Quality(i)
}
// Normalize value.
s = strings.ToLower(strings.TrimSpace(s))
// Human-readable quality levels.
if l, ok := QualityLevels[s]; ok && l > 0 {
return l
}
// Default to medium.
return QualityMedium
}

View file

@ -0,0 +1,83 @@
package thumb
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseQuality(t *testing.T) {
t.Run("Worst", func(t *testing.T) {
assert.Equal(t, QualityWorst, ParseQuality("worst"))
})
t.Run("Lowest", func(t *testing.T) {
assert.Equal(t, QualityWorst, ParseQuality("lowest"))
})
t.Run("bad", func(t *testing.T) {
assert.Equal(t, QualityBad, ParseQuality("bad"))
})
t.Run("low", func(t *testing.T) {
assert.Equal(t, QualityLow, ParseQuality("low"))
})
t.Run("Empty", func(t *testing.T) {
assert.Equal(t, QualityMedium, ParseQuality(""))
assert.Equal(t, QualityMedium, ParseQuality(" "))
})
t.Run("Default", func(t *testing.T) {
assert.Equal(t, QualityMedium, ParseQuality("default"))
})
t.Run("Medium", func(t *testing.T) {
assert.Equal(t, QualityMedium, ParseQuality("medium"))
assert.Equal(t, QualityMedium, ParseQuality(" \t medium \n\r"))
assert.Equal(t, QualityMedium, ParseQuality("MEDIUM"))
})
t.Run("Good", func(t *testing.T) {
assert.Equal(t, QualityHigh, ParseQuality("Good"))
assert.Equal(t, QualityHigh, ParseQuality("GOOD"))
})
t.Run("Best", func(t *testing.T) {
assert.Equal(t, QualityBest, ParseQuality("Best"))
})
t.Run("Ultra", func(t *testing.T) {
assert.Equal(t, QualityBest, ParseQuality("ultra"))
})
t.Run("0", func(t *testing.T) {
assert.Equal(t, QualityWorst, ParseQuality("0"))
})
t.Run("1", func(t *testing.T) {
assert.Equal(t, QualityBad, ParseQuality("1"))
})
t.Run("2", func(t *testing.T) {
assert.Equal(t, QualityLow, ParseQuality("2"))
})
t.Run("3", func(t *testing.T) {
assert.Equal(t, QualityMedium, ParseQuality("3"))
})
t.Run("4", func(t *testing.T) {
assert.Equal(t, QualityHigh, ParseQuality("4"))
})
t.Run("5", func(t *testing.T) {
assert.Equal(t, QualityBest, ParseQuality("5"))
})
t.Run("6", func(t *testing.T) {
assert.Equal(t, QualityMedium, ParseQuality("6"))
})
t.Run("50", func(t *testing.T) {
assert.Equal(t, Quality(50), ParseQuality("50"))
})
t.Run("66", func(t *testing.T) {
assert.Equal(t, Quality(66), ParseQuality("66"))
})
t.Run("77", func(t *testing.T) {
assert.Equal(t, Quality(77), ParseQuality("77"))
})
t.Run("89", func(t *testing.T) {
assert.Equal(t, Quality(89), ParseQuality("89"))
})
t.Run("90", func(t *testing.T) {
assert.Equal(t, Quality(90), ParseQuality("90"))
})
t.Run("100", func(t *testing.T) {
assert.Equal(t, Quality(100), ParseQuality("100"))
})
}

View file

@ -1,11 +1,9 @@
package thumb
var (
SizePrecached = 2048
SizeUncached = 7680
JpegQuality = 95
JpegQualitySmall = 80
Filter = ResampleLanczos
SizePrecached = 2048
SizeUncached = 7680
Filter = ResampleLanczos
)
func MaxSize() int {

View file

@ -22,10 +22,10 @@ func Int(s string) int {
// IntVal converts a string to a validated integer or a default if invalid.
func IntVal(s string, min, max, d int) (i int) {
s = strings.TrimSpace(s)
if s == "" {
return d
} else if s[0] == ' ' {
s = strings.TrimSpace(s)
}
result, err := strconv.ParseInt(s, 10, 32)
@ -49,9 +49,11 @@ func IntVal(s string, min, max, d int) (i int) {
func UInt(s string) uint {
if s == "" {
return 0
} else if s[0] == ' ' {
s = strings.TrimSpace(s)
}
result, err := strconv.ParseInt(strings.TrimSpace(s), 10, 32)
result, err := strconv.ParseInt(s, 10, 32)
if err != nil || result < 0 {
return 0