Skip to content

Commit

Permalink
feat: support webp
Browse files Browse the repository at this point in the history
Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>
  • Loading branch information
caarlos0 committed Oct 28, 2022
1 parent 6a14762 commit 325b683
Show file tree
Hide file tree
Showing 11 changed files with 104 additions and 54 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ will render them to the respective locations.
Output out.gif
Output out.mp4
Output out.webm
Output out.webp
Output frames/ # a directory of frames as a PNG sequence
```

Expand Down
8 changes: 6 additions & 2 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ func ExecuteOutput(c Command, v *VHS) {
v.Options.Video.CleanupFrames = false
case ".webm":
v.Options.Video.Output.WebM = c.Args
case ".webp":
v.Options.Video.Output.WebP = c.Args
default:
v.Options.Video.Output.GIF = c.Args
}
Expand Down Expand Up @@ -239,8 +241,10 @@ func ExecuteSetWidth(c Command, v *VHS) {
v.Options.Video.Width, _ = strconv.Atoi(c.Args)
}

const bitSize = 64
const base = 10
const (
bitSize = 64
base = 10
)

// ExecuteSetLetterSpacing applies letter spacing (also known as tracking) on the
// vhs.
Expand Down
1 change: 1 addition & 0 deletions examples/fixtures/all.tape
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Output all.gif
Output all.mp4
Output all.webm
Output all.webp

# Settings:
Set FontSize 22
Expand Down
1 change: 1 addition & 0 deletions examples/neofetch/neofetch.tape
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
Output examples/neofetch/neofetch.gif
Output examples/neofetch/neofetch.mp4
Output examples/neofetch/neofetch.webm
Output examples/neofetch/neofetch.webp
Output examples/neofetch/frames/

Set TypingSpeed 75ms
Expand Down
1 change: 1 addition & 0 deletions examples/tutorial.tape
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# Output <path>.gif Create a GIF output at the given <path>
# Output <path>.mp4 Create an MP4 output at the given <path>
# Output <path>.webm Create a WebM output at the given <path>
# Output <path>.webp Create a WebP output at the given <path>
#
# Settings:
# Set FontSize <number> Set the font size of the terminal
Expand Down
2 changes: 2 additions & 0 deletions lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ func TestLexTapeFile(t *testing.T) {
{STRING, "all.mp4"},
{OUTPUT, "Output"},
{STRING, "all.webm"},
{OUTPUT, "Output"},
{STRING, "all.webp"},
{COMMENT, " Settings:"},
{SET, "Set"},
{FONT_SIZE, "FontSize"},
Expand Down
76 changes: 37 additions & 39 deletions man.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ A tape file is a script made up of commands describing what actions to perform i
The following is a list of all possible commands in VHS:
* %Output% <path>.(gif|webm|mp4)
* %Output% <path>.(gif|webm|webp|mp4)
* %Set% <setting> <value>
* %Sleep% <time>
* %Type% "<string>"
Expand All @@ -41,7 +41,7 @@ The following is a list of all possible commands in VHS:
`

manOutput = `The Output command instructs VHS where to save the output of the recording.
File names with the extension %.gif%, %.webm%, %.mp4% will have the respective file types.
File names with the extension %.gif%, %.webm%, %.webp%, %.mp4% will have the respective file types.
`

manSettings = `The Set command allows VHS to adjust settings in the terminal, such as fonts, dimensions, and themes.
Expand All @@ -66,48 +66,46 @@ The following is a list of all possible setting commands in VHS:
manAuthor = "Charm <vt100@charm.sh>"
)

var (
manCmd = &cobra.Command{
Use: "manual",
Aliases: []string{"man"},
Short: "Generate man pages",
Args: cobra.NoArgs,
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
if isatty.IsTerminal(os.Stdout.Fd()) {
renderer, err := glamour.NewTermRenderer(
glamour.WithStyles(GlamourTheme),
)
if err != nil {
return err
}
v, err := renderer.Render(markdownManual())
if err != nil {
return err
}
fmt.Println(v)
return nil
var manCmd = &cobra.Command{
Use: "manual",
Aliases: []string{"man"},
Short: "Generate man pages",
Args: cobra.NoArgs,
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
if isatty.IsTerminal(os.Stdout.Fd()) {
renderer, err := glamour.NewTermRenderer(
glamour.WithStyles(GlamourTheme),
)
if err != nil {
return err
}

manPage, err := mcobra.NewManPage(1, rootCmd)
v, err := renderer.Render(markdownManual())
if err != nil {
return err
}

manPage = manPage.
WithLongDescription(sanitizeSpecial(manDescription)).
WithSection("Output", sanitizeSpecial(manOutput)).
WithSection("Settings", sanitizeSpecial(manSettings)).
WithSection("Bugs", sanitizeSpecial(manBugs)).
WithSection("Author", sanitizeSpecial(manAuthor)).
WithSection("Copyright", "(C) 2021-2022 Charmbracelet, Inc.\n"+
"Released under MIT license.")

fmt.Println(manPage.Build(roff.NewDocument()))
fmt.Println(v)
return nil
},
}
)
}

manPage, err := mcobra.NewManPage(1, rootCmd)
if err != nil {
return err
}

manPage = manPage.
WithLongDescription(sanitizeSpecial(manDescription)).
WithSection("Output", sanitizeSpecial(manOutput)).
WithSection("Settings", sanitizeSpecial(manSettings)).
WithSection("Bugs", sanitizeSpecial(manBugs)).
WithSection("Author", sanitizeSpecial(manAuthor)).
WithSection("Copyright", "(C) 2021-2022 Charmbracelet, Inc.\n"+
"Released under MIT license.")

fmt.Println(manPage.Build(roff.NewDocument()))
return nil
},
}

func markdownManual() string {
return fmt.Sprint(
Expand Down
3 changes: 1 addition & 2 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ Sleep Bar`

func TestParseTapeFile(t *testing.T) {
input, err := os.ReadFile("examples/fixtures/all.tape")

if err != nil {
t.Fatal("could not read fixture file")
}
Expand All @@ -104,6 +103,7 @@ func TestParseTapeFile(t *testing.T) {
{Type: OUTPUT, Options: ".gif", Args: "all.gif"},
{Type: OUTPUT, Options: ".mp4", Args: "all.mp4"},
{Type: OUTPUT, Options: ".webm", Args: "all.webm"},
{Type: OUTPUT, Options: ".webp", Args: "all.webp"},
{Type: SET, Options: "FontSize", Args: "22"},
{Type: SET, Options: "FontFamily", Args: "DejaVu Sans Mono"},
{Type: SET, Options: "Height", Args: "600"},
Expand Down Expand Up @@ -176,5 +176,4 @@ func TestParseTapeFile(t *testing.T) {
t.Errorf("Expected command %d to have options %s, got %s", i, expected[i].Options, cmd.Options)
}
}

}
1 change: 1 addition & 0 deletions serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ var serveCmd = &cobra.Command{
// Disable generating MP4 & WebM.
v.Options.Video.Output.MP4 = ""
v.Options.Video.Output.WebM = ""
v.Options.Video.Output.WebP = ""
})
if err != nil {
_ = s.Exit(1)
Expand Down
7 changes: 5 additions & 2 deletions vhs.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ type Options struct {
Video VideoOptions
}

const defaultFontSize = 22
const typingSpeed = 50 * time.Millisecond
const (
defaultFontSize = 22
typingSpeed = 50 * time.Millisecond
)

// DefaultVHSOptions returns the default set of options to use for the setup function.
func DefaultVHSOptions() Options {
Expand Down Expand Up @@ -139,6 +141,7 @@ func (vhs *VHS) Cleanup() {
cmds = append(cmds, MakeGIF(vhs.Options.Video))
cmds = append(cmds, MakeMP4(vhs.Options.Video))
cmds = append(cmds, MakeWebM(vhs.Options.Video))
cmds = append(cmds, MakeWebP(vhs.Options.Video))

for _, cmd := range cmds {
if cmd == nil {
Expand Down
57 changes: 48 additions & 9 deletions video.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import (
"path/filepath"
)

const textFrameFormat = "frame-text-%05d.png"
const cursorFrameFormat = "frame-cursor-%05d.png"
const (
textFrameFormat = "frame-text-%05d.png"
cursorFrameFormat = "frame-cursor-%05d.png"
)

// randomDir returns a random temporary directory to be used for storing frames
// from screenshots of the terminal.
Expand All @@ -34,6 +36,7 @@ func randomDir() string {
type VideoOutputs struct {
GIF string
WebM string
WebP string
MP4 string
}

Expand All @@ -51,12 +54,14 @@ type VideoOptions struct {
BackgroundColor string
}

const defaultFramerate = 50
const defaultHeight = 600
const defaultMaxColors = 256
const defaultPadding = 72
const defaultPlaybackSpeed = 1.0
const defaultWidth = 1200
const (
defaultFramerate = 50
defaultHeight = 600
defaultMaxColors = 256
defaultPadding = 72
defaultPlaybackSpeed = 1.0
defaultWidth = 1200
)

// DefaultVideoOptions is the set of default options for converting frames
// to a GIF, which are used if they are not overridden.
Expand All @@ -66,7 +71,7 @@ func DefaultVideoOptions() VideoOptions {
Framerate: defaultFramerate,
Input: randomDir(),
MaxColors: defaultMaxColors,
Output: VideoOutputs{GIF: "out.gif", WebM: "", MP4: ""},
Output: VideoOutputs{GIF: "out.gif", WebM: "", WebP: "", MP4: ""},
Width: defaultWidth,
Height: defaultHeight,
Padding: defaultPadding,
Expand Down Expand Up @@ -136,6 +141,40 @@ func MakeWebM(opts VideoOptions) *exec.Cmd {
)
}

// MakeWebP takes a list of images (as frames) and converts them to a WebP.
func MakeWebP(opts VideoOptions) *exec.Cmd {
if opts.Output.WebP == "" {
return nil
}

fmt.Println("Creating WebP...")

//nolint:gosec
return exec.Command(
"ffmpeg", "-y",
"-r", fmt.Sprint(opts.Framerate),
"-i", filepath.Join(opts.Input, textFrameFormat),
"-r", fmt.Sprint(opts.Framerate),
"-i", filepath.Join(opts.Input, cursorFrameFormat),
"-filter_complex",
fmt.Sprintf(`[0][1]overlay,scale=%d:%d:force_original_aspect_ratio=1,fps=%d,setpts=PTS/%f,pad=%d:%d:(ow-iw)/2:(oh-ih)/2:%s,fillborders=left=%d:right=%d:top=%d:bottom=%d:mode=fixed:color=%s`,
opts.Width-2*opts.Padding, opts.Height-2*opts.Padding,
opts.Framerate, opts.PlaybackSpeed,
opts.Width, opts.Height,
opts.BackgroundColor,
opts.Padding, opts.Padding, opts.Padding, opts.Padding,
opts.BackgroundColor,
),
"-vcodec", "libwebp",
"-pix_fmt", "yuv420p",
"-an",
"-crf", "30",
"-b:v", "0",
opts.Output.WebP,
// TODO: loop
)
}

// MakeMP4 takes a list of images (as frames) and converts them to an MP4.
func MakeMP4(opts VideoOptions) *exec.Cmd {
if opts.Output.MP4 == "" {
Expand Down

0 comments on commit 325b683

Please sign in to comment.