#!/usr/bin/env ruby =begin rdoc This program is copyright 2014 by Vincent Fourmond. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA =end # This program is a wrapper around ctioga2 to make it easy to make # movies. require 'open3' require 'fileutils' require 'optparse' require 'shellwords' # Path to ctioga2 executable ct2 = "ctioga2" tmpdir = "tmp" # The class in charge of running ffmpeg class EncodingJob # Target file attr_accessor :target # Bitrate attr_accessor :bitrate # Video codec attr_accessor :codec # Extra arguments attr_accessor :extra_args # Writes the given frame for encoding def write_frame(data) @encoder.write(data) end def add_args(args) @extra_args ||= [] @extra_args += args end # Start job def start_job(size) ffmpeg_args = ["ffmpeg", '-y', "-f", "rawvideo", "-r", "25", "-s", size, "-i", "-"] if @bitrate ffmpeg_args << "-b" << @bitrate end if @codec ffmpeg_args << "-vcodec" << @codec end ffmpeg_args += @extra_args if @extra_args ffmpeg_args << @target p ffmpeg_args @encoder = IO::popen(ffmpeg_args, "wb") end def close() @encoder.close end end DimensionConversion = { "pt" => (72.0/72.27), "bp" => 1.0, "in" => 72.0, "cm" => (72.0/2.54), "mm" => (72.0/25.4), } def dim_to_points(dim) if dim =~ /^\s*(\d+(\.\d*)?)\s*(pt|bp|cm|in|mm)\s*$/i return $1.to_f * DimensionConversion[$3.downcase] else raise "Invalid dimension: #{dim}" end end # Converts the given "real-size" resolution into postscript points def page_size_to_points(spec) if spec =~ /(.*)x(.*)/i return [dim_to_points($1), dim_to_points($2)] else raise "Invalid page size: #{spec}" end end # @todo Build a movie from a list of PDF files. Maybe less error # checking than directly. # The target resolution res = [600,600] # The target page size (in bp) ct2_size = nil # The corresponding number of points # The conversion factor (between points and inches) conv = 250.0 # The oversampling factor (to get something smooth in the end) oversampling = 2 # Whether we use pdftoppm or not for the conversion. Much faster than # convert use_pdftoppm = false # Whether we keep all intermediate PDF files, or we reuse the same # file over and over again. store_all = true encoders = [ EncodingJob.new ] cur_enc = encoders.first ct2_extra_args = [] opts = OptionParser.new do |opts| opts.banner = "Usage: #$0 [options] file.ct2 arguments..." ################################################## # Encoding-related options opts.on("-t", "--target FILE", "Target video file") do |t| if cur_enc.target cur_enc = EncodingJob.new encoders << cur_enc end cur_enc.target = t end opts.on("-b", "--bitrate RATE", "Bitrate (indicative)") do |v| cur_enc.bitrate = v end opts.on("", "--codec CODEC", "Target codec") do |v| cur_enc.codec = v end opts.on("", "--ffmpeg-args ARGS", "Extra ffmpeg args") do |v| cur_enc.add_args(Shellwords.split(v)) end ################################################## # opts.on("", "--dir DIR", "Temporary directory for storage") do |t| tmpdir = t end opts.on("", "--version", "Prints version string") do puts "0.1" end opts.on("-p", "--[no-]pdftoppm", "Whether or not to use pdftoppm") do |t| use_pdftoppm = t end opts.on("-r", "--page-size SIZE", "Set ctioga2 page size (in TeX dimensions)") do |v| ct2_size = page_size_to_points(v) p ct2_size end opts.on("", "--resolution RES", "Set target resolution (overridden to some extent by page-size)") do |r| r =~ /(\d+)x(\d+)/ res = [$1.to_f, $2.to_f] end opts.on("", "--[no-]store", "To store all or not..") do |v| store_all = v end opts.on("", "--ctioga2-args ARGS", "Extra args for ctioga2") do |v| ct2_extra_args += Shellwords.split(v) end opts.on("-s", "--set EXPR", "Sets the given variable") do |t| l = t.split(/\s*=\s*/, 2) if l.size != 2 puts "The argument of --set must be in the form 'variable=value'" exit 1 end ct2_extra_args += ['--set', *l] end end opts.parse!(ARGV) # First, we choose the target page size and resolution. if ct2_size ct2_page_size = ct2_size.map { |x| "#{x}bp" }.join("x") # maintain aspect ratio res[1] = res[0] * ct2_size[1]/ct2_size[0] conv = res[0]/ct2_size[0] * 72.27 else ct2_page_size = res.map { |x| "#{x/conv}in"}.join("x") end size = res.map { |x| "#{x.to_i}"}.join("x") puts "Producing #{ct2_page_size} PDF and converting to #{size} for the video" file = ARGV.shift cur_enc.target ||= file.sub(/(\.ct2)?$/, ".avi") args = [] for a in ARGV # Expansion ! if a =~ /^(.*)\.\.(.*):(\d+)\s*$/ s = $1.to_f e = $2.to_f nb = $3.to_i nb.times do |i| args << "#{s + (e-s)*i/(nb-1.0)}" end else args << a end end FileUtils::mkpath(tmpdir) # Now, we compute the ctioga2 real size # @todo Use other encoding programs ! # Start all encoders for enc in encoders enc.start_job(size) end format = if store_all "#{tmpdir}/file-%04d" else "#{tmpdir}/file" end index = 0 for f in args name = format % index ct2_cmdline = [ct2, *ct2_extra_args, "--set", "arg", f, "--set", "index", "#{index}", "-f", file, "--name", name, "-r", ct2_page_size] puts "Running: #{ct2_cmdline.join(" ")}" system(*ct2_cmdline) b = nil if use_pdftoppm b1, s = Open3.capture2( "pdftoppm", "-r", "#{(conv*oversampling).to_i}", "#{name}.pdf", :stdin_data=>"", :binmode=>true) b, s = Open3.capture2("convert", "PPM:-", "-resize", "#{size}!", "-depth", "8", "YUV:-", :stdin_data=>b1, :binmode=>true) else b, s = Open3.capture2("convert", "-density", "#{(conv*oversampling).to_i}", "#{name}.pdf", "-alpha", "Remove", "-resize", "#{size}!", "-depth", "8", "YUV:-", :stdin_data=>"", :binmode=>true) end for enc in encoders enc.write_frame(b) end index += 1 end for enc in encoders enc.close end