lib/pdfmult.rb in pdfmult-1.3.2 vs lib/pdfmult.rb in pdfmult-1.4.0

- old
+ new

@@ -1,6 +1,8 @@ #!/usr/bin/env ruby +# frozen_string_literal: true + # == Name # # pdfmult - put multiple copies of a PDF page on one page # # == Description @@ -14,37 +16,37 @@ # The full documentation for +pdfmult+ is available on the # project home page. # # == Author # -# Copyright (C) 2011-2013 Marcus Stollsteimer +# Copyright (C) 2011-2024 Marcus Stollsteimer # # License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> -require 'optparse' -require 'tempfile' -require 'open3' -require 'erb' +require "optparse" +require "tempfile" +require "open3" +require "erb" # This module contains the classes for the +pdfmult+ tool. module Pdfmult - PROGNAME = 'pdfmult' - VERSION = '1.3.2' - DATE = '2013-10-27' - HOMEPAGE = 'https://github.com/stomar/pdfmult/' - TAGLINE = 'puts multiple copies of a PDF page on one page' + PROGNAME = "pdfmult" + VERSION = "1.4.0" + DATE = "2024-01-05" + HOMEPAGE = "https://github.com/stomar/pdfmult/" + TAGLINE = "puts multiple copies of a PDF page on one page" - COPYRIGHT = <<-copyright.gsub(/^ +/, '') - Copyright (C) 2011-2013 Marcus Stollsteimer. + COPYRIGHT = <<~TEXT + Copyright (C) 2011-2024 Marcus Stollsteimer. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>. This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. - copyright + TEXT - PDFLATEX = '/usr/bin/pdflatex' - KPSEWHICH = '/usr/bin/kpsewhich' + PDFLATEX = "/usr/bin/pdflatex" + KPSEWHICH = "/usr/bin/kpsewhich" # Parser for the command line options. # The class method parse! does the job. class Optionparser @@ -54,24 +56,24 @@ # # +argv+ - array with the command line options # # Returns a hash containing the option parameters. def self.parse!(argv) - options = { - :force => false, - :infile => nil, - :latex => false, - :number => 2, - :outfile => nil, - :silent => false, - :pages => nil + force: false, + infile: nil, + latex: false, + number: 2, + outfile: nil, + silent: false, + pages: nil } opt_parser = OptionParser.new do |opt| opt.banner = "Usage: #{PROGNAME} [options] file" - opt.separator %q{ + opt.separator "" + opt.separator <<~DESCRIPTION pdfmult is a command line tool that 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. @@ -83,70 +85,71 @@ 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. - Options - }.gsub(/^ +/, '') + Options: + DESCRIPTION # process --version and --help first, # exit successfully (GNU Coding Standards) - opt.on_tail('-h', '--help', 'Print a brief help message and exit.') 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', - 'Print a brief version information and exit.') do + opt.on_tail("-v", "--version", + "Print a brief version information and exit.") do puts "#{PROGNAME} #{VERSION}" puts COPYRIGHT exit end - opt.on('-n', '--number NUMBER', ['2', '4', '8', '9', '16'], Integer, - 'Number of copies to put on one page: 2 (default), 4, 8, 9, 16.') do |n| + opt.on("-n", "--number NUMBER", %w[2 4 8 9 16], Integer, + "Number of copies to put on one page: 2 (default), 4, 8, 9, 16.") do |n| options[:number] = n end - opt.on('-f', '--[no-]force', 'Do not prompt before overwriting.') do |f| + opt.on("-f", "--[no-]force", "Do not prompt before overwriting.") do |f| options[:force] = f end - opt.on('-l', '--latex', 'Create a LaTeX file instead of a PDF file (default: file_2.tex).') do + opt.on("-l", "--latex", "Create a LaTeX file instead of a PDF file (default: file_2.tex).") do options[:latex] = true end - opt.on('-o', '--output FILE', String, - 'Output file (default: file_2.pdf). Use - to output to stdout.') do |f| + opt.on("-o", "--output FILE", String, + "Output file (default: file_2.pdf). Use - to output to stdout.") do |f| options[:outfile] = f end - opt.on('-p', '--pages NUMBER', Integer, - 'Number of pages to convert.', + opt.on("-p", "--pages NUMBER", Integer, + "Number of pages to convert.", "If given, #{PROGNAME} does not try to obtain the page count from the source PDF.") do |p| - raise(OptionParser::InvalidArgument, p) unless p > 0 + raise(OptionParser::InvalidArgument, p) unless p.positive? + options[:pages] = p end - opt.on('-s', '--[no-]silent', 'Do not output progress information.') do |s| + opt.on("-s", "--[no-]silent", "Do not output progress information.") do |s| options[:silent] = s end - opt.separator '' + 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].empty?) + 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' - infile_without_ext = options[:infile].gsub(/(.pdf)\Z/, '') - options[:outfile] ||= "#{infile_without_ext}_#{options[:number].to_s}.#{ext}" + ext = options[:latex] ? "tex" : "pdf" + infile_without_ext = options[:infile].delete_suffix(".pdf") + options[:outfile] ||= "#{infile_without_ext}_#{options[:number]}.#{ext}" options end end @@ -158,24 +161,24 @@ class Layout attr_reader :pages, :geometry GEOMETRY = { - 2 => '2x1', - 4 => '2x2', - 8 => '4x2', - 9 => '3x3', - 16 => '4x4' - } + 2 => "2x1", + 4 => "2x2", + 8 => "4x2", + 9 => "3x3", + 16 => "4x4" + }.freeze def initialize(pages) @pages = pages @geometry = GEOMETRY[pages] end def landscape? - ['2x1', '4x2'].include?(geometry) + %w[2x1 4x2].include?(geometry) end end # Class for the LaTeX document. # @@ -185,21 +188,21 @@ # The method +to_s+ returns the document as multiline string. class LaTeXDocument attr_reader :pdffile, :layout, :page_count - TEMPLATE = %q( + TEMPLATE = <<~'LATEX' \documentclass[<%= class_options %>]{article} \usepackage{pdfpages} \pagestyle{empty} \setlength{\parindent}{0pt} \begin{document} % pages_strings.each do |pages| - \includepdf[pages={<%= pages %>},nup=<%= geometry %>]{<%= pdffile %>}% + \includepdf[pages={<%= pages %>},nup=<%= geometry %>]{<%= pdffile %>}% % end \end{document} - ).gsub(/\A\n/,'').gsub(/^ +/, '') + LATEX # Initializes a LaTeXDocument instance. # Expects an argument hash with: # # +:pdffile+ - filename of input pdf file @@ -210,33 +213,35 @@ @layout = args[:layout] @page_count = args[:page_count] end def to_s - class_options = 'a4paper' - class_options << ',landscape' if layout.landscape? - latex = ERB.new(TEMPLATE, 0, '%<>') + latex = ERB.new(TEMPLATE, trim_mode: "%<>") latex.result(binding) end private def geometry layout.geometry end + def class_options + layout.landscape? ? "a4paper,landscape" : "a4paper" + end + def pages_per_sheet layout.pages end # Returns an array of pages strings. # For 4 copies and 2 pages: ["1,1,1,1", "2,2,2,2"]. def pages_strings pages = (1..page_count).to_a - pages.map {|page| ([page] * pages_per_sheet).join(',') } + pages.map {|page| ([page] * pages_per_sheet).join(",") } end end # A class for PDF meta data (up to now only used for the page count). # @@ -244,118 +249,124 @@ # +PDFInfo+ tries to use the +pdfinfo+ system tool to obtain meta data. # If successful, the attribute +page_count+ contains the page count, # else the attribute is set to +nil+. class PDFInfo - PDFINFOCMD = '/usr/bin/pdfinfo' + PDFINFOCMD = "/usr/bin/pdfinfo" # 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={}) + def initialize(file, options = {}) @file = file @binary = options[:pdfinfocmd] || PDFINFOCMD # for unit tests infos = retrieve_infos - @page_count = infos['Pages'] && infos['Pages'].to_i + @page_count = infos["Pages"]&.to_i end + # Returns true if default +pdfinfo+ system tool is available (for unit tests). + def self.infocmd_available? + Application.command_available?("#{PDFINFOCMD} -v") + 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/) + info_array = `#{command}`.split("\n") - Hash[info_array.map {|line| line.split(/\s*:\s*/, 2) }] + info_array.to_h {|line| line.split(/\s*:\s*/, 2) } end - - # Returns true if default +pdfinfo+ system tool is available (for unit tests). - def self.infocmd_available? - Application.command_available?("#{PDFINFOCMD} -v") - end end # The main program. It's run! method is called # if the script is run from the command line. # It parses the command line arguments and does the job. class Application - ERRORCODE = {:general => 1, :usage => 2} + ERRORCODE = { general: 1, usage: 2 }.freeze def initialize begin options = Optionparser.parse!(ARGV) - rescue => e + rescue StandardError => e usage_fail(e.message) end @infile = options[:infile] @outfile = options[:outfile] - @use_stdout = (@outfile == '-') + @use_stdout = (@outfile == "-") @silent = options[:silent] @force = options[:force] @latex = options[:latex] @number = options[:number] @pages = options[:pages] || PDFInfo.new(@infile).page_count || 1 end # The main program. def run! - # test for pdflatex installation unless @latex - message = 'seems not to be installed (you might try using the -l option)' + message = "seems not to be installed (you might try using the -l option)" general_fail("`#{PDFLATEX}' #{message}") unless self.class.command_available?("#{PDFLATEX} --version") general_fail("`pdfpages.sty' #{message}") unless self.class.command_available?("#{KPSEWHICH} pdfpages.sty") end # test input file usage_fail("no such file: `#{@infile}'") unless File.exist?(@infile) - usage_fail("specified input not of the type `file'") unless File.ftype(@infile) == 'file' + usage_fail("specified input not of the type `file'") unless File.ftype(@infile) == "file" # test for existing output file if !@use_stdout && !@force && File.exist?(@outfile) overwrite_ok = confirm("File `#{@outfile}' already exists. Overwrite?") exit unless overwrite_ok end # create LaTeX document args = { - :pdffile => @infile, - :layout => Layout.new(@number), - :page_count => @pages + pdffile: @infile, + layout: Layout.new(@number), + page_count: @pages } document = LaTeXDocument.new(args) output = nil if @latex 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) } + Dir.mktmpdir("pdfmult") do |dir| + texfile = "pdfmult.tex" + pdffile = "pdfmult.pdf" + File.write("#{dir}/#{texfile}", document.to_s) command = "#{PDFLATEX} -output-directory #{dir} #{texfile}" - Open3.popen3(command) do |stdin, stdout, stderr| + 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 output = File.read("#{dir}/#{pdffile}") end end # redirect stdout to output file - $stdout.reopen(@outfile, 'w') unless @use_stdout + $stdout.reopen(@outfile, "w") unless @use_stdout - warn "Writing on #{@outfile}." unless (@use_stdout || @silent) + warn "Writing on #{@outfile}." unless @use_stdout || @silent puts output end + # Tests silently whether the given system command is available. + # + # +command+ - command to test + def self.command_available?(command) # :nodoc: + !!system("#{command} >/dev/null 2>&1") + end + private # Asks for yes or no (y/n). # # +question+ - string to be printed @@ -363,11 +374,12 @@ # Returns +true+ if the answer is yes. def confirm(question) loop do $stderr.print "#{question} [y/n] " reply = $stdin.gets.chomp.downcase # $stdin avoids gets/ARGV problem - return reply == 'y' if /\A[yn]\Z/ =~ reply + return reply == "y" if reply.match?(/\A[yn]\z/) + warn "Please answer `y' or `n'." end end # Prints an error message and exits. @@ -380,20 +392,11 @@ def usage_fail(message) warn "#{PROGNAME}: #{message}" warn "Use `#{PROGNAME} --help' for valid options." exit ERRORCODE[:usage] end - - # Tests silently whether the given system command is available. - # - # +command+ - command to test - def self.command_available?(command) # :nodoc: - !!system("#{command} >/dev/null 2>&1") - end end -end # module +end ### call main method only if called on command line -if __FILE__ == $0 - Pdfmult::Application.new.run! -end +Pdfmult::Application.new.run! if __FILE__ == $PROGRAM_NAME