pdf_renderer 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/pdf_renderer.rb +5 -0
- data/lib/pdf_renderer/base.rb +137 -0
- data/lib/pdf_renderer/helpers.rb +33 -0
- data/lib/pdf_renderer/helpers/latex_helper.rb +28 -0
- data/lib/pdf_renderer/latex.rb +110 -0
- data/lib/pdf_renderer/pdf.rb +30 -0
- data/rails_generators/pdf_renderer/USAGE +15 -0
- data/rails_generators/pdf_renderer/pdf_renderer_generator.rb +26 -0
- data/rails_generators/pdf_renderer/templates/functional_test.rb +17 -0
- data/rails_generators/pdf_renderer/templates/pdf_renderer.rb +8 -0
- data/rails_generators/pdf_renderer/templates/view.erb +11 -0
- metadata +72 -0
data/lib/pdf_renderer.rb
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
module PdfRenderer
|
2
|
+
# Base class for rendering PDFs. PDFs are rendered in two passes. The first
|
3
|
+
# pass evaluates an LaTeX ERB template. In the second pass the evaluated
|
4
|
+
# output is piped through pdflatex. The resulting PDF is returned as a string.
|
5
|
+
#
|
6
|
+
# === Usage
|
7
|
+
#
|
8
|
+
# The usage is very similar to ActionMailer. Example:
|
9
|
+
#
|
10
|
+
# class BillPdfRenderer < PdfRenderer::Base
|
11
|
+
# def bill(model)
|
12
|
+
# body :variable => model
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# Render the PDF using
|
17
|
+
#
|
18
|
+
# pdf_as_string = BillPdfRenderer.render_bill(model)
|
19
|
+
#
|
20
|
+
# The default template path is
|
21
|
+
# <code>underscored_class_name/action_name.pdf.erb</code> and is looked for in
|
22
|
+
# all specified view_paths.
|
23
|
+
#
|
24
|
+
# === Saving PDFs
|
25
|
+
#
|
26
|
+
# You can also use PdfRenderer::Base to generate and save PDFs to the file
|
27
|
+
# system. Instead of calling <code>render_</code>
|
28
|
+
#
|
29
|
+
# === Helpers
|
30
|
+
#
|
31
|
+
# To use view helpers, declare them in class scope. You can declare them as
|
32
|
+
# symbols, strings or constants, camelized or underscored. Omit the "Helper"
|
33
|
+
# suffix when using strings or symbols.
|
34
|
+
#
|
35
|
+
# The LatexHelper included in the PdfRenderer gem is automatically added to
|
36
|
+
# the ActionView instance. This helper contains methods for escaping strings
|
37
|
+
# to LaTeX.
|
38
|
+
#
|
39
|
+
# === Options
|
40
|
+
#
|
41
|
+
# Inside of render actions, there are a couple of options you can use:
|
42
|
+
#
|
43
|
+
# preprocess:: Run pdflatex twice for assigning page numbers etc.
|
44
|
+
# debug:: Save the rendered tex source to the file system for debugging.
|
45
|
+
class Base
|
46
|
+
include ActionMailer::AdvAttrAccessor
|
47
|
+
include PdfRenderer::Helpers
|
48
|
+
|
49
|
+
adv_attr_accessor :body, :template_name, :preprocess, :debug
|
50
|
+
|
51
|
+
# Contains the input for LaTeX
|
52
|
+
attr_reader :tex_out
|
53
|
+
|
54
|
+
# Paths where views are looked for in.
|
55
|
+
class_inheritable_array :view_paths
|
56
|
+
|
57
|
+
# Sets default options
|
58
|
+
#
|
59
|
+
# preprocess:: <code>false</code>
|
60
|
+
# debug:: <code>false</code>
|
61
|
+
def initialize
|
62
|
+
preprocess false
|
63
|
+
debug false
|
64
|
+
end
|
65
|
+
|
66
|
+
# Depending on the method pattern, does one of three things:
|
67
|
+
#
|
68
|
+
# * If the method name starts with <code>render_</code>, the action is
|
69
|
+
# called on a new instance of the renderer and the rendered PDF is
|
70
|
+
# returned as a string.
|
71
|
+
# * If the method starts with <code>save_</code>, the action is called,
|
72
|
+
# the PDF is generated and saved to the path given in the first method
|
73
|
+
# argument.
|
74
|
+
# * Otherwise, the action is called on a new instance of the renderer and a
|
75
|
+
# Pdf object is returned.
|
76
|
+
def self.method_missing(method, *params)
|
77
|
+
if method.to_s =~ /^render_(.*)$/
|
78
|
+
pdf = send($1, *params)
|
79
|
+
pdf.render!
|
80
|
+
elsif method.to_s =~ /^save_(.*)$/
|
81
|
+
file_name = params.shift
|
82
|
+
pdf = send($1, *params)
|
83
|
+
pdf.save(file_name)
|
84
|
+
elsif instance_methods.include?(method.to_s)
|
85
|
+
renderer_instance = new
|
86
|
+
renderer_instance.template_name = method
|
87
|
+
renderer_instance.send(renderer_instance.template_name, *params)
|
88
|
+
Pdf.new(renderer_instance)
|
89
|
+
else
|
90
|
+
super
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# The root for view files.
|
95
|
+
def self.template_root
|
96
|
+
"#{RAILS_ROOT}/app/views"
|
97
|
+
end
|
98
|
+
|
99
|
+
# The directory in which templates are stored by default for this renderer.
|
100
|
+
def template_dir
|
101
|
+
self.class.name.underscore
|
102
|
+
end
|
103
|
+
|
104
|
+
# The complete path to the LaTeX ERB template.
|
105
|
+
def template_path
|
106
|
+
"#{template_dir}/#{template_name}"
|
107
|
+
end
|
108
|
+
|
109
|
+
# Delegates the render call to ActionView and runs the resulting LaTeX code
|
110
|
+
# through pdflatex.
|
111
|
+
def render(options)
|
112
|
+
@tex_out = template_instance.render options
|
113
|
+
Latex.new(:preprocess => preprocess, :debug => debug).generate_pdf(tex_out)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Renders the default template.
|
117
|
+
def render!
|
118
|
+
render :file => template_path
|
119
|
+
end
|
120
|
+
|
121
|
+
protected
|
122
|
+
def self.template_class
|
123
|
+
@template_class ||= returning Class.new(ActionView::Base) do |view_class|
|
124
|
+
view_class.send(:include, ApplicationController.master_helper_module) if Object.const_defined?(:ApplicationController)
|
125
|
+
view_class.send(:include, PdfRenderer::Helpers::LatexHelper)
|
126
|
+
view_class.send(:include, self.master_helper_module)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def template_instance
|
131
|
+
returning self.class.template_class.new(self.class.template_root, body || {}, self) do |view|
|
132
|
+
view.template_format = :pdf
|
133
|
+
view.view_paths = ActionView::Base.process_view_paths(self.view_paths)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module PdfRenderer
|
2
|
+
# Contains functionality for using helper modules in the views when rendering
|
3
|
+
# LaTeX ERB files.
|
4
|
+
module Helpers
|
5
|
+
def self.included(base)
|
6
|
+
base.class_inheritable_array :helpers
|
7
|
+
base.extend ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
# Class-level method that declares <code>helpers</code> as helpers for
|
12
|
+
# inclusion in the view. Specify the helpers as <code>:symbol</code>,
|
13
|
+
# <code>'string'</code>, or <code>ConstantName</code>.
|
14
|
+
def helper(*helpers)
|
15
|
+
write_inheritable_array :helpers, helpers
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
def master_helper_module
|
20
|
+
@master_helper_module ||= returning(Module.new) do |mod|
|
21
|
+
(helpers || []).each do |helper|
|
22
|
+
case helper
|
23
|
+
when Module
|
24
|
+
mod.send(:include, helper)
|
25
|
+
when String, Symbol
|
26
|
+
mod.send(:include, "#{helper.to_s}_helper".camelize.constantize)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module PdfRenderer
|
2
|
+
module Helpers
|
3
|
+
# Contains methods for escaping strings for LaTeX.
|
4
|
+
module LatexHelper
|
5
|
+
BS = "\\\\"
|
6
|
+
BACKSLASH = "#{BS}textbackslash{}"
|
7
|
+
HAT = "#{BS}textasciicircum{}"
|
8
|
+
TILDE = "#{BS}textasciitilde{}"
|
9
|
+
|
10
|
+
# Escapes the string, so it is suitable for LaTeX input. This method is
|
11
|
+
# aliased as <code>l</code>.
|
12
|
+
def latex_escape(s)
|
13
|
+
quote_count = 0
|
14
|
+
s.to_s.
|
15
|
+
gsub(/([{}_$&%#])/, "__LATEX_HELPER_TEMPORARY_BACKSLASH_PLACEHOLDER__\\1").
|
16
|
+
gsub(/\\/, BACKSLASH).
|
17
|
+
gsub(/__LATEX_HELPER_TEMPORARY_BACKSLASH_PLACEHOLDER__/, BS).
|
18
|
+
gsub(/\^/, HAT).
|
19
|
+
gsub(/~/, TILDE).
|
20
|
+
gsub(/"/) do
|
21
|
+
quote_count += 1
|
22
|
+
quote_count.odd? ? %{"`} : %{"'}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
alias :l :latex_escape
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module PdfRenderer
|
2
|
+
# This error is raised when no suitable LaTeX executable is found.
|
3
|
+
class LatexProcessorNotFound < StandardError; end
|
4
|
+
# This error is raised when there is a syntax error in the LaTeX input.
|
5
|
+
class LatexError < StandardError; end
|
6
|
+
# This error is raised when there is an illegal character in the LaTeX input.
|
7
|
+
class InvalidCharacter < StandardError; end
|
8
|
+
|
9
|
+
# This class wraps the LaTeX command invocation.
|
10
|
+
class Latex
|
11
|
+
attr_reader :options
|
12
|
+
|
13
|
+
# Option redirection for shell output (default is '> /dev/null 2>&1' )
|
14
|
+
cattr_accessor :shell_redirect
|
15
|
+
self.shell_redirect = '> /dev/null 2>&1'
|
16
|
+
# Temporary Directory
|
17
|
+
cattr_accessor :tempdir
|
18
|
+
self.tempdir = "#{File.expand_path(RAILS_ROOT)}/tmp"
|
19
|
+
# tex command to run
|
20
|
+
cattr_accessor :tex_command
|
21
|
+
self.tex_command = "pdflatex"
|
22
|
+
|
23
|
+
def initialize(options = {})
|
24
|
+
@options = options
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns the rendered PDF as string for the given input string (LaTeX
|
28
|
+
# source).
|
29
|
+
def generate_pdf(input)
|
30
|
+
create_debug_output(input) if options[:debug]
|
31
|
+
check_for_tex_presence!
|
32
|
+
create_pdf(input)
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
def processor
|
37
|
+
self.class.tex_command
|
38
|
+
end
|
39
|
+
|
40
|
+
def run(command)
|
41
|
+
%x{#{command}}
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_debug_output(output)
|
45
|
+
File.open("pdf_renderer_out.tex", "wb") {|f| f.puts output}
|
46
|
+
rescue
|
47
|
+
end
|
48
|
+
|
49
|
+
def check_for_tex_presence!
|
50
|
+
system_path = ENV["PATH"]
|
51
|
+
|
52
|
+
# This is one big ugly platform dependent kludge, but it's necessary.
|
53
|
+
# See: http://lists.radiantcms.org/pipermail/radiant/2007-April/004473.html
|
54
|
+
# In short, apache doesn't see environment variables like PATH.
|
55
|
+
system_path = "/bin:/usr/bin:/usr/local/bin" if system_path.nil?
|
56
|
+
|
57
|
+
# Check for the presence of the tex processor in the path.
|
58
|
+
unless File.executable?(processor) or system_path.split(":").any?{|path| File.executable?(File.join(path, processor))}
|
59
|
+
raise LatexProcessorNotFound
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def create_pdf(input)
|
64
|
+
create_temp_dir("pdf_renderer", self.class.tempdir) do
|
65
|
+
basename = "processed_pdf_renderer_file"
|
66
|
+
texfile = File.open("#{basename}.tex", "wb")
|
67
|
+
texfile.write(input)
|
68
|
+
texfile.close
|
69
|
+
|
70
|
+
tex_command = "#{processor} -interaction=nonstopmode #{texfile.path} #{self.class.shell_redirect}"
|
71
|
+
tex_return = ''
|
72
|
+
tex_return = run(tex_command)
|
73
|
+
tex_return = run(tex_command) if options[:preprocess] # One can wonder if it matters if it's always run twice...
|
74
|
+
|
75
|
+
if File.exists?("#{basename}.pdf")
|
76
|
+
# For some reason, File.read doesn't work, hence the call using the block
|
77
|
+
File.open("#{basename}.pdf",'rb') { |f| f.read }
|
78
|
+
else
|
79
|
+
if tex_return[/(Package inputenc Error: Unicode char .+)/,1]
|
80
|
+
raise InvalidCharacter.new($1)
|
81
|
+
end
|
82
|
+
raise LatexError.new("Could not generate PDF:\n>>#{tex_return}<<\n\nPath:#{texfile.path}\n\n\nInput:#{input}")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
@@temporary_dir_number = 0;
|
88
|
+
|
89
|
+
# Creates a temp dir in location and performs the supplied code block
|
90
|
+
def create_temp_dir(name, location)
|
91
|
+
@@temporary_dir_number += 1
|
92
|
+
pid = Process.pid # This doesn't work on some platforms, according to the docs. A better way to get it would be nice.
|
93
|
+
random_number = Kernel.rand(1000000000).to_s # This is to avoid a possible symlink attack vulnerability in the creation of temporary files.
|
94
|
+
complete_dir_name = "#{location}/#{name}.#{pid}.#{random_number}.#{@@temporary_dir_number}"
|
95
|
+
|
96
|
+
yield_result = Object.new
|
97
|
+
|
98
|
+
FileUtils.mkdir_p(complete_dir_name)
|
99
|
+
Dir.chdir(complete_dir_name) do
|
100
|
+
begin
|
101
|
+
yield_result = yield
|
102
|
+
ensure
|
103
|
+
FileUtils.rmtree([complete_dir_name])
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
yield_result
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module PdfRenderer
|
2
|
+
# Wrapper class that represents the PDF to be rendered. An instance of this
|
3
|
+
# class is returned by a PdfRenderer, when an action is called without a
|
4
|
+
# prefix (i.e. without <code>render_</code> or <code>save_</code>).
|
5
|
+
class Pdf
|
6
|
+
# Contains the instance variable assigns
|
7
|
+
attr_accessor :body
|
8
|
+
# Contains the LaTeX source for this PDF.
|
9
|
+
attr_accessor :source
|
10
|
+
# Contains the rendered PDF as string.
|
11
|
+
attr_accessor :rendered
|
12
|
+
|
13
|
+
def initialize(renderer)
|
14
|
+
@renderer = renderer
|
15
|
+
@body = renderer.body
|
16
|
+
end
|
17
|
+
|
18
|
+
# Renders the PDF.
|
19
|
+
def render!
|
20
|
+
@rendered = @renderer.render!
|
21
|
+
@source = @renderer.tex_out
|
22
|
+
@rendered
|
23
|
+
end
|
24
|
+
|
25
|
+
# Renders the PDF and saves it to the file system.
|
26
|
+
def save(filename)
|
27
|
+
File.open(filename, 'wb') { |file| file.print render! }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
Description:
|
2
|
+
Stubs out a new PDF renderer and its views. Pass the renderer name, either
|
3
|
+
CamelCased or under_scored, without PdfRenderer or _pdf_renderer as the suffix,
|
4
|
+
and an optional list of actions as arguments.
|
5
|
+
|
6
|
+
This generates a renderer class in app/pdf_renderers, view templates in
|
7
|
+
app/views/renderer_name, and a functional test in test/functional.
|
8
|
+
|
9
|
+
Example:
|
10
|
+
`./script/generate pdf_renderer Bill bill invoice reminder`
|
11
|
+
|
12
|
+
creates a bill renderer class, views, and test:
|
13
|
+
Renderer: app/pdf_renderers/bill_pdf_renderer.rb
|
14
|
+
Views: app/views/bill_pdf_renderer/bill.pdf.erb [...]
|
15
|
+
Test: test/functional/bill_pdf_renderer_test.rb
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class PdfRendererGenerator < Rails::Generator::NamedBase
|
2
|
+
def manifest
|
3
|
+
record do |m|
|
4
|
+
# Check for class naming collisions.
|
5
|
+
m.class_collisions "#{class_name}PdfRenderer", "#{class_name}PdfRendererTest"
|
6
|
+
|
7
|
+
# Renderer, view, and test directories.
|
8
|
+
m.directory File.join('app/pdf_renderers', class_path)
|
9
|
+
m.directory File.join('app/views', "#{file_path}_pdf_renderer")
|
10
|
+
m.directory File.join('test/functional', class_path)
|
11
|
+
|
12
|
+
# Renderer class and functional test.
|
13
|
+
m.template "pdf_renderer.rb", File.join('app/pdf_renderers', class_path, "#{file_name}_pdf_renderer.rb")
|
14
|
+
m.template "functional_test.rb", File.join('test/functional', class_path, "#{file_name}_pdf_renderer_test.rb")
|
15
|
+
|
16
|
+
# View template for each action.
|
17
|
+
actions.each do |action|
|
18
|
+
relative_path = File.join("#{file_path}_pdf_renderer", action)
|
19
|
+
view_path = File.join('app/views', "#{relative_path}.pdf.erb")
|
20
|
+
|
21
|
+
m.template "view.erb", view_path,
|
22
|
+
:assigns => { :action => action, :path => view_path }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class <%= class_name %>PdfRendererTest < ActiveSupport::TestCase
|
4
|
+
<% for action in actions -%>
|
5
|
+
test "<%= action %>" do
|
6
|
+
pdf = <%= class_name %>PdfRenderer.render_<%= action %>
|
7
|
+
assert pdf.source =~ /content/
|
8
|
+
end
|
9
|
+
|
10
|
+
<% end -%>
|
11
|
+
<% if actions.blank? -%>
|
12
|
+
# replace this with your real tests
|
13
|
+
test "the truth" do
|
14
|
+
assert true
|
15
|
+
end
|
16
|
+
<% end -%>
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pdf_renderer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 2
|
9
|
+
version: 0.0.2
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Thomas Kadauke
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-04-17 00:00:00 +02:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description:
|
22
|
+
email: entwicker@imedo.de
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files: []
|
28
|
+
|
29
|
+
files:
|
30
|
+
- lib/pdf_renderer/base.rb
|
31
|
+
- lib/pdf_renderer/helpers/latex_helper.rb
|
32
|
+
- lib/pdf_renderer/helpers.rb
|
33
|
+
- lib/pdf_renderer/latex.rb
|
34
|
+
- lib/pdf_renderer/pdf.rb
|
35
|
+
- lib/pdf_renderer.rb
|
36
|
+
- rails_generators/pdf_renderer/pdf_renderer_generator.rb
|
37
|
+
- rails_generators/pdf_renderer/templates/functional_test.rb
|
38
|
+
- rails_generators/pdf_renderer/templates/pdf_renderer.rb
|
39
|
+
- rails_generators/pdf_renderer/templates/view.erb
|
40
|
+
- rails_generators/pdf_renderer/USAGE
|
41
|
+
has_rdoc: true
|
42
|
+
homepage: http://www.imedo.de/
|
43
|
+
licenses: []
|
44
|
+
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options: []
|
47
|
+
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
segments:
|
55
|
+
- 0
|
56
|
+
version: "0"
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
segments:
|
62
|
+
- 0
|
63
|
+
version: "0"
|
64
|
+
requirements: []
|
65
|
+
|
66
|
+
rubyforge_project:
|
67
|
+
rubygems_version: 1.3.6
|
68
|
+
signing_key:
|
69
|
+
specification_version: 3
|
70
|
+
summary: Framework for rendering PDFs using LaTeX
|
71
|
+
test_files: []
|
72
|
+
|