diff --git a/api/graphql/generated.go b/api/graphql/generated.go
index e6d58d2..8bef0a7 100644
--- a/api/graphql/generated.go
+++ b/api/graphql/generated.go
@@ -151,30 +151,31 @@ type ComplexityRoot struct {
 	}
 
 	Mutation struct {
-		AuthorizeUser               func(childComplexity int, username string, password string) int
-		ChangeUserPreferences       func(childComplexity int, language *string) int
-		CombineFaceGroups           func(childComplexity int, destinationFaceGroupID int, sourceFaceGroupID int) int
-		CreateUser                  func(childComplexity int, username string, password *string, admin bool) int
-		DeleteShareToken            func(childComplexity int, token string) int
-		DeleteUser                  func(childComplexity int, id int) int
-		DetachImageFaces            func(childComplexity int, imageFaceIDs []int) int
-		FavoriteMedia               func(childComplexity int, mediaID int, favorite bool) int
-		InitialSetupWizard          func(childComplexity int, username string, password string, rootPath string) int
-		MoveImageFaces              func(childComplexity int, imageFaceIDs []int, destinationFaceGroupID int) int
-		ProtectShareToken           func(childComplexity int, token string, password *string) int
-		RecognizeUnlabeledFaces     func(childComplexity int) int
-		ResetAlbumCover             func(childComplexity int, albumID int) int
-		ScanAll                     func(childComplexity int) int
-		ScanUser                    func(childComplexity int, userID int) int
-		SetAlbumCover               func(childComplexity int, coverID int) int
-		SetFaceGroupLabel           func(childComplexity int, faceGroupID int, label *string) int
-		SetPeriodicScanInterval     func(childComplexity int, interval int) int
-		SetScannerConcurrentWorkers func(childComplexity int, workers int) int
-		ShareAlbum                  func(childComplexity int, albumID int, expire *time.Time, password *string) int
-		ShareMedia                  func(childComplexity int, mediaID int, expire *time.Time, password *string) int
-		UpdateUser                  func(childComplexity int, id int, username *string, password *string, admin *bool) int
-		UserAddRootPath             func(childComplexity int, id int, rootPath string) int
-		UserRemoveRootAlbum         func(childComplexity int, userID int, albumID int) int
+		AuthorizeUser                func(childComplexity int, username string, password string) int
+		ChangeUserPreferences        func(childComplexity int, language *string) int
+		CombineFaceGroups            func(childComplexity int, destinationFaceGroupID int, sourceFaceGroupID int) int
+		CreateUser                   func(childComplexity int, username string, password *string, admin bool) int
+		DeleteShareToken             func(childComplexity int, token string) int
+		DeleteUser                   func(childComplexity int, id int) int
+		DetachImageFaces             func(childComplexity int, imageFaceIDs []int) int
+		FavoriteMedia                func(childComplexity int, mediaID int, favorite bool) int
+		InitialSetupWizard           func(childComplexity int, username string, password string, rootPath string) int
+		MoveImageFaces               func(childComplexity int, imageFaceIDs []int, destinationFaceGroupID int) int
+		ProtectShareToken            func(childComplexity int, token string, password *string) int
+		RecognizeUnlabeledFaces      func(childComplexity int) int
+		ResetAlbumCover              func(childComplexity int, albumID int) int
+		ScanAll                      func(childComplexity int) int
+		ScanUser                     func(childComplexity int, userID int) int
+		SetAlbumCover                func(childComplexity int, coverID int) int
+		SetFaceGroupLabel            func(childComplexity int, faceGroupID int, label *string) int
+		SetPeriodicScanInterval      func(childComplexity int, interval int) int
+		SetScannerConcurrentWorkers  func(childComplexity int, workers int) int
+		SetThumbnailDownsampleMethod func(childComplexity int, method models.ThumbnailFilter) int
+		ShareAlbum                   func(childComplexity int, albumID int, expire *time.Time, password *string) int
+		ShareMedia                   func(childComplexity int, mediaID int, expire *time.Time, password *string) int
+		UpdateUser                   func(childComplexity int, id int, username *string, password *string, admin *bool) int
+		UserAddRootPath              func(childComplexity int, id int, rootPath string) int
+		UserRemoveRootAlbum          func(childComplexity int, userID int, albumID int) int
 	}
 
 	Notification struct {
@@ -236,6 +237,7 @@ type ComplexityRoot struct {
 		FaceDetectionEnabled func(childComplexity int) int
 		InitialSetup         func(childComplexity int) int
 		PeriodicScanInterval func(childComplexity int) int
+		ThumbnailMethod      func(childComplexity int) int
 	}
 
 	Subscription struct {
@@ -326,6 +328,7 @@ type MutationResolver interface {
 	UserRemoveRootAlbum(ctx context.Context, userID int, albumID int) (*models.Album, error)
 	SetPeriodicScanInterval(ctx context.Context, interval 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)
 	ResetAlbumCover(ctx context.Context, albumID 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
 
+	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":
 		if e.complexity.Mutation.ShareAlbum == nil {
 			break
@@ -1478,6 +1493,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		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":
 		if e.complexity.Subscription.Notification == nil {
 			break
@@ -2156,6 +2178,21 @@ func (ec *executionContext) field_Mutation_setScannerConcurrentWorkers_args(ctx
 	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) {
 	var err error
 	args := map[string]interface{}{}
@@ -7158,6 +7195,81 @@ func (ec *executionContext) fieldContext_Mutation_setScannerConcurrentWorkers(ct
 	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) {
 	fc, err := ec.fieldContext_Mutation_changeUserPreferences(ctx, field)
 	if err != nil {
@@ -8240,6 +8352,8 @@ func (ec *executionContext) fieldContext_Query_siteInfo(ctx context.Context, fie
 				return ec.fieldContext_SiteInfo_periodicScanInterval(ctx, field)
 			case "concurrentWorkers":
 				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)
 		},
@@ -10584,6 +10698,70 @@ func (ec *executionContext) fieldContext_SiteInfo_concurrentWorkers(ctx context.
 	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) {
 	fc, err := ec.fieldContext_Subscription_notification(ctx, field)
 	if err != nil {
@@ -14625,6 +14803,15 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
 				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 {
 				invalids++
 			}
@@ -15419,6 +15606,13 @@ func (ec *executionContext) _SiteInfo(ctx context.Context, sel ast.SelectionSet,
 
 			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 {
 				atomic.AddUint32(&invalids, 1)
 			}
@@ -16608,6 +16802,16 @@ func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.S
 	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) {
 	res, err := graphql.UnmarshalTime(v)
 	return res, graphql.ErrorOnPath(ctx, err)
diff --git a/api/graphql/models/generated.go b/api/graphql/models/generated.go
index abd8589..23d4744 100644
--- a/api/graphql/models/generated.go
+++ b/api/graphql/models/generated.go
@@ -251,3 +251,53 @@ func (e *OrderDirection) UnmarshalGQL(v interface{}) error {
 func (e OrderDirection) MarshalGQL(w io.Writer) {
 	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()))
+}
diff --git a/api/graphql/models/site_info.go b/api/graphql/models/site_info.go
index d358adb..57b966f 100644
--- a/api/graphql/models/site_info.go
+++ b/api/graphql/models/site_info.go
@@ -10,6 +10,7 @@ type SiteInfo struct {
 	InitialSetup         bool `gorm:"not null"`
 	PeriodicScanInterval int  `gorm:"not null"`
 	ConcurrentWorkers    int  `gorm:"not null"`
+	ThumbnailMethod   	 ThumbnailFilter  `gorm:"not null"`
 }
 
 func (SiteInfo) TableName() string {
@@ -26,6 +27,7 @@ func DefaultSiteInfo(db *gorm.DB) SiteInfo {
 		InitialSetup:         true,
 		PeriodicScanInterval: 0,
 		ConcurrentWorkers:    defaultConcurrentWorkers,
+		ThumbnailMethod:			ThumbnailFilterNearestNeighbor,
 	}
 }
 
diff --git a/api/graphql/models/site_info_test.go b/api/graphql/models/site_info_test.go
index 0881939..d967e87 100644
--- a/api/graphql/models/site_info_test.go
+++ b/api/graphql/models/site_info_test.go
@@ -22,6 +22,7 @@ func TestSiteInfo(t *testing.T) {
 	site_info.InitialSetup = false
 	site_info.PeriodicScanInterval = 360
 	site_info.ConcurrentWorkers = 10
+	site_info.ThumbnailMethod = models.ThumbnailFilterLanczos
 
 	if !assert.NoError(t, db.Session(&gorm.Session{AllowGlobalUpdate: true}).Save(&site_info).Error) {
 		return
@@ -36,6 +37,7 @@ func TestSiteInfo(t *testing.T) {
 		InitialSetup:         false,
 		PeriodicScanInterval: 360,
 		ConcurrentWorkers:    10,
+		ThumbnailMethod:    	models.ThumbnailFilterLanczos,
 	}, *site_info)
 
 }
diff --git a/api/graphql/resolvers/thumbnails.go b/api/graphql/resolvers/thumbnails.go
new file mode 100644
index 0000000..6305144
--- /dev/null
+++ b/api/graphql/resolvers/thumbnails.go
@@ -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
+	// }
+
+
+}
diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql
index bccc552..24fd737 100644
--- a/api/graphql/schema.graphql
+++ b/api/graphql/schema.graphql
@@ -163,6 +163,9 @@ type Mutation {
   "Set max number of concurrent scanner jobs running at once"
   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"
   changeUserPreferences(language: String): UserPreferences! @isAuthorized
 
@@ -247,6 +250,16 @@ type ShareToken {
   media: Media
 }
 
+"Supported downsampling filters for thumbnail generation"
+enum ThumbnailFilter {
+  NearestNeighbor,
+  Box,
+	Linear,
+	MitchellNetravali,
+	CatmullRom,
+	Lanczos,
+}
+
 "General information about the site"
 type SiteInfo {
   "Whether or not the initial setup wizard should be shown"
@@ -257,6 +270,8 @@ type SiteInfo {
   periodicScanInterval: Int! @isAdmin
   "How many max concurrent scanner jobs that should run at once"
   concurrentWorkers: Int! @isAdmin
+  "The filter to use when generating thumbnails"
+  thumbnailMethod: ThumbnailFilter! @isAdmin
 }
 
 type User {
diff --git a/api/scanner/media_encoding/encode_photo.go b/api/scanner/media_encoding/encode_photo.go
index fedc47c..adf4d8e 100644
--- a/api/scanner/media_encoding/encode_photo.go
+++ b/api/scanner/media_encoding/encode_photo.go
@@ -17,9 +17,25 @@ import (
 	"gopkg.in/vansante/go-ffprobe.v2"
 
 	_ "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))
 	if err != nil {
@@ -29,7 +45,7 @@ func EncodeThumbnail(inputPath string, outputPath string) (*media_utils.PhotoDim
 	dimensions := media_utils.PhotoDimensionsFromRect(inputImage.Bounds())
 	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 {
 		return nil, err
 	}
diff --git a/api/scanner/scanner_tasks/processing_tasks/process_photo_task.go b/api/scanner/scanner_tasks/processing_tasks/process_photo_task.go
index 042eac2..5ea94d3 100644
--- a/api/scanner/scanner_tasks/processing_tasks/process_photo_task.go
+++ b/api/scanner/scanner_tasks/processing_tasks/process_photo_task.go
@@ -128,7 +128,7 @@ func (t ProcessPhotoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *
 			updatedURLs = append(updatedURLs, thumbURL)
 			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 {
 				return []*models.MediaURL{}, errors.Wrap(err, "could not create thumbnail cached image")
 			}
diff --git a/api/scanner/scanner_tasks/processing_tasks/processing_functions.go b/api/scanner/scanner_tasks/processing_tasks/processing_functions.go
index 2f7b040..6e08ad5 100644
--- a/api/scanner/scanner_tasks/processing_tasks/processing_functions.go
+++ b/api/scanner/scanner_tasks/processing_tasks/processing_functions.go
@@ -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) {
 	thumbOutputPath := path.Join(photoCachePath, thumbnail_name)
 
-	thumbSize, err := media_encoding.EncodeThumbnail(baseImagePath, thumbOutputPath)
+	thumbSize, err := media_encoding.EncodeThumbnail(tx, baseImagePath, thumbOutputPath)
 	if err != nil {
 		return nil, errors.Wrap(err, "could not create thumbnail cached image")
 	}
diff --git a/ui/src/Pages/SettingsPage/SettingsPage.tsx b/ui/src/Pages/SettingsPage/SettingsPage.tsx
index 9da1757..4530198 100644
--- a/ui/src/Pages/SettingsPage/SettingsPage.tsx
+++ b/ui/src/Pages/SettingsPage/SettingsPage.tsx
@@ -5,6 +5,7 @@ import { useIsAdmin } from '../../components/routes/AuthorizedRoute'
 import Layout from '../../components/layout/Layout'
 import ScannerSection from './ScannerSection'
 import UserPreferences from './UserPreferences'
+import ThumbnailPreferences from './ThumbnailPreferences'
 import UsersTable from './Users/UsersTable'
 import VersionInfo from './VersionInfo'
 import classNames from 'classnames'
@@ -46,6 +47,7 @@ const SettingsPage = () => {
         <>
           <ScannerSection />
           <UsersTable />
+          <ThumbnailPreferences />
         </>
       )}
       <VersionInfo />
diff --git a/ui/src/Pages/SettingsPage/ThumbnailPreferences.test.tsx b/ui/src/Pages/SettingsPage/ThumbnailPreferences.test.tsx
new file mode 100644
index 0000000..879ba16
--- /dev/null
+++ b/ui/src/Pages/SettingsPage/ThumbnailPreferences.test.tsx
@@ -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()
+})
diff --git a/ui/src/Pages/SettingsPage/ThumbnailPreferences.tsx b/ui/src/Pages/SettingsPage/ThumbnailPreferences.tsx
new file mode 100644
index 0000000..520a794
--- /dev/null
+++ b/ui/src/Pages/SettingsPage/ThumbnailPreferences.tsx
@@ -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
diff --git a/ui/src/Pages/SettingsPage/__generated__/setThumbnailMethodMutation.ts b/ui/src/Pages/SettingsPage/__generated__/setThumbnailMethodMutation.ts
new file mode 100644
index 0000000..bd7f061
--- /dev/null
+++ b/ui/src/Pages/SettingsPage/__generated__/setThumbnailMethodMutation.ts
@@ -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
+}
diff --git a/ui/src/Pages/SettingsPage/__generated__/thumbnailMethodQuery.ts b/ui/src/Pages/SettingsPage/__generated__/thumbnailMethodQuery.ts
new file mode 100644
index 0000000..7c9cb00
--- /dev/null
+++ b/ui/src/Pages/SettingsPage/__generated__/thumbnailMethodQuery.ts
@@ -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
+}
diff --git a/ui/src/__generated__/globalTypes.ts b/ui/src/__generated__/globalTypes.ts
index 8fda05f..89eae65 100644
--- a/ui/src/__generated__/globalTypes.ts
+++ b/ui/src/__generated__/globalTypes.ts
@@ -48,6 +48,18 @@ export enum OrderDirection {
   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
 //==============================================================
