lib/pdfmult.rb in pdfmult-1.3.0 vs lib/pdfmult.rb in pdfmult-1.3.1

- old
+ new

@@ -1,80 +1,42 @@ -#!/usr/bin/ruby -w +#!/usr/bin/env ruby # == Name # # pdfmult - put multiple copies of a PDF page on one page # -# == Synopsis -# -# pdfmult [options] file -# # == Description # # +pdfmult+ rearranges multiple copies of a PDF page (shrunken) on one page. # -# The paper size of the produced PDF file is A4, -# the input file is also assumed to be in A4 format. -# The input PDF file may consist of several pages. -# If +pdfmult+ succeeds in obtaining the page count it will rearrange all pages, -# if not, only the first page is processed -# (unless the page count was specified via command line option). +# == See also # -# +pdfmult+ uses +pdflatex+ with the +pdfpages+ package, -# so both have to be installed on the system. -# If the --latex option is used, though, +pdflatex+ is not run -# and a LaTeX file is created instead of a PDF. +# Use <tt>pdfmult --help</tt> to display a brief help message. # -# == Options +# The full documentation for +pdfmult+ is available on the +# project home page. # -# -n, --number:: Number of copies to put on one page: 2 (default), 4, 8, 9, 16. -# -# -f, --[no-]force:: Do not prompt before overwriting. -# -# -l, --latex:: Create a LaTeX file instead of a PDF file (default: infile_NUMBER.tex). -# -# -o, --output:: Output file (default: infile_NUMBER.pdf). -# Use - to output to stdout. -# -# -p, --pages:: Number of pages to convert. -# If given, +pdfmult+ does not try to obtain the page count from the source PDF. -# -# -s, --[no-]silent:: Do not output progress information. -# -# -h, --help:: Prints a brief help message and exits. -# -# -v, --version:: Prints a brief version information and exits. -# -# == Examples -# -# pdfmult sample.pdf # => sample_2.pdf (2 copies) -# pdfmult -n 4 sample.pdf # => sample_4.pdf (4 copies) -# pdfmult sample.pdf -o outfile.pdf # => outfile.pdf (2 copies) -# pdfmult sample.pdf -p 3 # => processes 3 pages -# pdfmult sample.pdf -o - | lpr # => sends output via stdout to print command -# # == Author # -# Copyright (C) 2011-2012 Marcus Stollsteimer +# Copyright (C) 2011-2013 Marcus Stollsteimer # # License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> -# - require 'optparse' require 'tempfile' -require 'fileutils' require 'open3' +require 'erb' # This module contains the classes for the +pdfmult+ tool. module Pdfmult PROGNAME = 'pdfmult' - VERSION = '1.3.0' - DATE = '2012-09-22' + VERSION = '1.3.1' + DATE = '2013-01-04' HOMEPAGE = 'https://github.com/stomar/pdfmult/' + TAGLINE = 'puts multiple copies of a PDF page on one page' - COPYRIGHT = "Copyright (C) 2011-2012 Marcus Stollsteimer.\n" + + COPYRIGHT = "Copyright (C) 2011-2013 Marcus Stollsteimer.\n" + "License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.\n" + "This is free software: you are free to change and redistribute it.\n" + "There is NO WARRANTY, to the extent permitted by law." PDFLATEX = '/usr/bin/pdflatex' @@ -124,18 +86,18 @@ opt.separator 'Options' opt.separator '' # process --version and --help first, # exit successfully (GNU Coding Standards) - opt.on_tail('-h', '--help', 'Prints a brief help message and exits.') do + opt.on_tail('-h', '--help', 'Print a brief help message and exit.') do puts opt_parser puts "\nReport bugs on the #{PROGNAME} home page: <#{HOMEPAGE}>" exit end opt.on_tail('-v', '--version', - 'Prints a brief version information and exits.') do + 'Print a brief version information and exit.') do puts "#{PROGNAME} #{VERSION}" puts COPYRIGHT exit end @@ -171,11 +133,11 @@ opt.separator '' end opt_parser.parse!(argv) # only input file should be left in argv - raise(ArgumentError, 'wrong number of arguments') if (argv.size != 1 || argv[0] == '') + raise(ArgumentError, 'wrong number of arguments') if (argv.size != 1 || argv[0].empty?) options[:infile] = argv.pop # set output file unless set by option ext = options[:latex] ? 'tex' : 'pdf' @@ -184,75 +146,90 @@ options end end + # Class for the page layout. + # + # Create an instance with Layout.new, specifying + # the number of pages to put on one page. + # Layout#geometry returns the geometry string. + class Layout + + attr_reader :pages, :geometry + + GEOMETRY = { + 2 => '2x1', + 4 => '2x2', + 8 => '4x2', + 9 => '3x3', + 16 => '4x4' + } + + def initialize(pages) + @pages = pages + @geometry = GEOMETRY[pages] + end + + def landscape? + ['2x1', '4x2'].include?(geometry) + end + end + # Class for the LaTeX document. # # Create an instance with LaTeXDocument.new, specifying # the input file, the number of pages to put on one page, # and the page count of the input file. # # The method +to_s+ returns the document as multiline string. class LaTeXDocument - attr_accessor :infile, :number, :page_count + TEMPLATE = %q( + \documentclass[<%= class_options %>]{article} + \usepackage{pdfpages} + \pagestyle{empty} + \setlength{\parindent}{0pt} + \begin{document} + % pages_strings.each do |pages| + \includepdf[pages={<%= pages %>},nup=<%= geometry %>]{<%= @pdffile %>}% + % end + \end{document} + ).gsub(/\A\n/,'').gsub(/^ +/, '') - HEADER = - "\\documentclass[CLASSOPTIONS]{article}\n" + - "\\usepackage{pdfpages}\n" + - "\\pagestyle{empty}\n" + - "\\setlength{\\parindent}{0pt}\n" + - "\\begin{document}%\n" - - CONTENT = - "\\includepdf[pages={PAGES},nup=GEOMETRY]{FILENAME}%\n" - - FOOTER = - "\\end{document}\n" - # Initializes a LaTeXDocument instance. + # Expects an argument hash with: # - # +infile+ - input file name - # +number+ - number of pages to put on one page - # +page_count+ - page count of the input file - def initialize(infile, number, page_count) - @infile = infile - @number = number - @page_count = page_count + # +:pdffile+ - filename of input pdf file + # +:layout+ - page layout + # +:page_count+ - page count of the input file + def initialize(args) + @pdffile = args[:pdffile] + @layout = args[:layout] + @page_count = args[:page_count] end def to_s - class_options = 'a4paper' - page_string = 'PAGE,' * (@number - 1) + 'PAGE' # 4 copies: e.g. 1,1,1,1 + class_options = "a4paper" + class_options << ',landscape' if @layout.landscape? + latex = ERB.new(TEMPLATE, 0, '%<>') - case @number - when 2 - class_options << ',landscape' - geometry = '2x1' - when 4 - geometry = '2x2' - when 8 - class_options << ',landscape' - geometry = '4x2' - when 9 - geometry = '3x3' - when 16 - geometry = '4x4' - end + latex.result(binding) + end - content_template = CONTENT.gsub(/PAGES|GEOMETRY|FILENAME/, - 'PAGES' => page_string, - 'GEOMETRY' => geometry, - 'FILENAME' => @infile) + private - content = HEADER.gsub(/CLASSOPTIONS/, class_options) - @page_count.times do |i| - content << content_template.gsub(/PAGE/,"#{i+1}") - end + def geometry + @layout.geometry + end - content << FOOTER + # Returns an array of pages strings. + # For 4 copies and 2 pages: ["1,1,1,1", "2,2,2,2"]. + def pages_strings + template = 'PAGE,' * (@layout.pages - 1) + 'PAGE' + + Array.new(@page_count) {|i| template.gsub(/PAGE/, "#{i+1}") } end end # A class for PDF meta data (up to now only used for the page count). # @@ -262,35 +239,37 @@ # else the attribute is set to +nil+. class PDFInfo PDFINFOCMD = '/usr/bin/pdfinfo' - # Contains the page count of the input file, or nil. + # Returns the page count of the input file, or nil. attr_reader :page_count # This is the initialization method for the class. # # +file+ - file name of the PDF file def initialize(file, options={}) - @page_count = nil - infos = Hash.new + @file = file + @binary = options[:pdfinfocmd] || PDFINFOCMD # for unit tests + @infos = retrieve_infos + @page_count = @infos['Pages'] && @infos['Pages'].to_i + end - binary = options[:pdfinfocmd] || PDFINFOCMD # only for unit tests - command = "#{binary} #{file}" - if Application.command_available?(command) - infostring = `#{command}` - infostring.each_line do |line| - key, val = line.chomp.split(/\s*:\s*/, 2) - infos[key] = val - end - value = infos['Pages'] - @page_count = value.to_i unless value.nil? - end + private + + # Tries to retrieve the PDF infos for the file; returns an info hash. + def retrieve_infos + command = "#{@binary} #{@file}" + return {} unless Application.command_available?(command) + + info_array = `#{command}`.split(/\n/) + + Hash[info_array.map {|line| line.split(/\s*:\s*/, 2) }] end # Returns true if default +pdfinfo+ system tool is available (for unit tests). - def self.infocmd_available? # :nodoc: + def self.infocmd_available? Application.command_available?("#{PDFINFOCMD} -v") end end # The main program. It's run! method is called @@ -331,52 +310,50 @@ overwrite_ok = ask("File `#{outfile}' already exists. Overwrite?") exit unless overwrite_ok end # set page number (get PDF info if necessary) - pages = options[:pages] - pages ||= PDFInfo.new(infile).page_count - pages ||= 1 + pages = options[:pages] || PDFInfo.new(infile).page_count || 1 # create LaTeX document - document = LaTeXDocument.new(infile, options[:number], pages) + args = { + :pdffile => infile, + :layout => Layout.new(options[:number]), + :page_count => pages + } + document = LaTeXDocument.new(args) + output = nil if options[:latex] - if use_stdout - puts document.to_s - else - warn "Writing on #{outfile}." unless silent - open(outfile, 'w') {|f| f.write(document.to_s) } - end + output = document.to_s else Dir.mktmpdir('pdfmult') do |dir| texfile = 'pdfmult.tex' pdffile = 'pdfmult.pdf' open("#{dir}/#{texfile}", 'w') {|f| f.write(document.to_s) } command = "#{PDFLATEX} -output-directory #{dir} #{texfile}" Open3.popen3(command) do |stdin, stdout, stderr| stdout.each_line {|line| warn line.chomp } unless silent # redirect progress messages to stderr stderr.read # make sure all streams are read (and command has finished) end - if use_stdout - File.open("#{dir}/#{pdffile}") do |f| - f.each_line {|line| puts line } - end - else - warn "Writing on #{outfile}." unless silent - FileUtils::mv("#{dir}/#{pdffile}", outfile) - end + output = File.read("#{dir}/#{pdffile}") end end + + # redirect stdout to output file + $stdout.reopen(outfile, 'w') unless use_stdout + + warn "Writing on #{outfile}." unless (use_stdout || silent) + puts output end # Asks for yes or no (y/n). # # +question+ - string to be printed # # Returns +true+ if the answer is yes. def self.ask(question) # :nodoc: - while true + loop do $stderr.print "#{question} [y/n] " reply = $stdin.gets.chomp.downcase # $stdin: avoids gets / ARGV problem return true if reply == 'y' return false if reply == 'n' warn "Please answer `y' or `n'."