Init V4 community edition (#2265)
* Init V4 community edition * Init V4 community edition
This commit is contained in:
@@ -3,24 +3,21 @@ package thumb
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/gofrs/uuid"
|
||||
"image"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/gofrs/uuid"
|
||||
//"github.com/nfnt/resize"
|
||||
"golang.org/x/image/draw"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterGenerator(&Builtin{})
|
||||
}
|
||||
const thumbTempFolder = "thumb"
|
||||
|
||||
// Thumb 缩略图
|
||||
type Thumb struct {
|
||||
@@ -30,16 +27,15 @@ type Thumb struct {
|
||||
|
||||
// NewThumbFromFile 从文件数据获取新的Thumb对象,
|
||||
// 尝试通过文件名name解码图像
|
||||
func NewThumbFromFile(file io.Reader, name string) (*Thumb, error) {
|
||||
ext := strings.ToLower(filepath.Ext(name))
|
||||
func NewThumbFromFile(file io.Reader, ext string) (*Thumb, error) {
|
||||
// 无扩展名时
|
||||
if len(ext) == 0 {
|
||||
if ext == "" {
|
||||
return nil, fmt.Errorf("unknown image format: %w", ErrPassThrough)
|
||||
}
|
||||
|
||||
var err error
|
||||
var img image.Image
|
||||
switch ext[1:] {
|
||||
switch ext {
|
||||
case "jpg", "jpeg":
|
||||
img, err = jpeg.Decode(file)
|
||||
case "gif":
|
||||
@@ -47,7 +43,7 @@ func NewThumbFromFile(file io.Reader, name string) (*Thumb, error) {
|
||||
case "png":
|
||||
img, err = png.Decode(file)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown image format: %w", ErrPassThrough)
|
||||
return nil, fmt.Errorf("unknown image format %q: %w", ext, ErrPassThrough)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse image: %w (%w)", err, ErrPassThrough)
|
||||
@@ -72,12 +68,12 @@ func (image *Thumb) GetSize() (int, int) {
|
||||
}
|
||||
|
||||
// Save 保存图像到给定路径
|
||||
func (image *Thumb) Save(w io.Writer) (err error) {
|
||||
switch model.GetSettingByNameWithDefault("thumb_encode_method", "jpg") {
|
||||
func (image *Thumb) Save(w io.Writer, encodeSetting *setting.ThumbEncode) (err error) {
|
||||
switch encodeSetting.Format {
|
||||
case "png":
|
||||
err = png.Encode(w, image.src)
|
||||
default:
|
||||
err = jpeg.Encode(w, image.src, &jpeg.Options{Quality: model.GetIntSetting("thumb_encode_quality", 85)})
|
||||
err = jpeg.Encode(w, image.src, &jpeg.Options{Quality: encodeSetting.Quality})
|
||||
}
|
||||
|
||||
return err
|
||||
@@ -127,46 +123,35 @@ func Resize(newWidth, newHeight uint, img image.Image) image.Image {
|
||||
}
|
||||
|
||||
// CreateAvatar 创建头像
|
||||
func (image *Thumb) CreateAvatar(uid uint) error {
|
||||
// 读取头像相关设定
|
||||
savePath := util.RelativePath(model.GetSettingByName("avatar_path"))
|
||||
s := model.GetIntSetting("avatar_size_s", 50)
|
||||
m := model.GetIntSetting("avatar_size_m", 130)
|
||||
l := model.GetIntSetting("avatar_size_l", 200)
|
||||
|
||||
// 生成头像缩略图
|
||||
src := image.src
|
||||
for k, size := range []int{s, m, l} {
|
||||
out, err := util.CreatNestedFile(filepath.Join(savePath, fmt.Sprintf("avatar_%d_%d.png", uid, k)))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
image.src = Resize(uint(size), uint(size), src)
|
||||
err = image.Save(out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
func (image *Thumb) CreateAvatar(width int) {
|
||||
image.src = Resize(uint(width), uint(width), image.src)
|
||||
}
|
||||
|
||||
type Builtin struct{}
|
||||
type Builtin struct {
|
||||
settings setting.Provider
|
||||
}
|
||||
|
||||
func (b Builtin) Generate(ctx context.Context, file io.Reader, src, name string, options map[string]string) (*Result, error) {
|
||||
img, err := NewThumbFromFile(file, name)
|
||||
func NewBuiltinGenerator(settings setting.Provider) *Builtin {
|
||||
return &Builtin{
|
||||
settings: settings,
|
||||
}
|
||||
}
|
||||
|
||||
func (b Builtin) Generate(ctx context.Context, es entitysource.EntitySource, ext string, previous *Result) (*Result, error) {
|
||||
if es.Entity().Size() > b.settings.BuiltinThumbMaxSize(ctx) {
|
||||
return nil, fmt.Errorf("file is too big: %w", ErrPassThrough)
|
||||
}
|
||||
|
||||
img, err := NewThumbFromFile(es, ext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
img.GetThumb(thumbSize(options))
|
||||
w, h := b.settings.ThumbSize(ctx)
|
||||
img.GetThumb(uint(w), uint(h))
|
||||
tempPath := filepath.Join(
|
||||
util.RelativePath(model.GetSettingByName("temp_path")),
|
||||
"thumb",
|
||||
util.DataPath(b.settings.TempPath(ctx)),
|
||||
thumbTempFolder,
|
||||
fmt.Sprintf("thumb_%s", uuid.Must(uuid.NewV4()).String()),
|
||||
)
|
||||
|
||||
@@ -176,8 +161,8 @@ func (b Builtin) Generate(ctx context.Context, file io.Reader, src, name string,
|
||||
}
|
||||
|
||||
defer thumbFile.Close()
|
||||
if err := img.Save(thumbFile); err != nil {
|
||||
return nil, err
|
||||
if err := img.Save(thumbFile, b.settings.ThumbEncode(ctx)); err != nil {
|
||||
return &Result{Path: tempPath}, err
|
||||
}
|
||||
|
||||
return &Result{Path: tempPath}, nil
|
||||
@@ -187,6 +172,6 @@ func (b Builtin) Priority() int {
|
||||
return 300
|
||||
}
|
||||
|
||||
func (b Builtin) EnableFlag() string {
|
||||
return "thumb_builtin_enabled"
|
||||
func (b Builtin) Enabled(ctx context.Context) bool {
|
||||
return b.settings.BuiltinThumbGeneratorEnabled(ctx)
|
||||
}
|
||||
|
||||
@@ -4,90 +4,77 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterGenerator(&FfmpegGenerator{})
|
||||
const (
|
||||
urlTimeout = time.Duration(1) * time.Hour
|
||||
)
|
||||
|
||||
func NewFfmpegGenerator(l logging.Logger, settings setting.Provider) *FfmpegGenerator {
|
||||
return &FfmpegGenerator{l: l, settings: settings}
|
||||
}
|
||||
|
||||
type FfmpegGenerator struct {
|
||||
exts []string
|
||||
lastRawExts string
|
||||
l logging.Logger
|
||||
settings setting.Provider
|
||||
}
|
||||
|
||||
func (f *FfmpegGenerator) Generate(ctx context.Context, file io.Reader, src, name string, options map[string]string) (*Result, error) {
|
||||
const (
|
||||
thumbFFMpegPath = "thumb_ffmpeg_path"
|
||||
thumbFFMpegExts = "thumb_ffmpeg_exts"
|
||||
thumbFFMpegSeek = "thumb_ffmpeg_seek"
|
||||
thumbEncodeMethod = "thumb_encode_method"
|
||||
tempPath = "temp_path"
|
||||
)
|
||||
ffmpegOpts := model.GetSettingByNames(thumbFFMpegPath, thumbFFMpegExts, thumbFFMpegSeek, thumbEncodeMethod, tempPath)
|
||||
|
||||
if f.lastRawExts != ffmpegOpts[thumbFFMpegExts] {
|
||||
f.exts = strings.Split(ffmpegOpts[thumbFFMpegExts], ",")
|
||||
f.lastRawExts = ffmpegOpts[thumbFFMpegExts]
|
||||
}
|
||||
|
||||
if !util.IsInExtensionList(f.exts, name) {
|
||||
func (f *FfmpegGenerator) Generate(ctx context.Context, es entitysource.EntitySource, ext string, previous *Result) (*Result, error) {
|
||||
if !util.IsInExtensionListExt(f.settings.FFMpegThumbExts(ctx), ext) {
|
||||
return nil, fmt.Errorf("unsupported video format: %w", ErrPassThrough)
|
||||
}
|
||||
|
||||
if es.Entity().Size() > f.settings.FFMpegThumbMaxSize(ctx) {
|
||||
return nil, fmt.Errorf("file is too big: %w", ErrPassThrough)
|
||||
}
|
||||
|
||||
tempOutputPath := filepath.Join(
|
||||
util.RelativePath(ffmpegOpts[tempPath]),
|
||||
"thumb",
|
||||
fmt.Sprintf("thumb_%s.%s", uuid.Must(uuid.NewV4()).String(), ffmpegOpts[thumbEncodeMethod]),
|
||||
util.DataPath(f.settings.TempPath(ctx)),
|
||||
thumbTempFolder,
|
||||
fmt.Sprintf("thumb_%s.%s", uuid.Must(uuid.NewV4()).String(), f.settings.ThumbEncode(ctx).Format),
|
||||
)
|
||||
|
||||
tempInputPath := src
|
||||
if tempInputPath == "" {
|
||||
// If not local policy files, download to temp folder
|
||||
tempInputPath = filepath.Join(
|
||||
util.RelativePath(ffmpegOpts[tempPath]),
|
||||
"thumb",
|
||||
fmt.Sprintf("ffmpeg_%s%s", uuid.Must(uuid.NewV4()).String(), filepath.Ext(name)),
|
||||
)
|
||||
if err := util.CreatNestedFolder(filepath.Dir(tempOutputPath)); err != nil {
|
||||
return nil, fmt.Errorf("failed to create temp folder: %w", err)
|
||||
}
|
||||
|
||||
// Due to limitations of ffmpeg, we need to write the input file to disk first
|
||||
tempInputFile, err := util.CreatNestedFile(tempInputPath)
|
||||
input := ""
|
||||
expire := time.Now().Add(urlTimeout)
|
||||
if es.IsLocal() {
|
||||
input = es.LocalPath(ctx)
|
||||
} else {
|
||||
src, err := es.Url(driver.WithForcePublicEndpoint(ctx, false), entitysource.WithNoInternalProxy(), entitysource.WithContext(ctx), entitysource.WithExpire(&expire))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create temp file: %w", err)
|
||||
return &Result{Path: tempOutputPath}, fmt.Errorf("failed to get entity url: %w", err)
|
||||
}
|
||||
|
||||
defer os.Remove(tempInputPath)
|
||||
defer tempInputFile.Close()
|
||||
|
||||
if _, err = io.Copy(tempInputFile, file); err != nil {
|
||||
return nil, fmt.Errorf("failed to write input file: %w", err)
|
||||
}
|
||||
|
||||
tempInputFile.Close()
|
||||
input = src.Url
|
||||
}
|
||||
|
||||
// Invoke ffmpeg
|
||||
scaleOpt := fmt.Sprintf("scale=%s:%s:force_original_aspect_ratio=decrease", options["thumb_width"], options["thumb_height"])
|
||||
w, h := f.settings.ThumbSize(ctx)
|
||||
scaleOpt := fmt.Sprintf("scale=%d:%d:force_original_aspect_ratio=decrease", w, h)
|
||||
cmd := exec.CommandContext(ctx,
|
||||
ffmpegOpts[thumbFFMpegPath], "-ss", ffmpegOpts[thumbFFMpegSeek], "-i", tempInputPath,
|
||||
f.settings.FFMpegPath(ctx), "-ss", f.settings.FFMpegThumbSeek(ctx), "-i", input,
|
||||
"-vf", scaleOpt, "-vframes", "1", tempOutputPath)
|
||||
|
||||
// Redirect IO
|
||||
var stdErr bytes.Buffer
|
||||
cmd.Stdin = file
|
||||
cmd.Stderr = &stdErr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
util.Log().Warning("Failed to invoke ffmpeg: %s", stdErr.String())
|
||||
return nil, fmt.Errorf("failed to invoke ffmpeg: %w", err)
|
||||
f.l.Warning("Failed to invoke ffmpeg: %s", stdErr.String())
|
||||
return &Result{Path: tempOutputPath}, fmt.Errorf("failed to invoke ffmpeg: %w, raw output: %s", err, stdErr.String())
|
||||
}
|
||||
|
||||
return &Result{Path: tempOutputPath}, nil
|
||||
@@ -97,6 +84,6 @@ func (f *FfmpegGenerator) Priority() int {
|
||||
return 200
|
||||
}
|
||||
|
||||
func (f *FfmpegGenerator) EnableFlag() string {
|
||||
return "thumb_ffmpeg_enabled"
|
||||
func (f *FfmpegGenerator) Enabled(ctx context.Context) bool {
|
||||
return f.settings.FFMpegThumbGeneratorEnabled(ctx)
|
||||
}
|
||||
|
||||
@@ -1,283 +0,0 @@
|
||||
package thumb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterGenerator(&LibRawGenerator{})
|
||||
}
|
||||
|
||||
type LibRawGenerator struct {
|
||||
exts []string
|
||||
lastRawExts string
|
||||
}
|
||||
|
||||
func (f *LibRawGenerator) Generate(ctx context.Context, file io.Reader, _ string, name string, options map[string]string) (*Result, error) {
|
||||
const (
|
||||
thumbLibRawPath = "thumb_libraw_path"
|
||||
thumbLibRawExt = "thumb_libraw_exts"
|
||||
thumbTempPath = "temp_path"
|
||||
)
|
||||
|
||||
opts := model.GetSettingByNames(thumbLibRawPath, thumbLibRawExt, thumbTempPath)
|
||||
|
||||
if f.lastRawExts != opts[thumbLibRawExt] {
|
||||
f.exts = strings.Split(opts[thumbLibRawExt], ",")
|
||||
f.lastRawExts = opts[thumbLibRawExt]
|
||||
}
|
||||
|
||||
if !util.IsInExtensionList(f.exts, name) {
|
||||
return nil, fmt.Errorf("unsupported image format: %w", ErrPassThrough)
|
||||
}
|
||||
|
||||
inputFilePath := filepath.Join(
|
||||
util.RelativePath(opts[thumbTempPath]),
|
||||
"thumb",
|
||||
fmt.Sprintf("thumb_%s", uuid.Must(uuid.NewV4()).String()),
|
||||
)
|
||||
defer func() { _ = os.Remove(inputFilePath) }()
|
||||
|
||||
inputFile, err := util.CreatNestedFile(inputFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
|
||||
if _, err = io.Copy(inputFile, file); err != nil {
|
||||
_ = inputFile.Close()
|
||||
return nil, fmt.Errorf("failed to write input file: %w", err)
|
||||
}
|
||||
_ = inputFile.Close()
|
||||
|
||||
cmd := exec.CommandContext(ctx, opts[thumbLibRawPath], "-e", inputFilePath)
|
||||
|
||||
var stdErr bytes.Buffer
|
||||
cmd.Stderr = &stdErr
|
||||
if err = cmd.Run(); err != nil {
|
||||
util.Log().Warning("Failed to invoke LibRaw: %s", stdErr.String())
|
||||
return nil, fmt.Errorf("failed to invoke LibRaw: %w", err)
|
||||
}
|
||||
|
||||
outputFilePath := inputFilePath + ".thumb.jpg"
|
||||
defer func() { _ = os.Remove(outputFilePath) }()
|
||||
|
||||
ff, err := os.Open(outputFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open temp file: %w", err)
|
||||
}
|
||||
defer func() { _ = ff.Close() }()
|
||||
|
||||
// use builtin generator
|
||||
result, err := new(Builtin).Generate(ctx, ff, outputFilePath, filepath.Base(outputFilePath), options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate thumbnail: %w", err)
|
||||
}
|
||||
|
||||
orientation, err := getJpegOrientation(outputFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get jpeg orientation: %w", err)
|
||||
}
|
||||
if orientation == 1 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if err = rotateImg(result.Path, orientation); err != nil {
|
||||
return nil, fmt.Errorf("failed to rotate image: %w", err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func rotateImg(filePath string, orientation int) error {
|
||||
resultImg, err := os.OpenFile(filePath, os.O_RDWR, 0777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = resultImg.Close() }()
|
||||
|
||||
imgFlag := make([]byte, 3)
|
||||
if _, err = io.ReadFull(resultImg, imgFlag); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = resultImg.Seek(0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var img image.Image
|
||||
if bytes.Equal(imgFlag, []byte{0xFF, 0xD8, 0xFF}) {
|
||||
img, err = jpeg.Decode(resultImg)
|
||||
} else {
|
||||
img, err = png.Decode(resultImg)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch orientation {
|
||||
case 8:
|
||||
img = rotate90(img)
|
||||
case 3:
|
||||
img = rotate90(rotate90(img))
|
||||
case 6:
|
||||
img = rotate90(rotate90(rotate90(img)))
|
||||
case 2:
|
||||
img = mirrorImg(img)
|
||||
case 7:
|
||||
img = rotate90(mirrorImg(img))
|
||||
case 4:
|
||||
img = rotate90(rotate90(mirrorImg(img)))
|
||||
case 5:
|
||||
img = rotate90(rotate90(rotate90(mirrorImg(img))))
|
||||
}
|
||||
|
||||
if err = resultImg.Truncate(0); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = resultImg.Seek(0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if bytes.Equal(imgFlag, []byte{0xFF, 0xD8, 0xFF}) {
|
||||
return jpeg.Encode(resultImg, img, nil)
|
||||
}
|
||||
return png.Encode(resultImg, img)
|
||||
}
|
||||
|
||||
func getJpegOrientation(fileName string) (int, error) {
|
||||
f, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
header := make([]byte, 6)
|
||||
defer func() { header = nil }()
|
||||
if _, err = io.ReadFull(f, header); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// jpeg format header
|
||||
if !bytes.Equal(header[:3], []byte{0xFF, 0xD8, 0xFF}) {
|
||||
return 0, errors.New("not a jpeg")
|
||||
}
|
||||
|
||||
// not a APP1 marker
|
||||
if header[3] != 0xE1 {
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
// exif data total length
|
||||
totalLen := int(header[4])<<8 + int(header[5]) - 2
|
||||
buf := make([]byte, totalLen)
|
||||
defer func() { buf = nil }()
|
||||
if _, err = io.ReadFull(f, buf); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// remove Exif identifier code
|
||||
buf = buf[6:]
|
||||
|
||||
// byte order
|
||||
parse16, parse32, err := initParseMethod(buf[:2])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// version
|
||||
_ = buf[2:4]
|
||||
|
||||
// first IFD offset
|
||||
offset := parse32(buf[4:8])
|
||||
|
||||
// first DE offset
|
||||
offset += 2
|
||||
buf = buf[offset:]
|
||||
|
||||
const (
|
||||
orientationTag = 0x112
|
||||
deEntryLength = 12
|
||||
)
|
||||
for len(buf) > deEntryLength {
|
||||
tag := parse16(buf[:2])
|
||||
if tag == orientationTag {
|
||||
return int(parse32(buf[8:12])), nil
|
||||
}
|
||||
buf = buf[deEntryLength:]
|
||||
}
|
||||
|
||||
return 0, errors.New("orientation not found")
|
||||
}
|
||||
|
||||
func initParseMethod(buf []byte) (func([]byte) int16, func([]byte) int32, error) {
|
||||
if bytes.Equal(buf, []byte{0x49, 0x49}) {
|
||||
return littleEndian16, littleEndian32, nil
|
||||
}
|
||||
if bytes.Equal(buf, []byte{0x4D, 0x4D}) {
|
||||
return bigEndian16, bigEndian32, nil
|
||||
}
|
||||
return nil, nil, errors.New("invalid byte order")
|
||||
}
|
||||
|
||||
func littleEndian16(buf []byte) int16 {
|
||||
return int16(buf[0]) | int16(buf[1])<<8
|
||||
}
|
||||
|
||||
func bigEndian16(buf []byte) int16 {
|
||||
return int16(buf[1]) | int16(buf[0])<<8
|
||||
}
|
||||
|
||||
func littleEndian32(buf []byte) int32 {
|
||||
return int32(buf[0]) | int32(buf[1])<<8 | int32(buf[2])<<16 | int32(buf[3])<<24
|
||||
}
|
||||
|
||||
func bigEndian32(buf []byte) int32 {
|
||||
return int32(buf[3]) | int32(buf[2])<<8 | int32(buf[1])<<16 | int32(buf[0])<<24
|
||||
}
|
||||
|
||||
func rotate90(img image.Image) image.Image {
|
||||
bounds := img.Bounds()
|
||||
width, height := bounds.Dx(), bounds.Dy()
|
||||
newImg := image.NewRGBA(image.Rect(0, 0, height, width))
|
||||
for x := 0; x < width; x++ {
|
||||
for y := 0; y < height; y++ {
|
||||
newImg.Set(y, width-x-1, img.At(x, y))
|
||||
}
|
||||
}
|
||||
return newImg
|
||||
}
|
||||
|
||||
func mirrorImg(img image.Image) image.Image {
|
||||
bounds := img.Bounds()
|
||||
width, height := bounds.Dx(), bounds.Dy()
|
||||
newImg := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
for x := 0; x < width; x++ {
|
||||
for y := 0; y < height; y++ {
|
||||
newImg.Set(width-x-1, y, img.At(x, y))
|
||||
}
|
||||
}
|
||||
return newImg
|
||||
}
|
||||
|
||||
func (f *LibRawGenerator) Priority() int {
|
||||
return 250
|
||||
}
|
||||
|
||||
func (f *LibRawGenerator) EnableFlag() string {
|
||||
return "thumb_libraw_enabled"
|
||||
}
|
||||
|
||||
var _ Generator = (*LibRawGenerator)(nil)
|
||||
@@ -10,51 +10,46 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterGenerator(&LibreOfficeGenerator{})
|
||||
func NewLibreOfficeGenerator(l logging.Logger, settings setting.Provider) *LibreOfficeGenerator {
|
||||
return &LibreOfficeGenerator{l: l, settings: settings}
|
||||
}
|
||||
|
||||
type LibreOfficeGenerator struct {
|
||||
exts []string
|
||||
lastRawExts string
|
||||
settings setting.Provider
|
||||
l logging.Logger
|
||||
}
|
||||
|
||||
func (l *LibreOfficeGenerator) Generate(ctx context.Context, file io.Reader, src string, name string, options map[string]string) (*Result, error) {
|
||||
const (
|
||||
thumbLibreOfficePath = "thumb_libreoffice_path"
|
||||
thumbLibreOfficeExts = "thumb_libreoffice_exts"
|
||||
thumbEncodeMethod = "thumb_encode_method"
|
||||
tempPath = "temp_path"
|
||||
)
|
||||
sofficeOpts := model.GetSettingByNames(thumbLibreOfficePath, thumbLibreOfficeExts, thumbEncodeMethod, tempPath)
|
||||
|
||||
if l.lastRawExts != sofficeOpts[thumbLibreOfficeExts] {
|
||||
l.exts = strings.Split(sofficeOpts[thumbLibreOfficeExts], ",")
|
||||
l.lastRawExts = sofficeOpts[thumbLibreOfficeExts]
|
||||
func (l *LibreOfficeGenerator) Generate(ctx context.Context, es entitysource.EntitySource, ext string, previous *Result) (*Result, error) {
|
||||
if !util.IsInExtensionListExt(l.settings.LibreOfficeThumbExts(ctx), ext) {
|
||||
return nil, fmt.Errorf("unsupported video format: %w", ErrPassThrough)
|
||||
}
|
||||
|
||||
if !util.IsInExtensionList(l.exts, name) {
|
||||
return nil, fmt.Errorf("unsupported document format: %w", ErrPassThrough)
|
||||
if es.Entity().Size() > l.settings.LibreOfficeThumbMaxSize(ctx) {
|
||||
return nil, fmt.Errorf("file is too big: %w", ErrPassThrough)
|
||||
}
|
||||
|
||||
tempOutputPath := filepath.Join(
|
||||
util.RelativePath(sofficeOpts[tempPath]),
|
||||
"thumb",
|
||||
util.DataPath(l.settings.TempPath(ctx)),
|
||||
thumbTempFolder,
|
||||
fmt.Sprintf("soffice_%s", uuid.Must(uuid.NewV4()).String()),
|
||||
)
|
||||
|
||||
tempInputPath := src
|
||||
if tempInputPath == "" {
|
||||
tempInputPath := ""
|
||||
if es.IsLocal() {
|
||||
tempInputPath = es.LocalPath(ctx)
|
||||
} else {
|
||||
// If not local policy files, download to temp folder
|
||||
tempInputPath = filepath.Join(
|
||||
util.RelativePath(sofficeOpts[tempPath]),
|
||||
util.DataPath(l.settings.TempPath(ctx)),
|
||||
"thumb",
|
||||
fmt.Sprintf("soffice_%s%s", uuid.Must(uuid.NewV4()).String(), filepath.Ext(name)),
|
||||
fmt.Sprintf("soffice_%s.%s", uuid.Must(uuid.NewV4()).String(), ext),
|
||||
)
|
||||
|
||||
// Due to limitations of ffmpeg, we need to write the input file to disk first
|
||||
@@ -66,32 +61,32 @@ func (l *LibreOfficeGenerator) Generate(ctx context.Context, file io.Reader, src
|
||||
defer os.Remove(tempInputPath)
|
||||
defer tempInputFile.Close()
|
||||
|
||||
if _, err = io.Copy(tempInputFile, file); err != nil {
|
||||
return nil, fmt.Errorf("failed to write input file: %w", err)
|
||||
if _, err = io.Copy(tempInputFile, es); err != nil {
|
||||
return &Result{Path: tempOutputPath}, fmt.Errorf("failed to write input file: %w", err)
|
||||
}
|
||||
|
||||
tempInputFile.Close()
|
||||
}
|
||||
|
||||
// Convert the document to an image
|
||||
cmd := exec.CommandContext(ctx, sofficeOpts[thumbLibreOfficePath], "--headless",
|
||||
encode := l.settings.ThumbEncode(ctx)
|
||||
cmd := exec.CommandContext(ctx, l.settings.LibreOfficePath(ctx), "--headless",
|
||||
"-nologo", "--nofirststartwizard", "--invisible", "--norestore", "--convert-to",
|
||||
sofficeOpts[thumbEncodeMethod], "--outdir", tempOutputPath, tempInputPath)
|
||||
encode.Format, "--outdir", tempOutputPath, tempInputPath)
|
||||
|
||||
// Redirect IO
|
||||
var stdErr bytes.Buffer
|
||||
cmd.Stdin = file
|
||||
cmd.Stderr = &stdErr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
util.Log().Warning("Failed to invoke LibreOffice: %s", stdErr.String())
|
||||
return nil, fmt.Errorf("failed to invoke LibreOffice: %w", err)
|
||||
l.l.Warning("Failed to invoke LibreOffice: %s", stdErr.String())
|
||||
return &Result{Path: tempOutputPath}, fmt.Errorf("failed to invoke LibreOffice: %w, raw output: %s", err, stdErr.String())
|
||||
}
|
||||
|
||||
return &Result{
|
||||
Path: filepath.Join(
|
||||
tempOutputPath,
|
||||
strings.TrimSuffix(filepath.Base(tempInputPath), filepath.Ext(tempInputPath))+"."+sofficeOpts[thumbEncodeMethod],
|
||||
strings.TrimSuffix(filepath.Base(tempInputPath), filepath.Ext(tempInputPath))+"."+encode.Format,
|
||||
),
|
||||
Continue: true,
|
||||
Cleanup: []func(){func() { _ = os.RemoveAll(tempOutputPath) }},
|
||||
@@ -102,6 +97,6 @@ func (l *LibreOfficeGenerator) Priority() int {
|
||||
return 50
|
||||
}
|
||||
|
||||
func (l *LibreOfficeGenerator) EnableFlag() string {
|
||||
return "thumb_libreoffice_enabled"
|
||||
func (l *LibreOfficeGenerator) Enabled(ctx context.Context) bool {
|
||||
return l.settings.LibreOfficeThumbGeneratorEnabled(ctx)
|
||||
}
|
||||
|
||||
79
pkg/thumb/music.go
Normal file
79
pkg/thumb/music.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package thumb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/dhowden/tag"
|
||||
"github.com/gofrs/uuid"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func NewMusicCoverGenerator(l logging.Logger, settings setting.Provider) *MusicCoverGenerator {
|
||||
return &MusicCoverGenerator{l: l, settings: settings}
|
||||
}
|
||||
|
||||
type MusicCoverGenerator struct {
|
||||
l logging.Logger
|
||||
settings setting.Provider
|
||||
}
|
||||
|
||||
func (v *MusicCoverGenerator) Generate(ctx context.Context, es entitysource.EntitySource, ext string, previous *Result) (*Result, error) {
|
||||
if !util.IsInExtensionListExt(v.settings.MusicCoverThumbExts(ctx), ext) {
|
||||
return nil, fmt.Errorf("unsupported music format: %w", ErrPassThrough)
|
||||
}
|
||||
|
||||
if es.Entity().Size() > v.settings.MusicCoverThumbMaxSize(ctx) {
|
||||
return nil, fmt.Errorf("file is too big: %w", ErrPassThrough)
|
||||
}
|
||||
|
||||
m, err := tag.ReadFrom(es)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("faield to read audio tags from file: %w", err)
|
||||
}
|
||||
|
||||
p := m.Picture()
|
||||
if p == nil || len(p.Data) == 0 {
|
||||
return nil, fmt.Errorf("no cover found in given file")
|
||||
}
|
||||
|
||||
thumbExt := ".jpg"
|
||||
if p.Ext != "" {
|
||||
thumbExt = p.Ext
|
||||
}
|
||||
|
||||
tempPath := filepath.Join(
|
||||
util.DataPath(v.settings.TempPath(ctx)),
|
||||
thumbTempFolder,
|
||||
fmt.Sprintf("thumb_%s.%s", uuid.Must(uuid.NewV4()).String(), thumbExt),
|
||||
)
|
||||
|
||||
thumbFile, err := util.CreatNestedFile(tempPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
|
||||
defer thumbFile.Close()
|
||||
|
||||
if _, err := thumbFile.Write(p.Data); err != nil {
|
||||
return &Result{Path: tempPath}, fmt.Errorf("failed to write cover to file: %w", err)
|
||||
}
|
||||
|
||||
return &Result{
|
||||
Path: tempPath,
|
||||
Continue: true,
|
||||
Cleanup: []func(){func() { _ = os.Remove(tempPath) }},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *MusicCoverGenerator) Priority() int {
|
||||
return 50
|
||||
}
|
||||
|
||||
func (v *MusicCoverGenerator) Enabled(ctx context.Context) bool {
|
||||
return v.settings.MusicCoverThumbGeneratorEnabled(ctx)
|
||||
}
|
||||
@@ -4,93 +4,114 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Generator generates a thumbnail for a given reader.
|
||||
type Generator interface {
|
||||
// Generate generates a thumbnail for a given reader. Src is the original file path, only provided
|
||||
// for local policy files.
|
||||
Generate(ctx context.Context, file io.Reader, src string, name string, options map[string]string) (*Result, error)
|
||||
|
||||
// Priority of execution order, smaller value means higher priority.
|
||||
Priority() int
|
||||
|
||||
// EnableFlag returns the setting name to enable this generator.
|
||||
EnableFlag() string
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Path string
|
||||
Continue bool
|
||||
Cleanup []func()
|
||||
}
|
||||
|
||||
type (
|
||||
// Generator generates a thumbnail for a given reader.
|
||||
Generator interface {
|
||||
// Generate generates a thumbnail for a given reader. Src is the original file path, only provided
|
||||
// for local policy files. State is the result from previous generators, and can be read by current
|
||||
// generator for intermedia result.
|
||||
Generate(ctx context.Context, es entitysource.EntitySource, ext string, previous *Result) (*Result, error)
|
||||
|
||||
// Priority of execution order, smaller value means higher priority.
|
||||
Priority() int
|
||||
|
||||
// Enabled returns if current generator is enabled.
|
||||
Enabled(ctx context.Context) bool
|
||||
}
|
||||
Result struct {
|
||||
Path string
|
||||
Ext string
|
||||
Continue bool
|
||||
Cleanup []func()
|
||||
}
|
||||
GeneratorType string
|
||||
GeneratorList []Generator
|
||||
|
||||
generatorList []Generator
|
||||
pipeline struct {
|
||||
generators generatorList
|
||||
settings setting.Provider
|
||||
l logging.Logger
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
Generators = GeneratorList{}
|
||||
|
||||
ErrPassThrough = errors.New("pass through")
|
||||
ErrNotAvailable = fmt.Errorf("thumbnail not available: %w", ErrPassThrough)
|
||||
)
|
||||
|
||||
func (g GeneratorList) Len() int {
|
||||
func (g generatorList) Len() int {
|
||||
return len(g)
|
||||
}
|
||||
|
||||
func (g GeneratorList) Less(i, j int) bool {
|
||||
func (g generatorList) Less(i, j int) bool {
|
||||
return g[i].Priority() < g[j].Priority()
|
||||
}
|
||||
|
||||
func (g GeneratorList) Swap(i, j int) {
|
||||
func (g generatorList) Swap(i, j int) {
|
||||
g[i], g[j] = g[j], g[i]
|
||||
}
|
||||
|
||||
// RegisterGenerator registers a thumbnail generator.
|
||||
func RegisterGenerator(generator Generator) {
|
||||
Generators = append(Generators, generator)
|
||||
sort.Sort(Generators)
|
||||
// NewPipeline creates a new pipeline with all available generators.
|
||||
func NewPipeline(settings setting.Provider, l logging.Logger) Generator {
|
||||
generators := generatorList{}
|
||||
generators = append(
|
||||
generators,
|
||||
NewBuiltinGenerator(settings),
|
||||
NewFfmpegGenerator(l, settings),
|
||||
NewVipsGenerator(l, settings),
|
||||
NewLibreOfficeGenerator(l, settings),
|
||||
NewMusicCoverGenerator(l, settings),
|
||||
)
|
||||
sort.Sort(generators)
|
||||
|
||||
return pipeline{
|
||||
generators: generators,
|
||||
settings: settings,
|
||||
l: l,
|
||||
}
|
||||
}
|
||||
|
||||
func (p GeneratorList) Generate(ctx context.Context, file io.Reader, src, name string, options map[string]string) (*Result, error) {
|
||||
inputFile, inputSrc, inputName := file, src, name
|
||||
for _, generator := range p {
|
||||
if model.IsTrueVal(options[generator.EnableFlag()]) {
|
||||
res, err := generator.Generate(ctx, inputFile, inputSrc, inputName, options)
|
||||
func (p pipeline) Generate(ctx context.Context, es entitysource.EntitySource, ext string, state *Result) (*Result, error) {
|
||||
e := es.Entity()
|
||||
for _, generator := range p.generators {
|
||||
if generator.Enabled(ctx) {
|
||||
if _, err := es.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, fmt.Errorf("thumb: failed to seek to start of file: %w", err)
|
||||
}
|
||||
|
||||
res, err := generator.Generate(ctx, es, ext, state)
|
||||
if errors.Is(err, ErrPassThrough) {
|
||||
util.Log().Debug("Failed to generate thumbnail using %s for %s: %s, passing through to next generator.", reflect.TypeOf(generator).String(), name, err)
|
||||
p.l.Debug("Failed to generate thumbnail using %s for %s: %s, passing through to next generator.", reflect.TypeOf(generator).String(), e.Source(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
if res != nil && res.Continue {
|
||||
util.Log().Debug("Generator %s for %s returned continue, passing through to next generator.", reflect.TypeOf(generator).String(), name)
|
||||
p.l.Debug("Generator %s for %s returned continue, passing through to next generator.", reflect.TypeOf(generator).String(), e.Source())
|
||||
|
||||
// defer cleanup funcs
|
||||
// defer cleanup functions
|
||||
for _, cleanup := range res.Cleanup {
|
||||
defer cleanup()
|
||||
}
|
||||
|
||||
// prepare file reader for next generator
|
||||
intermediate, err := os.Open(res.Path)
|
||||
state = res
|
||||
es, err = es.CloneToLocalSrc(types.EntityTypeVersion, res.Path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open intermediate thumb file: %w", err)
|
||||
return nil, fmt.Errorf("thumb: failed to clone to local source: %w", err)
|
||||
}
|
||||
|
||||
defer intermediate.Close()
|
||||
inputFile = intermediate
|
||||
inputSrc = res.Path
|
||||
inputName = filepath.Base(res.Path)
|
||||
defer es.Close()
|
||||
ext = util.Ext(res.Path)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -100,23 +121,10 @@ func (p GeneratorList) Generate(ctx context.Context, file io.Reader, src, name s
|
||||
return nil, ErrNotAvailable
|
||||
}
|
||||
|
||||
func (p GeneratorList) Priority() int {
|
||||
func (p pipeline) Priority() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (p GeneratorList) EnableFlag() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func thumbSize(options map[string]string) (uint, uint) {
|
||||
w, h := uint(400), uint(300)
|
||||
if wParsed, err := strconv.Atoi(options["thumb_width"]); err == nil {
|
||||
w = uint(wParsed)
|
||||
}
|
||||
|
||||
if hParsed, err := strconv.Atoi(options["thumb_height"]); err == nil {
|
||||
h = uint(hParsed)
|
||||
}
|
||||
|
||||
return w, h
|
||||
func (p pipeline) Enabled(ctx context.Context) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -23,13 +23,28 @@ func TestGenerator(ctx context.Context, name, executable string) (string, error)
|
||||
return testFfmpegGenerator(ctx, executable)
|
||||
case "libreOffice":
|
||||
return testLibreOfficeGenerator(ctx, executable)
|
||||
case "libRaw":
|
||||
return testLibRawGenerator(ctx, executable)
|
||||
case "ffprobe":
|
||||
return testFFProbeGenerator(ctx, executable)
|
||||
default:
|
||||
return "", ErrUnknownGenerator
|
||||
}
|
||||
}
|
||||
|
||||
func testFFProbeGenerator(ctx context.Context, executable string) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, executable, "-version")
|
||||
var output bytes.Buffer
|
||||
cmd.Stdout = &output
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", fmt.Errorf("failed to invoke ffmpeg executable: %w", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(output.String(), "ffprobe") {
|
||||
return "", ErrUnknownOutput
|
||||
}
|
||||
|
||||
return output.String(), nil
|
||||
}
|
||||
|
||||
func testVipsGenerator(ctx context.Context, executable string) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, executable, "--version")
|
||||
var output bytes.Buffer
|
||||
@@ -74,18 +89,3 @@ func testLibreOfficeGenerator(ctx context.Context, executable string) (string, e
|
||||
|
||||
return output.String(), nil
|
||||
}
|
||||
|
||||
func testLibRawGenerator(ctx context.Context, executable string) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, executable)
|
||||
var output bytes.Buffer
|
||||
cmd.Stdout = &output
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", fmt.Errorf("failed to invoke libraw executable: %w", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(output.String(), "LibRaw") {
|
||||
return "", ErrUnknownOutput
|
||||
}
|
||||
|
||||
return output.String(), nil
|
||||
}
|
||||
|
||||
@@ -5,59 +5,90 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterGenerator(&VipsGenerator{})
|
||||
func NewVipsGenerator(l logging.Logger, settings setting.Provider) *VipsGenerator {
|
||||
return &VipsGenerator{l: l, settings: settings}
|
||||
}
|
||||
|
||||
type VipsGenerator struct {
|
||||
exts []string
|
||||
lastRawExts string
|
||||
l logging.Logger
|
||||
settings setting.Provider
|
||||
}
|
||||
|
||||
func (v *VipsGenerator) Generate(ctx context.Context, file io.Reader, src, name string, options map[string]string) (*Result, error) {
|
||||
const (
|
||||
thumbVipsPath = "thumb_vips_path"
|
||||
thumbVipsExts = "thumb_vips_exts"
|
||||
thumbEncodeQuality = "thumb_encode_quality"
|
||||
thumbEncodeMethod = "thumb_encode_method"
|
||||
tempPath = "temp_path"
|
||||
)
|
||||
vipsOpts := model.GetSettingByNames(thumbVipsPath, thumbVipsExts, thumbEncodeQuality, thumbEncodeMethod, tempPath)
|
||||
|
||||
if v.lastRawExts != vipsOpts[thumbVipsExts] {
|
||||
v.exts = strings.Split(vipsOpts[thumbVipsExts], ",")
|
||||
v.lastRawExts = vipsOpts[thumbVipsExts]
|
||||
func (v *VipsGenerator) Generate(ctx context.Context, es entitysource.EntitySource, ext string, previous *Result) (*Result, error) {
|
||||
if !util.IsInExtensionListExt(v.settings.VipsThumbExts(ctx), ext) {
|
||||
return nil, fmt.Errorf("unsupported video format: %w", ErrPassThrough)
|
||||
}
|
||||
|
||||
if !util.IsInExtensionList(v.exts, name) {
|
||||
return nil, fmt.Errorf("unsupported image format: %w", ErrPassThrough)
|
||||
if es.Entity().Size() > v.settings.VipsThumbMaxSize(ctx) {
|
||||
return nil, fmt.Errorf("file is too big: %w", ErrPassThrough)
|
||||
}
|
||||
|
||||
outputOpt := ".png"
|
||||
if vipsOpts[thumbEncodeMethod] == "jpg" {
|
||||
outputOpt = fmt.Sprintf(".jpg[Q=%s]", vipsOpts[thumbEncodeQuality])
|
||||
encode := v.settings.ThumbEncode(ctx)
|
||||
if encode.Format == "jpg" {
|
||||
outputOpt = fmt.Sprintf(".jpg[Q=%d]", encode.Quality)
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx,
|
||||
vipsOpts[thumbVipsPath], "thumbnail_source", "[descriptor=0]", outputOpt, options["thumb_width"],
|
||||
"--height", options["thumb_height"])
|
||||
input := "[descriptor=0]"
|
||||
usePipe := true
|
||||
if runtime.GOOS == "windows" {
|
||||
// Pipe IO is not working on Windows for VIPS
|
||||
if es.IsLocal() {
|
||||
// escape [ and ] in file name
|
||||
input = fmt.Sprintf("[filename=\"%s\"]", es.LocalPath(ctx))
|
||||
usePipe = false
|
||||
} else {
|
||||
usePipe = false
|
||||
// If not local policy files, download to temp folder
|
||||
tempPath := filepath.Join(
|
||||
util.DataPath(v.settings.TempPath(ctx)),
|
||||
"thumb",
|
||||
fmt.Sprintf("vips_%s.%s", uuid.Must(uuid.NewV4()).String(), ext),
|
||||
)
|
||||
input = fmt.Sprintf("[filename=\"%s\"]", tempPath)
|
||||
|
||||
outTempPath := filepath.Join(
|
||||
util.RelativePath(vipsOpts[tempPath]),
|
||||
"thumb",
|
||||
// Due to limitations of ffmpeg, we need to write the input file to disk first
|
||||
tempInputFile, err := util.CreatNestedFile(tempPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
|
||||
defer os.Remove(tempPath)
|
||||
defer tempInputFile.Close()
|
||||
|
||||
if _, err = io.Copy(tempInputFile, es); err != nil {
|
||||
return &Result{Path: tempPath}, fmt.Errorf("failed to write input file: %w", err)
|
||||
}
|
||||
|
||||
tempInputFile.Close()
|
||||
}
|
||||
}
|
||||
|
||||
w, h := v.settings.ThumbSize(ctx)
|
||||
cmd := exec.CommandContext(ctx,
|
||||
v.settings.VipsPath(ctx), "thumbnail_source", input, outputOpt, strconv.Itoa(w),
|
||||
"--height", strconv.Itoa(h))
|
||||
|
||||
tempPath := filepath.Join(
|
||||
util.DataPath(v.settings.TempPath(ctx)),
|
||||
thumbTempFolder,
|
||||
fmt.Sprintf("thumb_%s", uuid.Must(uuid.NewV4()).String()),
|
||||
)
|
||||
|
||||
thumbFile, err := util.CreatNestedFile(outTempPath)
|
||||
thumbFile, err := util.CreatNestedFile(tempPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
@@ -66,22 +97,24 @@ func (v *VipsGenerator) Generate(ctx context.Context, file io.Reader, src, name
|
||||
|
||||
// Redirect IO
|
||||
var vipsErr bytes.Buffer
|
||||
cmd.Stdin = file
|
||||
if usePipe {
|
||||
cmd.Stdin = es
|
||||
}
|
||||
cmd.Stdout = thumbFile
|
||||
cmd.Stderr = &vipsErr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
util.Log().Warning("Failed to invoke vips: %s", vipsErr.String())
|
||||
return nil, fmt.Errorf("failed to invoke vips: %w", err)
|
||||
v.l.Warning("Failed to invoke vips: %s", vipsErr.String())
|
||||
return &Result{Path: tempPath}, fmt.Errorf("failed to invoke vips: %w, raw output: %s", err, vipsErr.String())
|
||||
}
|
||||
|
||||
return &Result{Path: outTempPath}, nil
|
||||
return &Result{Path: tempPath}, nil
|
||||
}
|
||||
|
||||
func (v *VipsGenerator) Priority() int {
|
||||
return 100
|
||||
}
|
||||
|
||||
func (v *VipsGenerator) EnableFlag() string {
|
||||
return "thumb_vips_enabled"
|
||||
func (v *VipsGenerator) Enabled(ctx context.Context) bool {
|
||||
return v.settings.VipsThumbGeneratorEnabled(ctx)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user