use predefined resolutions for thumbnail generation

This commit is contained in:
David Christofas
2020-03-23 11:52:33 +01:00
parent 43bf107207
commit 3de4584a45
12 changed files with 125 additions and 56 deletions
+1 -1
View File
@@ -29,11 +29,11 @@ func Server(cfg *config.Config) *cli.Command {
Usage: "Start integrated server",
Flags: flagset.ServerWithConfig(cfg),
Before: func(c *cli.Context) error {
cfg.Thumbnail.Resolutions = c.StringSlice("thumbnail-resolution")
return nil
},
Action: func(c *cli.Context) error {
logger := NewLogger(cfg)
if cfg.Tracing.Enabled {
switch t := cfg.Tracing.Type; t {
case "agent":
+13 -7
View File
@@ -33,13 +33,12 @@ type Tracing struct {
// Config combines all available configuration parts.
type Config struct {
File string
Log Log
Debug Debug
Server Server
Tracing Tracing
FileSystemStorage FileSystemStorage
WebDavSource WebDavSource
File string
Log Log
Debug Debug
Server Server
Tracing Tracing
Thumbnail Thumbnail
}
// FileSystemStorage defines the available filesystem storage configuration.
@@ -52,6 +51,13 @@ type WebDavSource struct {
BaseURL string
}
// Thumbnail defines the available thumbnail related configuration.
type Thumbnail struct {
Resolutions []string
FileSystemStorage FileSystemStorage
WebDavSource WebDavSource
}
// New initializes a new configuration with or without defaults.
func New() *Config {
return &Config{}
+8 -2
View File
@@ -142,14 +142,20 @@ func ServerWithConfig(cfg *config.Config) []cli.Flag {
Value: filepath.Join(os.TempDir(), "ocis-thumbnails/"),
Usage: "Root path of the filesystem storage directory",
EnvVars: []string{"THUMBNAILS_FILESYSTEMSTORAGE_ROOT"},
Destination: &cfg.FileSystemStorage.RootDirectory,
Destination: &cfg.Thumbnail.FileSystemStorage.RootDirectory,
},
&cli.StringFlag{
Name: "webdavsource-baseurl",
Value: "http://localhost:9140/remote.php/webdav/",
Usage: "Base url for a webdav api",
EnvVars: []string{"THUMBNAILS_WEBDAVSOURCE_BASEURL"},
Destination: &cfg.WebDavSource.BaseURL,
Destination: &cfg.Thumbnail.WebDavSource.BaseURL,
},
&cli.StringSliceFlag{
Name: "thumbnail-resolution",
Value: cli.NewStringSlice("16x16", "32x32", "64x64", "128x128"),
Usage: "--thumbnail-resolution 16x16 [--thumbnail-resolution 32x32]",
EnvVars: []string{"THUMBNAILS_RESOLUTIONs"},
},
}
}
+1
View File
@@ -24,6 +24,7 @@ func NewService(opts ...Option) grpc.Service {
{
thumbnail = svc.NewService(
svc.Config(options.Config),
svc.Logger(options.Logger),
)
thumbnail = svc.NewInstrument(thumbnail, options.Metrics)
thumbnail = svc.NewLogging(thumbnail, options.Logger)
+21 -14
View File
@@ -8,23 +8,29 @@ import (
v0proto "github.com/owncloud/ocis-thumbnails/pkg/proto/v0"
"github.com/owncloud/ocis-thumbnails/pkg/thumbnails"
"github.com/owncloud/ocis-thumbnails/pkg/thumbnails/imgsource"
"github.com/owncloud/ocis-thumbnails/pkg/thumbnails/resolution"
"github.com/owncloud/ocis-thumbnails/pkg/thumbnails/storage"
)
// NewService returns a service implementation for Service.
func NewService(opts ...Option) v0proto.ThumbnailServiceHandler {
options := newOptions(opts...)
logger := options.Logger
resolutions, err := resolution.Init(options.Config.Thumbnail.Resolutions)
if err != nil {
logger.Fatal().Err(err).Msg("resolutions not configured correctly")
}
svc := Thumbnail{
manager: thumbnails.NewSimpleManager(
storage.NewFileSystemStorage(
options.Config.FileSystemStorage,
options.Logger,
options.Config.Thumbnail.FileSystemStorage,
logger,
),
options.Logger,
logger,
),
source: imgsource.NewWebDavSource(options.Config.WebDavSource),
logger: options.Logger,
resolutions: resolutions,
source: imgsource.NewWebDavSource(options.Config.Thumbnail.WebDavSource),
logger: logger,
}
return svc
@@ -32,9 +38,10 @@ func NewService(opts ...Option) v0proto.ThumbnailServiceHandler {
// Thumbnail implements the GRPC handler.
type Thumbnail struct {
manager thumbnails.Manager
source imgsource.Source
logger log.Logger
manager thumbnails.Manager
resolutions resolution.Resolutions
source imgsource.Source
logger log.Logger
}
// GetThumbnail retrieves a thumbnail for an image
@@ -44,12 +51,12 @@ func (g Thumbnail) GetThumbnail(ctx context.Context, req *v0proto.GetRequest, rs
// TODO: better error responses
return fmt.Errorf("can't be encoded. filetype %s not supported", req.Filetype.String())
}
r := g.resolutions.ClosestMatch(int(req.Width), int(req.Height))
tCtx := thumbnails.Context{
Width: int(req.Width),
Height: int(req.Height),
ImagePath: req.Filepath,
Encoder: encoder,
ETag: req.Etag,
Resolution: r,
ImagePath: req.Filepath,
Encoder: encoder,
ETag: req.Etag,
}
thumbnail := g.manager.GetStored(tCtx)
@@ -9,6 +9,20 @@ func TestParseWithEmptyString(t *testing.T) {
}
}
func TestParseWithInvalidWidth(t *testing.T) {
_, err := Parse("invalidx42")
if err == nil {
t.Error("Parse with invalid width should return an error.")
}
}
func TestParseWithInvalidHeight(t *testing.T) {
_, err := Parse("42xinvalid")
if err == nil {
t.Error("Parse with invalid height should return an error.")
}
}
func TestParse(t *testing.T) {
rStr := "42x23"
r, _ := Parse(rStr)
+28 -11
View File
@@ -3,6 +3,7 @@ package resolution
import (
"fmt"
"math"
"sort"
)
// Init creates an instance of Resolutions from resolution strings.
@@ -15,6 +16,16 @@ func Init(rStrs []string) (Resolutions, error) {
}
rs = append(rs, r)
}
sort.Slice(rs, func(i, j int) bool {
left := rs[i]
right := rs[j]
leftSize := left.Width * left.Height
rightSize := right.Width * right.Height
return leftSize < rightSize
})
return rs, nil
}
@@ -22,31 +33,37 @@ func Init(rStrs []string) (Resolutions, error) {
type Resolutions []Resolution
// ClosestMatch returns the resolution which is closest to the provided resolution.
// If there is no exact match the resolution will be the next higher one.
// If the given resolution is bigger than all available resolutions the biggest available one is used.
func (r Resolutions) ClosestMatch(width, height int) Resolution {
if len(r) == 0 {
return Resolution{Width: width, Height: height}
}
isLandscape := width > height
givenLen := math.Max(float64(width), float64(height))
givenLen := int(math.Max(float64(width), float64(height)))
// Initialize with the first resolution
match := r[0]
matchLen := dimensionLength(match, isLandscape)
minDiff := math.Abs(givenLen - float64(matchLen))
var match Resolution
minDiff := math.MaxInt32
for i := 1; i < len(r); i++ {
r := r[i]
rLen := dimensionLength(r, isLandscape)
diff := math.Abs(givenLen - float64(rLen))
if diff <= minDiff {
minDiff = diff
match = r
current := r[i]
len := dimensionLength(current, isLandscape)
diff := givenLen - len
if diff > 0 {
continue
}
absDiff := int(math.Abs(float64(diff)))
if absDiff < minDiff {
minDiff = absDiff
match = current
}
}
if match == (Resolution{}) {
match = r[len(r)-1]
}
return match
}
+23 -4
View File
@@ -52,6 +52,25 @@ func TestInitWithMultipleResolutions(t *testing.T) {
}
}
func TestInitWithMultipleResolutionsShouldBeSorted(t *testing.T) {
rStrs := []string{"32x32", "64x64", "16x16", "128x128"}
rs, err := Init(rStrs)
if err != nil {
t.Errorf("Init with valid parameter should not fail. Error: %s.\n", err.Error())
}
for i := 0; i < len(rs)-1; i++ {
current := rs[i]
currentSize := current.Width * current.Height
next := rs[i]
nextSize := next.Width * next.Height
if currentSize > nextSize {
t.Error("Resolutions are not sorted.")
}
}
}
func TestClosestMatchWithEmptyResolutions(t *testing.T) {
rs, _ := Init(nil)
width := 24
@@ -66,12 +85,12 @@ func TestClosestMatchWithEmptyResolutions(t *testing.T) {
func TestClosestMatch(t *testing.T) {
rs, _ := Init([]string{"16x16", "24x24", "32x32", "64x64", "128x128"})
table := [][]int{
[]int{17, 17, 16, 16},
[]int{12, 17, 16, 16},
[]int{17, 17, 24, 24},
[]int{12, 17, 24, 24},
[]int{24, 24, 24, 24},
[]int{20, 20, 24, 24},
[]int{20, 80, 64, 64},
[]int{80, 20, 64, 64},
[]int{20, 80, 128, 128},
[]int{80, 20, 128, 128},
[]int{48, 48, 64, 64},
[]int{1024, 1024, 128, 128},
}
+1 -2
View File
@@ -6,7 +6,6 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strconv"
"github.com/owncloud/ocis-pkg/v2/log"
"github.com/owncloud/ocis-thumbnails/pkg/config"
@@ -68,7 +67,7 @@ func (s FileSystem) Set(key string, img []byte) error {
func (s FileSystem) BuildKey(ctx Context) string {
etag := ctx.ETag
filetype := ctx.Types[0]
filename := strconv.Itoa(ctx.Width) + "x" + strconv.Itoa(ctx.Height) + "." + filetype
filename := ctx.Resolution.String() + "." + filetype
key := new(bytes.Buffer)
key.WriteString(etag[:2])
+1 -1
View File
@@ -32,7 +32,7 @@ func (s InMemory) Set(key string, thumbnail []byte) error {
func (s InMemory) BuildKey(ctx Context) string {
parts := []string{
ctx.ETag,
string(ctx.Width) + "x" + string(ctx.Height),
ctx.Resolution.String(),
strings.Join(ctx.Types, ","),
}
return strings.Join(parts, "+")
+5 -4
View File
@@ -1,11 +1,12 @@
package storage
import "github.com/owncloud/ocis-thumbnails/pkg/thumbnails/resolution"
// Context combines different attributes needed for storage operations.
type Context struct {
ETag string
Types []string
Width int
Height int
ETag string
Types []string
Resolution resolution.Resolution
}
// Storage defines the interface for a thumbnail store.
+9 -10
View File
@@ -6,16 +6,16 @@ import (
"github.com/nfnt/resize"
"github.com/owncloud/ocis-pkg/v2/log"
"github.com/owncloud/ocis-thumbnails/pkg/thumbnails/resolution"
"github.com/owncloud/ocis-thumbnails/pkg/thumbnails/storage"
)
// Context bundles information needed to generate a thumbnail for afile
type Context struct {
Width int
Height int
ImagePath string
Encoder Encoder
ETag string
Resolution resolution.Resolution
ImagePath string
Encoder Encoder
ETag string
}
// Manager is responsible for generating thumbnails
@@ -69,16 +69,15 @@ func (s SimpleManager) GetStored(ctx Context) []byte {
}
func (s SimpleManager) generate(ctx Context, img image.Image) image.Image {
thumbnail := resize.Thumbnail(uint(ctx.Width), uint(ctx.Height), img, resize.Lanczos2)
thumbnail := resize.Thumbnail(uint(ctx.Resolution.Width), uint(ctx.Resolution.Height), img, resize.Lanczos2)
return thumbnail
}
func mapToStorageContext(ctx Context) storage.Context {
sCtx := storage.Context{
ETag: ctx.ETag,
Width: ctx.Width,
Height: ctx.Height,
Types: ctx.Encoder.Types(),
ETag: ctx.ETag,
Resolution: ctx.Resolution,
Types: ctx.Encoder.Types(),
}
return sCtx
}