shrimp 0.0.1
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/.gitignore +17 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +159 -0
- data/Rakefile +5 -0
- data/lib/shrimp.rb +5 -0
- data/lib/shrimp/configuration.rb +49 -0
- data/lib/shrimp/middleware.rb +175 -0
- data/lib/shrimp/phantom.rb +93 -0
- data/lib/shrimp/rasterize.js +73 -0
- data/lib/shrimp/source.rb +25 -0
- data/lib/shrimp/version.rb +3 -0
- data/shrimp.gemspec +25 -0
- data/spec/shrimp/middleware_spec.rb +124 -0
- data/spec/shrimp/phantom_spec.rb +102 -0
- data/spec/shrimp/source_spec.rb +15 -0
- data/spec/shrimp/test_file.html +6 -0
- data/spec/spec_helper.rb +11 -0
- metadata +114 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 adeven GmbH Manuel Kniep
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
# Shrimp
|
2
|
+
|
3
|
+
Creates PDFs from URLs using phantomjs
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'shrimp'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install shrimp
|
18
|
+
|
19
|
+
|
20
|
+
### pantomjs
|
21
|
+
|
22
|
+
See http://phantomjs.org/download.html on how to install phatomjs
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
```
|
27
|
+
url = 'http://www.google.com'
|
28
|
+
options = { :margin => "1cm"}
|
29
|
+
Phantomjs::Phantomjs.new(url, options).to_pdf("/output.pdf")
|
30
|
+
```
|
31
|
+
## Configuration
|
32
|
+
|
33
|
+
```
|
34
|
+
Shrimp.configure do |config|
|
35
|
+
# The path to the phantomjs executable
|
36
|
+
# defaults to `where phantomjs`
|
37
|
+
# config.phantomjs = '/usr/local/bin/phantomjs'
|
38
|
+
|
39
|
+
# the default pdf output format
|
40
|
+
# e.g. "5in*7.5in", "10cm*20cm", "A4", "Letter"
|
41
|
+
# config.format = 'A4'
|
42
|
+
|
43
|
+
# the default margin
|
44
|
+
# config.margin = '1cm'
|
45
|
+
# the zoom factor
|
46
|
+
|
47
|
+
# config.zoom = 1
|
48
|
+
|
49
|
+
# the page orientation 'portrait' or 'landscape'
|
50
|
+
# config.orientation = 'portrait'
|
51
|
+
|
52
|
+
# a temporary dir used to store tempfiles
|
53
|
+
# config.tmpdir = Dir.tmpdir
|
54
|
+
|
55
|
+
# the timeout for phantomjs rendering process
|
56
|
+
# rendering_timeout = 90000
|
57
|
+
|
58
|
+
# the default rendering time
|
59
|
+
# config.rendering_time = 1000
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
## Middleware
|
64
|
+
|
65
|
+
Shrimp comes with a middleware that allows users to get a PDF view of any page on your site by appending .pdf to the URL.
|
66
|
+
|
67
|
+
### Middleware Setup
|
68
|
+
|
69
|
+
**Non-Rails Rack apps**
|
70
|
+
|
71
|
+
# in config.ru
|
72
|
+
require 'shrimp'
|
73
|
+
use Shrimp::Middleware
|
74
|
+
|
75
|
+
**Rails apps**
|
76
|
+
|
77
|
+
# in application.rb(Rails3) or environment.rb(Rails2)
|
78
|
+
require 'shrimp'
|
79
|
+
config.middleware.use PDFKit::Middleware
|
80
|
+
|
81
|
+
**With Shrimp options**
|
82
|
+
|
83
|
+
# options will be passed to Shrimp::Phantom.new
|
84
|
+
config.middleware.use Shrimp::Middleware, :margin => '0.5cm', :format => 'Letter'
|
85
|
+
|
86
|
+
**With conditions to limit routes that can be generated in pdf**
|
87
|
+
|
88
|
+
# conditions can be regexps (either one or an array)
|
89
|
+
config.middleware.use Shrimp::Middleware, {}, :only => %r[^/public]
|
90
|
+
config.middleware.use Shrimp::Middleware, {}, :only => [%r[^/invoice], %r[^/public]]
|
91
|
+
|
92
|
+
# conditions can be strings (either one or an array)
|
93
|
+
config.middleware.use Shrimp::Middleware, {}, :only => '/public'
|
94
|
+
config.middleware.use Shrimp::Middleware, {}, :only => ['/invoice', '/public']
|
95
|
+
|
96
|
+
# conditions can be regexps (either one or an array)
|
97
|
+
config.middleware.use Shrimp::Middleware, {}, :except => [%r[^/prawn], %r[^/secret]]
|
98
|
+
|
99
|
+
# conditions can be strings (either one or an array)
|
100
|
+
config.middleware.use Shrimp::Middleware, {}, :except => ['/secret']
|
101
|
+
|
102
|
+
|
103
|
+
### Polling
|
104
|
+
|
105
|
+
To avoid deadlocks Shrimp::Middleware renders the pdf in a separate process retuning a 503 Retry-After response Header.
|
106
|
+
you can setup the polling interval and the polling offset in seconds.
|
107
|
+
|
108
|
+
config.middleware.use Shrimp::Middleware, :polling_interval => 1, :polling_offset => 5
|
109
|
+
|
110
|
+
### Caching
|
111
|
+
|
112
|
+
To avoid rendering the page on each request you can setup some the cache ttl on seconds
|
113
|
+
|
114
|
+
config.middleware.use Shrimp::Middleware, :cache_ttl => 3600 # one hour
|
115
|
+
|
116
|
+
|
117
|
+
### Ajax requests
|
118
|
+
|
119
|
+
To include some fancy Ajax stuff with jquery
|
120
|
+
|
121
|
+
```js
|
122
|
+
|
123
|
+
var url = '/my_page.pdf'
|
124
|
+
var statusCodes = {
|
125
|
+
200: function() {
|
126
|
+
return window.location.assign(url);
|
127
|
+
},
|
128
|
+
504: function() {
|
129
|
+
console.log("Shit's beeing wired"
|
130
|
+
},
|
131
|
+
503: function(jqXHR, textStatus, errorThrown) {
|
132
|
+
var wait;
|
133
|
+
wait = parseInt(jqXHR.getResponseHeader('Retry-After'));
|
134
|
+
return setTimeout(function() {
|
135
|
+
return $.ajax({
|
136
|
+
url: url,
|
137
|
+
statusCode: statusCodes
|
138
|
+
});
|
139
|
+
}, wait * 1000);
|
140
|
+
}
|
141
|
+
}
|
142
|
+
$.ajax({
|
143
|
+
url: url,
|
144
|
+
statusCode: statusCodes
|
145
|
+
})
|
146
|
+
|
147
|
+
```
|
148
|
+
|
149
|
+
## Contributing
|
150
|
+
|
151
|
+
1. Fork it
|
152
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
153
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
154
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
155
|
+
5. Create new Pull Request
|
156
|
+
|
157
|
+
## Copyright
|
158
|
+
Shrimp is Copyright © 2012 adeven (Manuel Kniep). It is free software, and may be redistributed under the terms
|
159
|
+
specified in the LICENSE file.
|
data/Rakefile
ADDED
data/lib/shrimp.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
module Shrimp
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :default_options
|
4
|
+
attr_writer :phantomjs
|
5
|
+
|
6
|
+
[:format, :margin, :zoom, :orientation, :tmpdir, :rendering_timeout, :rendering_time].each do |m|
|
7
|
+
define_method("#{m}=") do |val|
|
8
|
+
@default_options[m]=val
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@default_options = {
|
14
|
+
:format => 'A4',
|
15
|
+
:margin => '1cm',
|
16
|
+
:zoom => 1,
|
17
|
+
:orientation => 'portrait',
|
18
|
+
:tmpdir => Dir.tmpdir,
|
19
|
+
:rendering_timeout => 90000,
|
20
|
+
:rendering_time => 1000
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def phantomjs
|
25
|
+
@phantomjs ||= (defined?(Bundler::GemfileError) ? `bundle exec which phantomjs` : `which phantomjs`).chomp
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class << self
|
30
|
+
attr_accessor :configuration
|
31
|
+
end
|
32
|
+
|
33
|
+
# Configure Phantomjs someplace sensible,
|
34
|
+
# like config/initializers/phantomjs.rb
|
35
|
+
#
|
36
|
+
# @example
|
37
|
+
# Shrimp.configure do |config|
|
38
|
+
# config.phantomjs = '/usr/local/bin/phantomjs'
|
39
|
+
# config.format = 'Letter'
|
40
|
+
# end
|
41
|
+
|
42
|
+
def self.configuration
|
43
|
+
@configuration ||= Configuration.new
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.configure
|
47
|
+
yield(configuration)
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
module Shrimp
|
2
|
+
class Middleware
|
3
|
+
def initialize(app, options = { }, conditions = { })
|
4
|
+
@app = app
|
5
|
+
@options = options
|
6
|
+
@conditions = conditions
|
7
|
+
@options[:polling_interval] ||= 1
|
8
|
+
@options[:polling_offset] ||= 1
|
9
|
+
@options[:cache_ttl] ||= 1
|
10
|
+
@options[:request_timeout] ||= @options[:polling_interval] * 10
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
@request = Rack::Request.new(env)
|
15
|
+
if render_as_pdf? #&& headers['Content-Type'] =~ /text\/html|application\/xhtml\+xml/
|
16
|
+
if already_rendered? && (up_to_date?(@options[:cache_ttl]) || @options[:cache_ttl] == 0)
|
17
|
+
if File.size(render_to) == 0
|
18
|
+
File.delete(render_to)
|
19
|
+
remove_rendering_flag
|
20
|
+
return error_response
|
21
|
+
end
|
22
|
+
return ready_response if env['HTTP_X_REQUESTED_WITH']
|
23
|
+
file = File.open(render_to, "rb")
|
24
|
+
body = file.read
|
25
|
+
file.close
|
26
|
+
File.delete(render_to) if @options[:cache_ttl] == 0
|
27
|
+
remove_rendering_flag
|
28
|
+
response = [body]
|
29
|
+
headers = { }
|
30
|
+
headers["Content-Length"] = (body.respond_to?(:bytesize) ? body.bytesize : body.size).to_s
|
31
|
+
headers["Content-Type"] = "application/pdf"
|
32
|
+
[200, headers, response]
|
33
|
+
else
|
34
|
+
if rendering_in_progress?
|
35
|
+
if rendering_timed_out?
|
36
|
+
remove_rendering_flag
|
37
|
+
error_response
|
38
|
+
else
|
39
|
+
reload_response(@options[:polling_interval])
|
40
|
+
end
|
41
|
+
else
|
42
|
+
File.delete(render_to) if already_rendered?
|
43
|
+
set_rendering_flag
|
44
|
+
fire_phantom
|
45
|
+
reload_response(@options[:polling_offset])
|
46
|
+
end
|
47
|
+
end
|
48
|
+
else
|
49
|
+
@app.call(env)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# Private: start phantom rendering in a separate process
|
56
|
+
def fire_phantom
|
57
|
+
Process::detach fork { Phantom.new(@request.url.sub(%r{\.pdf$}, ''), @options, @request.cookies).to_pdf(render_to) }
|
58
|
+
end
|
59
|
+
|
60
|
+
def render_to
|
61
|
+
file_name = Digest::MD5.hexdigest(@request.path) + ".pdf"
|
62
|
+
file_path = @options[:out_path]
|
63
|
+
"#{file_path}/#{file_name}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def already_rendered?
|
67
|
+
File.exists?(render_to)
|
68
|
+
end
|
69
|
+
|
70
|
+
def up_to_date?(ttl = 30)
|
71
|
+
(Time.now - File.new(render_to).mtime) <= ttl
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
def remove_rendering_flag
|
76
|
+
@request.session["phantom-rendering"] ||={ }
|
77
|
+
@request.session["phantom-rendering"].delete(render_to)
|
78
|
+
end
|
79
|
+
|
80
|
+
def set_rendering_flag
|
81
|
+
@request.session["phantom-rendering"] ||={ }
|
82
|
+
@request.session["phantom-rendering"][render_to] = Time.now
|
83
|
+
end
|
84
|
+
|
85
|
+
def rendering_timed_out?
|
86
|
+
Time.now - @request.session["phantom-rendering"][render_to] > @options[:request_timeout]
|
87
|
+
end
|
88
|
+
|
89
|
+
def rendering_in_progress?
|
90
|
+
@request.session["phantom-rendering"]||={ }
|
91
|
+
@request.session["phantom-rendering"][render_to]
|
92
|
+
end
|
93
|
+
|
94
|
+
def render_as_pdf?
|
95
|
+
request_path_is_pdf = !!@request.path.match(%r{\.pdf$})
|
96
|
+
|
97
|
+
if request_path_is_pdf && @conditions[:only]
|
98
|
+
rules = [@conditions[:only]].flatten
|
99
|
+
rules.any? do |pattern|
|
100
|
+
if pattern.is_a?(Regexp)
|
101
|
+
@request.path =~ pattern
|
102
|
+
else
|
103
|
+
@request.path[0, pattern.length] == pattern
|
104
|
+
end
|
105
|
+
end
|
106
|
+
elsif request_path_is_pdf && @conditions[:except]
|
107
|
+
rules = [@conditions[:except]].flatten
|
108
|
+
rules.map do |pattern|
|
109
|
+
if pattern.is_a?(Regexp)
|
110
|
+
return false if @request.path =~ pattern
|
111
|
+
else
|
112
|
+
return false if @request.path[0, pattern.length] == pattern
|
113
|
+
end
|
114
|
+
end
|
115
|
+
return true
|
116
|
+
else
|
117
|
+
request_path_is_pdf
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def concat(accepts, type)
|
122
|
+
(accepts || '').split(',').unshift(type).compact.join(',')
|
123
|
+
end
|
124
|
+
|
125
|
+
def reload_response(interval=1)
|
126
|
+
body = <<-HTML.gsub(/[ \n]+/, ' ').strip
|
127
|
+
<html>
|
128
|
+
<head>
|
129
|
+
</head>
|
130
|
+
<body onLoad="setTimeout(function(){ window.location.reload()}, #{interval * 1000});">
|
131
|
+
<h2>Prepareing pdf... </h2>
|
132
|
+
</body>
|
133
|
+
</ html>
|
134
|
+
HTML
|
135
|
+
headers = { }
|
136
|
+
headers["Content-Length"] = body.size.to_s
|
137
|
+
headers["Content-Type"] = "text/html"
|
138
|
+
headers["Retry-After"] = interval.to_s
|
139
|
+
|
140
|
+
[503, headers, [body]]
|
141
|
+
end
|
142
|
+
|
143
|
+
def ready_response
|
144
|
+
body = <<-HTML.gsub(/[ \n]+/, ' ').strip
|
145
|
+
<html>
|
146
|
+
<head>
|
147
|
+
</head>
|
148
|
+
<body>
|
149
|
+
<a href="#{@request.path}">PDF ready here</a>
|
150
|
+
</body>
|
151
|
+
</ html>
|
152
|
+
HTML
|
153
|
+
headers = { }
|
154
|
+
headers["Content-Length"] = body.size.to_s
|
155
|
+
headers["Content-Type"] = "text/html"
|
156
|
+
[200, headers, [body]]
|
157
|
+
end
|
158
|
+
|
159
|
+
def error_response
|
160
|
+
body = <<-HTML.gsub(/[ \n]+/, ' ').strip
|
161
|
+
<html>
|
162
|
+
<head>
|
163
|
+
</head>
|
164
|
+
<body>
|
165
|
+
<h2>Sorry request timed out... </h2>
|
166
|
+
</body>
|
167
|
+
</ html>
|
168
|
+
HTML
|
169
|
+
headers = { }
|
170
|
+
headers["Content-Length"] = body.size.to_s
|
171
|
+
headers["Content-Type"] = "text/html"
|
172
|
+
[504, headers, [body]]
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Shrimp
|
2
|
+
class NoExecutableError < StandardError
|
3
|
+
def initialize
|
4
|
+
msg = "No phantomjs executable found at #{Shrimp.configuration.phantomjs}\n"
|
5
|
+
msg << ">> Please install phantomjs - http://phantomjs.org/download.html"
|
6
|
+
super(msg)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class ImproperSourceError < StandardError
|
11
|
+
def initialize(msg = nil)
|
12
|
+
super("Improper Source: #{msg}")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
class Phantom
|
18
|
+
attr_accessor :source, :configuration, :outfile
|
19
|
+
attr_reader :options, :cookies
|
20
|
+
SCRIPT_FILE = File.expand_path('../rasterize.js', __FILE__)
|
21
|
+
|
22
|
+
# Public: Runs the phantomjs binary
|
23
|
+
#
|
24
|
+
# Returns the stdout output of phantomjs
|
25
|
+
def run
|
26
|
+
@result = `#{cmd}`
|
27
|
+
end
|
28
|
+
|
29
|
+
# Public: Returns the phantom rasterize command
|
30
|
+
def cmd
|
31
|
+
cookie_file = dump_cookies
|
32
|
+
format, zoom, margin, orientation = options[:format], options[:zoom], options[:margin], options[:orientation]
|
33
|
+
rendering_time, timeout = options[:rendering_time], options[:rendering_timeout]
|
34
|
+
@outfile ||= "#{options[:tmpdir]}/#{Digest::MD5.hexdigest((Time.now.to_i + rand(9001)).to_s)}.pdf"
|
35
|
+
|
36
|
+
[Shrimp.configuration.phantomjs, SCRIPT_FILE, @source.to_s, @outfile, format, zoom, margin, orientation, cookie_file, rendering_time, timeout].join(" ")
|
37
|
+
end
|
38
|
+
|
39
|
+
# Public: initializes a new Phantom Object
|
40
|
+
#
|
41
|
+
# url_or_file - The url of the html document to render
|
42
|
+
# options - a hash with options for rendering
|
43
|
+
# * format - the paper format for the output eg: "5in*7.5in", "10cm*20cm", "A4", "Letter"
|
44
|
+
# * zoom - the viewport zoom factor
|
45
|
+
# * margin - the margins for the pdf
|
46
|
+
# cookies - hash with cookies to use for rendering
|
47
|
+
# outfile - optional path for the output file a Tempfile will be created if not given
|
48
|
+
#
|
49
|
+
# Returns self
|
50
|
+
def initialize(url_or_file, options = { }, cookies={ }, outfile = nil)
|
51
|
+
@source = Source.new(url_or_file)
|
52
|
+
@options = Shrimp.configuration.default_options.merge(options)
|
53
|
+
@cookies = cookies
|
54
|
+
@outfile = outfile
|
55
|
+
raise NoExecutableError.new unless File.exists?(Shrimp.configuration.phantomjs)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Public: renders to pdf
|
59
|
+
# path - the destination path defaults to outfile
|
60
|
+
#
|
61
|
+
# Returns the path to the pdf file
|
62
|
+
def to_pdf(path=nil)
|
63
|
+
@outfile = path
|
64
|
+
self.run
|
65
|
+
@outfile
|
66
|
+
end
|
67
|
+
|
68
|
+
# Public: renders to pdf
|
69
|
+
# path - the destination path defaults to outfile
|
70
|
+
#
|
71
|
+
# Returns a File Handle of the Resulting pdf
|
72
|
+
def to_file(path=nil)
|
73
|
+
self.to_pdf(path)
|
74
|
+
File.new(@outfile)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Public: renders to pdf
|
78
|
+
# path - the destination path defaults to outfile
|
79
|
+
#
|
80
|
+
# Returns the binary string of the pdf
|
81
|
+
def to_string(path=nil)
|
82
|
+
self.to_pdf(path)
|
83
|
+
File.open(path).read
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
def dump_cookies
|
88
|
+
host = @source.url? ? URI::parse(@source.to_s).host : "/"
|
89
|
+
json = @cookies.inject([]) { |a, (k, v)| a.push({ name: k, value: v, domain: host }); a }.to_json
|
90
|
+
File.open("#{options[:tmpdir]}/#{rand}.cookies", 'w') { |f| f.puts json; f }.path
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
var page = require('webpage').create(),
|
2
|
+
fs = require('fs'),
|
3
|
+
system = require('system'),
|
4
|
+
margin = system.args[5] || '0cm',
|
5
|
+
orientation = system.args[6] || 'portrait',
|
6
|
+
cookie_file = system.args[7] ,
|
7
|
+
render_time = system.args[8] || 10000 ,
|
8
|
+
time_out = system.args[9] || 90000 ,
|
9
|
+
cookies = {},
|
10
|
+
address, output, size, statusCode;
|
11
|
+
|
12
|
+
window.setTimeout(function () {
|
13
|
+
console.log("Shit's being weird no result within: " + time_out + "ms");
|
14
|
+
phantom.exit(1);
|
15
|
+
}, time_out);
|
16
|
+
|
17
|
+
try {
|
18
|
+
f = fs.open(cookie_file, "r");
|
19
|
+
cookies = JSON.parse(f.read());
|
20
|
+
fs.remove(cookie_file)
|
21
|
+
} catch (e) {
|
22
|
+
console.log(e);
|
23
|
+
}
|
24
|
+
phantom.cookiesEnabled = true;
|
25
|
+
phantom.cookies = cookies;
|
26
|
+
|
27
|
+
if (system.args.length < 3 || system.args.length > 10) {
|
28
|
+
console.log('Usage: rasterize.js URL filename [paperwidth*paperheight|paperformat] [zoom] [margin] [orientation] [cookie_file] [render_time] [time_out]');
|
29
|
+
console.log(' paper (pdf output) examples: "5in*7.5in", "10cm*20cm", "A4", "Letter"');
|
30
|
+
phantom.exit(1);
|
31
|
+
} else {
|
32
|
+
address = system.args[1];
|
33
|
+
output = system.args[2];
|
34
|
+
page.viewportSize = { width:600, height:600 };
|
35
|
+
if (system.args.length > 3 && system.args[2].substr(-4) === ".pdf") {
|
36
|
+
size = system.args[3].split('*');
|
37
|
+
page.paperSize = size.length === 2 ? { width:size[0], height:size[1], margin:'0px' }
|
38
|
+
: { format:system.args[3], orientation:orientation, margin:margin };
|
39
|
+
}
|
40
|
+
if (system.args.length > 4) {
|
41
|
+
page.zoomFactor = system.args[4];
|
42
|
+
}
|
43
|
+
|
44
|
+
// determine the statusCode
|
45
|
+
page.onResourceReceived = function (resource) {
|
46
|
+
if (resource.url == address) {
|
47
|
+
statusCode = resource.status;
|
48
|
+
}
|
49
|
+
};
|
50
|
+
|
51
|
+
page.open(address, function (status) {
|
52
|
+
if (status !== 'success' || (statusCode != 200 && statusCode != null)) {
|
53
|
+
console.log(statusCode, 'Unable to load the address!');
|
54
|
+
if (fs.exists(output)) {
|
55
|
+
fs.remove(output);
|
56
|
+
}
|
57
|
+
fs.touch(output);
|
58
|
+
phantom.exit();
|
59
|
+
} else {
|
60
|
+
window.setTimeout(function () {
|
61
|
+
page.render(output + '_tmp.pdf');
|
62
|
+
|
63
|
+
if (fs.exists(output)) {
|
64
|
+
fs.remove(output);
|
65
|
+
}
|
66
|
+
|
67
|
+
fs.move(output + '_tmp.pdf', output);
|
68
|
+
console.log('rendered to: ' + output, new Date().getTime());
|
69
|
+
phantom.exit();
|
70
|
+
}, render_time);
|
71
|
+
}
|
72
|
+
});
|
73
|
+
}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'uri'
|
2
|
+
module Shrimp
|
3
|
+
class Source
|
4
|
+
def initialize(url_or_file)
|
5
|
+
@source = url_or_file
|
6
|
+
raise ImproperSourceError.new unless url? || file?
|
7
|
+
end
|
8
|
+
|
9
|
+
def url?
|
10
|
+
@source.is_a?(String) && @source.match(URI::regexp)
|
11
|
+
end
|
12
|
+
|
13
|
+
def file?
|
14
|
+
@source.kind_of?(File)
|
15
|
+
end
|
16
|
+
|
17
|
+
def html?
|
18
|
+
!(url? || file?)
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
file? ? @source.path : @source
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/shrimp.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'shrimp/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "shrimp"
|
8
|
+
gem.version = Shrimp::VERSION
|
9
|
+
gem.authors = ["Manuel Kniep"]
|
10
|
+
gem.email = %w(manuel@adeven.com)
|
11
|
+
gem.description = %q{html to pdf with phantomjs}
|
12
|
+
gem.summary = %q{a phantomjs based pdf renderer}
|
13
|
+
gem.homepage = "http://github.com/adeven/shrimp"
|
14
|
+
gem.files = `git ls-files`.split($/)
|
15
|
+
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
16
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
17
|
+
gem.require_paths = %w(lib)
|
18
|
+
gem.requirements << 'phantomjs, v1.6 or greater'
|
19
|
+
gem.add_runtime_dependency "json"
|
20
|
+
|
21
|
+
# Developmnet Dependencies
|
22
|
+
gem.add_development_dependency(%q<rake>, [">=0.9.2"])
|
23
|
+
gem.add_development_dependency(%q<rspec>, [">= 2.2.0"])
|
24
|
+
gem.add_development_dependency(%q<rack-test>, [">= 0.5.6"])
|
25
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
def app;
|
4
|
+
Rack::Lint.new(@app)
|
5
|
+
end
|
6
|
+
|
7
|
+
def options
|
8
|
+
{ :margin => "1cm", :out_path => Dir.tmpdir,
|
9
|
+
:polling_offset => 10, :polling_interval => 1, :cache_ttl => 3600,
|
10
|
+
:request_timeout => 1 }
|
11
|
+
end
|
12
|
+
|
13
|
+
def mock_app(options = { }, conditions = { })
|
14
|
+
main_app = lambda { |env|
|
15
|
+
headers = { 'Content-Type' => "text/html" }
|
16
|
+
[200, headers, ['Hello world!']]
|
17
|
+
}
|
18
|
+
|
19
|
+
@middleware = Shrimp::Middleware.new(main_app, options, conditions)
|
20
|
+
@app = Rack::Session::Cookie.new(@middleware, :key => 'rack.session')
|
21
|
+
@middleware.should_receive(:fire_phantom).any_number_of_times
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
describe Shrimp::Middleware do
|
26
|
+
before { mock_app(options) }
|
27
|
+
|
28
|
+
context "matching pdf" do
|
29
|
+
it "should render as pdf" do
|
30
|
+
get '/test.pdf'
|
31
|
+
@middleware.send(:'render_as_pdf?').should be true
|
32
|
+
end
|
33
|
+
it "should return 503 the first time" do
|
34
|
+
get '/test.pdf'
|
35
|
+
last_response.status.should eq 503
|
36
|
+
last_response.header["Retry-After"].should eq "10"
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should return 503 the with polling interval the second time" do
|
40
|
+
get '/test.pdf'
|
41
|
+
get '/test.pdf'
|
42
|
+
last_response.status.should eq 503
|
43
|
+
last_response.header["Retry-After"].should eq "1"
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should set render to to outpath" do
|
47
|
+
get '/test.pdf'
|
48
|
+
@middleware.send(:render_to).should match (Regexp.new("^#{options[:out_path]}"))
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should return 504 on timeout" do
|
52
|
+
get '/test.pdf'
|
53
|
+
sleep 1
|
54
|
+
get '/test.pdf'
|
55
|
+
last_response.status.should eq 504
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should retry rendering after timeout" do
|
59
|
+
get '/test.pdf'
|
60
|
+
sleep 1
|
61
|
+
get '/test.pdf'
|
62
|
+
get '/test.pdf'
|
63
|
+
last_response.status.should eq 503
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should return a pdf with 200 after rendering" do
|
67
|
+
mock_file = mock(File, :read => "Hello World", :close => true, :mtime => Time.now)
|
68
|
+
File.should_receive(:'exists?').and_return true
|
69
|
+
File.should_receive(:'size').and_return 1000
|
70
|
+
File.should_receive(:'open').and_return mock_file
|
71
|
+
File.should_receive(:'new').and_return mock_file
|
72
|
+
get '/test.pdf'
|
73
|
+
last_response.status.should eq 200
|
74
|
+
last_response.body.should eq "Hello World"
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
end
|
79
|
+
context "not matching pdf" do
|
80
|
+
it "should skip pdf rendering" do
|
81
|
+
get 'http://www.example.org/test'
|
82
|
+
last_response.body.should include "Hello world!"
|
83
|
+
@middleware.send(:'render_as_pdf?').should be false
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe "Conditions" do
|
89
|
+
context "only" do
|
90
|
+
before { mock_app(options, :only => [%r[^/invoice], %r[^/public]]) }
|
91
|
+
it "render pdf for set only option" do
|
92
|
+
get '/invoice/test.pdf'
|
93
|
+
@middleware.send(:'render_as_pdf?').should be true
|
94
|
+
end
|
95
|
+
|
96
|
+
it "render pdf for set only option" do
|
97
|
+
get '/public/test.pdf'
|
98
|
+
@middleware.send(:'render_as_pdf?').should be true
|
99
|
+
end
|
100
|
+
|
101
|
+
it "not render pdf for any other path" do
|
102
|
+
get '/secret/test.pdf'
|
103
|
+
@middleware.send(:'render_as_pdf?').should be false
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context "except" do
|
108
|
+
before { mock_app(options, :except => %w(/secret)) }
|
109
|
+
it "render pdf for set only option" do
|
110
|
+
get '/invoice/test.pdf'
|
111
|
+
@middleware.send(:'render_as_pdf?').should be true
|
112
|
+
end
|
113
|
+
|
114
|
+
it "render pdf for set only option" do
|
115
|
+
get '/public/test.pdf'
|
116
|
+
@middleware.send(:'render_as_pdf?').should be true
|
117
|
+
end
|
118
|
+
|
119
|
+
it "not render pdf for any other path" do
|
120
|
+
get '/secret/test.pdf'
|
121
|
+
@middleware.send(:'render_as_pdf?').should be false
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
#encoding: UTF-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
def valid_pdf(io)
|
5
|
+
case io
|
6
|
+
when File
|
7
|
+
io.read[0...4] == "%PDF"
|
8
|
+
when String
|
9
|
+
io[0...4] == "%PDF" || File.open(io).read[0...4] == "%PDF"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def testfile
|
14
|
+
File.expand_path('../test_file.html', __FILE__)
|
15
|
+
end
|
16
|
+
|
17
|
+
Shrimp.configure do |config|
|
18
|
+
config.rendering_time = 1000
|
19
|
+
end
|
20
|
+
|
21
|
+
describe Shrimp::Phantom do
|
22
|
+
before do
|
23
|
+
Shrimp.configure do |config|
|
24
|
+
config.rendering_time = 1000
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should initialize attributes" do
|
29
|
+
phantom = Shrimp::Phantom.new("file://#{testfile}", { :margin => "2cm" }, { }, "#{Dir.tmpdir}/test.pdf")
|
30
|
+
phantom.source.to_s.should eq "file://#{testfile}"
|
31
|
+
phantom.options[:margin].should eq "2cm"
|
32
|
+
phantom.outfile.should eq "#{Dir.tmpdir}/test.pdf"
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should render a pdf file" do
|
36
|
+
#phantom = Shrimp::Phantom.new("file://#{@path}")
|
37
|
+
#phantom.to_pdf("#{Dir.tmpdir}/test.pdf").first should eq "#{Dir.tmpdir}/test.pdf"
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should accept a local file url" do
|
41
|
+
phantom = Shrimp::Phantom.new("file://#{testfile}")
|
42
|
+
phantom.source.should be_url
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should accept a URL as source" do
|
46
|
+
phantom = Shrimp::Phantom.new("http://google.com")
|
47
|
+
phantom.source.should be_url
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should parse options into a cmd line" do
|
51
|
+
phantom = Shrimp::Phantom.new("file://#{testfile}", { :margin => "2cm" }, { }, "#{Dir.tmpdir}/test.pdf")
|
52
|
+
phantom.cmd.should include "test.pdf A4 1 2cm portrait"
|
53
|
+
phantom.cmd.should include "file://#{testfile}"
|
54
|
+
phantom.cmd.should include "lib/shrimp/rasterize.js"
|
55
|
+
end
|
56
|
+
|
57
|
+
context "rendering to a file" do
|
58
|
+
before(:all) do
|
59
|
+
phantom = Shrimp::Phantom.new("file://#{testfile}", { :margin => "2cm" }, { }, "#{Dir.tmpdir}/test.pdf")
|
60
|
+
@result = phantom.to_file
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should return a File" do
|
64
|
+
@result.should be_a File
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should be a valid pdf" do
|
68
|
+
valid_pdf(@result)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context "rendering to a pdf" do
|
73
|
+
before(:all) do
|
74
|
+
@phantom = Shrimp::Phantom.new("file://#{testfile}", { :margin => "2cm" }, { })
|
75
|
+
@result = @phantom.to_pdf("#{Dir.tmpdir}/test.pdf")
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should return a path to pdf" do
|
79
|
+
@result.should be_a String
|
80
|
+
@result.should eq "#{Dir.tmpdir}/test.pdf"
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should be a valid pdf" do
|
84
|
+
valid_pdf(@result)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context "rendering to a String" do
|
89
|
+
before(:all) do
|
90
|
+
phantom = Shrimp::Phantom.new("file://#{testfile}", { :margin => "2cm" }, { })
|
91
|
+
@result = phantom.to_string("#{Dir.tmpdir}/test.pdf")
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should return the File IO String" do
|
95
|
+
@result.should be_a String
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should be a valid pdf" do
|
99
|
+
valid_pdf(@result)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
#encoding: UTF-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Shrimp::Source do
|
5
|
+
context "url" do
|
6
|
+
it "should match file urls" do
|
7
|
+
source = Shrimp::Source.new("file:///test/test.html")
|
8
|
+
source.should be_url
|
9
|
+
end
|
10
|
+
it "should match http urls" do
|
11
|
+
source = Shrimp::Source.new("http:///test/test.html")
|
12
|
+
source.should be_url
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: shrimp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Manuel Kniep
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-12-17 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: json
|
16
|
+
requirement: &70106322524180 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70106322524180
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rake
|
27
|
+
requirement: &70106322523660 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.9.2
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70106322523660
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rspec
|
38
|
+
requirement: &70106322523140 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 2.2.0
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70106322523140
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rack-test
|
49
|
+
requirement: &70106322522660 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.5.6
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70106322522660
|
58
|
+
description: html to pdf with phantomjs
|
59
|
+
email:
|
60
|
+
- manuel@adeven.com
|
61
|
+
executables: []
|
62
|
+
extensions: []
|
63
|
+
extra_rdoc_files: []
|
64
|
+
files:
|
65
|
+
- .gitignore
|
66
|
+
- .travis.yml
|
67
|
+
- Gemfile
|
68
|
+
- LICENSE.txt
|
69
|
+
- README.md
|
70
|
+
- Rakefile
|
71
|
+
- lib/shrimp.rb
|
72
|
+
- lib/shrimp/configuration.rb
|
73
|
+
- lib/shrimp/middleware.rb
|
74
|
+
- lib/shrimp/phantom.rb
|
75
|
+
- lib/shrimp/rasterize.js
|
76
|
+
- lib/shrimp/source.rb
|
77
|
+
- lib/shrimp/version.rb
|
78
|
+
- shrimp.gemspec
|
79
|
+
- spec/shrimp/middleware_spec.rb
|
80
|
+
- spec/shrimp/phantom_spec.rb
|
81
|
+
- spec/shrimp/source_spec.rb
|
82
|
+
- spec/shrimp/test_file.html
|
83
|
+
- spec/spec_helper.rb
|
84
|
+
homepage: http://github.com/adeven/shrimp
|
85
|
+
licenses: []
|
86
|
+
post_install_message:
|
87
|
+
rdoc_options: []
|
88
|
+
require_paths:
|
89
|
+
- lib
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
91
|
+
none: false
|
92
|
+
requirements:
|
93
|
+
- - ! '>='
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
requirements:
|
103
|
+
- phantomjs, v1.6 or greater
|
104
|
+
rubyforge_project:
|
105
|
+
rubygems_version: 1.8.10
|
106
|
+
signing_key:
|
107
|
+
specification_version: 3
|
108
|
+
summary: a phantomjs based pdf renderer
|
109
|
+
test_files:
|
110
|
+
- spec/shrimp/middleware_spec.rb
|
111
|
+
- spec/shrimp/phantom_spec.rb
|
112
|
+
- spec/shrimp/source_spec.rb
|
113
|
+
- spec/shrimp/test_file.html
|
114
|
+
- spec/spec_helper.rb
|