feature(thumbnails): add the ability to define custom image processors (#7409)

* feature(thumbnails): add the ability to define custom image processors

* fix(ci): add exported member comment

* docs(thumbnails): mention processors in readme

* fix: codacy and code review feedback

* fix: thumbnail readme markdown

Co-authored-by: Martin <github@diemattels.at>

---------

Co-authored-by: Martin <github@diemattels.at>
This commit is contained in:
Florian Schade
2023-10-17 09:44:44 +02:00
committed by GitHub
parent 224f439e08
commit 9abcd8a7f3
18 changed files with 458 additions and 97 deletions

View File

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

View File

@@ -36,6 +36,8 @@ type GetThumbnailRequest struct {
Width int32 `protobuf:"varint,3,opt,name=width,proto3" json:"width,omitempty"`
// The height of the thumbnail
Height int32 `protobuf:"varint,4,opt,name=height,proto3" json:"height,omitempty"`
// Indicates which processor should be used
Processor string `protobuf:"bytes,5,opt,name=processor,proto3" json:"processor,omitempty"`
// Types that are assignable to Source:
//
// *GetThumbnailRequest_WebdavSource
@@ -103,6 +105,13 @@ func (x *GetThumbnailRequest) GetHeight() int32 {
return 0
}
func (x *GetThumbnailRequest) GetProcessor() string {
if x != nil {
return x.Processor
}
return ""
}
func (m *GetThumbnailRequest) GetSource() isGetThumbnailRequest_Source {
if m != nil {
return m.Source
@@ -129,11 +138,11 @@ type isGetThumbnailRequest_Source interface {
}
type GetThumbnailRequest_WebdavSource struct {
WebdavSource *v0.WebdavSource `protobuf:"bytes,5,opt,name=webdav_source,json=webdavSource,proto3,oneof"`
WebdavSource *v0.WebdavSource `protobuf:"bytes,6,opt,name=webdav_source,json=webdavSource,proto3,oneof"`
}
type GetThumbnailRequest_Cs3Source struct {
Cs3Source *v0.CS3Source `protobuf:"bytes,6,opt,name=cs3_source,json=cs3Source,proto3,oneof"`
Cs3Source *v0.CS3Source `protobuf:"bytes,7,opt,name=cs3_source,json=cs3Source,proto3,oneof"`
}
func (*GetThumbnailRequest_WebdavSource) isGetThumbnailRequest_Source() {}
@@ -220,7 +229,7 @@ var file_ocis_services_thumbnails_v0_thumbnails_proto_rawDesc = []byte{
0x69, 0x6c, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x76, 0x32, 0x2f,
0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xd7, 0x02, 0x0a, 0x13, 0x47, 0x65,
0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf5, 0x02, 0x0a, 0x13, 0x47, 0x65,
0x74, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x70, 0x61, 0x74, 0x68, 0x12, 0x51, 0x0a,
@@ -231,58 +240,59 @@ var file_ocis_services_thumbnails_v0_thumbnails_proto_rawDesc = []byte{
0x65, 0x52, 0x0d, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x54, 0x79, 0x70, 0x65,
0x12, 0x14, 0x0a, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52,
0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74,
0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x50,
0x0a, 0x0d, 0x77, 0x65, 0x62, 0x64, 0x61, 0x76, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18,
0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73,
0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73,
0x2e, 0x76, 0x30, 0x2e, 0x57, 0x65, 0x62, 0x64, 0x61, 0x76, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x48, 0x00, 0x52, 0x0c, 0x77, 0x65, 0x62, 0x64, 0x61, 0x76, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x12, 0x47, 0x0a, 0x0a, 0x63, 0x73, 0x33, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x06,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73, 0x73,
0x61, 0x67, 0x65, 0x73, 0x2e, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2e,
0x76, 0x30, 0x2e, 0x43, 0x53, 0x33, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x00, 0x52, 0x09,
0x63, 0x73, 0x33, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x22, 0x7e, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e,
0x61, 0x69, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x64,
0x61, 0x74, 0x61, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0c, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74,
0x12, 0x25, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x5f, 0x74, 0x6f, 0x6b,
0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66,
0x65, 0x72, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x74,
0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x74,
0x79, 0x70, 0x65, 0x32, 0x87, 0x01, 0x0a, 0x10, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69,
0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x73, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x54,
0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x12, 0x30, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e,
0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x1c,
0x0a, 0x09, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28,
0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x12, 0x50, 0x0a, 0x0d,
0x77, 0x65, 0x62, 0x64, 0x61, 0x76, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x06, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61,
0x67, 0x65, 0x73, 0x2e, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2e, 0x76,
0x30, 0x2e, 0x57, 0x65, 0x62, 0x64, 0x61, 0x76, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x00,
0x52, 0x0c, 0x77, 0x65, 0x62, 0x64, 0x61, 0x76, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x47,
0x0a, 0x0a, 0x63, 0x73, 0x33, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x07, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
0x65, 0x73, 0x2e, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2e, 0x76, 0x30,
0x2e, 0x43, 0x53, 0x33, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x00, 0x52, 0x09, 0x63, 0x73,
0x33, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63,
0x65, 0x22, 0x7e, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69,
0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x64, 0x61, 0x74,
0x61, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0c, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x25,
0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72,
0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x74, 0x79, 0x70,
0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x74, 0x79, 0x70,
0x65, 0x32, 0x87, 0x01, 0x0a, 0x10, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x53,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x73, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x54, 0x68, 0x75,
0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x12, 0x30, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x73, 0x65,
0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c,
0x73, 0x2e, 0x76, 0x30, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69,
0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e,
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61,
0x69, 0x6c, 0x73, 0x2e, 0x76, 0x30, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e,
0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x6f, 0x63, 0x69,
0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x74, 0x68, 0x75, 0x6d, 0x62,
0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2e, 0x76, 0x30, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x68, 0x75, 0x6d,
0x62, 0x6e, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0xe9, 0x02,
0x5a, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x77, 0x6e,
0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x67, 0x65, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x73, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73,
0x2f, 0x76, 0x30, 0x92, 0x41, 0xa2, 0x02, 0x12, 0xb8, 0x01, 0x0a, 0x22, 0x6f, 0x77, 0x6e, 0x43,
0x6c, 0x6f, 0x75, 0x64, 0x20, 0x49, 0x6e, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x65, 0x20, 0x53, 0x63,
0x61, 0x6c, 0x65, 0x20, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x47,
0x0a, 0x0d, 0x6f, 0x77, 0x6e, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x20, 0x47, 0x6d, 0x62, 0x48, 0x12,
0x20, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6f, 0x63, 0x69,
0x73, 0x1a, 0x14, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x40, 0x6f, 0x77, 0x6e, 0x63, 0x6c,
0x6f, 0x75, 0x64, 0x2e, 0x63, 0x6f, 0x6d, 0x2a, 0x42, 0x0a, 0x0a, 0x41, 0x70, 0x61, 0x63, 0x68,
0x65, 0x2d, 0x32, 0x2e, 0x30, 0x12, 0x34, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67,
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f,
0x75, 0x64, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x62, 0x6c, 0x6f, 0x62, 0x2f, 0x6d, 0x61, 0x73,
0x74, 0x65, 0x72, 0x2f, 0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, 0x32, 0x05, 0x31, 0x2e, 0x30,
0x2e, 0x30, 0x2a, 0x02, 0x01, 0x02, 0x32, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x3a, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x72, 0x3d, 0x0a, 0x10, 0x44, 0x65,
0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x72, 0x20, 0x4d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x12, 0x29,
0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64,
0x2e, 0x64, 0x65, 0x76, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x74, 0x68,
0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x33,
0x61, 0x69, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0xe9, 0x02, 0x5a, 0x41,
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c,
0x6f, 0x75, 0x64, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65,
0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69,
0x63, 0x65, 0x73, 0x2f, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2f, 0x76,
0x30, 0x92, 0x41, 0xa2, 0x02, 0x12, 0xb8, 0x01, 0x0a, 0x22, 0x6f, 0x77, 0x6e, 0x43, 0x6c, 0x6f,
0x75, 0x64, 0x20, 0x49, 0x6e, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x65, 0x20, 0x53, 0x63, 0x61, 0x6c,
0x65, 0x20, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x47, 0x0a, 0x0d,
0x6f, 0x77, 0x6e, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x20, 0x47, 0x6d, 0x62, 0x48, 0x12, 0x20, 0x68,
0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
0x6d, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x1a,
0x14, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x40, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75,
0x64, 0x2e, 0x63, 0x6f, 0x6d, 0x2a, 0x42, 0x0a, 0x0a, 0x41, 0x70, 0x61, 0x63, 0x68, 0x65, 0x2d,
0x32, 0x2e, 0x30, 0x12, 0x34, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64,
0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x62, 0x6c, 0x6f, 0x62, 0x2f, 0x6d, 0x61, 0x73, 0x74, 0x65,
0x72, 0x2f, 0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, 0x32, 0x05, 0x31, 0x2e, 0x30, 0x2e, 0x30,
0x2a, 0x02, 0x01, 0x02, 0x32, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x3a, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x72, 0x3d, 0x0a, 0x10, 0x44, 0x65, 0x76, 0x65,
0x6c, 0x6f, 0x70, 0x65, 0x72, 0x20, 0x4d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x12, 0x29, 0x68, 0x74,
0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x64,
0x65, 0x76, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x74, 0x68, 0x75, 0x6d,
0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (

View File

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

View File

@@ -10,9 +10,9 @@ The relevant environment variables defining file locations are:
- (2) `STORAGE_USERS_OCIS_ROOT`
- (3) `THUMBNAILS_FILESYSTEMSTORAGE_ROOT`
(1) ... Having a default set by the Infinite Scale code, but if defined, used as base path for other services.
(2) ... Source files, defaults to (1) plus path component, but can be freely defined if required.
(3) ... Target files, defaults to (1) plus path component, but can be freely defined if required.
(1) ... Having a default set by the Infinite Scale code, but if defined, used as base path for other services.
(2) ... Source files, defaults to (1) plus path component, but can be freely defined if required.
(3) ... Target files, defaults to (1) plus path component, but can be freely defined if required.
For details and defaults for these environment variables see the ocis admin documentation.
@@ -45,9 +45,20 @@ Various resolutions can be defined via `THUMBNAILS_RESOLUTIONS`. A requestor can
Example:
Requested: 18x12
Available: 30x20, 15x10, 9x6
Returned: 15x10
Requested: 18x12
Available: 30x20, 15x10, 9x6
Returned: 15x10
## Thumbnail Processors
Image generation can be configured by defining different processors, following processors are available:
* `resize`
* `fit`
* `fill`
* `thumbnail`
To apply one of those, a query parameter has to be added to the request, e.g. `?processor=fit`
## Deleting Thumbnails

View File

@@ -16,6 +16,10 @@ 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"
@@ -23,9 +27,6 @@ import (
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.
@@ -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())
}
@@ -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())
}

View File

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

View File

@@ -1,7 +1,6 @@
package thumbnail
import (
"errors"
"image"
"image/color"
"image/draw"
@@ -11,36 +10,34 @@ import (
"github.com/disintegration/imaging"
)
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")
)
// Generator generates a web friendly file version.
type Generator interface {
GenerateThumbnail(image.Rectangle, interface{}) (interface{}, error)
Generate(image.Rectangle, interface{}, Processor) (interface{}, error)
}
// SimpleGenerator is the default image generator and is used for all image types expect gif.
type SimpleGenerator struct{}
func (g SimpleGenerator) GenerateThumbnail(size image.Rectangle, img interface{}) (interface{}, error) {
// Generate generates a alternative image version.
func (g SimpleGenerator) Generate(size image.Rectangle, img interface{}, processor Processor) (interface{}, error) {
m, ok := img.(image.Image)
if !ok {
return nil, ErrInvalidType2
return nil, ErrInvalidType
}
return imaging.Thumbnail(m, size.Dx(), size.Dy(), imaging.Lanczos), nil
return processor.Process(m, size.Dx(), size.Dy(), imaging.Lanczos), nil
}
// GifGenerator is used to create a web friendly version of the provided gif image.
type GifGenerator struct{}
func (g GifGenerator) GenerateThumbnail(size image.Rectangle, img interface{}) (interface{}, error) {
// Generate generates a alternative gif version.
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)
if !ok {
return nil, ErrInvalidType2
return nil, ErrInvalidType
}
// Create a new RGBA image to hold the incremental frames.
srcX, srcY := m.Config.Width, m.Config.Height
@@ -51,8 +48,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:

View File

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

View File

@@ -0,0 +1,98 @@
package thumbnail_test
import (
"testing"
"github.com/disintegration/imaging"
tAssert "github.com/stretchr/testify/assert"
"github.com/owncloud/ocis/v2/services/thumbnails/pkg/thumbnail"
)
func TestProcessorFor(t *testing.T) {
tests := []struct {
id string
fileType string
wantP thumbnail.Processor
wantE error
}{
{
id: "fit",
fileType: "",
wantP: thumbnail.DefinableProcessor{Slug: "fit", Converter: imaging.Fit},
wantE: nil,
},
{
id: "fit",
fileType: "jpg",
wantP: thumbnail.DefinableProcessor{Slug: "fit"},
wantE: nil,
},
{
id: "FIT",
fileType: "jpg",
wantP: thumbnail.DefinableProcessor{Slug: "fit"},
wantE: nil,
},
{
id: "resize",
fileType: "jpg",
wantP: thumbnail.DefinableProcessor{Slug: "resize"},
wantE: nil,
},
{
id: "RESIZE",
fileType: "jpg",
wantP: thumbnail.DefinableProcessor{Slug: "resize"},
wantE: nil,
},
{
id: "fill",
fileType: "jpg",
wantP: thumbnail.DefinableProcessor{Slug: "fill"},
wantE: nil,
},
{
id: "FILL",
fileType: "jpg",
wantP: thumbnail.DefinableProcessor{Slug: "fill"},
wantE: nil,
},
{
id: "thumbnail",
fileType: "jpg",
wantP: thumbnail.DefinableProcessor{Slug: "thumbnail"},
wantE: nil,
},
{
id: "THUMBNAIL",
fileType: "jpg",
wantP: thumbnail.DefinableProcessor{Slug: "thumbnail"},
wantE: nil,
},
{
id: "",
fileType: "jpg",
wantP: thumbnail.DefinableProcessor{},
wantE: nil,
},
{
id: "",
fileType: "gif",
wantP: thumbnail.DefinableProcessor{},
wantE: nil,
},
}
assert := tAssert.New(t)
for _, tt := range tests {
tt := tt
t.Run("", func(t *testing.T) {
p, e := thumbnail.ProcessorFor(tt.id, tt.fileType)
assert.Equal(p.ID(), tt.wantP.ID())
assert.Equal(e, tt.wantE)
})
}
}

View File

@@ -5,10 +5,12 @@ import (
"os"
"path/filepath"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/services/thumbnails/pkg/config"
"github.com/pkg/errors"
)
const (
@@ -82,7 +84,14 @@ func (s FileSystem) Put(key string, img []byte) error {
func (s FileSystem) BuildKey(r Request) string {
checksum := r.Checksum
filetype := r.Types[0]
filename := strconv.Itoa(r.Resolution.Dx()) + "x" + strconv.Itoa(r.Resolution.Dy()) + "." + filetype
return filepath.Join(checksum[:2], checksum[2:4], checksum[4:], filename)
parts := []string{strconv.Itoa(r.Resolution.Dx()), "x", strconv.Itoa(r.Resolution.Dy())}
if r.Characteristic != "" {
parts = append(parts, "-", r.Characteristic)
}
parts = append(parts, ".", filetype)
return filepath.Join(checksum[:2], checksum[2:4], checksum[4:], strings.Join(parts, ""))
}

View File

@@ -0,0 +1,65 @@
package storage_test
import (
"image"
"testing"
tAssert "github.com/stretchr/testify/assert"
"github.com/owncloud/ocis/v2/services/thumbnails/pkg/thumbnail/storage"
)
func TestFileSystem_BuildKey(t *testing.T) {
tests := []struct {
r storage.Request
want string
}{
{
r: storage.Request{
Checksum: "120EA8A25E5D487BF68B5F7096440019",
Types: []string{"png", "jpg"},
Resolution: image.Rectangle{
Min: image.Point{
X: 1,
Y: 2,
},
Max: image.Point{
X: 3,
Y: 4,
},
},
Characteristic: "",
},
want: "12/0E/A8A25E5D487BF68B5F7096440019/2x2.png",
},
{
r: storage.Request{
Checksum: "120EA8A25E5D487BF68B5F7096440019",
Types: []string{"png", "jpg"},
Resolution: image.Rectangle{
Min: image.Point{
X: 1,
Y: 2,
},
Max: image.Point{
X: 3,
Y: 4,
},
},
Characteristic: "fill",
},
want: "12/0E/A8A25E5D487BF68B5F7096440019/2x2-fill.png",
},
}
s := storage.FileSystem{}
assert := tAssert.New(t)
for _, tt := range tests {
tt := tt
t.Run("", func(t *testing.T) {
assert.Equal(s.BuildKey(tt.r), tt.want)
})
}
}

View File

@@ -38,7 +38,13 @@ func (s InMemory) BuildKey(r Request) string {
parts := []string{
r.Checksum,
r.Resolution.String(),
strings.Join(r.Types, ","),
}
if r.Characteristic != "" {
parts = append(parts, r.Characteristic)
}
parts = append(parts, strings.Join(r.Types, ","))
return strings.Join(parts, "+")
}

View File

@@ -0,0 +1,65 @@
package storage_test
import (
"image"
"testing"
tAssert "github.com/stretchr/testify/assert"
"github.com/owncloud/ocis/v2/services/thumbnails/pkg/thumbnail/storage"
)
func TestInMemory_BuildKey(t *testing.T) {
tests := []struct {
r storage.Request
want string
}{
{
r: storage.Request{
Checksum: "cs",
Types: []string{"png", "jpg"},
Resolution: image.Rectangle{
Min: image.Point{
X: 1,
Y: 2,
},
Max: image.Point{
X: 3,
Y: 4,
},
},
Characteristic: "",
},
want: "cs+(1,2)-(3,4)+png,jpg",
},
{
r: storage.Request{
Checksum: "cs",
Types: []string{"png", "jpg"},
Resolution: image.Rectangle{
Min: image.Point{
X: 1,
Y: 2,
},
Max: image.Point{
X: 3,
Y: 4,
},
},
Characteristic: "fill",
},
want: "cs+(1,2)-(3,4)+fill+png,jpg",
},
}
s := storage.InMemory{}
assert := tAssert.New(t)
for _, tt := range tests {
tt := tt
t.Run("", func(t *testing.T) {
assert.Equal(s.BuildKey(tt.r), tt.want)
})
}
}

View File

@@ -15,6 +15,12 @@ type Request struct {
Types []string
// The resolution of the thumbnail
Resolution image.Rectangle
// Characteristic defines the different image characteristics,
// for example, if its scaled up to fit in the bounding box or not,
// is it a chroma version of the image, and so on...
// the main propose for this is to be able to differentiate between images which have
// the same resolution but different characteristics.
Characteristic string
}
// Storage defines the interface for a thumbnail store.

View File

@@ -30,6 +30,7 @@ type Request struct {
Encoder Encoder
Generator Generator
Checksum string
Processor Processor
}
// Manager is responsible for generating thumbnails
@@ -69,7 +70,7 @@ func (s SimpleManager) Generate(r Request, img interface{}) (string, error) {
match = s.resolutions.ClosestMatch(r.Resolution, m.Bounds())
}
thumbnail, err := r.Generator.GenerateThumbnail(match, img)
thumbnail, err := r.Generator.Generate(match, img, r.Processor)
if err != nil {
return "", err
}
@@ -98,9 +99,10 @@ func (s SimpleManager) GetThumbnail(key string) ([]byte, error) {
func mapToStorageRequest(r Request) storage.Request {
return storage.Request{
Checksum: r.Checksum,
Resolution: r.Resolution,
Types: r.Encoder.Types(),
Checksum: r.Checksum,
Resolution: r.Resolution,
Types: r.Encoder.Types(),
Characteristic: r.Processor.ID(),
}
}
@@ -115,7 +117,7 @@ func IsMimeTypeSupported(m string) bool {
}
// PrepareRequest prepare the request based on image parameters
func PrepareRequest(width, height int, tType, checksum string) (Request, error) {
func PrepareRequest(width, height int, tType, checksum, pID string) (Request, error) {
generator, err := GeneratorForType(tType)
if err != nil {
return Request{}, err
@@ -124,11 +126,16 @@ func PrepareRequest(width, height int, tType, checksum string) (Request, error)
if err != nil {
return Request{}, err
}
processor, err := ProcessorFor(pID, tType)
if err != nil {
return Request{}, err
}
return Request{
Resolution: image.Rect(0, 0, width, height),
Generator: generator,
Encoder: encoder,
Checksum: checksum,
Processor: processor,
}, nil
}

View File

@@ -4,9 +4,11 @@ import (
"image"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/services/thumbnails/pkg/thumbnail/storage"
)
@@ -118,13 +120,15 @@ func TestPrepareRequest(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := PrepareRequest(tt.args.width, tt.args.height, tt.args.tType, tt.args.checksum)
got, err := PrepareRequest(tt.args.width, tt.args.height, tt.args.tType, tt.args.checksum, "")
if (err != nil) != tt.wantErr {
t.Errorf("PrepareRequest() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("PrepareRequest() got = %v, want %v", got, tt.want)
// func's are not reflactable, ignore
if diff := cmp.Diff(tt.want, got, cmpopts.IgnoreFields(Request{}, "Processor")); diff != "" {
t.Errorf("PrepareRequest(): %v", diff)
}
})
}

View File

@@ -36,6 +36,8 @@ type ThumbnailRequest struct {
Height int32
// In case of a public share the public link token.
PublicLinkToken string
// Indicates which image processor to use
Processor string
// The Identifier from the requested URL
Identifier string
}
@@ -73,6 +75,7 @@ func ParseThumbnailRequest(r *http.Request) (*ThumbnailRequest, error) {
Extension: filepath.Ext(fp),
Width: int32(width),
Height: int32(height),
Processor: q.Get("processor"),
PublicLinkToken: chi.URLParam(r, "token"),
Identifier: id,
}, nil

View File

@@ -243,6 +243,7 @@ func (g Webdav) SpacesThumbnail(w http.ResponseWriter, r *http.Request) {
ThumbnailType: extensionToThumbnailType(strings.TrimLeft(tr.Extension, ".")),
Width: tr.Width,
Height: tr.Height,
Processor: tr.Processor,
Source: &thumbnailssvc.GetThumbnailRequest_Cs3Source{
Cs3Source: &thumbnailsmsg.CS3Source{
Path: fullPath,
@@ -335,6 +336,7 @@ func (g Webdav) Thumbnail(w http.ResponseWriter, r *http.Request) {
ThumbnailType: extensionToThumbnailType(strings.TrimLeft(tr.Extension, ".")),
Width: tr.Width,
Height: tr.Height,
Processor: tr.Processor,
Source: &thumbnailssvc.GetThumbnailRequest_Cs3Source{
Cs3Source: &thumbnailsmsg.CS3Source{
Path: fullPath,
@@ -375,6 +377,7 @@ func (g Webdav) PublicThumbnail(w http.ResponseWriter, r *http.Request) {
ThumbnailType: extensionToThumbnailType(strings.TrimLeft(tr.Extension, ".")),
Width: tr.Width,
Height: tr.Height,
Processor: tr.Processor,
Source: &thumbnailssvc.GetThumbnailRequest_WebdavSource{
WebdavSource: &thumbnailsmsg.WebdavSource{
Url: g.config.OcisPublicURL + r.URL.RequestURI(),
@@ -416,6 +419,7 @@ func (g Webdav) PublicThumbnailHead(w http.ResponseWriter, r *http.Request) {
ThumbnailType: extensionToThumbnailType(strings.TrimLeft(tr.Extension, ".")),
Width: tr.Width,
Height: tr.Height,
Processor: tr.Processor,
Source: &thumbnailssvc.GetThumbnailRequest_WebdavSource{
WebdavSource: &thumbnailsmsg.WebdavSource{
Url: g.config.OcisPublicURL + r.URL.RequestURI(),