Config: Improve flag descriptions of JPEG and thumbnail parameters #2215
This commit is contained in:
parent
342904a4fa
commit
44efdd232a
10 changed files with 229 additions and 48 deletions
|
@ -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{
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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
89
internal/thumb/quality.go
Normal 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
|
||||
}
|
83
internal/thumb/quality_test.go
Normal file
83
internal/thumb/quality_test.go
Normal 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"))
|
||||
})
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue