精简后的项目:仅保留 api/ui 源码、文档、构建依赖
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
package media_encoding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/utils"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding/executable_worker"
|
||||
"github.com/photoview/photoview/api/scanner/media_type"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/vansante/go-ffprobe.v2"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Dimension presents the Dimension of a image.
|
||||
type Dimension struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
// ThumbnailScale generates a new dimension for thumbnails.
|
||||
func (d *Dimension) ThumbnailScale() Dimension {
|
||||
if d.Height == 0 || d.Width == 0 {
|
||||
return Dimension{Width: 0, Height: 0}
|
||||
}
|
||||
|
||||
aspect := float64(d.Width) / float64(d.Height)
|
||||
|
||||
var width, height int
|
||||
|
||||
if aspect > 1 {
|
||||
width = 1024
|
||||
height = int(1024 / aspect)
|
||||
} else {
|
||||
width = int(1024 * aspect)
|
||||
height = 1024
|
||||
}
|
||||
|
||||
if width > d.Width {
|
||||
width = d.Width
|
||||
height = d.Height
|
||||
}
|
||||
|
||||
return Dimension{
|
||||
Width: width,
|
||||
Height: height,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPhotoDimensions returns the dimension of the image `imagePath`.
|
||||
func GetPhotoDimensions(imagePath string) (Dimension, error) {
|
||||
w, h, err := executable_worker.Magick.IdentifyDimension(imagePath)
|
||||
if err != nil {
|
||||
return Dimension{}, fmt.Errorf("identify dimension %q error: %w", imagePath, err)
|
||||
}
|
||||
|
||||
return Dimension{
|
||||
Width: int(w),
|
||||
Height: int(h),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EncodeThumbnail encodes a thumbnail of `inputPath`, and store it as `outputPath`.
|
||||
// It returns the dimension of the thumbnail. The thumbnail will be not bigger than 1024x1024.
|
||||
func EncodeThumbnail(db *gorm.DB, inputPath string, outputPath string) (Dimension, error) {
|
||||
w, h, err := executable_worker.Magick.IdentifyDimension(inputPath)
|
||||
if err != nil {
|
||||
return Dimension{}, fmt.Errorf("can't generate thumbnail of file %q: %w", inputPath, err)
|
||||
}
|
||||
|
||||
origin := Dimension{
|
||||
Width: int(w),
|
||||
Height: int(h),
|
||||
}
|
||||
thumbnail := origin.ThumbnailScale()
|
||||
|
||||
if err := executable_worker.Magick.GenerateThumbnail(inputPath, outputPath, uint(thumbnail.Width), uint(thumbnail.Height)); err != nil {
|
||||
return Dimension{}, fmt.Errorf("can't generate thumbnail of file %q: %w", inputPath, err)
|
||||
}
|
||||
|
||||
w, h, err = executable_worker.Magick.IdentifyDimension(outputPath)
|
||||
if err != nil {
|
||||
return Dimension{}, fmt.Errorf("can't generate thumbnail of file %q: %w", inputPath, err)
|
||||
}
|
||||
thumbnail = Dimension{
|
||||
Width: int(w),
|
||||
Height: int(h),
|
||||
}
|
||||
|
||||
return thumbnail, nil
|
||||
}
|
||||
|
||||
// EncodeMediaData is used to easily decode media data, with a cache so expensive operations are not repeated
|
||||
type EncodeMediaData struct {
|
||||
Media *models.Media
|
||||
CounterpartPath *string
|
||||
_photoImage image.Image
|
||||
_contentType media_type.MediaType
|
||||
_videoMetadata *ffprobe.ProbeData
|
||||
}
|
||||
|
||||
func NewEncodeMediaData(media *models.Media) EncodeMediaData {
|
||||
fileType := media_type.GetMediaType(media.Path)
|
||||
|
||||
return EncodeMediaData{
|
||||
Media: media,
|
||||
_contentType: fileType,
|
||||
}
|
||||
}
|
||||
|
||||
// ContentType reads the image to determine its content type
|
||||
func (img *EncodeMediaData) ContentType() (media_type.MediaType, error) {
|
||||
if img._contentType != media_type.TypeUnknown {
|
||||
return img._contentType, nil
|
||||
}
|
||||
|
||||
imgType := media_type.GetMediaType(img.Media.Path)
|
||||
if imgType == media_type.TypeUnknown {
|
||||
return imgType, fmt.Errorf("unknown type of %q", img.Media.Path)
|
||||
}
|
||||
|
||||
img._contentType = imgType
|
||||
return imgType, nil
|
||||
}
|
||||
|
||||
func (img *EncodeMediaData) EncodeHighRes(outputPath string) error {
|
||||
contentType, err := img.ContentType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !contentType.IsSupported() {
|
||||
return errors.New("could not convert photo as file format is not supported")
|
||||
}
|
||||
|
||||
// Use magick if there is no counterpart JPEG file to use instead
|
||||
if contentType.IsImage() && !contentType.IsWebCompatible() {
|
||||
imgPath := img.Media.Path
|
||||
if img.CounterpartPath != nil {
|
||||
imgPath = *img.CounterpartPath
|
||||
}
|
||||
|
||||
err := executable_worker.Magick.EncodeJpeg(imgPath, outputPath, 70)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert RAW photo %q to JPEG: %w", imgPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (enc *EncodeMediaData) VideoMetadata() (*ffprobe.ProbeData, error) {
|
||||
|
||||
if enc._videoMetadata != nil {
|
||||
return enc._videoMetadata, nil
|
||||
}
|
||||
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), utils.MediaProbeTimeout())
|
||||
defer cancelFn()
|
||||
data, err := ffprobe.ProbeURL(ctx, enc.Media.Path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not read video metadata (%s)", enc.Media.Title)
|
||||
}
|
||||
|
||||
enc._videoMetadata = data
|
||||
return enc._videoMetadata, nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package executable_worker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/photoview/photoview/api/log"
|
||||
"gopkg.in/vansante/go-ffprobe.v2"
|
||||
)
|
||||
|
||||
var ErrNoDependency = errors.New("dependency not found")
|
||||
var ErrDisabledFunction = errors.New("function disabled")
|
||||
|
||||
// Initialize Initializes all workers. It returns a function to terminate workers, which should be called before the program closing.
|
||||
func Initialize() func() {
|
||||
Magick = newMagickWand()
|
||||
Ffmpeg = newFfmpegCli()
|
||||
|
||||
if err := SetFfprobePath(); err != nil {
|
||||
log.Error(nil, "Init ffprobe fail.", "error", err)
|
||||
}
|
||||
|
||||
return func() {
|
||||
Magick.Terminate()
|
||||
Magick = nil
|
||||
}
|
||||
}
|
||||
|
||||
var Magick *MagickWand = nil
|
||||
var Ffmpeg *FfmpegCli = nil
|
||||
|
||||
type ExecutableWorker interface {
|
||||
Path() string
|
||||
}
|
||||
|
||||
func SetFfprobePath() error {
|
||||
path, err := exec.LookPath("ffprobe")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Executable ffprobe not found: %w", err)
|
||||
}
|
||||
|
||||
version, err := exec.Command(path, "-version").Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Executable ffprobe(%q) not executable: %w", path, err)
|
||||
}
|
||||
|
||||
log.Info(nil, "Found ffprobe", "path", path, "version", strings.Split(string(version), "\n")[0])
|
||||
ffprobe.SetFFProbeBinPath(path)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package executable_worker
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
_ "github.com/photoview/photoview/api/test_utils/flags"
|
||||
)
|
||||
|
||||
const testdataBinPath = "./test_data/mock_bin"
|
||||
|
||||
// SetPathWithCurrent sets PATH env to `paths` in the directory of testing files. The PATH will restore to the previous value when the test is done.
|
||||
func SetPathWithCurrent(t *testing.T, paths ...string) {
|
||||
_, file, _, ok := runtime.Caller(1)
|
||||
if !ok {
|
||||
t.Log("Can't get the test file. Ignore setting PATH.")
|
||||
return
|
||||
}
|
||||
|
||||
base := filepath.Dir(file)
|
||||
|
||||
for i, path := range paths {
|
||||
paths[i] = filepath.Join(base, path)
|
||||
}
|
||||
|
||||
t.Setenv("PATH", strings.Join(paths, ":"))
|
||||
}
|
||||
|
||||
func TestInitFfprobePath(t *testing.T) {
|
||||
t.Run("PathFail", func(t *testing.T) {
|
||||
SetPathWithCurrent(t, "non_exist_path")
|
||||
|
||||
err := SetFfprobePath()
|
||||
if err == nil {
|
||||
t.Fatalf("InitFfprobePath() returns nil, want an error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("VersionFail", func(t *testing.T) {
|
||||
SetPathWithCurrent(t, testdataBinPath)
|
||||
t.Setenv("FAIL_WITH", "expect failure")
|
||||
|
||||
err := SetFfprobePath()
|
||||
if err == nil {
|
||||
t.Fatalf("InitFfprobePath() returns nil, want an error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Succeed", func(t *testing.T) {
|
||||
SetPathWithCurrent(t, testdataBinPath)
|
||||
|
||||
err := SetFfprobePath()
|
||||
if err != nil {
|
||||
t.Fatalf("InitFfprobePath() returns %v, want nil", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package executable_worker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/photoview/photoview/api/log"
|
||||
"github.com/photoview/photoview/api/utils"
|
||||
"gopkg.in/vansante/go-ffprobe.v2"
|
||||
)
|
||||
|
||||
const defaultCodec = "h264"
|
||||
|
||||
var hwAccToCodec = map[string]string{
|
||||
"qsv": defaultCodec + "_qsv",
|
||||
"vaapi": defaultCodec + "_vaapi",
|
||||
"nvenc": defaultCodec + "_nvenc",
|
||||
}
|
||||
|
||||
type FfmpegCli struct {
|
||||
path string
|
||||
videoCodec string
|
||||
err error
|
||||
}
|
||||
|
||||
func newFfmpegCli() *FfmpegCli {
|
||||
if utils.EnvDisableVideoEncoding.GetBool() {
|
||||
log.Warn(nil, "Executable ffmpeg worker disabled", utils.EnvDisableVideoEncoding.GetName(), utils.EnvDisableVideoEncoding.GetValue())
|
||||
return &FfmpegCli{
|
||||
err: ErrDisabledFunction,
|
||||
}
|
||||
}
|
||||
|
||||
path, err := exec.LookPath("ffmpeg")
|
||||
if err != nil {
|
||||
log.Error(nil, "Executable ffmpeg worker not found")
|
||||
return &FfmpegCli{
|
||||
err: ErrNoDependency,
|
||||
}
|
||||
}
|
||||
|
||||
version, err := exec.Command(path, "-version").Output()
|
||||
if err != nil {
|
||||
log.Error(nil, "Executable ffmpeg worker getting version error", "error", err)
|
||||
return &FfmpegCli{
|
||||
err: ErrNoDependency,
|
||||
}
|
||||
}
|
||||
|
||||
hwAcc := utils.EnvVideoHardwareAcceleration.GetValue()
|
||||
codec, ok := hwAccToCodec[hwAcc]
|
||||
if !ok {
|
||||
if strings.HasPrefix(hwAcc, "_") {
|
||||
// A secret way to set the codec directly.
|
||||
codec = hwAcc[1:]
|
||||
} else {
|
||||
codec = defaultCodec
|
||||
}
|
||||
}
|
||||
|
||||
log.Info(nil, "Found executable worker: ffmpeg", "version", strings.Split(string(version), "\n")[0], "codec", codec)
|
||||
|
||||
return &FfmpegCli{
|
||||
path: path,
|
||||
videoCodec: codec,
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *FfmpegCli) IsInstalled() bool {
|
||||
return cli.err == nil
|
||||
}
|
||||
|
||||
func (cli *FfmpegCli) EncodeMp4(inputPath string, outputPath string) error {
|
||||
if cli.err != nil {
|
||||
return fmt.Errorf("encoding video %q error: ffmpeg: %w", inputPath, cli.err)
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-i",
|
||||
inputPath,
|
||||
"-vcodec", cli.videoCodec,
|
||||
"-acodec", "aac",
|
||||
"-vf", "scale='min(1080,iw)':'min(1080,ih)':force_original_aspect_ratio=decrease:force_divisible_by=2",
|
||||
"-movflags", "+faststart+use_metadata_tags",
|
||||
outputPath,
|
||||
}
|
||||
|
||||
cmd := exec.Command(cli.path, args...)
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("encoding video with %q %v error: %w", cli.path, args, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *FfmpegCli) EncodeVideoThumbnail(inputPath string, outputPath string, probeData *ffprobe.ProbeData) error {
|
||||
if cli.err != nil {
|
||||
return fmt.Errorf("encoding video thumbnail %q error: ffmpeg: %w", inputPath, cli.err)
|
||||
}
|
||||
|
||||
thumbnailOffsetSeconds := fmt.Sprintf("%.f", probeData.Format.DurationSeconds*0.25)
|
||||
|
||||
args := []string{
|
||||
"-ss", thumbnailOffsetSeconds, // grab frame at time offset
|
||||
"-i",
|
||||
inputPath,
|
||||
"-vframes", "1", // output one frame
|
||||
"-an", // disable audio
|
||||
"-vf", "scale='min(1024,iw)':'min(1024,ih)':force_original_aspect_ratio=decrease:force_divisible_by=2",
|
||||
outputPath,
|
||||
}
|
||||
|
||||
cmd := exec.Command(cli.path, args...)
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("encoding video thumbnail with %q %v error: %w", cli.path, args, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package executable_worker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/photoview/photoview/api/utils"
|
||||
"gopkg.in/vansante/go-ffprobe.v2"
|
||||
)
|
||||
|
||||
func TestFfmpegNotExist(t *testing.T) {
|
||||
SetPathWithCurrent(t, "")
|
||||
|
||||
Ffmpeg = newFfmpegCli()
|
||||
|
||||
if got, want := Ffmpeg.err, ErrNoDependency; got != want {
|
||||
t.Errorf("Ffmpeg.err = %v, want: %v", got, want)
|
||||
}
|
||||
|
||||
if Ffmpeg.IsInstalled() {
|
||||
t.Error("Ffmpeg should not be installed, but is found:", Ffmpeg)
|
||||
}
|
||||
|
||||
if got, want := Ffmpeg.EncodeMp4("input", "output"), ErrNoDependency; !errors.Is(got, want) {
|
||||
t.Errorf("Ffmpge.EncodeMp4() = %v, want: %v", got, want)
|
||||
}
|
||||
|
||||
if got, want := Ffmpeg.EncodeVideoThumbnail("input", "output", nil), ErrNoDependency; !errors.Is(got, want) {
|
||||
t.Errorf("Ffmpge.EncodeMp4() = %v, want: %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFfmpegVersionFail(t *testing.T) {
|
||||
SetPathWithCurrent(t, testdataBinPath)
|
||||
t.Setenv("FAIL_WITH", "expect failure")
|
||||
|
||||
Ffmpeg = newFfmpegCli()
|
||||
|
||||
if got, want := Ffmpeg.err, ErrNoDependency; got != want {
|
||||
t.Errorf("Ffmpeg.err = %v, want: %v", got, want)
|
||||
}
|
||||
|
||||
if Ffmpeg.IsInstalled() {
|
||||
t.Error("Ffmpeg should not be installed, but is found:", Ffmpeg)
|
||||
}
|
||||
|
||||
if got, want := Ffmpeg.EncodeMp4("input", "output"), ErrNoDependency; !errors.Is(got, want) {
|
||||
t.Errorf("Ffmpge.EncodeMp4() = %v, want: %v", got, want)
|
||||
}
|
||||
|
||||
if got, want := Ffmpeg.EncodeVideoThumbnail("input", "output", nil), ErrNoDependency; !errors.Is(got, want) {
|
||||
t.Errorf("Ffmpge.EncodeMp4() = %v, want: %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFfmpegIgnore(t *testing.T) {
|
||||
SetPathWithCurrent(t, testdataBinPath)
|
||||
t.Setenv("PHOTOVIEW_DISABLE_VIDEO_ENCODING", "true")
|
||||
|
||||
Ffmpeg = newFfmpegCli()
|
||||
|
||||
if got, want := Ffmpeg.err, ErrDisabledFunction; got != want {
|
||||
t.Errorf("Ffmpeg.err = %v, want: %v", got, want)
|
||||
}
|
||||
|
||||
if Ffmpeg.IsInstalled() {
|
||||
t.Error("Ffmpeg should be ignored (as it is disabled), but is initialized:", Ffmpeg)
|
||||
}
|
||||
|
||||
if got, want := Ffmpeg.EncodeMp4("input", "output"), ErrDisabledFunction; !errors.Is(got, want) {
|
||||
t.Errorf("Ffmpge.EncodeMp4() = %v, want: %v", got, want)
|
||||
}
|
||||
|
||||
if got, want := Ffmpeg.EncodeVideoThumbnail("input", "output", nil), ErrDisabledFunction; !errors.Is(got, want) {
|
||||
t.Errorf("Ffmpge.EncodeMp4() = %v, want: %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFfmpeg(t *testing.T) {
|
||||
SetPathWithCurrent(t, testdataBinPath)
|
||||
|
||||
Ffmpeg = newFfmpegCli()
|
||||
|
||||
if !Ffmpeg.IsInstalled() {
|
||||
t.Fatal("Ffmpeg should be installed")
|
||||
}
|
||||
|
||||
t.Run("EncodeMp4Failed", func(t *testing.T) {
|
||||
t.Setenv("FAIL_WITH", "expect failure")
|
||||
|
||||
err := Ffmpeg.EncodeMp4("input", "output")
|
||||
if err == nil {
|
||||
t.Fatalf("Ffmpeg.EncodeMp4(...) = nil, should be an error.")
|
||||
}
|
||||
if got, want := err.Error(), `^encoding video with ".*/test_data/mock_bin/ffmpeg" \[-i input -vcodec h264 .* output\] error: .*$`; !regexp.MustCompile(want).MatchString(got) {
|
||||
t.Errorf("Ffmpeg.EncodeMp4(...) = %q, should be as reg pattern %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EncodeMp4Succeeded", func(t *testing.T) {
|
||||
err := Ffmpeg.EncodeMp4("input", "output")
|
||||
if err != nil {
|
||||
t.Fatalf("Ffmpeg.EncodeMp4(...) = %v, should be nil.", err)
|
||||
}
|
||||
})
|
||||
|
||||
probeData := &ffprobe.ProbeData{
|
||||
Format: &ffprobe.Format{
|
||||
DurationSeconds: 10,
|
||||
},
|
||||
}
|
||||
t.Run("EncodeVideoThumbnailMp4Failed", func(t *testing.T) {
|
||||
t.Setenv("FAIL_WITH", "expect failure")
|
||||
|
||||
err := Ffmpeg.EncodeVideoThumbnail("input", "output", probeData)
|
||||
if err == nil {
|
||||
t.Fatalf("Ffmpeg.EncodeVideoThumbnail(...) = nil, should be an error.")
|
||||
}
|
||||
if got, want := err.Error(), `^encoding video thumbnail with ".*/test_data/mock_bin/ffmpeg" \[-ss 2 -i input .* output\] error: .*$`; !regexp.MustCompile(want).MatchString(got) {
|
||||
t.Errorf("Ffmpeg.EncodeVideoThumbnail(...) = %q, should be as reg pattern %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EncodeVideoThumbnailSucceeded", func(t *testing.T) {
|
||||
err := Ffmpeg.EncodeVideoThumbnail("input", "output", probeData)
|
||||
if err != nil {
|
||||
t.Fatalf("Ffmpeg.EncodeVideoThumbnail(...) = %v, should be nil.", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFfmpegWithHWAcc(t *testing.T) {
|
||||
SetPathWithCurrent(t, testdataBinPath)
|
||||
t.Setenv(utils.EnvVideoHardwareAcceleration.GetName(), "qsv")
|
||||
|
||||
Ffmpeg = newFfmpegCli()
|
||||
|
||||
t.Setenv("FAIL_WITH", "expect failure")
|
||||
|
||||
err := Ffmpeg.EncodeMp4("input", "output")
|
||||
if err == nil {
|
||||
t.Fatalf("Ffmpeg.EncodeMp4(...) = nil, should be an error.")
|
||||
}
|
||||
if got, want := err.Error(), `^encoding video with ".*/test_data/mock_bin/ffmpeg" \[-i input -vcodec h264_qsv .* output\] error: .*$`; !regexp.MustCompile(want).MatchString(got) {
|
||||
t.Errorf("Ffmpeg.EncodeMp4(...) = %q, should be as reg pattern %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFfmpegWithCustomCodec(t *testing.T) {
|
||||
SetPathWithCurrent(t, testdataBinPath)
|
||||
t.Setenv(utils.EnvVideoHardwareAcceleration.GetName(), "_custom")
|
||||
|
||||
Ffmpeg = newFfmpegCli()
|
||||
|
||||
t.Setenv("FAIL_WITH", "expect failure")
|
||||
|
||||
err := Ffmpeg.EncodeMp4("input", "output")
|
||||
if err == nil {
|
||||
t.Fatalf("Ffmpeg.EncodeMp4(...) = nil, should be an error.")
|
||||
}
|
||||
if got, want := err.Error(), `^encoding video with ".*/test_data/mock_bin/ffmpeg" \[-i input -vcodec custom .* output\] error: .*$`; !regexp.MustCompile(want).MatchString(got) {
|
||||
t.Errorf("Ffmpeg.EncodeMp4(...) = %q, should be as reg pattern %q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package executable_worker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/photoview/photoview/api/log"
|
||||
"gopkg.in/gographics/imagick.v3/imagick"
|
||||
)
|
||||
|
||||
type MagickWand struct {
|
||||
initialized bool
|
||||
}
|
||||
|
||||
func newMagickWand() *MagickWand {
|
||||
imagick.Initialize()
|
||||
|
||||
verstr, vernum := imagick.GetVersion()
|
||||
|
||||
log.Info(nil, "Found magickwand worker: "+verstr, "version", vernum)
|
||||
|
||||
return &MagickWand{
|
||||
initialized: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *MagickWand) Terminate() {
|
||||
cli.initialized = false
|
||||
imagick.Terminate()
|
||||
}
|
||||
|
||||
func (cli *MagickWand) IsInstalled() bool {
|
||||
return cli != nil && cli.initialized
|
||||
}
|
||||
|
||||
func (cli *MagickWand) EncodeJpeg(inputPath string, outputPath string, jpegQuality uint) error {
|
||||
wand, err := cli.createWandFromFile(inputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer wand.Destroy()
|
||||
|
||||
if err := wand.SetFormat("JPEG"); err != nil {
|
||||
return fmt.Errorf("ImagickWand set JPEG format for %q error: %w", inputPath, err)
|
||||
}
|
||||
|
||||
if err := wand.SetImageCompressionQuality(jpegQuality); err != nil {
|
||||
return fmt.Errorf("ImagickWand set JPEG quality %d for %q error: %w", jpegQuality, inputPath, err)
|
||||
}
|
||||
|
||||
if err := wand.WriteImage(outputPath); err != nil {
|
||||
return fmt.Errorf("ImagickWand write %q error: %w", outputPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *MagickWand) GenerateThumbnail(inputPath string, outputPath string, width, height uint) error {
|
||||
wand, err := cli.createWandFromFile(inputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer wand.Destroy()
|
||||
|
||||
if err := wand.ThumbnailImage(width, height); err != nil {
|
||||
return fmt.Errorf("ImagickWand generate thumbnail for %q error: %w", inputPath, err)
|
||||
}
|
||||
|
||||
if err := wand.SetFormat("JPEG"); err != nil {
|
||||
return fmt.Errorf("ImagickWand set JPEG format for %q error: %w", inputPath, err)
|
||||
}
|
||||
|
||||
if err := wand.SetImageCompressionQuality(70); err != nil {
|
||||
return fmt.Errorf("ImagickWand set JPEG quality %d for %q error: %w", 70, inputPath, err)
|
||||
}
|
||||
|
||||
if err := wand.WriteImage(outputPath); err != nil {
|
||||
return fmt.Errorf("ImagickWand write %q error: %w", outputPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *MagickWand) IdentifyDimension(inputPath string) (width, height uint, reterr error) {
|
||||
wand, err := cli.createWandFromFile(inputPath)
|
||||
if err != nil {
|
||||
reterr = err
|
||||
return
|
||||
}
|
||||
defer wand.Destroy()
|
||||
|
||||
width = wand.GetImageWidth()
|
||||
height = wand.GetImageHeight()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (cli *MagickWand) createWandFromFile(inputPath string) (*imagick.MagickWand, error) {
|
||||
if !cli.IsInstalled() {
|
||||
return nil, fmt.Errorf("ImagickWand is not initialized")
|
||||
}
|
||||
|
||||
wand := imagick.NewMagickWand()
|
||||
|
||||
if err := wand.ReadImage(inputPath); err != nil {
|
||||
return nil, fmt.Errorf("ImagickWand read %q error: %w", inputPath, err)
|
||||
}
|
||||
|
||||
if err := wand.AutoOrientImage(); err != nil {
|
||||
return nil, fmt.Errorf("ImagickWand auto-orient %q error: %w", inputPath, err)
|
||||
}
|
||||
|
||||
// Reset EXIF orientation to 1 (top-left) since image is now properly oriented
|
||||
if err := wand.SetImageOrientation(imagick.ORIENTATION_TOP_LEFT); err != nil {
|
||||
return nil, fmt.Errorf("ImagickWand set orientation for %q error: %w", inputPath, err)
|
||||
}
|
||||
|
||||
return wand, nil
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: ${FAIL_WITH=""}
|
||||
|
||||
case "$1" in
|
||||
"--version")
|
||||
echo ffmpeg: version fake
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "${FAIL_WITH}" != "" ]
|
||||
then
|
||||
echo ${FAIL_WITH}
|
||||
exit -1
|
||||
fi
|
||||
|
||||
echo $@
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: ${FAIL_WITH=""}
|
||||
|
||||
case "$1" in
|
||||
"--version")
|
||||
echo ffprobe: version fake
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "${FAIL_WITH}" != "" ]
|
||||
then
|
||||
echo ${FAIL_WITH}
|
||||
exit -1
|
||||
fi
|
||||
|
||||
echo $@
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: ${FAIL_WITH=""}
|
||||
: ${INVALID_OUTPUT=""}
|
||||
|
||||
if [ "${FAIL_WITH}" != "" ]
|
||||
then
|
||||
echo ${FAIL_WITH}
|
||||
exit -1
|
||||
fi
|
||||
|
||||
if [ "$#" = "0" ] # no args
|
||||
then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "$1" in
|
||||
"-version")
|
||||
echo magick: version fake
|
||||
exit 0
|
||||
;;
|
||||
"identify")
|
||||
if [ "${INVALID_OUTPUT}" != "" ]
|
||||
then
|
||||
echo -n ${INVALID_OUTPUT}
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo -n '{"height":800, "width":1000}'
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
echo $@
|
||||
Reference in New Issue
Block a user