Skip to content

Commit

Permalink
feature(thumbnails): add the ability to define custom image processors
Browse files Browse the repository at this point in the history
  • Loading branch information
fschade committed Oct 4, 2023
1 parent 0bf863b commit 148ff7a
Show file tree
Hide file tree
Showing 17 changed files with 434 additions and 83 deletions.
22 changes: 22 additions & 0 deletions changelog/unreleased/thumbnail-processors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Enhancement: Thumbnail generation with image processors

Thumbnails can now be changed during creation, previously the images were always scaled to fit the given frame,
but it could happen that the images were cut off because they could not be placed better due to the aspect ratio.

This pr introduces the possibility of specifying how the behavior should be, following processors are available

* resize
* fit
* fill
* thumbnail

the processor can be applied by adding the processor query param to the request, e.g. `processor=fit`, `processor=fill`, ...

to find out more how the individual processors work please read https://github.com/disintegration/imaging

if no processor is provided it behaves the same as before (resize for gif's and thumbnail for all other)

https://github.com/owncloud/ocis/pull/7409
https://github.com/owncloud/enterprise/issues/6057
https://github.com/owncloud/ocis/issues/5179
https://github.com/owncloud/web/issues/7728
116 changes: 63 additions & 53 deletions protogen/gen/ocis/services/thumbnails/v0/thumbnails.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions protogen/proto/ocis/services/thumbnails/v0/thumbnails.proto
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@ message GetThumbnailRequest {
int32 width = 3;
// The height of the thumbnail
int32 height = 4;
// Indicates which image processor to use
string processor = 5;
oneof source {
ocis.messages.thumbnails.v0.WebdavSource webdav_source = 5;
ocis.messages.thumbnails.v0.CS3Source cs3_source = 6;
ocis.messages.thumbnails.v0.WebdavSource webdav_source = 6;
ocis.messages.thumbnails.v0.CS3Source cs3_source = 7;
}
}

Expand Down
11 changes: 6 additions & 5 deletions services/thumbnails/pkg/service/grpc/v0/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@ import (
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/golang-jwt/jwt/v4"
"github.com/pkg/errors"
merrors "go-micro.dev/v4/errors"
"google.golang.org/grpc/metadata"

"github.com/owncloud/ocis/v2/ocis-pkg/log"
thumbnailssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/thumbnails/v0"
"github.com/owncloud/ocis/v2/services/thumbnails/pkg/preprocessor"
"github.com/owncloud/ocis/v2/services/thumbnails/pkg/service/grpc/v0/decorators"
tjwt "github.com/owncloud/ocis/v2/services/thumbnails/pkg/service/jwt"
"github.com/owncloud/ocis/v2/services/thumbnails/pkg/thumbnail"
"github.com/owncloud/ocis/v2/services/thumbnails/pkg/thumbnail/imgsource"
"github.com/pkg/errors"
merrors "go-micro.dev/v4/errors"
"google.golang.org/grpc/metadata"
)

// NewService returns a service implementation for Service.
Expand Down Expand Up @@ -124,7 +125,7 @@ func (g Thumbnail) handleCS3Source(ctx context.Context, req *thumbnailssvc.GetTh
if tType == "" {
tType = req.GetThumbnailType().String()
}
tr, err := thumbnail.PrepareRequest(int(req.Width), int(req.Height), tType, sRes.GetInfo().GetChecksum().GetSum())
tr, err := thumbnail.PrepareRequest(int(req.Width), int(req.Height), tType, sRes.GetInfo().GetChecksum().GetSum(), req.Processor)
if err != nil {
return "", merrors.BadRequest(g.serviceID, err.Error())
}
Expand Down Expand Up @@ -207,7 +208,7 @@ func (g Thumbnail) handleWebdavSource(ctx context.Context, req *thumbnailssvc.Ge
if tType == "" {
tType = req.GetThumbnailType().String()
}
tr, err := thumbnail.PrepareRequest(int(req.Width), int(req.Height), tType, sRes.GetInfo().GetChecksum().GetSum())
tr, err := thumbnail.PrepareRequest(int(req.Width), int(req.Height), tType, sRes.GetInfo().GetChecksum().GetSum(), req.Processor)
if err != nil {
return "", merrors.BadRequest(g.serviceID, err.Error())
}
Expand Down
4 changes: 2 additions & 2 deletions services/thumbnails/pkg/thumbnail/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ func EncoderForType(fileType string) (Encoder, error) {
}

// GetExtForMime return the supported extension by mime
func GetExtForMime(mime string) string {
ext := strings.TrimPrefix(strings.TrimSpace(strings.ToLower(mime)), "image/")
func GetExtForMime(fileType string) string {
ext := strings.TrimPrefix(strings.TrimSpace(strings.ToLower(fileType)), "image/")
switch ext {
case typeJpg, typeJpeg, typePng, typeGif:
return ext
Expand Down
14 changes: 6 additions & 8 deletions services/thumbnails/pkg/thumbnail/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,26 @@ import (
var (
// ErrInvalidType represents the error when a type can't be encoded.
ErrInvalidType2 = errors.New("can't encode this type")
// ErrNoGeneratorForType represents the error when no generator could be found for a type.
ErrNoGeneratorForType = errors.New("no generator for this type found")
)

type Generator interface {
GenerateThumbnail(image.Rectangle, interface{}) (interface{}, error)
Generate(image.Rectangle, interface{}, Processor) (interface{}, error)
}

type SimpleGenerator struct{}

func (g SimpleGenerator) GenerateThumbnail(size image.Rectangle, img interface{}) (interface{}, error) {
func (g SimpleGenerator) Generate(size image.Rectangle, img interface{}, processor Processor) (interface{}, error) {
m, ok := img.(image.Image)
if !ok {
return nil, ErrInvalidType2
}

return imaging.Thumbnail(m, size.Dx(), size.Dy(), imaging.Lanczos), nil
return processor.Process(m, size.Dx(), size.Dy(), imaging.Lanczos), nil
}

type GifGenerator struct{}

func (g GifGenerator) GenerateThumbnail(size image.Rectangle, img interface{}) (interface{}, error) {
func (g GifGenerator) Generate(size image.Rectangle, img interface{}, processor Processor) (interface{}, error) {
// Code inspired by https://github.com/willnorris/gifresize/blob/db93a7e1dcb1c279f7eeb99cc6d90b9e2e23e871/gifresize.go

m, ok := img.(*gif.GIF)
Expand All @@ -51,8 +49,8 @@ func (g GifGenerator) GenerateThumbnail(size image.Rectangle, img interface{}) (
bounds := frame.Bounds()
prev := tmp
draw.Draw(tmp, bounds, frame, bounds.Min, draw.Over)
scaled := imaging.Resize(tmp, size.Dx(), size.Dy(), imaging.Lanczos)
m.Image[i] = g.imageToPaletted(scaled, frame.Palette)
processed := processor.Process(tmp, size.Dx(), size.Dy(), imaging.Lanczos)
m.Image[i] = g.imageToPaletted(processed, frame.Palette)

switch m.Disposal[i] {
case gif.DisposalBackground:
Expand Down
51 changes: 51 additions & 0 deletions services/thumbnails/pkg/thumbnail/processor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package thumbnail

import (
"image"
"strings"

"github.com/disintegration/imaging"
)

// Processor processes the thumbnail by applying different transformations to it.
type Processor interface {
ID() string
Process(img image.Image, width, height int, filter imaging.ResampleFilter) *image.NRGBA
}

// DefinableProcessor is the most simple processor, it holds a replaceable image converter function.
type DefinableProcessor struct {
Slug string
Converter func(img image.Image, width, height int, filter imaging.ResampleFilter) *image.NRGBA
}

// ID returns the processor identification.
func (p DefinableProcessor) ID() string { return p.Slug }

// Process transforms the given image.
func (p DefinableProcessor) Process(img image.Image, width, height int, filter imaging.ResampleFilter) *image.NRGBA {
return p.Converter(img, width, height, filter)
}

// ProcessorFor returns a matching Processor
func ProcessorFor(id, fileType string) (Processor, error) {
switch strings.ToLower(id) {
case "fit":
return DefinableProcessor{Slug: strings.ToLower(id), Converter: imaging.Fit}, nil
case "resize":
return DefinableProcessor{Slug: strings.ToLower(id), Converter: imaging.Resize}, nil
case "fill":
return DefinableProcessor{Slug: strings.ToLower(id), Converter: func(img image.Image, width, height int, filter imaging.ResampleFilter) *image.NRGBA {
return imaging.Fill(img, width, height, imaging.Center, filter)
}}, nil
case "thumbnail":
return DefinableProcessor{Slug: strings.ToLower(id), Converter: imaging.Thumbnail}, nil
default:
switch strings.ToLower(fileType) {
case typeGif:
return DefinableProcessor{Converter: imaging.Resize}, nil
default:
return DefinableProcessor{Converter: imaging.Thumbnail}, nil
}
}
}
Loading

0 comments on commit 148ff7a

Please sign in to comment.