require 'rbconfig' module Docsplit class PdfExtractor @@executable = nil @@version_string = nil # Provide a set of helper functions to determine the OS. HOST_OS = (defined?("RbConfig") ? RbConfig : Config)::CONFIG['host_os'] def windows? !!HOST_OS.match(/mswin|windows|cygwin/i) end def osx? !!HOST_OS.match(/darwin/i) end def linux? !!HOST_OS.match(/linux/i) end # The first line of the help output holds the name and version number # of the office software to be used for extraction. def version_string unless @@version_string null = windows? ? "NUL" : "/dev/null" @@version_string = `#{office_executable} -h 2>#{null}`.split("\n").first if !!@@version_string.to_s.match(/[0-9]*/) @@version_string = `#{office_executable} --version`.split("\n").first end end @@version_string end def libre_office? !!version_string.match(/^LibreOffice/) end def open_office? !!version_string.match(/^OpenOffice.org/) end # A set of default locations to search for office software # These have been extracted from JODConverter. Each listed # path should contain a directory "program" which in turn # contains the "soffice" executable. # see: https://github.com/mirkonasato/jodconverter/blob/master/jodconverter-core/src/main/java/org/artofsolving/jodconverter/office/OfficeUtils.java#L63-L91 def office_search_paths if windows? office_names = ["LibreOffice 3", "LibreOffice 4", "OpenOffice.org 3"] program_files_path = ENV["CommonProgramFiles"] search_paths = office_names.map{ |program| File.join(program_files_path, program) } elsif osx? search_paths = %w( /Applications/LibreOffice.app/Contents /Applications/OpenOffice.org.app/Contents ) else # probably linux/unix # heroku libreoffice buildpack: https://github.com/rishihahs/heroku-buildpack-libreoffice search_paths = %w( /usr/lib/libreoffice /usr/lib64/libreoffice /opt/libreoffice /usr/lib/openoffice /usr/lib64/openoffice /opt/openoffice.org3 /app/vendor/libreoffice /usr/bin/libreoffice /usr/local/bin /usr/lib64/libreoffice /usr/lib64/openoffice.org3 ) end search_paths end # Identify the path to a working office executable. def office_executable paths = office_search_paths # If an OFFICE_PATH has been specified on the commandline # raise an error if that path isn't valid, otherwise, add # it to the front of our search paths. if ENV['OFFICE_PATH'] raise ArgumentError, "No such file or directory #{ENV['OFFICE_PATH']}" unless File.exists? ENV['OFFICE_PATH'] paths.unshift(ENV['OFFICE_PATH']) end # The location of the office executable is OS dependent path_pieces = ["soffice"] if windows? path_pieces += [["program", "soffice.bin"]] elsif osx? path_pieces += [["MacOS", "soffice"], ["Contents", "MacOS", "soffice"]] else path_pieces += [["program", "soffice"]] end # Search for the first suitable office executable # and short circuit an executable is found. paths.each do |path| if File.exists? path @@executable ||= path unless File.directory? path path_pieces.each do |pieces| check_path = File.join(path, pieces) @@executable ||= check_path if File.exists? check_path end end break if @@executable end raise OfficeNotFound, "No office software found" unless @@executable @@executable end # Used to specify the office location for JODConverter def office_path File.dirname(File.dirname(office_executable)) end # Convert documents to PDF. def extract(docs, opts) out = opts[:output] || '.' FileUtils.mkdir_p out unless File.exists?(out) [docs].flatten.each do |doc| ext = File.extname(doc) basename = File.basename(doc, ext) escaped_doc, escaped_out, escaped_basename = [doc, out, basename].map(&ESCAPE) if GM_FORMATS.include?(`file -b --mime #{ESCAPE[doc]}`.strip.split(/[:;]\s+/)[0]) `gm convert #{escaped_doc} #{escaped_out}/#{escaped_basename}.pdf` else if libre_office? # Set the LibreOffice user profile, so that parallel uses of cloudcrowd don't trip over each other. ENV['SYSUSERCONFIG']="file://#{File.expand_path(escaped_out)}" options = "--headless --invisible --norestore --nolockcheck --convert-to pdf --outdir #{escaped_out} #{escaped_doc}" cmd = "#{office_executable} #{options} 2>&1" result = `#{cmd}`.chomp raise ExtractionFailed, result if $? != 0 true else # open office presumably, rely on JODConverter to figure it out. options = "-jar #{ESCAPED_ROOT}/vendor/jodconverter/jodconverter-core-3.0-beta-4.jar -r #{ESCAPED_ROOT}/vendor/conf/document-formats.js" run_jod "#{options} #{escaped_doc} #{escaped_out}/#{escaped_basename}.pdf", [], {} end end end end CLASSPATH = "#{ESCAPED_ROOT}/build#{File::PATH_SEPARATOR}#{ESCAPED_ROOT}/vendor/'*'" LOGGING = "-Djava.util.logging.config.file=#{ESCAPED_ROOT}/vendor/logging.properties" HEADLESS = "-Djava.awt.headless=true" private # Runs a Java command, with quieted logging, and the classpath set properly. def run_jod(command, pdfs, opts, return_output=false) pdfs = [pdfs].flatten.map{|pdf| "\"#{pdf}\""}.join(' ') office = osx? ? "-Doffice.home=#{office_path}" : office_path cmd = "java #{HEADLESS} #{LOGGING} #{office} -cp #{CLASSPATH} #{command} #{pdfs} 2>&1" result = `#{cmd}`.chomp raise ExtractionFailed, result if $? != 0 return return_output ? (result.empty? ? nil : result) : true end class OfficeNotFound < StandardError; end end end