Config: Improve thumbnail generation option parsing and defaults #1474

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2024-05-18 10:59:37 +02:00
parent c60c4c7075
commit fd853e088f
23 changed files with 398 additions and 189 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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.

View File

@@ -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).
@@ -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).

View File

@@ -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) {
@@ -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())

View File

@@ -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{
@@ -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{

View File

@@ -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())},

View File

@@ -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
}

30
internal/thumb/color.go Normal file
View File

@@ -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
}
}

View File

@@ -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))
})
}

View File

@@ -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)

View File

@@ -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"
@@ -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
@@ -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
@@ -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
}
}

View File

@@ -0,0 +1,28 @@
package thumb
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseFilter(t *testing.T) {
t.Run("Vips", func(t *testing.T) {
assert.Equal(t, ResampleAuto, ParseFilter("", LibVips))
assert.Equal(t, ResampleAuto, ParseFilter("auto", LibVips))
assert.Equal(t, ResampleAuto, ParseFilter("blackman", LibVips))
assert.Equal(t, ResampleAuto, ParseFilter("lanczos", LibVips))
assert.Equal(t, ResampleAuto, ParseFilter("cubic", LibVips))
assert.Equal(t, ResampleAuto, ParseFilter("linear", LibVips))
assert.Equal(t, ResampleAuto, ParseFilter("invalid", LibVips))
})
t.Run("Imaging", func(t *testing.T) {
assert.Equal(t, ResampleAuto, ParseFilter("", LibImaging))
assert.Equal(t, ResampleAuto, ParseFilter("auto", LibImaging))
assert.Equal(t, ResampleBlackman, ParseFilter("blackman", LibImaging))
assert.Equal(t, ResampleLanczos, ParseFilter("lanczos", LibImaging))
assert.Equal(t, ResampleCubic, ParseFilter("cubic", LibImaging))
assert.Equal(t, ResampleLinear, ParseFilter("linear", LibImaging))
assert.Equal(t, ResampleAuto, ParseFilter("invalid", LibImaging))
})
}

View File

@@ -1,9 +1,11 @@
package thumb
type Lib = string
// Supported image processing libraries.
const (
LibVips = "vips"
LibImaging = "imaging"
LibVips Lib = "vips"
LibImaging Lib = "imaging"
)
// Library specifies the image library to be used.

View File

@@ -42,7 +42,7 @@ func Jpeg(srcFile, jpgFile string, orientation int) (img image.Image, err error)
}
// Get JPEG quality setting.
quality := JpegQuality.EncodeOption()
quality := JpegQualityDefault.EncodeOption()
// Save JPEG file.
if err = imaging.Save(img, jpgFile, quality); err != nil {

View File

@@ -9,9 +9,6 @@ import (
"github.com/photoprism/photoprism/pkg/fs"
)
// StandardRGB configures whether colors in the Apple Display P3 color space should be converted to standard RGB.
var StandardRGB = true
// Open loads an image from disk, rotates it, and converts the color profile if necessary.
func Open(fileName string, orientation int) (result image.Image, err error) {
// Filename missing?
@@ -24,8 +21,8 @@ func Open(fileName string, orientation int) (result image.Image, err error) {
return result, err
}
// Open JPEG?
if StandardRGB && fs.FileType(fileName) == fs.ImageJPEG {
// Open JPEG as sRGB image?
if Color == ColorSRGB && fs.FileType(fileName) == fs.ImageJPEG {
return OpenJpeg(fileName, orientation)
}

View File

@@ -4,11 +4,24 @@ import (
"strconv"
"strings"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/disintegration/imaging"
"github.com/photoprism/photoprism/pkg/txt"
)
// Standard JPEG image quality levels,
// see https://docs.photoprism.app/user-guide/settings/advanced/#jpeg-quality
const (
QualityMax Quality = 90
QualityHigh Quality = 85
QualityMedium Quality = 83
QualityLow Quality = 78
QualityMin Quality = 70
)
// JpegQualityDefault sets the compression level of newly created JPEGs.
var JpegQualityDefault = QualityMedium
// Quality represents a JPEG image quality.
type Quality int
@@ -27,53 +40,56 @@ func (q Quality) Int() int {
return int(q)
}
// Common Quality levels.
// see https://docs.photoprism.app/user-guide/settings/advanced/#jpeg-quality
const (
QualityBest Quality = 95
QualityHigh Quality = 88
QualityDefault Quality = 82
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": QualityDefault,
"": QualityDefault,
"ok": QualityDefault,
"default": QualityDefault,
"standard": QualityDefault,
"medium": QualityDefault,
"2": QualityLow,
"low": QualityLow,
"small": QualityLow,
"1": QualityBad,
"bad": QualityBad,
"0": QualityWorst,
"worst": QualityWorst,
"lowest": QualityWorst,
"max": QualityMax,
"ultra": QualityMax,
"best": QualityMax,
"6": QualityMax,
"5": QualityMax,
"high": QualityHigh,
"good": QualityHigh,
"4": QualityHigh,
"medium": QualityMedium,
"med": QualityMedium,
"default": QualityMedium,
"standard": QualityMedium,
"auto": QualityMedium,
"": QualityMedium,
"3": QualityMedium,
"low": QualityLow,
"small": QualityLow,
"2": QualityLow,
"min": QualityMin,
"1": QualityMin,
"0": QualityMin,
}
// Current Quality settings.
var (
JpegQuality = QualityDefault
JpegQualitySmall = QualityLow
)
// JpegQuality returns the JPEG image quality depending on the image size.
func JpegQuality(width, height int) Quality {
// Use default quality for images larger than 150 pixels.
if width > 150 || height > 150 {
return JpegQualityDefault
}
// Use lower quality for very small thumbnails.
return JpegQualitySmall()
}
// JpegQualitySmall returns the quality for images that should be more heavily compressed.
func JpegQualitySmall() Quality {
if q := JpegQualityDefault - 5; q < QualityMin || q > QualityMax {
return JpegQualityDefault
} else {
return q
}
}
// ParseQuality returns the matching quality based on a config value string.
func ParseQuality(s string) Quality {
// Default if empty.
if s == "" {
return QualityDefault
return QualityMedium
}
// Try to parse as positive integer.
@@ -89,5 +105,5 @@ func ParseQuality(s string) Quality {
return l
}
return QualityDefault
return QualityMedium
}

View File

@@ -6,61 +6,79 @@ import (
"github.com/stretchr/testify/assert"
)
func TestParseQuality(t *testing.T) {
t.Run("Worst", func(t *testing.T) {
assert.Equal(t, QualityWorst, ParseQuality("worst"))
func TestJpegQuality(t *testing.T) {
t.Run("Large", func(t *testing.T) {
assert.Equal(t, JpegQualityDefault, JpegQuality(100, 500))
})
t.Run("Lowest", func(t *testing.T) {
assert.Equal(t, QualityWorst, ParseQuality("lowest"))
t.Run("Small", func(t *testing.T) {
assert.Equal(t, JpegQualityDefault-5, JpegQuality(50, 150))
})
}
func TestJpegQualitySmall(t *testing.T) {
t.Run("Default", func(t *testing.T) {
assert.Equal(t, JpegQualityDefault-5, JpegQualitySmall())
})
}
func TestParseQuality(t *testing.T) {
t.Run("Max", func(t *testing.T) {
assert.Equal(t, QualityMax, ParseQuality("max"))
})
t.Run("Min", func(t *testing.T) {
assert.Equal(t, QualityMin, ParseQuality("min"))
})
t.Run("bad", func(t *testing.T) {
assert.Equal(t, QualityBad, ParseQuality("bad"))
assert.Equal(t, QualityMedium, ParseQuality("bad"))
})
t.Run("low", func(t *testing.T) {
assert.Equal(t, QualityLow, ParseQuality("low"))
})
t.Run("high", func(t *testing.T) {
assert.Equal(t, QualityHigh, ParseQuality("high"))
})
t.Run("Empty", func(t *testing.T) {
assert.Equal(t, QualityDefault, ParseQuality(""))
assert.Equal(t, QualityDefault, ParseQuality(" "))
assert.Equal(t, QualityMedium, ParseQuality(""))
assert.Equal(t, QualityMedium, ParseQuality(" "))
})
t.Run("Default", func(t *testing.T) {
assert.Equal(t, QualityDefault, ParseQuality("default"))
assert.Equal(t, QualityMedium, ParseQuality("default"))
})
t.Run("Medium", func(t *testing.T) {
assert.Equal(t, QualityDefault, ParseQuality("medium"))
assert.Equal(t, QualityDefault, ParseQuality(" \t medium \n\r"))
assert.Equal(t, QualityDefault, ParseQuality("MEDIUM"))
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"))
assert.Equal(t, QualityMax, ParseQuality("Best"))
})
t.Run("Ultra", func(t *testing.T) {
assert.Equal(t, QualityBest, ParseQuality("ultra"))
assert.Equal(t, QualityMax, ParseQuality("ultra"))
})
t.Run("0", func(t *testing.T) {
assert.Equal(t, QualityWorst, ParseQuality("0"))
assert.Equal(t, QualityMin, ParseQuality("0"))
})
t.Run("1", func(t *testing.T) {
assert.Equal(t, QualityBad, ParseQuality("1"))
assert.Equal(t, QualityMin, 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, QualityDefault, ParseQuality("3"))
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"))
assert.Equal(t, QualityMax, ParseQuality("5"))
})
t.Run("6", func(t *testing.T) {
assert.Equal(t, QualityDefault, ParseQuality("6"))
assert.Equal(t, QualityMax, ParseQuality("6"))
})
t.Run("50", func(t *testing.T) {
assert.Equal(t, Quality(50), ParseQuality("50"))
@@ -83,9 +101,9 @@ func TestParseQuality(t *testing.T) {
}
func TestQuality_String(t *testing.T) {
assert.Equal(t, "95", QualityBest.String())
assert.Equal(t, "88", QualityHigh.String())
assert.Equal(t, "82", QualityDefault.String())
assert.Equal(t, "75", QualityBad.String())
assert.Equal(t, "90", QualityMax.String())
assert.Equal(t, "85", QualityHigh.String())
assert.Equal(t, "83", QualityMedium.String())
assert.Equal(t, "78", QualityLow.String())
assert.Equal(t, "70", QualityMin.String())
}

View File

@@ -25,7 +25,7 @@ func (s Size) Bounds() image.Rectangle {
// Uncached tests if thumbnail type exceeds the cached thumbnails size limit.
func (s Size) Uncached() bool {
return s.Width > SizePrecached || s.Height > SizePrecached
return s.Width > SizeCached || s.Height > SizeCached
}
// ExceedsLimit tests if thumbnail type is too large, and can not be rendered at all.

View File

@@ -1,17 +1,17 @@
package thumb
var (
SizePrecached = 2048
SizeUncached = 7680
SizeCached = SizeFit1920.Width
SizeOnDemand = SizeFit7680.Width
)
// MaxSize returns the max supported size in pixels.
func MaxSize() int {
if SizePrecached > SizeUncached {
return SizePrecached
if SizeCached > SizeOnDemand {
return SizeCached
}
return SizeUncached
return SizeOnDemand
}
// InvalidSize tests if the size in pixels is invalid.

View File

@@ -7,18 +7,18 @@ import (
)
func TestMaxSize(t *testing.T) {
SizePrecached = 7680
SizeUncached = 1024
SizeCached = 7680
SizeOnDemand = 1024
assert.Equal(t, MaxSize(), 7680)
SizePrecached = 2048
SizeUncached = 7680
SizeCached = 2048
SizeOnDemand = 7680
}
func TestSize_ExceedsLimit(t *testing.T) {
SizePrecached = 1024
SizeUncached = 2048
SizeCached = 1024
SizeOnDemand = 2048
fit4096 := Sizes[Fit4096]
assert.True(t, fit4096.ExceedsLimit())
@@ -29,13 +29,13 @@ func TestSize_ExceedsLimit(t *testing.T) {
tile500 := Sizes[Tile500]
assert.False(t, tile500.ExceedsLimit())
SizePrecached = 2048
SizeUncached = 7680
SizeCached = 2048
SizeOnDemand = 7680
}
func TestSize_Uncached(t *testing.T) {
SizePrecached = 1024
SizeUncached = 2048
SizeCached = 1024
SizeOnDemand = 2048
fit4096 := Sizes[Fit4096]
assert.True(t, fit4096.Uncached())
@@ -46,8 +46,8 @@ func TestSize_Uncached(t *testing.T) {
tile500 := Sizes[Tile500]
assert.False(t, tile500.Uncached())
SizePrecached = 2048
SizeUncached = 7680
SizeCached = 2048
SizeOnDemand = 7680
}
func TestResampleFilter_Imaging(t *testing.T) {

View File

@@ -88,20 +88,12 @@ func Vips(imageName string, imageBuffer []byte, hash, thumbPath string, width, h
return "", nil, err
}
// Export to PNG or JPEG.
if fs.FileType(thumbName) == fs.ImagePNG {
params := vips.NewPngExportParams()
thumbBuffer, _, err = img.ExportPng(params)
} else {
params := vips.NewJpegExportParams()
if width <= 150 && height <= 150 {
params.Quality = JpegQualitySmall.Int()
} else {
params.Quality = JpegQuality.Int()
}
thumbBuffer, _, err = img.ExportJpeg(params)
// Export to standard image format.
switch fs.FileType(thumbName) {
case fs.ImagePNG:
thumbBuffer, _, err = img.ExportPng(VipsPngExportParams(width, height))
default:
thumbBuffer, _, err = img.ExportJpeg(VipsJpegExportParams(width, height))
}
// Check if export failed.
@@ -127,6 +119,44 @@ func VipsImportParams() *vips.ImportParams {
return params
}
// VipsPngExportParams returns PNG image encoding parameters for libvips.
func VipsPngExportParams(width, height int) *vips.PngExportParams {
params := vips.NewPngExportParams()
params.Filter = vips.PngFilterNone
params.Interlace = false
params.Palette = false
// Set compression depending on image size.
if width > 20 || height > 20 {
params.Compression = 6
} else {
params.Compression = 0
}
return params
}
// VipsJpegExportParams returns JPEG image encoding parameters for libvips.
func VipsJpegExportParams(width, height int) *vips.JpegExportParams {
params := vips.NewJpegExportParams()
params.Quality = JpegQuality(width, height).Int()
params.Interlace = true
params.SubsampleMode = vips.VipsForeignSubsampleAuto
// Enable quality enhancements depending on image size.
if width > 150 || height > 150 {
params.OptimizeCoding = true
// The following options can only be set if libvips
// has been compiled with the "--with-mozjpeg" flag:
// params.QuantTable = 3
// params.TrellisQuant = true
// params.OvershootDeringing = true
// params.OptimizeScans = true
}
return params
}
// VipsRotate rotates a vips image based on the Exif orientation.
func VipsRotate(img *vips.ImageRef, orientation int) error {
var err error

View File

@@ -127,6 +127,77 @@ func TestVips(t *testing.T) {
})
}
func TestVipsImportParams(t *testing.T) {
t.Run("Default", func(t *testing.T) {
result := VipsImportParams()
if result == nil {
t.Fatal("result is nil")
}
assert.True(t, result.AutoRotate.Get())
assert.False(t, result.FailOnError.Get())
})
}
func TestVipsPngExportParams(t *testing.T) {
t.Run("Standard", func(t *testing.T) {
result := VipsPngExportParams(500, 500)
if result == nil {
t.Fatal("result is nil")
}
assert.False(t, result.Interlace)
assert.Equal(t, vips.PngFilterNone, result.Filter)
assert.Equal(t, 0, result.Quality)
assert.Equal(t, 6, result.Compression)
})
t.Run("Small", func(t *testing.T) {
result := VipsPngExportParams(3, 3)
if result == nil {
t.Fatal("result is nil")
}
assert.False(t, result.Interlace)
assert.Equal(t, vips.PngFilterNone, result.Filter)
assert.Equal(t, 0, result.Quality)
assert.Equal(t, 0, result.Compression)
})
}
func TestVipsJpegExportParams(t *testing.T) {
t.Run("Standard", func(t *testing.T) {
result := VipsJpegExportParams(1920, 1200)
if result == nil {
t.Fatal("result is nil")
}
assert.True(t, result.Interlace)
assert.False(t, result.TrellisQuant)
assert.False(t, result.OptimizeScans)
assert.True(t, result.OptimizeCoding)
assert.False(t, result.OvershootDeringing)
assert.Equal(t, JpegQualityDefault.Int(), result.Quality)
})
t.Run("Small", func(t *testing.T) {
result := VipsJpegExportParams(50, 50)
if result == nil {
t.Fatal("result is nil")
}
assert.True(t, result.Interlace)
assert.False(t, result.TrellisQuant)
assert.False(t, result.OptimizeScans)
assert.False(t, result.OptimizeCoding)
assert.False(t, result.OvershootDeringing)
assert.Equal(t, JpegQualitySmall().Int(), result.Quality)
})
}
func TestVipsRotate(t *testing.T) {
if err := os.MkdirAll("testdata/vips/rotate", fs.ModeDir); err != nil {
t.Fatal(err)