giffer

Create .gif images from sites like youtube.com
Log | Files | Refs | LICENSE

commit 7cda7146def9244f915310fc1f6c22d7e1acfef4
parent 7f89a00ccfdb2262546b2ec6b7b63f39006c2dac
Author: Jack Mordaunt <jackmordaunt@gmail.com>
Date:   Wed, 21 Nov 2018 15:12:57 +1300

[+] Engine type represents a more flexible abstraction than Transcoder.

Diffstat:
Aengine.go | 214+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 214 insertions(+), 0 deletions(-)

diff --git a/engine.go b/engine.go @@ -0,0 +1,214 @@ +package giffer + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/pkg/errors" +) + +// Engine implements video and image manipulation. +type Engine struct { + FFmpeg string // Path to FFmpeg binary. + Convert string // Path to imagemagick Convert binary. + Debug bool // Print commands used. + Out io.Writer // Writer to use if debug is true. + Junk []string // Temporary files to cleanup. +} + +// Cut and merge the target file into the specified time slices. +// Cuts is a slice of int pairs which are start and end times (in seconds) +// respectively. +// Returns a filepath to the merged file. +func (eng *Engine) Cut(video string, cuts ...[2]int) (string, error) { + var ( + cutfiles []string + entries []string + filelist = "tmp_file_list.txt" + merged = fmt.Sprintf("merged%s", filepath.Ext(video)) + ) + defer func() { + eng.Junk = append(eng.Junk, append(cutfiles, filelist, merged)...) + }() + for ii, c := range cuts { + start, end := c[0], c[1] + if start > end { + return "", fmt.Errorf("start > end: %d > %d", start, end) + } + output := fmt.Sprintf("tmp_%d%s", ii, filepath.Ext(video)) + cutSlice := eng.command( + eng.FFmpeg, + "-ss", fmt.Sprintf("%d", start), + "-t", fmt.Sprintf("%d", end-start), + "-i", video, + output, + ) + if out, err := cutSlice.CombinedOutput(); err != nil { + return "", errors.Wrapf(err, "cutting video: %s", string(out)) + } + cutfiles = append(cutfiles, output) + } + for _, f := range cutfiles { + entries = append(entries, fmt.Sprintf("file '%s'", f)) + } + if err := ioutil.WriteFile( + filelist, + []byte(strings.Join(entries, "\n")), + 0644, + ); err != nil { + return "", errors.Wrap(err, "creating file list for concatentation") + } + merge := eng.command( + eng.FFmpeg, + "-f", "concat", + "-i", filelist, + "-c", "copy", + merged, + ) + if out, err := merge.CombinedOutput(); err != nil { + return "", errors.Wrapf(err, "merging cut files: %s", string(out)) + } + return merged, nil +} + +// Transcode the target video file into a gif. +// Returns a filepath to the gif image. +func (eng *Engine) Transcode( + video string, + start, end int, + width, height int, + fps float64, +) (string, error) { + var ( + duration = end - start + filters string + palettegen string + output = fmt.Sprintf("%s.gif", strings.Split(video, ".")[0]) + ) + if height < -2 { + height = -2 + } + if width < -2 { + width = -2 + } + if fps > 0.0 { + filters += fmt.Sprintf("fps=%2f", fps) + } + if width > 0.0 || height > 0.0 { + if filters != "" { + filters += "," + } + filters += fmt.Sprintf("scale=%d:%d:flags=lanczos", width, height) + } + if len(filters) > 0 { + palettegen = fmt.Sprintf("%s,palettegen", filters) + } else { + palettegen = "palettegen" + } + defer func() { + eng.Junk = append(eng.Junk, "palette.png", output) + }() + genPalette := eng.command( + eng.FFmpeg, + "-ss", fmt.Sprintf("%d;omitempty", start), + "-t", fmt.Sprintf("%d;omitempty", duration), + "-i", video, + "-vf", palettegen, + "-y", "palette.png", + ) + if out, err := genPalette.CombinedOutput(); err != nil { + return "", errors.Wrapf(err, "generating palette: %s", string(out)) + } + makeGif := eng.command( + eng.FFmpeg, + "-ss", fmt.Sprintf("%d;omitempty", start), + "-t", fmt.Sprintf("%d;omitempty", duration), + "-i", video, "-i", "palette.png", + "-lavfi", fmt.Sprintf("%s [x]; [x][1:v] paletteuse", filters), + "-y", output, + ) + if out, err := makeGif.CombinedOutput(); err != nil { + return "", errors.Wrapf(err, "making gif: %s", string(out)) + } + return output, nil +} + +// Crush reduces the file size of a gif image. +// Accepts a filepath to the gif image and replaces it with the crushed gif. +// Fuzz is a percentage value between 0 and 100, where 0 is best quality, 100 is +// smallest file size. Optimal is typically 2-5. +func (eng *Engine) Crush(gif string, fuzz int) error { + args := []string{gif} + if fuzz > 0 { + args = append(args, "-fuzz", fmt.Sprintf("%d%%", fuzz)) + } + args = append(args, "-layers", "Optimize", gif) + crushGif := eng.command(eng.Convert, args...) + if out, err := crushGif.CombinedOutput(); err != nil { + return errors.Wrap(err, string(out)) + } + return nil +} + +// Clean the temporary files. +func (eng *Engine) Clean() error { + for _, f := range eng.Junk { + if err := os.Remove(f); err != nil { + return err + } + } + return nil +} + +// command creates a new exec.Cmd after removing empty arguments. +// If an argument value contains "<value>;omitempty" and <value> is a zero +// value, the argument value and it's corresponding argument specifier are +// considered "empty" and omitted. +func (eng *Engine) command(cmd string, args ...string) *exec.Cmd { + var a []string + for ii := 0; ii < len(args); ii++ { + arg := args[ii] + if arg[0] == '-' { // This is an argument specifier eg "-h". + if v := args[ii+1]; v == "" { + ii++ + continue + } else if strings.Contains(v, ";omitempty") { + // Handle special ;omitempty directive. + v = strings.Split(v, ";omitempty")[0] + if v == "" { + ii++ + continue + } + x, _ := strconv.Atoi(v) + if x == 0 { + ii++ + continue + } + fl, _ := strconv.ParseFloat(v, 64) + if fl <= 0.0 { + ii++ + continue + } + args[ii+1] = v + } + } + a = append(a, arg) + } + if eng.Debug { + eng.logf("%s %s\n", cmd, strings.Join(a, " ")) + } + return exec.Command(cmd, a...) +} + +func (eng *Engine) logf(f string, v ...interface{}) (int, error) { + if eng.Debug && eng.Out != nil { + return fmt.Fprintf(eng.Out, f, v...) + } + return 0, nil +}