pdf_renderer 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|