1
Fork 0

Merge pull request #725 from PJ-Watson/thumbnail_testing

Thumbnail Rendering Options
This commit is contained in:
Viktor Strate Kløvedal 2022-08-09 10:15:08 +02:00 committed by GitHub
commit ced9e3209f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 597 additions and 28 deletions

View File

@ -151,30 +151,31 @@ type ComplexityRoot struct {
} }
Mutation struct { Mutation struct {
AuthorizeUser func(childComplexity int, username string, password string) int AuthorizeUser func(childComplexity int, username string, password string) int
ChangeUserPreferences func(childComplexity int, language *string) int ChangeUserPreferences func(childComplexity int, language *string) int
CombineFaceGroups func(childComplexity int, destinationFaceGroupID int, sourceFaceGroupID int) int CombineFaceGroups func(childComplexity int, destinationFaceGroupID int, sourceFaceGroupID int) int
CreateUser func(childComplexity int, username string, password *string, admin bool) int CreateUser func(childComplexity int, username string, password *string, admin bool) int
DeleteShareToken func(childComplexity int, token string) int DeleteShareToken func(childComplexity int, token string) int
DeleteUser func(childComplexity int, id int) int DeleteUser func(childComplexity int, id int) int
DetachImageFaces func(childComplexity int, imageFaceIDs []int) int DetachImageFaces func(childComplexity int, imageFaceIDs []int) int
FavoriteMedia func(childComplexity int, mediaID int, favorite bool) int FavoriteMedia func(childComplexity int, mediaID int, favorite bool) int
InitialSetupWizard func(childComplexity int, username string, password string, rootPath string) int InitialSetupWizard func(childComplexity int, username string, password string, rootPath string) int
MoveImageFaces func(childComplexity int, imageFaceIDs []int, destinationFaceGroupID int) int MoveImageFaces func(childComplexity int, imageFaceIDs []int, destinationFaceGroupID int) int
ProtectShareToken func(childComplexity int, token string, password *string) int ProtectShareToken func(childComplexity int, token string, password *string) int
RecognizeUnlabeledFaces func(childComplexity int) int RecognizeUnlabeledFaces func(childComplexity int) int
ResetAlbumCover func(childComplexity int, albumID int) int ResetAlbumCover func(childComplexity int, albumID int) int
ScanAll func(childComplexity int) int ScanAll func(childComplexity int) int
ScanUser func(childComplexity int, userID int) int ScanUser func(childComplexity int, userID int) int
SetAlbumCover func(childComplexity int, coverID int) int SetAlbumCover func(childComplexity int, coverID int) int
SetFaceGroupLabel func(childComplexity int, faceGroupID int, label *string) int SetFaceGroupLabel func(childComplexity int, faceGroupID int, label *string) int
SetPeriodicScanInterval func(childComplexity int, interval int) int SetPeriodicScanInterval func(childComplexity int, interval int) int
SetScannerConcurrentWorkers func(childComplexity int, workers int) int SetScannerConcurrentWorkers func(childComplexity int, workers int) int
ShareAlbum func(childComplexity int, albumID int, expire *time.Time, password *string) int SetThumbnailDownsampleMethod func(childComplexity int, method models.ThumbnailFilter) int
ShareMedia func(childComplexity int, mediaID int, expire *time.Time, password *string) int ShareAlbum func(childComplexity int, albumID int, expire *time.Time, password *string) int
UpdateUser func(childComplexity int, id int, username *string, password *string, admin *bool) int ShareMedia func(childComplexity int, mediaID int, expire *time.Time, password *string) int
UserAddRootPath func(childComplexity int, id int, rootPath string) int UpdateUser func(childComplexity int, id int, username *string, password *string, admin *bool) int
UserRemoveRootAlbum func(childComplexity int, userID int, albumID int) int UserAddRootPath func(childComplexity int, id int, rootPath string) int
UserRemoveRootAlbum func(childComplexity int, userID int, albumID int) int
} }
Notification struct { Notification struct {
@ -236,6 +237,7 @@ type ComplexityRoot struct {
FaceDetectionEnabled func(childComplexity int) int FaceDetectionEnabled func(childComplexity int) int
InitialSetup func(childComplexity int) int InitialSetup func(childComplexity int) int
PeriodicScanInterval func(childComplexity int) int PeriodicScanInterval func(childComplexity int) int
ThumbnailMethod func(childComplexity int) int
} }
Subscription struct { Subscription struct {
@ -326,6 +328,7 @@ type MutationResolver interface {
UserRemoveRootAlbum(ctx context.Context, userID int, albumID int) (*models.Album, error) UserRemoveRootAlbum(ctx context.Context, userID int, albumID int) (*models.Album, error)
SetPeriodicScanInterval(ctx context.Context, interval int) (int, error) SetPeriodicScanInterval(ctx context.Context, interval int) (int, error)
SetScannerConcurrentWorkers(ctx context.Context, workers int) (int, error) SetScannerConcurrentWorkers(ctx context.Context, workers int) (int, error)
SetThumbnailDownsampleMethod(ctx context.Context, method models.ThumbnailFilter) (models.ThumbnailFilter, error)
ChangeUserPreferences(ctx context.Context, language *string) (*models.UserPreferences, error) ChangeUserPreferences(ctx context.Context, language *string) (*models.UserPreferences, error)
ResetAlbumCover(ctx context.Context, albumID int) (*models.Album, error) ResetAlbumCover(ctx context.Context, albumID int) (*models.Album, error)
SetAlbumCover(ctx context.Context, coverID int) (*models.Album, error) SetAlbumCover(ctx context.Context, coverID int) (*models.Album, error)
@ -1057,6 +1060,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.SetScannerConcurrentWorkers(childComplexity, args["workers"].(int)), true return e.complexity.Mutation.SetScannerConcurrentWorkers(childComplexity, args["workers"].(int)), true
case "Mutation.setThumbnailDownsampleMethod":
if e.complexity.Mutation.SetThumbnailDownsampleMethod == nil {
break
}
args, err := ec.field_Mutation_setThumbnailDownsampleMethod_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.SetThumbnailDownsampleMethod(childComplexity, args["method"].(models.ThumbnailFilter)), true
case "Mutation.shareAlbum": case "Mutation.shareAlbum":
if e.complexity.Mutation.ShareAlbum == nil { if e.complexity.Mutation.ShareAlbum == nil {
break break
@ -1478,6 +1493,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.SiteInfo.PeriodicScanInterval(childComplexity), true return e.complexity.SiteInfo.PeriodicScanInterval(childComplexity), true
case "SiteInfo.thumbnailMethod":
if e.complexity.SiteInfo.ThumbnailMethod == nil {
break
}
return e.complexity.SiteInfo.ThumbnailMethod(childComplexity), true
case "Subscription.notification": case "Subscription.notification":
if e.complexity.Subscription.Notification == nil { if e.complexity.Subscription.Notification == nil {
break break
@ -2156,6 +2178,21 @@ func (ec *executionContext) field_Mutation_setScannerConcurrentWorkers_args(ctx
return args, nil return args, nil
} }
func (ec *executionContext) field_Mutation_setThumbnailDownsampleMethod_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 models.ThumbnailFilter
if tmp, ok := rawArgs["method"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("method"))
arg0, err = ec.unmarshalNThumbnailFilter2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐThumbnailFilter(ctx, tmp)
if err != nil {
return nil, err
}
}
args["method"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_shareAlbum_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { func (ec *executionContext) field_Mutation_shareAlbum_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error var err error
args := map[string]interface{}{} args := map[string]interface{}{}
@ -7158,6 +7195,81 @@ func (ec *executionContext) fieldContext_Mutation_setScannerConcurrentWorkers(ct
return fc, nil return fc, nil
} }
func (ec *executionContext) _Mutation_setThumbnailDownsampleMethod(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_setThumbnailDownsampleMethod(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
directive0 := func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().SetThumbnailDownsampleMethod(rctx, fc.Args["method"].(models.ThumbnailFilter))
}
directive1 := func(ctx context.Context) (interface{}, error) {
if ec.directives.IsAdmin == nil {
return nil, errors.New("directive isAdmin is not implemented")
}
return ec.directives.IsAdmin(ctx, nil, directive0)
}
tmp, err := directive1(rctx)
if err != nil {
return nil, graphql.ErrorOnPath(ctx, err)
}
if tmp == nil {
return nil, nil
}
if data, ok := tmp.(models.ThumbnailFilter); ok {
return data, nil
}
return nil, fmt.Errorf(`unexpected type %T from directive, should be github.com/photoview/photoview/api/graphql/models.ThumbnailFilter`, tmp)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(models.ThumbnailFilter)
fc.Result = res
return ec.marshalNThumbnailFilter2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐThumbnailFilter(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Mutation_setThumbnailDownsampleMethod(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Mutation",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type ThumbnailFilter does not have child fields")
},
}
defer func() {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Mutation_setThumbnailDownsampleMethod_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return
}
return fc, nil
}
func (ec *executionContext) _Mutation_changeUserPreferences(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { func (ec *executionContext) _Mutation_changeUserPreferences(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_changeUserPreferences(ctx, field) fc, err := ec.fieldContext_Mutation_changeUserPreferences(ctx, field)
if err != nil { if err != nil {
@ -8240,6 +8352,8 @@ func (ec *executionContext) fieldContext_Query_siteInfo(ctx context.Context, fie
return ec.fieldContext_SiteInfo_periodicScanInterval(ctx, field) return ec.fieldContext_SiteInfo_periodicScanInterval(ctx, field)
case "concurrentWorkers": case "concurrentWorkers":
return ec.fieldContext_SiteInfo_concurrentWorkers(ctx, field) return ec.fieldContext_SiteInfo_concurrentWorkers(ctx, field)
case "thumbnailMethod":
return ec.fieldContext_SiteInfo_thumbnailMethod(ctx, field)
} }
return nil, fmt.Errorf("no field named %q was found under type SiteInfo", field.Name) return nil, fmt.Errorf("no field named %q was found under type SiteInfo", field.Name)
}, },
@ -10584,6 +10698,70 @@ func (ec *executionContext) fieldContext_SiteInfo_concurrentWorkers(ctx context.
return fc, nil return fc, nil
} }
func (ec *executionContext) _SiteInfo_thumbnailMethod(ctx context.Context, field graphql.CollectedField, obj *models.SiteInfo) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_SiteInfo_thumbnailMethod(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
directive0 := func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.ThumbnailMethod, nil
}
directive1 := func(ctx context.Context) (interface{}, error) {
if ec.directives.IsAdmin == nil {
return nil, errors.New("directive isAdmin is not implemented")
}
return ec.directives.IsAdmin(ctx, obj, directive0)
}
tmp, err := directive1(rctx)
if err != nil {
return nil, graphql.ErrorOnPath(ctx, err)
}
if tmp == nil {
return nil, nil
}
if data, ok := tmp.(models.ThumbnailFilter); ok {
return data, nil
}
return nil, fmt.Errorf(`unexpected type %T from directive, should be github.com/photoview/photoview/api/graphql/models.ThumbnailFilter`, tmp)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(models.ThumbnailFilter)
fc.Result = res
return ec.marshalNThumbnailFilter2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐThumbnailFilter(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_SiteInfo_thumbnailMethod(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "SiteInfo",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type ThumbnailFilter does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Subscription_notification(ctx context.Context, field graphql.CollectedField) (ret func(ctx context.Context) graphql.Marshaler) { func (ec *executionContext) _Subscription_notification(ctx context.Context, field graphql.CollectedField) (ret func(ctx context.Context) graphql.Marshaler) {
fc, err := ec.fieldContext_Subscription_notification(ctx, field) fc, err := ec.fieldContext_Subscription_notification(ctx, field)
if err != nil { if err != nil {
@ -14625,6 +14803,15 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
return ec._Mutation_setScannerConcurrentWorkers(ctx, field) return ec._Mutation_setScannerConcurrentWorkers(ctx, field)
}) })
if out.Values[i] == graphql.Null {
invalids++
}
case "setThumbnailDownsampleMethod":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_setThumbnailDownsampleMethod(ctx, field)
})
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
@ -15419,6 +15606,13 @@ func (ec *executionContext) _SiteInfo(ctx context.Context, sel ast.SelectionSet,
out.Values[i] = ec._SiteInfo_concurrentWorkers(ctx, field, obj) out.Values[i] = ec._SiteInfo_concurrentWorkers(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
case "thumbnailMethod":
out.Values[i] = ec._SiteInfo_thumbnailMethod(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1) atomic.AddUint32(&invalids, 1)
} }
@ -16608,6 +16802,16 @@ func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.S
return res return res
} }
func (ec *executionContext) unmarshalNThumbnailFilter2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐThumbnailFilter(ctx context.Context, v interface{}) (models.ThumbnailFilter, error) {
var res models.ThumbnailFilter
err := res.UnmarshalGQL(v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNThumbnailFilter2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐThumbnailFilter(ctx context.Context, sel ast.SelectionSet, v models.ThumbnailFilter) graphql.Marshaler {
return v
}
func (ec *executionContext) unmarshalNTime2timeᚐTime(ctx context.Context, v interface{}) (time.Time, error) { func (ec *executionContext) unmarshalNTime2timeᚐTime(ctx context.Context, v interface{}) (time.Time, error) {
res, err := graphql.UnmarshalTime(v) res, err := graphql.UnmarshalTime(v)
return res, graphql.ErrorOnPath(ctx, err) return res, graphql.ErrorOnPath(ctx, err)

View File

@ -251,3 +251,53 @@ func (e *OrderDirection) UnmarshalGQL(v interface{}) error {
func (e OrderDirection) MarshalGQL(w io.Writer) { func (e OrderDirection) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String())) fmt.Fprint(w, strconv.Quote(e.String()))
} }
// Supported downsampling filters for thumbnail generation
type ThumbnailFilter string
const (
ThumbnailFilterNearestNeighbor ThumbnailFilter = "NearestNeighbor"
ThumbnailFilterBox ThumbnailFilter = "Box"
ThumbnailFilterLinear ThumbnailFilter = "Linear"
ThumbnailFilterMitchellNetravali ThumbnailFilter = "MitchellNetravali"
ThumbnailFilterCatmullRom ThumbnailFilter = "CatmullRom"
ThumbnailFilterLanczos ThumbnailFilter = "Lanczos"
)
var AllThumbnailFilter = []ThumbnailFilter{
ThumbnailFilterNearestNeighbor,
ThumbnailFilterBox,
ThumbnailFilterLinear,
ThumbnailFilterMitchellNetravali,
ThumbnailFilterCatmullRom,
ThumbnailFilterLanczos,
}
func (e ThumbnailFilter) IsValid() bool {
switch e {
case ThumbnailFilterNearestNeighbor, ThumbnailFilterBox, ThumbnailFilterLinear, ThumbnailFilterMitchellNetravali, ThumbnailFilterCatmullRom, ThumbnailFilterLanczos:
return true
}
return false
}
func (e ThumbnailFilter) String() string {
return string(e)
}
func (e *ThumbnailFilter) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = ThumbnailFilter(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid ThumbnailFilter", str)
}
return nil
}
func (e ThumbnailFilter) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}

View File

@ -10,6 +10,7 @@ type SiteInfo struct {
InitialSetup bool `gorm:"not null"` InitialSetup bool `gorm:"not null"`
PeriodicScanInterval int `gorm:"not null"` PeriodicScanInterval int `gorm:"not null"`
ConcurrentWorkers int `gorm:"not null"` ConcurrentWorkers int `gorm:"not null"`
ThumbnailMethod ThumbnailFilter `gorm:"not null"`
} }
func (SiteInfo) TableName() string { func (SiteInfo) TableName() string {
@ -26,6 +27,7 @@ func DefaultSiteInfo(db *gorm.DB) SiteInfo {
InitialSetup: true, InitialSetup: true,
PeriodicScanInterval: 0, PeriodicScanInterval: 0,
ConcurrentWorkers: defaultConcurrentWorkers, ConcurrentWorkers: defaultConcurrentWorkers,
ThumbnailMethod: ThumbnailFilterNearestNeighbor,
} }
} }

View File

@ -22,6 +22,7 @@ func TestSiteInfo(t *testing.T) {
site_info.InitialSetup = false site_info.InitialSetup = false
site_info.PeriodicScanInterval = 360 site_info.PeriodicScanInterval = 360
site_info.ConcurrentWorkers = 10 site_info.ConcurrentWorkers = 10
site_info.ThumbnailMethod = models.ThumbnailFilterLanczos
if !assert.NoError(t, db.Session(&gorm.Session{AllowGlobalUpdate: true}).Save(&site_info).Error) { if !assert.NoError(t, db.Session(&gorm.Session{AllowGlobalUpdate: true}).Save(&site_info).Error) {
return return
@ -36,6 +37,7 @@ func TestSiteInfo(t *testing.T) {
InitialSetup: false, InitialSetup: false,
PeriodicScanInterval: 360, PeriodicScanInterval: 360,
ConcurrentWorkers: 10, ConcurrentWorkers: 10,
ThumbnailMethod: models.ThumbnailFilterLanczos,
}, *site_info) }, *site_info)
} }

View File

@ -0,0 +1,36 @@
package resolvers
import (
"context"
"github.com/photoview/photoview/api/graphql/models"
// "github.com/pkg/errors"
"gorm.io/gorm"
)
func (r *mutationResolver) SetThumbnailDownsampleMethod(ctx context.Context, method models.ThumbnailFilter) (models.ThumbnailFilter, error) {
db := r.DB(ctx)
// if method > 5 {
// return 0, errors.New("The requested filter is unsupported, defaulting to nearest neighbor")
// }
if err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).Model(&models.SiteInfo{}).Update("thumbnail_method", method).Error; err != nil {
return models.ThumbnailFilterNearestNeighbor, err
}
var siteInfo models.SiteInfo
if err := db.First(&siteInfo).Error; err != nil {
return models.ThumbnailFilterNearestNeighbor, err
}
return siteInfo.ThumbnailMethod, nil
// var langTrans *models.LanguageTranslation = nil
// if language != nil {
// lng := models.LanguageTranslation(*language)
// langTrans = &lng
// }
}

View File

@ -163,6 +163,9 @@ type Mutation {
"Set max number of concurrent scanner jobs running at once" "Set max number of concurrent scanner jobs running at once"
setScannerConcurrentWorkers(workers: Int!): Int! @isAdmin setScannerConcurrentWorkers(workers: Int!): Int! @isAdmin
"Set the filter to be used when generating thumbnails"
setThumbnailDownsampleMethod(method: ThumbnailFilter!): ThumbnailFilter! @isAdmin
"Change user preferences for the logged in user" "Change user preferences for the logged in user"
changeUserPreferences(language: String): UserPreferences! @isAuthorized changeUserPreferences(language: String): UserPreferences! @isAuthorized
@ -247,6 +250,16 @@ type ShareToken {
media: Media media: Media
} }
"Supported downsampling filters for thumbnail generation"
enum ThumbnailFilter {
NearestNeighbor,
Box,
Linear,
MitchellNetravali,
CatmullRom,
Lanczos,
}
"General information about the site" "General information about the site"
type SiteInfo { type SiteInfo {
"Whether or not the initial setup wizard should be shown" "Whether or not the initial setup wizard should be shown"
@ -257,6 +270,8 @@ type SiteInfo {
periodicScanInterval: Int! @isAdmin periodicScanInterval: Int! @isAdmin
"How many max concurrent scanner jobs that should run at once" "How many max concurrent scanner jobs that should run at once"
concurrentWorkers: Int! @isAdmin concurrentWorkers: Int! @isAdmin
"The filter to use when generating thumbnails"
thumbnailMethod: ThumbnailFilter! @isAdmin
} }
type User { type User {

View File

@ -17,9 +17,25 @@ import (
"gopkg.in/vansante/go-ffprobe.v2" "gopkg.in/vansante/go-ffprobe.v2"
_ "github.com/strukturag/libheif/go/heif" _ "github.com/strukturag/libheif/go/heif"
"gorm.io/gorm"
) )
func EncodeThumbnail(inputPath string, outputPath string) (*media_utils.PhotoDimensions, error) { var thumbFilter = map[models.ThumbnailFilter]imaging.ResampleFilter{
models.ThumbnailFilterNearestNeighbor: imaging.NearestNeighbor,
models.ThumbnailFilterBox: imaging.Box,
models.ThumbnailFilterLinear: imaging.Linear,
models.ThumbnailFilterMitchellNetravali: imaging.MitchellNetravali,
models.ThumbnailFilterCatmullRom: imaging.CatmullRom,
models.ThumbnailFilterLanczos: imaging.Lanczos,
}
func EncodeThumbnail(db *gorm.DB, inputPath string, outputPath string) (*media_utils.PhotoDimensions, error) {
var siteInfo models.SiteInfo
if err := db.First(&siteInfo).Error; err != nil {
return nil, err
}
inputImage, err := imaging.Open(inputPath, imaging.AutoOrientation(true)) inputImage, err := imaging.Open(inputPath, imaging.AutoOrientation(true))
if err != nil { if err != nil {
@ -29,7 +45,7 @@ func EncodeThumbnail(inputPath string, outputPath string) (*media_utils.PhotoDim
dimensions := media_utils.PhotoDimensionsFromRect(inputImage.Bounds()) dimensions := media_utils.PhotoDimensionsFromRect(inputImage.Bounds())
dimensions = dimensions.ThumbnailScale() dimensions = dimensions.ThumbnailScale()
thumbImage := imaging.Resize(inputImage, dimensions.Width, dimensions.Height, imaging.NearestNeighbor) thumbImage := imaging.Resize(inputImage, dimensions.Width, dimensions.Height, thumbFilter[siteInfo.ThumbnailMethod])
if err = encodeImageJPEG(thumbImage, outputPath, 60); err != nil { if err = encodeImageJPEG(thumbImage, outputPath, 60); err != nil {
return nil, err return nil, err
} }

View File

@ -128,7 +128,7 @@ func (t ProcessPhotoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *
updatedURLs = append(updatedURLs, thumbURL) updatedURLs = append(updatedURLs, thumbURL)
fmt.Printf("Thumbnail photo found in database but not in cache, re-encoding photo to cache: %s\n", thumbURL.MediaName) fmt.Printf("Thumbnail photo found in database but not in cache, re-encoding photo to cache: %s\n", thumbURL.MediaName)
_, err := media_encoding.EncodeThumbnail(baseImagePath, thumbPath) _, err := media_encoding.EncodeThumbnail(ctx.GetDB(), baseImagePath, thumbPath)
if err != nil { if err != nil {
return []*models.MediaURL{}, errors.Wrap(err, "could not create thumbnail cached image") return []*models.MediaURL{}, errors.Wrap(err, "could not create thumbnail cached image")
} }

View File

@ -59,7 +59,7 @@ func generateSaveHighResJPEG(tx *gorm.DB, media *models.Media, imageData *media_
func generateSaveThumbnailJPEG(tx *gorm.DB, media *models.Media, thumbnail_name string, photoCachePath string, baseImagePath string, mediaURL *models.MediaURL) (*models.MediaURL, error) { func generateSaveThumbnailJPEG(tx *gorm.DB, media *models.Media, thumbnail_name string, photoCachePath string, baseImagePath string, mediaURL *models.MediaURL) (*models.MediaURL, error) {
thumbOutputPath := path.Join(photoCachePath, thumbnail_name) thumbOutputPath := path.Join(photoCachePath, thumbnail_name)
thumbSize, err := media_encoding.EncodeThumbnail(baseImagePath, thumbOutputPath) thumbSize, err := media_encoding.EncodeThumbnail(tx, baseImagePath, thumbOutputPath)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "could not create thumbnail cached image") return nil, errors.Wrap(err, "could not create thumbnail cached image")
} }

View File

@ -5,6 +5,7 @@ import { useIsAdmin } from '../../components/routes/AuthorizedRoute'
import Layout from '../../components/layout/Layout' import Layout from '../../components/layout/Layout'
import ScannerSection from './ScannerSection' import ScannerSection from './ScannerSection'
import UserPreferences from './UserPreferences' import UserPreferences from './UserPreferences'
import ThumbnailPreferences from './ThumbnailPreferences'
import UsersTable from './Users/UsersTable' import UsersTable from './Users/UsersTable'
import VersionInfo from './VersionInfo' import VersionInfo from './VersionInfo'
import classNames from 'classnames' import classNames from 'classnames'
@ -46,6 +47,7 @@ const SettingsPage = () => {
<> <>
<ScannerSection /> <ScannerSection />
<UsersTable /> <UsersTable />
<ThumbnailPreferences />
</> </>
)} )}
<VersionInfo /> <VersionInfo />

View File

@ -0,0 +1,52 @@
import React from 'react'
import { MockedProvider } from '@apollo/client/testing'
import { render, screen } from '@testing-library/react'
import { ThumbnailFilter } from '../../__generated__/globalTypes'
import ThumbnailPreferences, {
THUMBNAIL_METHOD_QUERY,
SET_THUMBNAIL_METHOD_MUTATION,
} from './ThumbnailPreferences'
test('load ThumbnailPreferences', () => {
const graphqlMocks = [
{
request: {
query: THUMBNAIL_METHOD_QUERY,
},
result: {
data: {
siteInfo: { method: ThumbnailFilter.NearestNeighbor },
},
},
},
{
request: {
query: SET_THUMBNAIL_METHOD_MUTATION,
variables: {
method: ThumbnailFilter.Lanczos,
},
},
result: {
data: {},
},
},
]
render(
<MockedProvider
mocks={graphqlMocks}
addTypename={false}
defaultOptions={{
// disable cache, required to make fragments work
watchQuery: { fetchPolicy: 'no-cache' },
query: { fetchPolicy: 'no-cache' },
}}
>
<ThumbnailPreferences />
</MockedProvider>
)
expect(screen.getByText('Downsampling method')).toBeInTheDocument()
})

View File

@ -0,0 +1,135 @@
import { gql } from '@apollo/client'
import React, { useRef, useState } from 'react'
import { useMutation, useQuery } from '@apollo/client'
import {
SectionTitle,
InputLabelDescription,
InputLabelTitle,
} from './SettingsPage'
import { useTranslation } from 'react-i18next'
import { ThumbnailFilter } from '../../__generated__/globalTypes'
import { thumbnailMethodQuery } from './__generated__/thumbnailMethodQuery'
import {
setThumbnailMethodMutation,
setThumbnailMethodMutationVariables,
} from './__generated__/setThumbnailMethodMutation'
import Dropdown, { DropdownItem } from '../../primitives/form/Dropdown'
import Loader from '../../primitives/Loader'
export const THUMBNAIL_METHOD_QUERY = gql`
query thumbnailMethodQuery {
siteInfo {
thumbnailMethod
}
}
`
export const SET_THUMBNAIL_METHOD_MUTATION = gql`
mutation setThumbnailMethodMutation($method: ThumbnailFilter!) {
setThumbnailDownsampleMethod(method: $method)
}
`
const ThumbnailPreferences = () => {
const { t } = useTranslation()
const downsampleMethodServerValue = useRef<null | number>(null)
const [downsampleMethod, setDownsampleMethod] = useState(0)
const downsampleMethodQuery = useQuery<thumbnailMethodQuery>(
THUMBNAIL_METHOD_QUERY,
{
onCompleted(data) {
setDownsampleMethod(data.siteInfo.thumbnailMethod)
downsampleMethodServerValue.current = data.siteInfo.thumbnailMethod
},
}
)
const [setDownsampleMutation, downsampleMutationData] = useMutation<
setThumbnailMethodMutation,
setThumbnailMethodMutationVariables
>(SET_THUMBNAIL_METHOD_MUTATION)
const updateDownsampleMethod = (downsampleMethod: number) => {
if (downsampleMethodServerValue.current != downsampleMethod) {
downsampleMethodServerValue.current = downsampleMethod
setDownsampleMutation({
variables: {
method: downsampleMethod,
},
})
}
}
const methodItems: DropdownItem[] = [
{
label: t(
'settings.thumbnails.method.filter.nearest_neighbor',
'Nearest Neighbor (default)'
),
value: ThumbnailFilter.NearestNeighbor,
},
{
label: t('settings.thumbnails.method.filter.box', 'Box'),
value: ThumbnailFilter.Box,
},
{
label: t('settings.thumbnails.method.filter.linear', 'Linear'),
value: ThumbnailFilter.Linear,
},
{
label: t(
'settings.thumbnails.method.filter.mitchell_netravali',
'Mitchell-Netravali'
),
value: ThumbnailFilter.MitchellNetravali,
},
{
label: t('settings.thumbnails.method.filter.catmull_rom', 'Catmull-Rom'),
value: ThumbnailFilter.CatmullRom,
},
{
label: t(
'settings.thumbnails.method.filter.Lanczos',
'Lanczos (highest quality)'
),
value: ThumbnailFilter.Lanczos,
},
]
return (
<div>
<SectionTitle>
{t('settings.thumbnails.title', 'Thumbnail preferences')}
</SectionTitle>
<label htmlFor="thumbnail_method_field">
<InputLabelTitle>
{t('settings.thumbnails.method.label', 'Downsampling method')}
</InputLabelTitle>
<InputLabelDescription>
{t(
'settings.thumbnails.method.description',
'The filter to use when generating thumbnails'
)}
</InputLabelDescription>
</label>
<Dropdown
aria-label="Method"
items={methodItems}
selected={downsampleMethod}
setSelected={value => {
setDownsampleMethod(value)
updateDownsampleMethod(value)
}}
/>
<Loader
active={downsampleMethodQuery.loading || downsampleMutationData.loading}
size="small"
style={{ marginLeft: 16 }}
/>
</div>
)
}
export default ThumbnailPreferences

View File

@ -0,0 +1,21 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { ThumbnailFilter } from './../../../__generated__/globalTypes'
// ====================================================
// GraphQL mutation operation: setThumbnailMethodMutation
// ====================================================
export interface setThumbnailMethodMutation {
/**
* Set the filter to be used when generating thumbnails
*/
setThumbnailDownsampleMethod: ThumbnailFilter
}
export interface setThumbnailMethodMutationVariables {
method: ThumbnailFilter
}

View File

@ -0,0 +1,22 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { ThumbnailFilter } from './../../../__generated__/globalTypes'
// ====================================================
// GraphQL query operation: thumbnailMethodQuery
// ====================================================
export interface thumbnailMethodQuery_siteInfo {
__typename: 'SiteInfo'
/**
* The filter to use when generating thumbnails
*/
thumbnailMethod: ThumbnailFilter
}
export interface thumbnailMethodQuery {
siteInfo: thumbnailMethodQuery_siteInfo
}

View File

@ -48,6 +48,18 @@ export enum OrderDirection {
DESC = 'DESC', DESC = 'DESC',
} }
/**
* Supported downsampling filters for thumbnail generation
*/
export enum ThumbnailFilter {
Box = 'Box',
CatmullRom = 'CatmullRom',
Lanczos = 'Lanczos',
Linear = 'Linear',
MitchellNetravali = 'MitchellNetravali',
NearestNeighbor = 'NearestNeighbor',
}
//============================================================== //==============================================================
// END Enums and Input Objects // END Enums and Input Objects
//============================================================== //==============================================================