class IMGKit KNOWN_FORMATS = [:jpg, :jpeg, :png] class NoExecutableError < StandardError def initialize msg = "No wkhtmltoimage executable found at #{IMGKit.configuration.wkhtmltoimage}\n" msg << ">> Install wkhtmltoimage by hand or try running `imgkit --install-wkhtmltoimage`" super(msg) end end class ImproperSourceError < StandardError def initialize(msg) super("Improper Source: #{msg}") end end class CommandFailedError < RuntimeError attr_reader :command, :stderr def initialize(command, stderr) @command = command @stderr = stderr super("Command failed: #{command}: #{stderr}") end end class UnknownFormatError < StandardError def initialize(format) super("Unknown Format: #{format}") end end attr_accessor :source, :stylesheets, :javascripts attr_reader :options def initialize(url_file_or_html, options = {}) @source = Source.new(url_file_or_html) @stylesheets = [] @javascripts = [] @options = IMGKit.configuration.default_options.merge(options) @options.merge! find_options_in_meta(url_file_or_html) unless source.url? raise NoExecutableError.new unless File.exists?(IMGKit.configuration.wkhtmltoimage) end def command(output_file = nil) args = [executable] args += normalize_options(@options).to_a.flatten.compact if @source.html? args << '-' # Get HTML from stdin else args << @source.to_s end if output_file args << output_file.to_s else args << '-' # Read IMG from stdout end args end def executable default = IMGKit.configuration.wkhtmltoimage return default if default !~ /^\// # its not a path, so nothing we can do if File.exist?(default) default else default.split('/').last end end if Open3.respond_to? :capture3 def capture3(*opts) Open3.capture3 *opts end else # Lifted from ruby 1.9.2-p290 sources for ruby 1.8 compatibility # and modified to work on 1.8 def capture3(*cmd, &block) if Hash === cmd.last opts = cmd.pop.dup else opts = {} end stdin_data = opts.delete(:stdin_data) || '' binmode = opts.delete(:binmode) Open3.popen3(*cmd) {|i, o, e| if binmode i.binmode o.binmode e.binmode end out_reader = Thread.new { o.read } err_reader = Thread.new { e.read } i.write stdin_data i.close [out_reader.value, err_reader.value] } end end def to_img(format = nil, path = nil) append_stylesheets append_javascripts set_format(format) opts = @source.html? ? {:stdin_data => @source.to_s} : {} result, stderr = capture3(*(command(path) + [opts])) result.force_encoding("ASCII-8BIT") if result.respond_to? :force_encoding raise CommandFailedError.new(command.join(' '), stderr) if path.nil? and result.size == 0 result end def to_file(path) format = File.extname(path).gsub(/^\./,'').to_sym self.to_img(format, path) File.new(path) end def method_missing(name, *args, &block) if(m = name.to_s.match(/^to_(\w+)/)) self.send(:to_img, m[1].to_sym) else super end end protected def find_options_in_meta(body) imgkit_meta_tags(body).inject({}) do |found, tag| name = tag.attributes["name"].sub(/^#{IMGKit.configuration.meta_tag_prefix}/, '').to_sym found.merge(name => tag.attributes["content"]) end end def imgkit_meta_tags(body) require 'rexml/document' xml_body = REXML::Document.new(body) found = [] xml_body.elements.each("html/head/meta") do |tag| found << tag if tag.attributes['name'].to_s =~ /^#{IMGKit.configuration.meta_tag_prefix}/ end found rescue # rexml random crash on invalid xml [] end def style_tag_for(stylesheet) "" end def script_tag_for(javascript) if javascript.respond_to?(:read) "" else "" end end def append_stylesheets raise ImproperSourceError.new('Stylesheets may only be added to an HTML source') if stylesheets.any? && !@source.html? stylesheets.each do |stylesheet| if @source.to_s.match(/<\/head>/) @source.to_s.gsub!(/(<\/head>)/, style_tag_for(stylesheet)+'\1') else @source.to_s.insert(0, style_tag_for(stylesheet)) end end end def append_javascripts raise ImproperSourceError.new('Javascripts may only be added to an HTML source') if javascripts.any? && !@source.html? javascripts.each do |javascript| if @source.to_s.match(/<\/head>/) @source.to_s.gsub!(/(<\/head>)/, script_tag_for(javascript)+'\1') else @source.to_s.insert(0, script_tag_for(javascript)) end end end def normalize_options(options) normalized_options = {} options.each do |key, value| next if !value normalized_key = "--#{normalize_arg key}" normalized_options[normalized_key] = normalize_value(value) end normalized_options end def normalize_arg(arg) arg.to_s.downcase.gsub(/[^a-z0-9]/,'-') end def normalize_value(value) case value when TrueClass nil when Array value else value.to_s end end def set_format(format) format = IMGKit.configuration.default_format unless format @options.merge!(:format => format.to_s) unless @options[:format] raise UnknownFormatError.new(format) unless KNOWN_FORMATS.include?(@options[:format].to_sym) end end