change image library and refactor thumbnails service

This commit is contained in:
David Christofas
2020-11-26 16:36:51 +01:00
parent c5a75291f9
commit c46a5598d4
18 changed files with 355 additions and 321 deletions

View File

@@ -0,0 +1,6 @@
Change: replace the library which scales the images
The library went out of support.
Also did some refactoring of the thumbnails service code.
https://github.com/owncloud/ocis/pull/910

View File

@@ -160,8 +160,6 @@ github.com/aws/aws-sdk-go v1.23.19/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpi
github.com/aws/aws-sdk-go v1.25.31/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.28.2/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.33.19/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/aws/aws-sdk-go v1.35.9 h1:b1HiUpdkFLJyoOQ7zas36YHzjNHH0ivHx/G5lWBeg+U=
github.com/aws/aws-sdk-go v1.35.9/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k=
github.com/aws/aws-sdk-go v1.35.23 h1:SCP0d0XvyJTDmfnHEQPvBaYi3kea1VNUo7uQmkVgFts=
github.com/aws/aws-sdk-go v1.35.23/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k=
github.com/aws/aws-xray-sdk-go v0.9.4/go.mod h1:XtMKdBQfpVut+tJEwI7+dJFRxxRdxHDyVNp2tHXRq04=
@@ -217,7 +215,6 @@ github.com/bwmarrin/discordgo v0.20.2/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVO
github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw=
github.com/caddyserver/certmagic v0.10.6 h1:sCya6FmfaN74oZE46kqfaFOVoROD/mF36rTQfjN7TZc=
github.com/caddyserver/certmagic v0.10.6/go.mod h1:Y8jcUBctgk/IhpAzlHKfimZNyXCkfGgRTC0orl8gROQ=
github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY=
github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
@@ -233,7 +230,6 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/cheggaaa/pb v1.0.28/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s=
github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30=
@@ -295,7 +291,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/crewjam/httperr v0.0.0-20190612203328-a946449404da h1:WXnT88cFG2davqSFqvaFfzkSMC0lqh/8/rKZ+z7tYvI=
github.com/crewjam/httperr v0.0.0-20190612203328-a946449404da/go.mod h1:+rmNIXRvYMqLQeR4DHyTvs6y0MEMymTz4vyFpFkKTPs=
github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo=
github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4=
@@ -305,20 +300,11 @@ github.com/cs3org/cato v0.0.0-20200626150132-28a40e643719/go.mod h1:XJEZ3/EQuI3B
github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e/go.mod h1:XJEZ3/EQuI3BXTp/6DUzFr850vlxq11I6satRtz0YQ4=
github.com/cs3org/go-cs3apis v0.0.0-20191128165347-19746c015c83/go.mod h1:IsVGyZrOLUQD48JIhlM/xb3Vz6He5o2+W0ZTfUGY+IU=
github.com/cs3org/go-cs3apis v0.0.0-20200730121022-c4f3d4f7ddfd/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
github.com/cs3org/go-cs3apis v0.0.0-20201007120910-416ed6cf8b00 h1:LVl25JaflluOchVvaHWtoCynm5OaM+VNai0IYkcCSe0=
github.com/cs3org/go-cs3apis v0.0.0-20201007120910-416ed6cf8b00/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
github.com/cs3org/go-cs3apis v0.0.0-20201118090759-87929f5bae21 h1:mZpylrgnCgSeaZ5EznvHIPIKuaQHMHZDi2wkJtk4M8Y=
github.com/cs3org/go-cs3apis v0.0.0-20201118090759-87929f5bae21/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
github.com/cs3org/reva v0.0.2-0.20200115110931-4c7513415ec5/go.mod h1:Hk3eCcdhtv4eIhKvRK736fQuOyS1HuHnUcz0Dq6NK1A=
github.com/cs3org/reva v1.1.0/go.mod h1:fBzTrNuAKdQ62ybjpdu8nyhBin90/3/3s6DGQDCdBp4=
github.com/cs3org/reva v1.3.1-0.20201023144216-cdb3d6688da5 h1:nkmk9ywGKpJthWeMYGBiXh4DD6mTCOZLRfGDjp9rsKg=
github.com/cs3org/reva v1.3.1-0.20201023144216-cdb3d6688da5/go.mod h1:V50GXMiT524bvxACFkrkZqBzK4BoXLSprxcPshBlSOY=
github.com/cs3org/reva v1.3.1-0.20201112131316-1c425035c8a2 h1:vBgCFcQMxcu7wDPo44Onw3ZXXZc3DrARz6V8rECZjVs=
github.com/cs3org/reva v1.3.1-0.20201112131316-1c425035c8a2/go.mod h1:oqkkfe0g/dvrFFrmwd/VVfmrxhfswHp7+IB2PNeADSE=
github.com/cs3org/reva v1.4.1-0.20201120104232-f5afafc04c3b/go.mod h1:oqkkfe0g/dvrFFrmwd/VVfmrxhfswHp7+IB2PNeADSE=
github.com/cs3org/reva v1.4.1-0.20201123062044-b2c4af4e897d/go.mod h1:MTBlfobTE8W2hgXQ9+r+75jpJa1TxD04IZm5TpS9H48=
github.com/cs3org/reva v1.4.1-0.20201125144025-57da0c27434c h1:x7HkQqFGA+7FJUfyDO9tM2gF5i540PQePx+1kGUUYgA=
github.com/cs3org/reva v1.4.1-0.20201125144025-57da0c27434c/go.mod h1:MTBlfobTE8W2hgXQ9+r+75jpJa1TxD04IZm5TpS9H48=
github.com/cs3org/reva v1.4.1-0.20201125172625-a5ab834a565d h1:Sr6ZWGjTds5cWDei3mJev2+RPJ0iejKnVrYklr5mO+M=
github.com/cs3org/reva v1.4.1-0.20201125172625-a5ab834a565d/go.mod h1:MTBlfobTE8W2hgXQ9+r+75jpJa1TxD04IZm5TpS9H48=
github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d h1:SwD98825d6bdB+pEuTxWOXiSjBrHdOl/UVp75eI7JT8=
@@ -1119,7 +1105,6 @@ github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OS
github.com/nats-io/stan.go v0.5.0/go.mod h1:dYqB+vMN3C2F9pT1FRQpg9eHbjPj6mP0yYuyBNuXHZE=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/netdata/go-orchestrator v0.0.0-20190905093727-c793edba0e8f/go.mod h1:ECF8anFVCt/TfTIWVPgPrNaYJXtAtpAOF62ugDbw41A=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
@@ -1601,6 +1586,7 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -1782,7 +1768,6 @@ golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201022201747-fb209a7c41cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1 h1:a/mKvvZr9Jcc8oKfcmgzyp7OwF73JPWsQLvH1z2Kxck=
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -8,6 +8,7 @@ require (
contrib.go.opencensus.io/exporter/zipkin v0.1.1
github.com/UnnoTed/fileb0x v1.1.4
github.com/cespare/reflex v0.2.0
github.com/cs3org/go-cs3apis v0.0.0-20201007120910-416ed6cf8b00
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31 // indirect
github.com/gogo/protobuf v1.3.1 // indirect
github.com/golang/protobuf v1.4.3
@@ -31,7 +32,9 @@ require (
github.com/stretchr/testify v1.6.1
go.opencensus.io v0.22.5
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
golang.org/x/image v0.0.0-20190802002840-cff245a6509b
google.golang.org/genproto v0.0.0-20200918140846-d0d605568037 // indirect
google.golang.org/grpc v1.33.1
gopkg.in/square/go-jose.v2 v2.5.1
)

View File

@@ -1212,6 +1212,7 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=

View File

@@ -2,6 +2,7 @@ package svc
import (
"context"
"image"
"gopkg.in/square/go-jose.v2/jwt"
@@ -10,7 +11,6 @@ import (
v0proto "github.com/owncloud/ocis/thumbnails/pkg/proto/v0"
"github.com/owncloud/ocis/thumbnails/pkg/thumbnail"
"github.com/owncloud/ocis/thumbnails/pkg/thumbnail/imgsource"
"github.com/owncloud/ocis/thumbnails/pkg/thumbnail/resolution"
"github.com/pkg/errors"
)
@@ -18,19 +18,19 @@ import (
func NewService(opts ...Option) v0proto.ThumbnailServiceHandler {
options := newOptions(opts...)
logger := options.Logger
resolutions, err := resolution.New(options.Config.Thumbnail.Resolutions)
resolutions, err := thumbnail.ParseResolutions(options.Config.Thumbnail.Resolutions)
if err != nil {
logger.Fatal().Err(err).Msg("resolutions not configured correctly")
}
svc := Thumbnail{
serviceID: options.Config.Server.Namespace + "." + options.Config.Server.Name,
manager: thumbnail.NewSimpleManager(
resolutions,
options.ThumbnailStorage,
logger,
),
resolutions: resolutions,
source: options.ImageSource,
logger: logger,
source: options.ImageSource,
logger: logger,
}
return svc
@@ -38,11 +38,10 @@ func NewService(opts ...Option) v0proto.ThumbnailServiceHandler {
// Thumbnail implements the GRPC handler.
type Thumbnail struct {
serviceID string
manager thumbnail.Manager
resolutions resolution.Resolutions
source imgsource.Source
logger log.Logger
serviceID string
manager thumbnail.Manager
source imgsource.Source
logger log.Logger
}
// GetThumbnail retrieves a thumbnail for an image
@@ -52,7 +51,6 @@ func (g Thumbnail) GetThumbnail(ctx context.Context, req *v0proto.GetRequest, rs
g.logger.Debug().Str("filetype", req.Filetype.String()).Msg("unsupported filetype")
return nil
}
r := g.resolutions.ClosestMatch(int(req.Width), int(req.Height))
auth := req.Authorization
if auth == "" {
@@ -64,8 +62,7 @@ func (g Thumbnail) GetThumbnail(ctx context.Context, req *v0proto.GetRequest, rs
}
tr := thumbnail.Request{
Resolution: r,
ImagePath: req.Filepath,
Resolution: image.Rect(0, 0, int(req.Width), int(req.Height)),
Encoder: encoder,
ETag: req.Etag,
Username: username,
@@ -78,8 +75,8 @@ func (g Thumbnail) GetThumbnail(ctx context.Context, req *v0proto.GetRequest, rs
return nil
}
sCtx := imgsource.WithAuthorization(ctx, auth)
img, err := g.source.Get(sCtx, tr.ImagePath)
sCtx := imgsource.ContextSetAuthorization(ctx, auth)
img, err := g.source.Get(sCtx, req.Filepath)
if err != nil {
return merrors.InternalServerError(g.serviceID, "could not get image from source: %v", err.Error())
}

View File

@@ -2,12 +2,12 @@ package imgsource
import (
"context"
"fmt"
"image"
"os"
"path/filepath"
"github.com/owncloud/ocis/thumbnails/pkg/config"
"github.com/pkg/errors"
)
// NewFileSystemSource return a new FileSystem instance
@@ -27,12 +27,12 @@ func (s FileSystem) Get(ctx context.Context, file string) (image.Image, error) {
imgPath := filepath.Join(s.basePath, file)
f, err := os.Open(filepath.Clean(imgPath))
if err != nil {
return nil, fmt.Errorf("failed to load the file %s from %s error %s", file, imgPath, err.Error())
return nil, errors.Wrapf(err, "failed to load the file %s from %s", file, imgPath)
}
img, _, err := image.Decode(f)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "Get: Decode:")
}
return img, nil

View File

@@ -16,15 +16,16 @@ type Source interface {
Get(ctx context.Context, path string) (image.Image, error)
}
// WithAuthorization puts the authorization in the context.
func WithAuthorization(parent context.Context, authorization string) context.Context {
// ContextSetAuthorization puts the authorization in the context.
func ContextSetAuthorization(parent context.Context, authorization string) context.Context {
return context.WithValue(parent, auth, authorization)
}
func authorization(ctx context.Context) string {
// ContextGetAuthorization gets the authorization from the context.
func ContextGetAuthorization(ctx context.Context) (string, bool) {
val := ctx.Value(auth)
if val == nil {
return ""
return "", false
}
return val.(string)
return val.(string), true
}

View File

@@ -10,6 +10,7 @@ import (
"path"
"github.com/owncloud/ocis/thumbnails/pkg/config"
"github.com/pkg/errors"
)
// NewWebDavSource creates a new webdav instance.
@@ -32,13 +33,13 @@ func (s WebDav) Get(ctx context.Context, file string) (image.Image, error) {
u.Path = path.Join(u.Path, file)
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return nil, fmt.Errorf("could not get the image \"%s\" error: %s", file, err.Error())
return nil, errors.Wrapf(err, `could not get the image "%s"`, file)
}
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: s.insecure}
auth := authorization(ctx)
if auth == "" {
auth, ok := ContextGetAuthorization(ctx)
if !ok {
return nil, fmt.Errorf("could not get image \"%s\" error: authorization is missing", file)
}
req.Header.Add("Authorization", auth)
@@ -46,7 +47,7 @@ func (s WebDav) Get(ctx context.Context, file string) (image.Image, error) {
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("could not get the image \"%s\" error: %s", file, err.Error())
return nil, errors.Wrapf(err, `could not get the image "%s"`, file)
}
if resp.StatusCode != http.StatusOK {
@@ -55,7 +56,7 @@ func (s WebDav) Get(ctx context.Context, file string) (image.Image, error) {
img, _, err := image.Decode(resp.Body)
if err != nil {
return nil, fmt.Errorf("could not decode the image \"%s\". error: %s", file, err.Error())
return nil, errors.Wrapf(err, `could not decode the image "%s"`, file)
}
return img, nil
}

View File

@@ -1,37 +0,0 @@
package resolution
import (
"fmt"
"strconv"
"strings"
)
// Parse parses a resolution string in the form <width>x<height> and returns a resolution instance.
func Parse(s string) (Resolution, error) {
parts := strings.Split(s, "x")
if len(parts) != 2 {
return Resolution{}, fmt.Errorf("failed to parse resolution: %s. Expected format <width>x<height>", s)
}
width, err := strconv.Atoi(parts[0])
if err != nil {
return Resolution{}, fmt.Errorf("width: %s has an invalid value. Expected an integer", parts[0])
}
height, err := strconv.Atoi(parts[1])
if err != nil {
return Resolution{}, fmt.Errorf("height: %s has an invalid value. Expected an integer", parts[1])
}
return Resolution{Width: width, Height: height}, nil
}
// Resolution defines represents the width and height of a thumbnail.
type Resolution struct {
Width int
Height int
}
// String returns the resolution in the format:
//
// <width>x<height>
func (r Resolution) String() string {
return strconv.Itoa(r.Width) + "x" + strconv.Itoa(r.Height)
}

View File

@@ -1,40 +0,0 @@
package resolution
import "testing"
func TestParseWithEmptyString(t *testing.T) {
_, err := Parse("")
if err == nil {
t.Error("Parse with empty string should return an error.")
}
}
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)
if r.Width != 42 || r.Height != 23 {
t.Errorf("Expected resolution %s got %s", rStr, r.String())
}
}
func TestString(t *testing.T) {
r := Resolution{Width: 42, Height: 23}
expected := "42x23"
if r.String() != expected {
t.Errorf("Expected string %s got %s", expected, r.String())
}
}

View File

@@ -1,74 +0,0 @@
package resolution
import (
"fmt"
"math"
"sort"
)
// New creates an instance of Resolutions from resolution strings.
func New(rStrs []string) (Resolutions, error) {
var rs Resolutions
for _, rStr := range rStrs {
r, err := Parse(rStr)
if err != nil {
return nil, fmt.Errorf("failed to initialize resolutions: %s", err.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
}
// Resolutions represents the available thumbnail resolutions.
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 := int(math.Max(float64(width), float64(height)))
// Initialize with the first resolution
var match Resolution
minDiff := math.MaxInt32
for _, current := range r {
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
}
func dimensionLength(r Resolution, landscape bool) int {
if landscape {
return r.Width
}
return r.Height
}

View File

@@ -1,111 +0,0 @@
package resolution
import (
"testing"
)
func TestInitWithEmptyArray(t *testing.T) {
rs, err := New([]string{})
if err != nil {
t.Errorf("Init with an empty array should not fail. Error: %s.\n", err.Error())
}
if len(rs) != 0 {
t.Error("Init with an empty array should return an empty Resolutions instance.\n")
}
}
func TestInitWithNil(t *testing.T) {
rs, err := New(nil)
if err != nil {
t.Errorf("Init with nil parameter should not fail. Error: %s.\n", err.Error())
}
if len(rs) != 0 {
t.Error("Init with nil parameter should return an empty Resolutions instance.\n")
}
}
func TestInitWithInvalidValuesInArray(t *testing.T) {
_, err := New([]string{"invalid"})
if err == nil {
t.Error("Init with invalid parameter should fail.\n")
}
}
func TestInit(t *testing.T) {
rs, err := New([]string{"16x16"})
if err != nil {
t.Errorf("Init with valid parameter should not fail. Error: %s.\n", err.Error())
}
if len(rs) != 1 {
t.Errorf("resolutions has size %d, expected size %d.\n", len(rs), 1)
}
}
func TestInitWithMultipleResolutions(t *testing.T) {
rStrs := []string{"16x16", "32x32", "64x64", "128x128"}
rs, err := New(rStrs)
if err != nil {
t.Errorf("Init with valid parameter should not fail. Error: %s.\n", err.Error())
}
if len(rs) != len(rStrs) {
t.Errorf("resolutions has size %d, expected size %d.\n", len(rs), len(rStrs))
}
}
func TestInitWithMultipleResolutionsShouldBeSorted(t *testing.T) {
rStrs := []string{"32x32", "64x64", "16x16", "128x128"}
rs, err := New(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, _ := New(nil)
width := 24
height := 24
r := rs.ClosestMatch(width, height)
if r.Width != width || r.Height != height {
t.Errorf("ClosestMatch from empty resolutions should return the given resolution")
}
}
func TestClosestMatch(t *testing.T) {
rs, _ := New([]string{"16x16", "24x24", "32x32", "64x64", "128x128"})
table := [][]int{
// width, height, expectedWidth, expectedHeight
[]int{17, 17, 24, 24},
[]int{12, 17, 24, 24},
[]int{24, 24, 24, 24},
[]int{20, 20, 24, 24},
[]int{20, 80, 128, 128},
[]int{80, 20, 128, 128},
[]int{48, 48, 64, 64},
[]int{1024, 1024, 128, 128},
}
for _, row := range table {
width := row[0]
height := row[1]
expectedWidth := row[2]
expectedHeight := row[3]
match := rs.ClosestMatch(width, height)
if match.Width != expectedWidth || match.Height != expectedHeight {
t.Errorf("Expected resolution %dx%d got %s", expectedWidth, expectedHeight, match.String())
}
}
}

View File

@@ -0,0 +1,110 @@
package thumbnail
import (
"fmt"
"image"
"math"
"strconv"
"strings"
"github.com/pkg/errors"
)
const (
_resolutionSeperator = "x"
)
// ParseResolution returns an image.Rectangle representing the resolution given as a string
func ParseResolution(s string) (image.Rectangle, error) {
parts := strings.Split(s, _resolutionSeperator)
if len(parts) != 2 {
return image.Rectangle{}, fmt.Errorf("failed to parse resolution: %s. Expected format <width>x<height>", s)
}
width, err := strconv.Atoi(parts[0])
if err != nil {
return image.Rectangle{}, fmt.Errorf("width: %s has an invalid value. Expected an integer", parts[0])
}
height, err := strconv.Atoi(parts[1])
if err != nil {
return image.Rectangle{}, fmt.Errorf("height: %s has an invalid value. Expected an integer", parts[1])
}
return image.Rect(0, 0, width, height), nil
}
// Resolutions is a list of image.Rectangle representing resolutions.
type Resolutions []image.Rectangle
// ParseResolutions creates an instance of Resolutions from resolution strings.
func ParseResolutions(strs []string) (Resolutions, error) {
var rs Resolutions
for _, s := range strs {
r, err := ParseResolution(s)
if err != nil {
return nil, errors.Wrap(err, "could not parse resolutions")
}
rs = append(rs, r)
}
return rs, nil
}
// 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 (rs Resolutions) ClosestMatch(requested image.Rectangle, sourceSize image.Rectangle) image.Rectangle {
isLandscape := sourceSize.Dx() > sourceSize.Dy()
sourceLen := dimensionLength(sourceSize, isLandscape)
requestedLen := dimensionLength(requested, isLandscape)
isSourceSmaller := sourceLen < requestedLen
// We don't want to scale images up.
if isSourceSmaller {
return sourceSize
}
if len(rs) == 0 {
return requested
}
var match image.Rectangle
// Since we want to search for the smallest difference we start with the highest possible number
minDiff := math.MaxInt32
for _, current := range rs {
cLen := dimensionLength(current, isLandscape)
diff := requestedLen - cLen
if diff > 0 {
// current is smaller
continue
}
// Convert diff to positive value
// Multiplying by -1 is safe since we aren't getting postive numbers here
// because of the check above
absDiff := diff * -1
if absDiff < minDiff {
minDiff = absDiff
match = current
}
}
if (match == image.Rectangle{}) {
match = rs[len(rs)-1]
}
return match
}
func mapRatio(given image.Rectangle, other image.Rectangle) image.Rectangle {
isLandscape := given.Dx() > given.Dy()
ratio := float64(given.Dx()) / float64(given.Dy())
if isLandscape {
return image.Rect(0, 0, other.Dx(), int(float64(other.Dx())/ratio))
}
return image.Rect(0, 0, int(float64(other.Dy())*ratio), other.Dy())
}
func dimensionLength(rect image.Rectangle, isLandscape bool) int {
if isLandscape {
return rect.Dx()
}
return rect.Dy()
}

View File

@@ -0,0 +1,139 @@
package thumbnail
import (
"image"
"testing"
)
func TestInitWithEmptyArray(t *testing.T) {
rs, err := ParseResolutions([]string{})
if err != nil {
t.Errorf("Init with an empty array should not fail. Error: %s.\n", err.Error())
}
if len(rs) != 0 {
t.Error("Init with an empty array should return an empty Resolutions instance.\n")
}
}
func TestInitWithNil(t *testing.T) {
rs, err := ParseResolutions(nil)
if err != nil {
t.Errorf("Init with nil parameter should not fail. Error: %s.\n", err.Error())
}
if len(rs) != 0 {
t.Error("Init with nil parameter should return an empty Resolutions instance.\n")
}
}
func TestInitWithInvalidValuesInArray(t *testing.T) {
_, err := ParseResolutions([]string{"invalid"})
if err == nil {
t.Error("Init with invalid parameter should fail.\n")
}
}
func TestInit(t *testing.T) {
rs, err := ParseResolutions([]string{"16x16"})
if err != nil {
t.Errorf("Init with valid parameter should not fail. Error: %s.\n", err.Error())
}
if len(rs) != 1 {
t.Errorf("resolutions has size %d, expected size %d.\n", len(rs), 1)
}
}
func TestInitWithMultipleResolutions(t *testing.T) {
rStrs := []string{"16x16", "32x32", "64x64", "128x128"}
rs, err := ParseResolutions(rStrs)
if err != nil {
t.Errorf("Init with valid parameter should not fail. Error: %s.\n", err.Error())
}
if len(rs) != len(rStrs) {
t.Errorf("resolutions has size %d, expected size %d.\n", len(rs), len(rStrs))
}
}
func TestClosestMatchWithEmptyResolutions(t *testing.T) {
rs, _ := ParseResolutions(nil)
want := image.Rect(0, 0, 24, 24)
imgSize := image.Rect(0, 0, 24, 24)
r := rs.ClosestMatch(want, imgSize)
if r.Dx() != want.Dx() || r.Dy() != want.Dy() {
t.Errorf("ClosestMatch from empty resolutions should return the given resolution")
}
}
func TestClosestMatch(t *testing.T) {
rs, _ := ParseResolutions([]string{"16x16", "24x24", "32x32", "64x64", "128x128"})
testData := [][]image.Rectangle{
{image.Rect(0, 0, 17, 17), image.Rect(0, 0, 1920, 1080), image.Rect(0, 0, 24, 24)},
{image.Rect(0, 0, 12, 17), image.Rect(0, 0, 1080, 1920), image.Rect(0, 0, 24, 24)},
{image.Rect(0, 0, 24, 24), image.Rect(0, 0, 1920, 1080), image.Rect(0, 0, 24, 24)},
{image.Rect(0, 0, 20, 20), image.Rect(0, 0, 1920, 1080), image.Rect(0, 0, 24, 24)},
{image.Rect(0, 0, 20, 80), image.Rect(0, 0, 1080, 1920), image.Rect(0, 0, 128, 128)},
{image.Rect(0, 0, 80, 20), image.Rect(0, 0, 1920, 1080), image.Rect(0, 0, 128, 128)},
{image.Rect(0, 0, 48, 48), image.Rect(0, 0, 1920, 1080), image.Rect(0, 0, 64, 64)},
{image.Rect(0, 0, 1024, 1024), image.Rect(0, 0, 1920, 1080), image.Rect(0, 0, 128, 128)},
{image.Rect(0, 0, 1920, 1080), image.Rect(0, 0, 256, 36), image.Rect(0, 0, 256, 36)},
}
for _, row := range testData {
given := row[0]
imgSize := row[1]
expected := row[2]
match := rs.ClosestMatch(given, imgSize)
if match != expected {
t.Errorf("Expected resolution %dx%d got %dx%d", expected.Dx(), expected.Dy(), match.Dx(), match.Dy())
}
}
}
func TestParseWithEmptyString(t *testing.T) {
_, err := ParseResolution("")
if err == nil {
t.Error("Parse with empty string should return an error.")
}
}
func TestParseWithInvalidWidth(t *testing.T) {
_, err := ParseResolution("invalidx42")
if err == nil {
t.Error("Parse with invalid width should return an error.")
}
}
func TestParseWithInvalidHeight(t *testing.T) {
_, err := ParseResolution("42xinvalid")
if err == nil {
t.Error("Parse with invalid height should return an error.")
}
}
func TestParseResolution(t *testing.T) {
rStr := "42x23"
r, _ := ParseResolution(rStr)
if r.Dx() != 42 || r.Dy() != 23 {
t.Errorf("Expected resolution %s got %s", rStr, r.String())
}
}
func TestMapRatio(t *testing.T) {
testData := [][]image.Rectangle{
{image.Rect(0, 0, 1920, 1080), image.Rect(0, 0, 32, 32), image.Rect(0, 0, 32, 18)},
{image.Rect(0, 0, 1080, 1920), image.Rect(0, 0, 32, 32), image.Rect(0, 0, 18, 32)},
{image.Rect(0, 0, 1024, 735), image.Rect(0, 0, 32, 32), image.Rect(0, 0, 32, 22)},
}
for _, row := range testData {
given := row[0]
other := row[1]
expected := row[2]
mapped := mapRatio(given, other)
if mapped.Dx() != expected.Dx() || mapped.Dy() != expected.Dy() {
t.Errorf("Expected %dx%d got %dx%d", expected.Dx(), expected.Dy(), mapped.Dx(), mapped.Dy())
}
}
}

View File

@@ -6,6 +6,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
@@ -70,7 +71,7 @@ func (s *FileSystem) Set(username string, key string, img []byte) error {
func (s *FileSystem) BuildKey(r Request) string {
etag := r.ETag
filetype := r.Types[0]
filename := r.Resolution.String() + "." + filetype
filename := strconv.Itoa(r.Resolution.Dx()) + "x" + strconv.Itoa(r.Resolution.Dy()) + "." + filetype
return filepath.Join(etag[:2], etag[2:4], etag[4:], filename)
}

View File

@@ -1,12 +1,14 @@
package storage
import "github.com/owncloud/ocis/thumbnails/pkg/thumbnail/resolution"
import (
"image"
)
// Request combines different attributes needed for storage operations.
type Request struct {
ETag string
Types []string
Resolution resolution.Resolution
Resolution image.Rectangle
}
// Storage defines the interface for a thumbnail store.

View File

@@ -4,16 +4,14 @@ import (
"bytes"
"image"
"github.com/nfnt/resize"
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/owncloud/ocis/thumbnails/pkg/thumbnail/resolution"
"github.com/owncloud/ocis/thumbnails/pkg/thumbnail/storage"
"golang.org/x/image/draw"
)
// Request bundles information needed to generate a thumbnail for afile
type Request struct {
Resolution resolution.Resolution
ImagePath string
Resolution image.Rectangle
Encoder Encoder
ETag string
Username string
@@ -29,22 +27,25 @@ type Manager interface {
}
// NewSimpleManager creates a new instance of SimpleManager
func NewSimpleManager(storage storage.Storage, logger log.Logger) SimpleManager {
func NewSimpleManager(resolutions Resolutions, storage storage.Storage, logger log.Logger) SimpleManager {
return SimpleManager{
storage: storage,
logger: logger,
storage: storage,
logger: logger,
resolutions: resolutions,
}
}
// SimpleManager is a simple implementation of Manager
type SimpleManager struct {
storage storage.Storage
logger log.Logger
storage storage.Storage
logger log.Logger
resolutions Resolutions
}
// Get implements the Get Method of Manager
func (s SimpleManager) Get(r Request, img image.Image) ([]byte, error) {
thumbnail := s.generate(r, img)
match := s.resolutions.ClosestMatch(r.Resolution, img.Bounds())
thumbnail := s.generate(match, img)
key := s.storage.BuildKey(mapToStorageRequest(r))
@@ -69,8 +70,10 @@ func (s SimpleManager) GetStored(r Request) []byte {
return stored
}
func (s SimpleManager) generate(r Request, img image.Image) image.Image {
thumbnail := resize.Thumbnail(uint(r.Resolution.Width), uint(r.Resolution.Height), img, resize.Lanczos2)
func (s SimpleManager) generate(r image.Rectangle, img image.Image) image.Image {
targetResolution := mapRatio(img.Bounds(), r)
thumbnail := image.NewRGBA(targetResolution)
draw.ApproxBiLinear.Scale(thumbnail, targetResolution, img, img.Bounds(), draw.Over, nil)
return thumbnail
}

View File

@@ -0,0 +1,47 @@
package thumbnail
import (
"image"
"os"
"path/filepath"
"testing"
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/owncloud/ocis/thumbnails/pkg/thumbnail/storage"
)
type NoOpManager struct {
storage.Storage
}
func (m NoOpManager) BuildKey(r storage.Request) string {
return ""
}
func (m NoOpManager) Set(username, key string, thumbnail []byte) error {
return nil
}
func BenchmarkGet(b *testing.B) {
sut := NewSimpleManager(
Resolutions{},
NoOpManager{},
log.NewLogger(),
)
res, _ := ParseResolution("32x32")
req := Request{
Resolution: res,
ETag: "1872ade88f3013edeb33decd74a4f947",
}
cwd, _ := os.Getwd()
p := filepath.Join(cwd, "../../testdata/oc.png")
f, _ := os.Open(p)
defer f.Close()
img, ext, _ := image.Decode(f)
req.Encoder = EncoderForType(ext)
for i := 0; i < b.N; i++ {
sut.Get(req, img)
}
}