From b55344459df5fd479cd91d3096acd6fe4236f5fd Mon Sep 17 00:00:00 2001 From: Aaron Liu <912394456@qq.com> Date: Fri, 7 Apr 2023 19:30:10 +0800 Subject: [PATCH] feat(thumb): use libvips to generate thumb --- models/defaults.go | 1 + pkg/filesystem/image.go | 6 ++-- pkg/thumb/builtin.go | 3 +- pkg/thumb/pipeline.go | 7 ++-- pkg/thumb/vips.go | 78 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 pkg/thumb/vips.go diff --git a/models/defaults.go b/models/defaults.go index 587066a..e1031fb 100644 --- a/models/defaults.go +++ b/models/defaults.go @@ -111,6 +111,7 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti {Name: "thumb_vips_enabled", Value: "0", Type: "thumb"}, {Name: "thumb_ffmpeg_enabled", Value: "0", Type: "thumb"}, {Name: "thumb_vips_path", Value: "vips", Type: "thumb"}, + {Name: "thumb_vips_exts", Value: "csv,mat,img,hdr,pbm,pgm,ppm,pfm,pnm,svg,svgz,j2k,jp2,jpt,j2c,jpc,gif,png,jpg,jpeg,jpe,webp,tif,tiff,fits,fit,fts,exr,jxl,pdf,heic,heif,avif,svs,vms,vmu,ndpi,scn,mrxs,svslide,bif,raw", Type: "thumb"}, {Name: "thumb_ffmpeg_path", Value: "ffmpeg", Type: "thumb"}, {Name: "thumb_proxy_enabled", Value: "0", Type: "thumb"}, {Name: "thumb_proxy_policy", Value: "[]", Type: "thumb"}, diff --git a/pkg/filesystem/image.go b/pkg/filesystem/image.go index 6cc5074..b861d2b 100644 --- a/pkg/filesystem/image.go +++ b/pkg/filesystem/image.go @@ -137,20 +137,20 @@ func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) e } defer source.Close() - thumbPath, err := thumb.Generators.Generate(source, file.Name, model.GetSettingByNames( + thumbPath, err := thumb.Generators.Generate(ctx, source, file.Name, model.GetSettingByNames( "thumb_width", "thumb_height", "thumb_builtin_enabled", "thumb_vips_enabled", "thumb_ffmpeg_enabled", - "thumb_vips_path", - "thumb_ffmpeg_path", )) if err != nil { _ = updateThumbStatus(file, model.ThumbStatusNotAvailable) return fmt.Errorf("failed to generate thumb for %q: %w", file.Name, err) } + defer os.Remove(thumbPath) + thumbFile, err := os.Open(thumbPath) if err != nil { return fmt.Errorf("failed to open temp thumb %q: %w", thumbFile, err) diff --git a/pkg/thumb/builtin.go b/pkg/thumb/builtin.go index 16c0618..2de4952 100644 --- a/pkg/thumb/builtin.go +++ b/pkg/thumb/builtin.go @@ -1,6 +1,7 @@ package thumb import ( + "context" "fmt" "image" "image/gif" @@ -156,7 +157,7 @@ func (image *Thumb) CreateAvatar(uid uint) error { type Builtin struct{} -func (b Builtin) Generate(file io.Reader, name string, options map[string]string) (string, error) { +func (b Builtin) Generate(ctx context.Context, file io.Reader, name string, options map[string]string) (string, error) { img, err := NewThumbFromFile(file, name) if err != nil { return "", err diff --git a/pkg/thumb/pipeline.go b/pkg/thumb/pipeline.go index c4a4eb7..676f8cf 100644 --- a/pkg/thumb/pipeline.go +++ b/pkg/thumb/pipeline.go @@ -1,6 +1,7 @@ package thumb import ( + "context" "errors" "fmt" model "github.com/cloudreve/Cloudreve/v3/models" @@ -12,7 +13,7 @@ import ( // Generator generates a thumbnail for a given reader. type Generator interface { - Generate(file io.Reader, name string, options map[string]string) (string, error) + Generate(ctx context.Context, file io.Reader, name string, options map[string]string) (string, error) // Priority of execution order, smaller value means higher priority. Priority() int @@ -51,10 +52,10 @@ func RegisterGenerator(generator Generator) { sort.Sort(Generators) } -func (p GeneratorList) Generate(file io.Reader, name string, options map[string]string) (string, error) { +func (p GeneratorList) Generate(ctx context.Context, file io.Reader, name string, options map[string]string) (string, error) { for _, generator := range p { if model.IsTrueVal(options[generator.EnableFlag()]) { - res, err := generator.Generate(file, name, options) + res, err := generator.Generate(ctx, file, name, options) if errors.Is(err, ErrPassThrough) { util.Log().Debug("Failed to generate thumbnail for %s: %s, passing through to next generator.", name, err) continue diff --git a/pkg/thumb/vips.go b/pkg/thumb/vips.go new file mode 100644 index 0000000..3908e4c --- /dev/null +++ b/pkg/thumb/vips.go @@ -0,0 +1,78 @@ +package thumb + +import ( + "bytes" + "context" + "fmt" + model "github.com/cloudreve/Cloudreve/v3/models" + "github.com/cloudreve/Cloudreve/v3/pkg/util" + "github.com/gofrs/uuid" + "io" + "os/exec" + "path/filepath" + "strings" +) + +func init() { + RegisterGenerator(&VipsGenerator{}) +} + +type VipsGenerator struct { + exts []string + lastRawExts string +} + +func (v VipsGenerator) Generate(ctx context.Context, file io.Reader, name string, options map[string]string) (string, error) { + vipsOpts := model.GetSettingByNames("thumb_vips_path", "thumb_vips_exts", "thumb_encode_quality", "thumb_encode_method", "temp_path") + + if v.lastRawExts != vipsOpts["thumb_vips_exts"] { + v.exts = strings.Split(vipsOpts["thumb_vips_exts"], ",") + } + + if !util.IsInExtensionList(v.exts, name) { + return "", fmt.Errorf("unsupported image format: %w", ErrPassThrough) + } + + outputOpt := ".png" + if vipsOpts["thumb_encode_method"] == "jpg" { + outputOpt = fmt.Sprintf(".jpg[Q=%s]", vipsOpts["thumb_encode_quality"]) + } + + cmd := exec.CommandContext(ctx, + vipsOpts["thumb_vips_path"], "thumbnail_source", "[descriptor=0]", outputOpt, options["thumb_width"], + "--height", options["thumb_height"]) + + tempPath := filepath.Join( + util.RelativePath(vipsOpts["temp_path"]), + "thumb", + fmt.Sprintf("thumb_%s", uuid.Must(uuid.NewV4()).String()), + ) + + thumbFile, err := util.CreatNestedFile(tempPath) + if err != nil { + return "", fmt.Errorf("failed to create temp file: %w", err) + } + + defer thumbFile.Close() + + // Redirect IO + var vipsErr bytes.Buffer + cmd.Stdin = file + cmd.Stdout = thumbFile + cmd.Stderr = &vipsErr + + if err := cmd.Run(); err != nil { + util.Log().Warning("Failed to invoke vips: %s", vipsErr.String()) + return "", fmt.Errorf("failed to invoke vips: %w", err) + } + + return tempPath, nil +} + +func (v VipsGenerator) Priority() int { + return 100 +} + +func (v VipsGenerator) EnableFlag() string { + return "thumb_vips_enabled" +}