diff --git a/README.mdown b/README.mdown index 23d6c98..244dabf 100644 --- a/README.mdown +++ b/README.mdown @@ -1,19 +1,97 @@ -# Tilt::PDF::Rails +# Tilt::PDF -Integrates Tilt::PDF into Rails ActionView +Integrates PDF generation into a Tilt flow for maxxed out ease of use. -See [Tilt::PDF](https://github.com/lloeki/tilt-pdf) for details about the templates themselves. +Contrary to other solutions, all files will be rendered locally without relying +on a web server. It follows that even if used in a web server, no concurrent +requests are being made to render assets. -## Usage +## Dependencies + +This gem depends on PDFKit, which in turn requires `wkhtmltopdf`. It is +recommended to use the statically compiled version of the latter, as it is +built against a patched QT that supports more features. + +## Usage as a Tilt template + +Add `tilt-pdf` to your Gemfile. Also add any template engine you may optionally +want, such as `less` or `slim`. + +A `foo` template is currently threefold: + +- `foo.rpdf`: this file is a YAML file containing options pertaining to the PDF + generation process, such as page size, orientation, metadata, support files, + headers and footers. Some options are passed as is to PDFKit, and + subsequently to `wkhtmltopdf`. +- `foo.html`: this document can be written in any template language you need + (such as ERB or Slim), and the Tilt template resolution system via extension + chaining will apply. Tilt will pass the render block to be yielded to this + document. +- `foo.css`: this stylesheet can be written in any template language you need + (such as Sass or Less), and the Tilt template resolution system via extension + chaining will apply. Tilt will *not* pass the block to be yielded to this + template. + +The three files must currently be stored in the *same* directory. + +Rendering is done the usual Tilt way: ```ruby -gem 'tilt-pdf-rails' +require 'tilt-pdf' + +pdf = Tilt.new('foo.rpdf').render() ``` -Have a `FooController` `respond_to :pdf` and `render some_view` as usual. +## The rpdf file -Put your `some_view` template files either *together* in the relevant -`app/views/foo` view directory, or use absolute paths using application/engine -root. +This file contains options. If empty, it is made to 'just work' as +summplemental files will be looked up according to its basename. -Work is in progress to enable better integration with Rails file layout (notably assets). +- `main`: document body, overriding the default derived from the basename. +- `footer` and `header`: html that will get used for (surprise!) header and + footers. +- `stylesheets`: list of stylesheets to include (used for all html, incl. + headers/footers). Defaults to one file from the basename. +- `javascripts`: list of javascripts to include (used for all html, incl. + headers/footers). Defaults to one file from the basename. +- `pdfkit`: While a few PDFKit options are made available at the toplevel for + convenience, this key passes all options as-is to PDFKit. + +Example: + +``` +title: Foorever young +page-size: A4 +orientation: landscape +grayscale: true +margin-left: 0 +margin-right: 0 +margin-top: 0 +margin-bottom: 0 +pdfkit: + print-media-type: true +main: foorever_young.html.slim +stylesheets: + - novel.css.less + - common.css +javascripts: + - page_numbering.js.coffee +footer: footer.html.slim +``` + +Filenames can be relative or absolute. When relative, they will be evaluated +as based from the rpdf file. + +## Rails and ActionView integration + +Require `tilt/pdf/rails` if you want to set up and register `tilt-pdf` as an +ActionView template handler. You can do it in an initializer, or straight from +the Gemfile: + +```ruby +gem 'tilt-pdf', require: 'tilt/pdf/rails' +``` + +Put your three template files *together* in the relevant `app/views/foo` view +directory, or use absolute paths using application/engine root. Work is in +progress to enable better integration with Rails file layout. diff --git a/lib/tilt-pdf-rails.rb b/lib/tilt-pdf-rails.rb deleted file mode 100644 index 4432e02..0000000 --- a/lib/tilt-pdf-rails.rb +++ /dev/null @@ -1 +0,0 @@ -require 'tilt/pdf/rails' diff --git a/lib/tilt-pdf.rb b/lib/tilt-pdf.rb new file mode 100644 index 0000000..4f638a3 --- /dev/null +++ b/lib/tilt-pdf.rb @@ -0,0 +1 @@ +require 'tilt/pdf' diff --git a/lib/tilt/pdf.rb b/lib/tilt/pdf.rb new file mode 100644 index 0000000..d580869 --- /dev/null +++ b/lib/tilt/pdf.rb @@ -0,0 +1,211 @@ +require 'pdfkit' +require 'tilt' +require 'tilt/template' +require 'yaml' +require 'tempfile' +require 'pry' + +module Tilt + class PDFTemplate < Template + self.default_mime_type = 'application/pdf' + + def prepare; end + + def evaluate(scope, locals, &block) + files = aux_files + files << main_html_file + render_to_tmp(*files, scope, locals, block) do |tmp| + opts = pdfkit_options + + if header + htmp = tmp.select { |_, f, _| f == header }.first[2] + opts.merge!('header-html' => htmp) if header + end + + if footer + ftmp = tmp.select { |_, f, _| f == footer }.first[2] + opts.merge!('footer-html' => ftmp) if footer + end + + main = tmp.select { |_, f, _| f == main_html_file }.first[2] + kit = PDFKit.new(File.read(main), opts) + + @output = kit.to_pdf + end + + @output + end + + private + + def absolutize(path) + Pathname.new(path).absolute? ? path : File.join(dirname, path) + end + + def config + @config = (YAML.load(data) || {}) + end + + def aux_files + files = [] + files.concat css_files + files.concat js_files + files << header if header + files << footer if footer + + files + end + + def header + if (f = config['header']) + absolutize(f) + end + end + + def footer + if (f = config['footer']) + absolutize(f) + end + end + + def main_html_file + main_html_from_config || find_html + end + + def main_html_from_config + if (f = config['main']) + absolutize(f) + end + end + + def css_files + css_from_config || find_css + end + + def css_from_config + return unless config.key?('stylesheets') + + config.fetch('stylesheets', []).map { |f| absolutize(f) } + end + + def js_files + js_from_config || find_js + end + + def js_from_config + return unless config.key?('javascripts') + + config.fetch('javascripts', []).map { |f| absolutize(f) } + end + + def pdfkit_options + toplevel_options = %w[title + orientation + grayscale + page-size + margin-left + margin-right + margin-top + margin-bottom] + + options = config.select { |k, _| toplevel_options.include?(k) } + + options.merge config.fetch('pdfkit', {}) + end + + def dirname + File.dirname(eval_file) + end + + def find_html + Dir.glob(File.join(dirname, name + '.html*')).first + end + + def find_css + Dir.glob(File.join(dirname, name + '.css*')) + end + + def find_js + Dir.glob(File.join(dirname, name + '.js*')) + end + + def render_html(file, scope, locals, &block) + Tilt.new(file).render(scope, locals, &block) + end + + def inject_css!(document, stylesheet) + append_to_head!(document, "") + end + + def inject_js!(document, script) + append_to_head!(document, "") + end + + def append_to_head!(document, tag) + if document.match(/<\/head>/) + document.gsub!(/(<\/head>)/) { |s| tag + s } + else + document.insert(0, tag) + end + end + + def render_to_tmp(*files, scope, locals, block) + tmps = [] + no_tilt = %w[html css js] + + result = files.map do |file| + ext = File.extname(file).sub(/^\./, '') + if no_tilt.include?(ext) + mime = case ext + when 'js', 'javascript' then 'application/javascript' + else "text/#{ext}" + end + rendered = File.read(file) + else + template = Tilt.new(file) + mime = template.class.default_mime_type + ext = mime.split('/').last + rendered = template.render(scope, locals, &block) + end + + [ext, mime, file, rendered] + end + + result.sort! do |a, b| + a[1] <=> b[1] # css < html + end + + styles = result.select { |_, mime, _, _| mime == 'text/css' } + scripts = result.select { |_, mime, _, _| mime == 'application/javascript' } + result.map! do |ext, mime, file, rendered| + if mime == 'text/html' + styles.each do |_, _, _, style| + inject_css!(rendered, style) + end + scripts.each do |_, _, _, script| + inject_js!(rendered, script) + end + end + + [ext, mime, file, rendered] + end + + result.map! do |ext, mime, file, rendered| + tmp = Tempfile.new([File.basename(file), '.' + ext]) + tmps << tmp + tmp.write(rendered) + tmp.close + + path = tmp.path + + [mime, file, path] + end + + yield result + ensure + tmps.each { |tmp| tmp.close! } + end + end +end + +Tilt.register Tilt::PDFTemplate, 'rpdf' diff --git a/lib/tilt/pdf/rails/version.rb b/lib/tilt/pdf/rails/version.rb deleted file mode 100644 index 8607c22..0000000 --- a/lib/tilt/pdf/rails/version.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Tilt - module PDF - module Rails - VERSION = '0.10.0' - end - end -end diff --git a/lib/tilt/pdf/version.rb b/lib/tilt/pdf/version.rb new file mode 100644 index 0000000..05c0a13 --- /dev/null +++ b/lib/tilt/pdf/version.rb @@ -0,0 +1,5 @@ +module Tilt + module PDF + VERSION = '0.9.0' + end +end diff --git a/tilt-pdf-rails.gemspec b/tilt-pdf-rails.gemspec deleted file mode 100644 index 9bf7900..0000000 --- a/tilt-pdf-rails.gemspec +++ /dev/null @@ -1,21 +0,0 @@ -# -*- encoding: utf-8 -*- -$LOAD_PATH.push File.expand_path('../lib', __FILE__) -require 'tilt/pdf/rails/version' - -Gem::Specification.new do |s| - s.name = 'tilt-pdf-rails' - s.version = Tilt::PDF::Rails::VERSION - s.authors = ['Loic Nageleisen'] - s.email = ['loic.nageleisen@gmail.com'] - s.homepage = 'http://github.com/lloeki/tilt-pdf-rails' - s.summary = %q{PDF files in Rails via Tilt} - s.description = %q{Integrates PDF generation with Tilt::PDF into ActionView} - s.license = 'MIT' - s.files = `git ls-files`.split("\n") - s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") - s.executables = `git ls-files -- bin/*`.split("\n") - .map { |f| File.basename(f) } - s.require_paths = ['lib'] - - s.add_dependency 'tilt-pdf', '~> 0.10.0' -end diff --git a/tilt-pdf.gemspec b/tilt-pdf.gemspec new file mode 100644 index 0000000..be402c3 --- /dev/null +++ b/tilt-pdf.gemspec @@ -0,0 +1,31 @@ +# -*- encoding: utf-8 -*- +$LOAD_PATH.push File.expand_path('../lib', __FILE__) +require 'tilt/pdf/version' + +Gem::Specification.new do |s| + s.name = 'tilt-pdf' + s.version = Tilt::PDF::VERSION + s.authors = ['Loic Nageleisen'] + s.email = ['loic.nageleisen@gmail.com'] + s.homepage = 'http://github.com/lloeki/tilt-pdf' + s.summary = %q{PDF files via Tilt} + s.description = %q{Integrates PDF generation into a Tilt flow} + s.license = 'MIT' + s.files = `git ls-files`.split("\n") + s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") + s.executables = `git ls-files -- bin/*`.split("\n") + .map { |f| File.basename(f) } + s.require_paths = ['lib'] + + s.add_dependency 'tilt', '~> 1.4.1' + s.add_dependency 'pdfkit', '~> 0.5.4' + + s.add_development_dependency 'therubyracer' + s.add_development_dependency 'less' + s.add_development_dependency 'coffee-script' + s.add_development_dependency 'slim' + s.add_development_dependency 'rspec', '~> 2.14' + s.add_development_dependency 'rake' + s.add_development_dependency 'guard-rspec' + s.add_development_dependency 'pry' +end