├── .gitignore ├── Rakefile ├── lib ├── acts_as_flying_saucer │ ├── version.rb │ ├── java │ │ ├── jar │ │ │ ├── iText-2.1.7.jar │ │ │ ├── core-renderer.jar │ │ │ ├── acts_as_flying_saucer.jar │ │ │ └── xml-apis-xerces-2.9.1.jar │ │ └── src │ │ │ ├── Xhtml2Pdf.java │ │ │ └── encryptPdf.java │ ├── config.rb │ ├── xhtml2pdf.rb │ └── acts_as_flying_saucer_controller.rb └── acts_as_flying_saucer.rb ├── Gemfile ├── acts_as_flying_saucer.gemspec └── README.markdown /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | *.gem 3 | .bundle 4 | .idea/ 5 | .idea/* 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/acts_as_flying_saucer/version.rb: -------------------------------------------------------------------------------- 1 | module ActsAsFlyingSaucer 2 | VERSION = "1.0.4" 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in acts_as_flying_saucer.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/acts_as_flying_saucer/java/jar/iText-2.1.7.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amardaxini/acts_as_flying_saucer/HEAD/lib/acts_as_flying_saucer/java/jar/iText-2.1.7.jar -------------------------------------------------------------------------------- /lib/acts_as_flying_saucer/java/jar/core-renderer.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amardaxini/acts_as_flying_saucer/HEAD/lib/acts_as_flying_saucer/java/jar/core-renderer.jar -------------------------------------------------------------------------------- /lib/acts_as_flying_saucer/java/jar/acts_as_flying_saucer.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amardaxini/acts_as_flying_saucer/HEAD/lib/acts_as_flying_saucer/java/jar/acts_as_flying_saucer.jar -------------------------------------------------------------------------------- /lib/acts_as_flying_saucer/java/jar/xml-apis-xerces-2.9.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amardaxini/acts_as_flying_saucer/HEAD/lib/acts_as_flying_saucer/java/jar/xml-apis-xerces-2.9.1.jar -------------------------------------------------------------------------------- /lib/acts_as_flying_saucer.rb: -------------------------------------------------------------------------------- 1 | require 'acts_as_flying_saucer/config' 2 | require 'acts_as_flying_saucer/xhtml2pdf' 3 | require 'acts_as_flying_saucer/acts_as_flying_saucer_controller' 4 | require 'nailgun' 5 | require 'tidy_ffi' 6 | require 'net/http' 7 | require 'uri' 8 | if defined?(Rails) 9 | ActionController::Base.send(:include, ActsAsFlyingSaucer::Controller) 10 | elsif defined?(Sinatra) 11 | Sinatra::Base.send(:include, ActsAsFlyingSaucer::Controller) 12 | class Sinatra::Base 13 | acts_as_flying_saucer 14 | end 15 | end 16 | 17 | module ActsAsFlyingSaucer 18 | 19 | end 20 | -------------------------------------------------------------------------------- /lib/acts_as_flying_saucer/java/src/Xhtml2Pdf.java: -------------------------------------------------------------------------------- 1 | import java.io.*; 2 | import org.xhtmlrenderer.pdf.ITextRenderer; 3 | 4 | public class Xhtml2Pdf 5 | { 6 | public static void main(String[] args) throws Exception { 7 | 8 | String input = args[0]; 9 | String url = new File(input).toURI().toURL().toString(); 10 | String output = args[1]; 11 | 12 | OutputStream os = new FileOutputStream(output); 13 | ITextRenderer renderer = new ITextRenderer(); 14 | renderer.setDocument(url); 15 | renderer.layout(); 16 | renderer.createPDF(os); 17 | os.close(); 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/acts_as_flying_saucer/java/src/encryptPdf.java: -------------------------------------------------------------------------------- 1 | import java.io.*; 2 | import org.xhtmlrenderer.pdf.ITextRenderer; 3 | import com.lowagie.text.pdf.PdfReader; 4 | import com.lowagie.text.pdf.PdfWriter; 5 | import com.lowagie.text.pdf.PdfEncryptor; 6 | public class encryptPdf 7 | { 8 | public static void main(String[] args) throws Exception { 9 | 10 | String input = args[0]; 11 | String output = args[1]; 12 | String password = args[2]; 13 | 14 | PdfReader pr = new PdfReader(input); 15 | OutputStream os = new FileOutputStream(output); 16 | PdfEncryptor.encrypt(pr,os, false,password,password, PdfWriter.AllowAssembly | 17 | PdfWriter.AllowCopy | PdfWriter.AllowDegradedPrinting | PdfWriter.AllowFillIn | PdfWriter.AllowModifyAnnotations | PdfWriter.AllowModifyContents | PdfWriter.AllowPrinting | PdfWriter.AllowScreenReaders); 18 | os.close(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /acts_as_flying_saucer.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "acts_as_flying_saucer/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "acts_as_flying_saucer" 7 | s.version = ActsAsFlyingSaucer::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Amar Daxini"] 10 | s.email = ["amardaxini@gmail.com"] 11 | s.homepage = "http://rubygems.org/gems/acts_as_flying_saucer" 12 | s.summary = %q{XHTML to PDF using Flying Saucer java library} 13 | s.description = %q{XHTML to PDF using Flying Saucer java library} 14 | s.add_dependency "nailgun" 15 | s.add_dependency "tidy_ffi" 16 | s.rubyforge_project = "acts_as_flying_saucer" 17 | 18 | s.files = `git ls-files`.split("\n") 19 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 20 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 21 | s.require_paths = ["lib"] 22 | end 23 | -------------------------------------------------------------------------------- /lib/acts_as_flying_saucer/config.rb: -------------------------------------------------------------------------------- 1 | module ActsAsFlyingSaucer 2 | 3 | class Config 4 | # default options 5 | class << self 6 | attr_accessor :options 7 | 8 | end 9 | ActsAsFlyingSaucer::Config.options = { 10 | :java_bin => "java", 11 | :classpath_separator => ':', 12 | :tmp_path => "/tmp", 13 | :run_mode => :once, 14 | :max_memory_mb => 50, 15 | :nailgun=> false, 16 | :nailgun_port => '2113', 17 | :nailgun_host => 'localhost', 18 | } 19 | def self.setup_nailgun 20 | if ActsAsFlyingSaucer::Config.options[:nailgun] 21 | Nailgun::NailgunConfig.options= { 22 | :java_bin => ActsAsFlyingSaucer::Config.options[:java_bin], 23 | :server_address => ActsAsFlyingSaucer::Config.options[:nailgun_host], 24 | :port_no=>ActsAsFlyingSaucer::Config.options[:nailgun_port] 25 | } 26 | Nailgun::NailgunServer.new(["start"]).daemonize 27 | count =0 28 | while(!system("lsof -i -n -P|grep #{ActsAsFlyingSaucer::Config.options[:nailgun_port]}") && count<9) 29 | sleep(1) 30 | count+=1 31 | end 32 | java_dir = File.join(File.expand_path(File.dirname(__FILE__)), "java") 33 | Dir.glob("#{java_dir}/jar/*.jar") do |jar| 34 | Nailgun::NgCommand.ng_cp(jar) 35 | end 36 | # ADD IN NAILGUN CLASS 37 | 38 | Nailgun::NgCommand.ng_alias("Xhtml2Pdf","acts_as_flying_saucer.Xhtml2Pdf") 39 | Nailgun::NgCommand.ng_alias("encryptPdf", "acts_as_flying_saucer.encryptPdf") 40 | 41 | end 42 | end 43 | # cattr_accessor :options 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /lib/acts_as_flying_saucer/xhtml2pdf.rb: -------------------------------------------------------------------------------- 1 | require 'nailgun' 2 | module ActsAsFlyingSaucer 3 | 4 | # Xhtml2Pdf 5 | # 6 | class Xhtml2Pdf 7 | def self.write_pdf(options) 8 | 9 | if !File.exists?(options[:input_file]) 10 | File.open(options[:input_file], 'w') do |file| 11 | file << options[:html] 12 | end 13 | end 14 | class_path = "" 15 | java_dir = File.join(File.expand_path(File.dirname(__FILE__)), "java") 16 | Dir.glob("#{java_dir}/jar/*.jar") do |jar| 17 | class_path << "#{options[:classpath_separator]}#{jar}" 18 | end 19 | 20 | 21 | if options[:nailgun] 22 | command = "#{Nailgun::NgCommand::NGPATH} --nailgun-server #{ActsAsFlyingSaucer::Config.options[:nailgun_host]} --nailgun-port #{ ActsAsFlyingSaucer::Config.options[:nailgun_port]} Xhtml2Pdf #{options[:input_file]} #{options[:output_file]}" 23 | else 24 | command = "#{options[:java_bin]} -Xmx#{options[:max_memory_mb]}m -Djava.awt.headless=true -cp #{class_path} acts_as_flying_saucer.Xhtml2Pdf #{options[:input_file]} #{options[:output_file]}" 25 | end 26 | system(command) 27 | end 28 | 29 | def self.encrypt_pdf(options,output_file_name,password) 30 | java_dir = File.join(File.expand_path(File.dirname(__FILE__)), "java") 31 | class_path = "'.#{options[:classpath_separator]}#{java_dir}/jar/acts_as_flying_saucer.jar'" 32 | if options[:nailgun] 33 | command = "#{Nailgun::NgCommand::NGPATH} --nailgun-server #{ActsAsFlyingSaucer::Config.options[:nailgun_host]} --nailgun-port #{ ActsAsFlyingSaucer::Config.options[:nailgun_port]} encryptPdf #{options[:input_file]} #{options[:output_file]}" 34 | else 35 | command = "#{options[:java_bin]} -Xmx#{options[:max_memory_mb]}m -Djava.awt.headless=true -cp #{class_path} acts_as_flying_saucer.encryptPdf #{options[:output_file]} #{output_file_name} #{password}" 36 | end 37 | system(command) 38 | end 39 | 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/acts_as_flying_saucer/acts_as_flying_saucer_controller.rb: -------------------------------------------------------------------------------- 1 | # ActsAsFlyingSaucer 2 | module ActsAsFlyingSaucer 3 | 4 | module Controller 5 | def self.included(base) 6 | base.extend(ClassMethods) 7 | end 8 | 9 | private 10 | 11 | # ClassMethods 12 | # 13 | module ClassMethods 14 | 15 | # acts_as_flying_saucer 16 | # 17 | def acts_as_flying_saucer 18 | self.send(:include, ActsAsFlyingSaucer::Controller::InstanceMethods) 19 | class_eval do 20 | attr_accessor :pdf_mode 21 | end 22 | end 23 | end 24 | 25 | # InstanceMethods 26 | # 27 | module InstanceMethods 28 | 29 | # render_pdf 30 | # 31 | def render_pdf(options = {}) 32 | tidy_clean = options[:clean] || false 33 | self.pdf_mode = :create 34 | if options[:url] 35 | tidy_clean = true 36 | if options[:url].match(/\Ahttp/) 37 | html = Net::HTTP.get_response(URI.parse(options[:url])).body rescue options[:url] 38 | elsif File.exist?(options[:url]) 39 | html = File.read(options[:url]) rescue "" 40 | else 41 | html = options[:url] 42 | end 43 | elsif defined?(Rails) 44 | host = ActionController::Base.asset_host 45 | ActionController::Base.asset_host = request.protocol + request.host_with_port if host.blank? 46 | html = render_to_string options 47 | if options[:debug_html] 48 | # ActionController::Base.asset_host = host 49 | response.header["Content-Type"] = "text/html; charset=utf-8" 50 | render :text => html and return 51 | end 52 | #sinatra 53 | elsif defined?(Sinatra) 54 | html = options[:template] 55 | if options[:debug_html] 56 | response.header["Content-Type"] = "text/html; charset=utf-8" 57 | response.body << html and return 58 | end 59 | end 60 | # saving the file 61 | tmp_dir = ActsAsFlyingSaucer::Config.options[:tmp_path] 62 | html = TidyFFI::Tidy.new(html,:output_xhtml=>true,:numeric_entities=>true).clean if tidy_clean 63 | html_digest = Digest::MD5.hexdigest(html) 64 | input_file =File.join(File.expand_path("#{tmp_dir}"),"#{html_digest}.html") 65 | 66 | #logger.debug("html file: #{input_file}") 67 | 68 | output_file = (options.has_key?(:pdf_file)) ? options[:pdf_file] : File.join(File.expand_path("#{tmp_dir}"),"#{html_digest}.pdf") 69 | password = (options.has_key?(:password)) ? options[:password] : "" 70 | 71 | 72 | generate_options = ActsAsFlyingSaucer::Config.options.merge({ 73 | :input_file => input_file, 74 | :output_file => output_file, 75 | :html => html 76 | }) 77 | 78 | ActsAsFlyingSaucer::Xhtml2Pdf.write_pdf(generate_options) 79 | if password != "" 80 | op=output_file.split(".") 81 | op.pop 82 | op << "a" 83 | op=op.to_s+".pdf" 84 | output_file_name = op 85 | ActsAsFlyingSaucer::Xhtml2Pdf.encrypt_pdf(generate_options,output_file_name,password) 86 | output_file = op 87 | end 88 | # restoring the host 89 | if defined?(Rails) 90 | ActionController::Base.asset_host = host 91 | end 92 | 93 | # sending the file to the client 94 | if options[:send_to_client] == false 95 | output_file 96 | else 97 | if options[:send_file] 98 | 99 | send_file_options = { 100 | :filename => File.basename(output_file) 101 | #:x_sendfile => true, 102 | } 103 | send_file_options.merge!(options[:send_file]) if options.respond_to?(:merge) 104 | send_file(output_file, send_file_options) 105 | end 106 | end 107 | end 108 | end 109 | end 110 | 111 | end 112 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | acts\_as\_flying\_saucer 2 | ===================== 3 | 4 | acts\_as\_flying\_saucer is a library that allows to save rendered views as pdf documents using the [Flying Saucer][1] java library. 5 | 6 | [1]: https://code.google.com/p/flying-saucer/ 7 | 8 | Install 9 | ------- 10 | 11 | Grab the last version from Github: 12 | 13 | sudo gem install acts_as_flying_saucer 14 | 15 | 16 | Requirements 17 | ------------ 18 | 19 | * JDK 1.5.x or 1.6.x 20 | * Tidy required 21 | * Ubuntu Installation 22 | * Install tidy with dev and ruby binding. 23 | 24 | * apt-get install tidy 25 | * apt-get install libtidy-dev 26 | * apt-get install libtidy-ruby 27 | 28 | 29 | Usage 30 | ----- 31 | ### Rails 32 | Just call the acts\_as\_flying\_saucer method inside the controller you want to enable to generate pdf documents. 33 | Then you can call the render\_pdf method. 34 | It accepts the same options as ActionController::Base#render plus the following ones: 35 | 36 | 37 | :pdf\_file - absolute path for the generated pdf file. 38 | 39 | :send\_file - sends the generated pdf file to the browser. It's the hash the ActionController::Streaming#send\_file method will receive. 40 | 41 | :password attach password for generated pdf 42 | 43 | :debug_html - (boolean expected) generates html output to the browser for debugging purposes 44 | 45 | :clean - (boolean expected) It cleans up html using tidy (It uses tidy_ffi gem) default is false 46 | 47 | :send_to_client - (boolean expected) If it is false it returns output pdf and attachment is not sent to client. 48 | 49 | :url - url can be file path or any http url (with http or https) or string. This will generated pdf from file,url or string. 50 | 51 | class FooController < ActionController::Base 52 | acts_as_flying_saucer 53 | 54 | def create 55 | render_pdf :template => 'foo/pdf_template' 56 | end 57 | end 58 | 59 | 60 | 61 | 62 | Examples 63 | -------- 64 | 65 | # Renders the template located at '/foo/bar/pdf.html.erb' and stores the pdf 66 | # in the temp path with a filename based on its md5 digest 67 | render_pdf :file => '/foo/bar/pdf.html.erb' 68 | 69 | # renders the template located at 'app/views/foo.html.erb' and saves the pdf 70 | # in '/www/docs/foo.pdf' 71 | render_pdf :template => 'foo', :pdf_file => '/www/docs/foo.pdf' 72 | 73 | # renders the 'app/views/foo.html.erb' template, saves the pdf in the temp path 74 | # and sends it to the browser with the name 'bar.pdf' 75 | render_pdf :template => 'foo', :send_file => { :filename => 'bar.pdf' } 76 | 77 | # To send file with password protection 78 | render_pdf :template => 'foo', :send_file => { :filename => 'bar.pdf' },:password=>"xxx" 79 | Now pdf is password protected 80 | 81 | Easy as pie 82 | 83 | While converting the xhtml document into a pdf, the css stylesheets and images should be referenced with absolute URLs(either local or remote) or Flying Saucer will not be able to access them. 84 | If there is no asset host defined, it will set automatically during the pdf generation so the parser can access the requested resources: 85 | 86 | View rendered in the browser: 87 | 88 | <%= stylesheet_link_tag("styles.css") %> 89 | # 90 | 91 | 92 | <%= image_tag("rails.png") %> 93 | # Rails 94 | 95 | View rendered as pdf: 96 | 97 | <%= stylesheet_link_tag("styles.css") %> 98 | # 99 | 100 | 101 | <%= image_tag("rails.png") %> 102 | # Rails 103 | 104 | The stylesheet media type will be set to 'print' if none was given(otherwise it would not be parsed) 105 | 106 | If you need to distinguish if the view is being rendered in the browser or as a pdf, you can use the @pdf\_mode variable, whose value will be set to :create 107 | when generating the pdf version 108 | 109 | Sinatra 110 | ------- 111 | get '/' do 112 | content_type 'application/pdf' 113 | html_content = erb :index 114 | render_pdf(:template=>html_content,:pdf_file=>"test.pdf", 115 | :send_file=> {:file_name=>"test.pdf",:stream=>false},:password=>"xxx") 116 | end 117 | Ruby 118 | ---- 119 | require 'digest/sha1' 120 | class Pdf 121 | include ActsAsFlyingSaucer::Controller 122 | acts_as_flying_saucer 123 | def generate_pdf(input_file_html_or_string,output_pdf) 124 | options = ActsAsFlyingSaucer::Config.options.merge({:url=>input_file_html_or_string,:pdf_file=>output_pdf}) 125 | render_pdf(options) 126 | end 127 | end 128 | 129 | 130 | Configuration 131 | ------------- 132 | 133 | These are the default settings which can be overwritten in your enviroment configuration file: 134 | 135 | ActsAsFlyingSaucer::Config.options = { 136 | :java_bin => "java", # java binary 137 | :classpath_separator => ':', # classpath separator. unixes system use ':' and windows ';' 138 | :tmp_path => "/tmp", # path where temporary files will be stored 139 | :max_memory_mb=>512, 140 | :nailgun =>false, 141 | :nailgun_port => '2113', 142 | :nailgun_host => 'localhost' 143 | } 144 | 145 | 146 | Advance Configuration (TODO: Manually) 147 | ------------------- 148 | Now acts_as_flying_saucer call java each time on creating pdf this will speed down speed of generation of pdf.To overcome this start nailgun server that reads data from specific port and rendered pdf.so there is no need to launch the jvm every time a new pdf is generated. 149 | Generate pdf with nailgun you have to overwrite Configuration make **nailgun option to true** 150 | 151 | 152 | So to start nailgun with acts_as_flying_saucer gem: 153 | 154 | 155 | sudo gem install nailgun 156 | 157 | 158 | Start nailgun server.Before starting nailgun server make sure that your **classpath environment variable** set and point to jre/lib 159 | 160 | On startup or manually write following line 161 | You can write into config/initializers/ 162 | After setting nailgun host and port 163 | 164 | ActsAsFlyingSaucer::Config.setup_nailgun 165 | 166 | 167 | You can Manage nailgun using 168 | 169 | nailgun 170 | 171 | It will generate nailgun_config binary and set configuration parameter 172 | and now you can start and stop nailgun server 173 | 174 | 175 | 176 | 177 | --------------------------------------------------------------------------------