xing/pdf_cover

View on GitHub
lib/pdf_cover/converter.rb

Summary

Maintainability
A
55 mins
Test Coverage
require "open3"

module PdfCover
  class Converter
    # NOTE: Update the generate_jpegs.sh script when changing these values
    DEFAULT_FORMAT = "jpeg".freeze
    DEFAULT_QUALITY = 85
    DEFAULT_RESOLUTION = 300
    DEFAULT_ANTIALIASING = 4

    COMMAND_EXECUTION_SUCCESS_CODE = 0
    COMMAND_NOT_FOUND_CODE = 127 # @see http://www.tldp.org/LDP/abs/html/exitcodes.html

    class CommandFailedError < StandardError
      def initialize(stdout_str, stderr_str)
        super("PDF conversion failed:\nSTDOUT: #{stdout_str}\nSTDERR: #{stderr_str}")
      end
    end

    class CommandNotFoundError < StandardError
      def initialize
        super("Could not run the `gs` command. Make sure that GhostScript is in your PATH.")
      end
    end

    class InvalidOption < StandardError
      def initialize(option_name, option_value)
        super("Invalid option '#{option_name}' with value: '#{option_value}'")
      end
    end

    def initialize(file, options = {})
      @file = file
      extract_options(options)
    end

    # @raises PdfCover::Converter::CommandNotFoundError if GhostScript is not found
    # @raises PdfCover::Converter::CommandFailedError if GhostScript returns a non-zero status
    def converted_file
      parameters = build_parameters(file_path, device)
      stdout_str, stderr_str, status = execute_command("gs #{parameters}")

      case status
        when COMMAND_EXECUTION_SUCCESS_CODE then destination_file
        when COMMAND_NOT_FOUND_CODE then fail CommandNotFoundError
        else fail CommandFailedError.new(stdout_str, stderr_str)
      end
    rescue => e
      destination_file.close
      raise e
    end

    private

    def basename
      @basename ||= File.basename(@file.path, File.extname(@file.path))
    end

    def build_parameters(source, device)
      %W(-sOutputFile="#{destination_path}" -dNOPAUSE
         -sDEVICE="#{device}" -dJPEGQ=#{@quality}
         -dFirstPage=1 -dLastPage=1
         -dGraphicsAlphaBits=#{@antialiasing}
         -r#{@resolution} -q "#{source}"
         -c quit)
        .join(" ")
    end

    def destination_file
      @destination_file ||= Tempfile.new([basename, ".#{@format}"]).tap do |file|
        file.binmode
      end
    end

    def destination_path
      File.expand_path(destination_file.path)
    end

    def device
      @format.to_s == "jpg" ? "jpeg" : @format.to_s
    end

    def execute_command(command)
      Open3.capture3(command)
    end

    def extract_options(options)
      @format = options.fetch(:format, DEFAULT_FORMAT)
      @quality = options.fetch(:quality, DEFAULT_QUALITY).to_i
      @resolution = options.fetch(:resolution, DEFAULT_RESOLUTION).to_i
      @antialiasing = options.fetch(:antialiasing, DEFAULT_ANTIALIASING).to_i

      validate_options
    end

    def file_path
      @file_path ||= File.expand_path(@file.path)
    end

    def validate_options
      fail InvalidOption.new(:format, @format) unless %w(jpg jpeg png).include?(@format)
      fail InvalidOption.new(:quality, @quality) unless @quality.between?(1, 100)
      fail InvalidOption.new(:resolution, @resolution) unless @resolution > 1
      fail InvalidOption.new(:antialiasing, @antialiasing) unless @antialiasing >= 1 && @antialiasing <= 4
    end
  end
end