Skip to content

Instantly share code, notes, and snippets.

@meinside
Last active July 6, 2022 04:33
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save meinside/62c442900e6d73d5d3d9 to your computer and use it in GitHub Desktop.
Save meinside/62c442900e6d73d5d3d9 to your computer and use it in GitHub Desktop.
Slice & convert given video files to .gif format. GIF 짤방 제조용 스크립트. (Tested on OSX & ffmpeg built with 'brew install ffmpeg --with-libvpx --with-libvorbis')
#!/usr/bin/env ruby
# frozen_string_literal: true
# 2gif.rb
#
# https://gist.github.com/meinside/62c442900e6d73d5d3d9
#
# Convert video to gif.
# * referenced: http://blog.pkh.me/p/21-high-quality-gif-with-ffmpeg.html
#
# $ brew install ffmpeg --with-libvpx --with-libvorbis --with-faac
# $ brew install gifsicle
# $ gem install thor
#
# created on : 2014.11.27.
# last update: 2022.07.06.
#
# by meinside@gmail.com
# * how to encode video with subtitles:
#
# $ ffmpeg -i subtitle.smi subtitle.srt
#
# $ ffmpeg -i input.mp4 -vf "subtitles=subtitle.srt:force_style='FontName=맑은 고딕,Fontsize=30'" \
# -c:V libx264 -c:a aac output.mp4
require 'bundler/setup'
require 'thor'
# ffmpeg module
module Ffmpeg
# ToGif class
class ToGif < Thor
DEFAULT_FPS = 18
PALETTE_STATS_MODE = 'full' # or 'diff'
PALETTE_DITHERING = 'floyd_steinberg' # or 'bayer', 'heckbert', 'sierra2', 'sierra2_4a'
SCALE_FILTER = 'lanczos'
default_task :convert
desc 'convert', 'convert a video file to .gif format'
long_desc <<~CONVERT_DESC
* Usage
# will convert original.mp4 to original.mp4.gif
$ #{__FILE__} -i original.mp4
# will convert original.mp4 to converted.gif
$ #{__FILE__} -i original.mp4 -o converted.gif
# will convert original.mp4 into 320x240
$ #{__FILE__} -i original.mp4 -w 320 -h 240
# will slice & convert original.mp4 from 01:20:40.0 to 01:20:50.0 (for 10.0 seconds)
$ #{__FILE__} -i original.mp4 -s 01:20:40.0 -d 10.0
# will crop resulting gif
$ #{__FILE__} -i original.mp4 -s 01:20:40.0 -d 10.0 -c 100,100+320x480
CONVERT_DESC
method_option :in_filepath, type: :string, aliases: '-i', desc: 'input video file\'s path', required: true
method_option :out_filepath, type: :string, aliases: '-o', desc: 'output video file\'s path'
method_option :start, type: :string, aliases: '-s', desc: 'start point of input video'
method_option :duration, type: :string, aliases: '-d', desc: 'length of input video'
method_option :width, type: :numeric, aliases: '-w', desc: 'width of output video'
method_option :height, type: :numeric, aliases: '-h', desc: 'height of output video'
method_option :fps, type: :numeric, aliases: '-f', desc: "number of frames per second (default: #{DEFAULT_FPS})"
method_option :rotation, type: :numeric, aliases: '-r',
desc: 'rotate given video counter-clockwisely (90, 180, 270 degrees)'
method_option :crop, type: :string, aliases: '-c',
desc: 'crop the resulting gif (X,Y+WxH format, eg. 100,100+320x480)'
method_option :test, type: :boolean, aliases: '-t', desc: 'print ffmpeg command instead of running it'
def convert
# time
start = options[:start] ? parse_time(options[:start]) : nil
duration = options[:duration] ? parse_time(options[:duration]) : nil
timing = "#{start ? "-ss #{start}" : ''} #{duration ? "-t #{duration}" : ''}".strip
fps = options[:fps] ? options[:fps].to_i : DEFAULT_FPS
# size
width = options[:width]
height = options[:height]
# rotation
rotation = case options[:rotation]
when 90
'transpose=1'
when 180
'transpose=1,transpose=1'
when 270
'transpose=2'
end
# crop
crop = options[:crop] || nil
if crop && crop !~ /^(\d+),(\d+)\+(\d+)x(\d+)$/
puts "crop parameter should be in 'x,y+widthxheight' format, but is: #{crop}"
exit 1
end
# filepath
in_filepath = File.expand_path(options[:in_filepath])
palette_filepath = File.join('/tmp', "#{File.basename(in_filepath, '.*')}_palette.png")
out_filepath = options[:out_filepath] ? File.expand_path(options[:out_filepath]) : "#{in_filepath}.gif"
# filters
filters = ["fps=#{fps}", "scale=#{width || -1}:#{height || -1}:flags=#{SCALE_FILTER}",
rotation].compact.join(',')
# run ffmpeg
cmd_ffmpeg = "ffmpeg #{timing} -i \"#{in_filepath}\" -vf \"#{filters},palettegen=stats_mode=#{PALETTE_STATS_MODE}\" -y \"#{palette_filepath}\" && ffmpeg #{timing} -i \"#{in_filepath}\" -i \"#{palette_filepath}\" -lavfi \"#{filters},paletteuse=dither=#{PALETTE_DITHERING} [x]; [x][1:v] paletteuse\" -r #{fps} \"#{out_filepath}\" -y"
# run gifsicle
cmd_gifsicle = "gifsicle --crop \"#{crop}\" \"#{out_filepath}\" --output \"#{out_filepath.gsub('.gif',
'_cropped.gif')}\""
# run or show command
if options.test?
puts '* will run ffmpeg command:'
puts cmd_ffmpeg.to_s
puts
puts '* and gifsicle command:'
puts cmd_gifsicle.to_s if crop
elsif crop
`#{cmd_ffmpeg} && #{cmd_gifsicle}`
else
`#{cmd_ffmpeg}`
end
end
private
def parse_time(time)
separated = (time || '').split(':')
seconds = (separated[-1] || 0).to_f
minutes = (separated[-2] || 0).to_i
hours = (separated[-3] || 0).to_i
minutes += seconds / 60
hours += minutes / 60
seconds = seconds % 60.0
minutes = minutes % 60
format('%02d:%02d:%07.4f', hours, minutes, seconds)
end
end
end
trap('SIGINT') do
puts
exit 1
end
Ffmpeg::ToGif.start(ARGV)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment