Last active
July 6, 2022 04:33
-
-
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')
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 [email protected] | |
# * 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