page_print 0.1.9 → 0.1.10
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.
- checksums.yaml +4 -4
- data/README.md +121 -200
- data/docs/configuration.md +204 -0
- data/docs/production.md +148 -0
- data/ext/page_print/page_print.c +211 -103
- data/lib/page_print/rails_resource_fetcher.rb +4 -1
- data/lib/page_print/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ca32ba67da6a5c3cfe5c66b3eac1242ffc1068ccce7838820d4550fffbe9d73d
|
|
4
|
+
data.tar.gz: 90ad2b4d3612d123f562ebd43098ed110c2ccb608d058409c496ce76e455e25d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 68dc83310a8f0d25281d22f1d66c771c2d87a855c4802690df8ecf2e3bc74127602d405ccc4746ddf4d1495f94aa079aa3cc4668f023ca68bccec3360096b46a
|
|
7
|
+
data.tar.gz: 3fa636884d4c816a4aea7c3b34262a3f21c1d09e59c44cc8b0fde3c2bb145cdfbc26a5a9468c636d92057cfa8736906c671933e0b97f1b2ba1bafd183f21d3d5
|
data/README.md
CHANGED
|
@@ -1,284 +1,205 @@
|
|
|
1
1
|
# PagePrint
|
|
2
2
|
|
|
3
|
+
Fast, in-process HTML-to-PDF rendering for Ruby, powered by [PlutoBook](https://github.com/plutoprint/plutobook).
|
|
4
|
+
|
|
3
5
|
> [!IMPORTANT]
|
|
4
|
-
> PagePrint is
|
|
6
|
+
> PagePrint is still pre-1.0 and is not yet recommended as a drop-in production dependency without application-specific testing. Native packaging and platform compatibility are still being validated. See the [production guide](docs/production.md) before deploying it.
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
PagePrint turns an HTML string into PDF bytes—or writes it directly to a file—through a small native C extension. It is designed for server-generated documents such as invoices, reports, labels, and statements where the HTML is known in advance and does not depend on JavaScript.
|
|
7
9
|
|
|
8
|
-
It exists as a faster, simpler alternative to PDFKit and other `wkhtmltopdf`-based gems. There is no external renderer process to shell out to, and the public Ruby API is intentionally small.
|
|
9
10
|
|
|
10
|
-
##
|
|
11
|
+
## Why PagePrint?
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
PDFKit and Wicked PDF render documents by launching the `wkhtmltopdf` executable. PagePrint instead embeds a purpose-built paged-media renderer in the Ruby process.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
gem "page_print"
|
|
16
|
-
```
|
|
15
|
+
That gives PagePrint a few useful properties:
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
- **No renderer subprocess:** no executable discovery, shelling out, or process startup per render.
|
|
18
|
+
- **A small Ruby API:** pass HTML in and receive PDF bytes, or write directly to a path.
|
|
19
|
+
- **Print-oriented CSS:** PlutoBook supports paged-media features such as `@page`, page counters, and running headers and footers.
|
|
20
|
+
- **Controlled resource loading:** PagePrint does not fetch unresolved HTTP URLs. Your application decides which stylesheets, images, and fonts may be loaded.
|
|
21
|
+
- **Rails asset integration:** Propshaft and `public/assets` resources work without HTTP requests back into the application.
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
gem install page_print
|
|
22
|
-
```
|
|
23
|
+
### How does it compare with PDFKit and Wicked PDF?
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
PDFKit and Wicked PDF are Ruby integrations for `wkhtmltopdf`; their rendering behavior and operational requirements come from that executable.
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
| | PagePrint / PlutoBook | PDFKit or Wicked PDF / `wkhtmltopdf` |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| Runtime model | Native library in the Ruby process | External `wkhtmltopdf` executable |
|
|
30
|
+
| Rendering engine | Purpose-built for static documents and CSS paged media | Legacy Qt WebKit browser engine |
|
|
31
|
+
| JavaScript | Not supported | Supported by legacy Qt WebKit |
|
|
32
|
+
| Network access | Denied unless your fetcher supplies the resource | Built in |
|
|
33
|
+
| CSS compatibility | Print-focused and partial; no CSS Grid | Limited by the old WebKit engine |
|
|
34
|
+
| Operational footprint | Native gem and bundled libraries on supported platforms | `wkhtmltopdf` binary plus Qt/WebKit dependencies |
|
|
35
|
+
| Upstream status | PagePrint currently packages PlutoBook 0.19.0 | `wkhtmltopdf` was archived in 2023 |
|
|
27
36
|
|
|
28
|
-
PagePrint is
|
|
37
|
+
PagePrint is a strong fit when you control the templates, value predictable print layout, and do not need JavaScript. Stay with a `wkhtmltopdf`-based gem if your existing templates depend on its JavaScript or WebKit-specific rendering behavior. When migrating, compare representative output carefully because changing engines can alter pagination, fonts, and layout.
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
The `wkhtmltopdf` project was [archived in 2023](https://github.com/wkhtmltopdf/wkhtmltopdf) and its own [status document](https://github.com/wkhtmltopdf/wkhtmltopdf/blob/master/docs/status.md) describes its Qt 4 / WebKit stack as unsupported and outdated. PlutoBook is purpose-built for static paged output, but has a narrower feature set; review its [feature matrix](https://github.com/plutoprint/plutobook/blob/main/FEATURES.md) before migrating a complex template.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
Add the gem to your `Gemfile`:
|
|
31
44
|
|
|
32
45
|
```ruby
|
|
33
|
-
|
|
34
|
-
def pdf
|
|
35
|
-
html = render_to_string(template: "prints/pdf", formats: [:html], layout: "pdf")
|
|
36
|
-
pdf = PagePrint.render(
|
|
37
|
-
html,
|
|
38
|
-
page_size: :a4,
|
|
39
|
-
margins: :normal,
|
|
40
|
-
media: :print,
|
|
41
|
-
metadata: { title: "Print PDF", author: "PagePrint" }
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
send_data pdf, filename: "print.pdf", type: "application/pdf", disposition: "inline"
|
|
45
|
-
end
|
|
46
|
-
end
|
|
46
|
+
gem "page_print"
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Then run:
|
|
50
50
|
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
<%= image_tag "logo.png" %>
|
|
51
|
+
```sh
|
|
52
|
+
bundle install
|
|
54
53
|
```
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
You can still override `base_url` explicitly:
|
|
55
|
+
PagePrint requires Ruby 3.2 or newer. Native gems with vendored PlutoBook libraries are published for:
|
|
59
56
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
```
|
|
57
|
+
- `x86_64-linux`
|
|
58
|
+
- `arm64-darwin` (Apple Silicon)
|
|
63
59
|
|
|
64
|
-
|
|
60
|
+
Other platforms build from source and require PlutoBook development headers and libraries. See [Installing on other platforms](docs/production.md#installing-on-other-platforms).
|
|
65
61
|
|
|
66
|
-
|
|
67
|
-
# config/initializers/page_print.rb
|
|
68
|
-
PagePrint.configure do |config|
|
|
69
|
-
config.resource_fetcher = MyResourceFetcher.new
|
|
70
|
-
end
|
|
71
|
-
```
|
|
62
|
+
## Quick start
|
|
72
63
|
|
|
73
|
-
|
|
64
|
+
Require PagePrint and render an HTML string:
|
|
74
65
|
|
|
75
66
|
```ruby
|
|
76
67
|
require "page_print"
|
|
77
|
-
require "tmpdir"
|
|
78
68
|
|
|
79
69
|
html = <<~HTML
|
|
70
|
+
<!doctype html>
|
|
80
71
|
<html>
|
|
72
|
+
<head>
|
|
73
|
+
<meta charset="utf-8">
|
|
74
|
+
<style>
|
|
75
|
+
@page {
|
|
76
|
+
size: A4;
|
|
77
|
+
|
|
78
|
+
@bottom-center {
|
|
79
|
+
content: counter(page) " / " counter(pages);
|
|
80
|
+
color: #666;
|
|
81
|
+
font-size: 10pt;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
body { font-family: sans-serif; }
|
|
86
|
+
</style>
|
|
87
|
+
</head>
|
|
81
88
|
<body>
|
|
82
|
-
<h1>
|
|
83
|
-
<p>
|
|
89
|
+
<h1>Invoice #1042</h1>
|
|
90
|
+
<p>Thank you for your business.</p>
|
|
84
91
|
</body>
|
|
85
92
|
</html>
|
|
86
93
|
HTML
|
|
87
94
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
PagePrint.render_to_file(html, output_path, base_url: "https://example.com")
|
|
95
|
+
pdf = PagePrint.render(html, page_size: :a4, margins: :normal)
|
|
91
96
|
```
|
|
92
97
|
|
|
93
|
-
|
|
98
|
+
For large documents, write directly to a file instead of retaining the PDF in a Ruby string:
|
|
94
99
|
|
|
95
100
|
```ruby
|
|
96
|
-
|
|
101
|
+
PagePrint.render_to_file(html, "invoice.pdf", page_size: :a4, margins: :normal)
|
|
97
102
|
```
|
|
98
103
|
|
|
99
|
-
|
|
104
|
+
`render_to_file` returns `true`; `render` returns an `ASCII-8BIT` Ruby string containing the PDF.
|
|
100
105
|
|
|
101
|
-
|
|
102
|
-
pdf = PagePrint.render(
|
|
103
|
-
html,
|
|
104
|
-
page_size: { width: 100, height: 150, unit: :mm },
|
|
105
|
-
margins: { top: 5, right: 6, bottom: 7, left: 8, unit: :mm }
|
|
106
|
-
)
|
|
107
|
-
```
|
|
106
|
+
## Rails
|
|
108
107
|
|
|
109
|
-
|
|
108
|
+
PagePrint integrates automatically with Rails applications that use Propshaft. Render a template to HTML, pass it to PagePrint, then send the resulting bytes:
|
|
110
109
|
|
|
111
110
|
```ruby
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
class InvoicesController < ApplicationController
|
|
112
|
+
def show
|
|
113
|
+
html = render_to_string(template: "invoices/show", formats: [:html], layout: "pdf")
|
|
114
|
+
pdf = PagePrint.render(html, page_size: :a4, margins: :normal, metadata: { title: "Invoice ##{params[:id]}", author: "Example Ltd" })
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
send_data pdf, filename: "invoice-#{params[:id]}.pdf", type: "application/pdf", disposition: "inline"
|
|
117
117
|
end
|
|
118
118
|
end
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
Supported keyword options:
|
|
124
|
-
|
|
125
|
-
- `base_url:` string used to resolve relative URLs in the HTML
|
|
126
|
-
- `page_size:` one of `:a3`, `:a4`, `:a5`, `:b4`, `:b5`, `:letter`, `:legal`, `:ledger`, or `{ width:, height:, unit: }`
|
|
127
|
-
- `margins:` one of `:none`, `:normal`, `:narrow`, `:moderate`, `:wide`, or `{ top:, right:, bottom:, left:, unit: }`
|
|
128
|
-
- `media:` one of `:print`, `:screen`
|
|
129
|
-
- `resource_fetcher:` callable that receives a URL and returns `nil` or `{ content:, mime_type:, text_encoding: nil }`
|
|
130
|
-
- `metadata:` hash with `:title`, `:author`, `:subject`, `:keywords`, `:creation_date`, or `:modification_date`
|
|
131
|
-
|
|
132
|
-
Unresolved URLs are not fetched over the network. Provide assets through `resource_fetcher`, or return `nil` to skip a URL.
|
|
133
|
-
|
|
134
|
-
`metadata[:creation_date]` and `metadata[:modification_date]` should be ISO-8601 strings, for example `2026-05-10T12:00:00Z`.
|
|
135
|
-
|
|
136
|
-
Custom dimensions and margins require `unit:`. Supported units are `:pt`, `:pc`, `:in`, `:cm`, `:mm`, and `:px`.
|
|
137
|
-
|
|
138
|
-
## Benchmarking
|
|
139
|
-
|
|
140
|
-
```sh
|
|
141
|
-
RUNS=30 WARMUPS=3 bundle exec ruby benchmark/pdf_renderers.rb
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
Measured on 2026-05-30 with Ruby 3.4.7 on Apple Silicon:
|
|
145
|
-
|
|
146
|
-
| Renderer | Avg wall | P95 wall | Avg CPU | Avg peak RSS | Avg PDF |
|
|
147
|
-
| --- | ---: | ---: | ---: | ---: | ---: |
|
|
148
|
-
| `page_print` | 78.2ms | 89.6ms | 93.4ms | 42.1MB | 33.0KB |
|
|
149
|
-
| PDFKit | 782.2ms | 2127.1ms | 824.8ms | 59.4MB | 27.9KB |
|
|
150
|
-
|
|
151
|
-
For options and CSV output, see `benchmark/README.md`.
|
|
152
|
-
|
|
153
|
-
## Requirements
|
|
154
|
-
|
|
155
|
-
- Ruby 3.2+
|
|
156
|
-
- Native gems are published for `x86_64-linux` and `arm64-darwin`.
|
|
157
|
-
- Native gems vendor PlutoBook but compile the small Ruby extension during install so it links against your local Ruby.
|
|
158
|
-
- Source builds on unsupported platforms require PlutoBook development headers and library files.
|
|
159
|
-
|
|
160
|
-
Supported platforms:
|
|
161
|
-
|
|
162
|
-
| Platform | Install Type |
|
|
163
|
-
| --- | --- |
|
|
164
|
-
| `x86_64-linux` | Vendored PlutoBook, local Ruby extension build |
|
|
165
|
-
| `arm64-darwin` | Vendored PlutoBook, local Ruby extension build |
|
|
166
|
-
| Other platforms | Source build |
|
|
167
|
-
|
|
168
|
-
Source build requirements on macOS with Homebrew:
|
|
169
|
-
|
|
170
|
-
```sh
|
|
171
|
-
brew install plutobook pkg-config
|
|
172
|
-
```
|
|
121
|
+
Normal asset helpers can be used in the PDF template:
|
|
173
122
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
gem install page_print -- --with-plutobook-include=/path/to/include --with-plutobook-lib=/path/to/lib
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
Native gems bundle PlutoBook and required non-system shared libraries. Optional PlutoBook features for curl, TurboJPEG, and WebP are disabled in native gems to keep the bundled dependency set smaller.
|
|
181
|
-
|
|
182
|
-
## Notes
|
|
183
|
-
|
|
184
|
-
- The extension supports writing to a file path with `render_to_file` or returning PDF bytes with `render`.
|
|
185
|
-
- JavaScript execution is intentionally unsupported.
|
|
186
|
-
- Native gems disable PlutoBook's optional curl, TurboJPEG, and WebP features.
|
|
187
|
-
|
|
188
|
-
## Development
|
|
189
|
-
|
|
190
|
-
Install development dependencies:
|
|
191
|
-
|
|
192
|
-
```sh
|
|
193
|
-
bundle install
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
Compile the native extension into `lib/page_print`:
|
|
197
|
-
|
|
198
|
-
```sh
|
|
199
|
-
bundle exec rake compile
|
|
123
|
+
```erb
|
|
124
|
+
<%= stylesheet_link_tag "pdf" %>
|
|
125
|
+
<%= image_tag "logo.png", alt: "Example Ltd" %>
|
|
200
126
|
```
|
|
201
127
|
|
|
202
|
-
|
|
128
|
+
During a controller action, PagePrint uses `request.base_url` to resolve relative URLs. Its Rails resource fetcher reads `/assets/...` from Propshaft or `public/assets`; it does not make an HTTP request back to Rails.
|
|
203
129
|
|
|
204
|
-
|
|
205
|
-
bundle exec irb -Ilib
|
|
206
|
-
```
|
|
130
|
+
## Assets and network access
|
|
207
131
|
|
|
208
|
-
|
|
132
|
+
PagePrint intentionally does not download unresolved resources. Supply a `resource_fetcher` when non-Rails HTML needs external styles, images, or fonts:
|
|
209
133
|
|
|
210
134
|
```ruby
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
You can also do a quick one-shot smoke test from the shell:
|
|
135
|
+
assets = {
|
|
136
|
+
"asset:pdf.css" => {
|
|
137
|
+
content: File.binread("app/assets/stylesheets/pdf.css"),
|
|
138
|
+
mime_type: "text/css"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
220
141
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
Or use the development console, which compiles the native extension first and then starts IRB with `PagePrint` loaded:
|
|
226
|
-
|
|
227
|
-
```sh
|
|
228
|
-
bin/console
|
|
142
|
+
pdf = PagePrint.render(
|
|
143
|
+
"<link rel=\"stylesheet\" href=\"asset:pdf.css\"><h1>Report</h1>",
|
|
144
|
+
resource_fetcher: ->(url) { assets[url] }
|
|
145
|
+
)
|
|
229
146
|
```
|
|
230
147
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
Run the test suite only:
|
|
148
|
+
The fetcher receives each resolved URL and returns `{ content:, mime_type: }` or `nil`. Returning `nil` skips the resource. This makes resource access explicit, but it does not make untrusted HTML safe; see [Security](docs/production.md#security).
|
|
234
149
|
|
|
235
|
-
|
|
236
|
-
bundle exec rake test
|
|
237
|
-
```
|
|
150
|
+
## Configuration
|
|
238
151
|
|
|
239
|
-
|
|
152
|
+
The common rendering options are:
|
|
240
153
|
|
|
241
|
-
```
|
|
242
|
-
|
|
154
|
+
```ruby
|
|
155
|
+
PagePrint.render(
|
|
156
|
+
html,
|
|
157
|
+
base_url: "https://example.test",
|
|
158
|
+
page_size: :letter,
|
|
159
|
+
margins: :narrow,
|
|
160
|
+
media: :print,
|
|
161
|
+
metadata: { title: "Quarterly report" },
|
|
162
|
+
resource_fetcher: fetcher
|
|
163
|
+
)
|
|
243
164
|
```
|
|
244
165
|
|
|
245
|
-
|
|
166
|
+
Defaults are A4 paper, normal margins, and print media. See the [configuration reference](docs/configuration.md) for every preset, custom dimensions, metadata fields, global configuration, and resource fetcher behavior.
|
|
246
167
|
|
|
247
|
-
|
|
168
|
+
## Performance
|
|
248
169
|
|
|
249
|
-
|
|
250
|
-
gem build page_print.gemspec
|
|
251
|
-
gem install ./page_print-*.gem
|
|
252
|
-
```
|
|
170
|
+
The repository includes a reproducible benchmark against PDFKit using the same static HTML input. On May 30, 2026, the recorded run used Ruby 3.4.7 on an Apple Silicon development machine:
|
|
253
171
|
|
|
254
|
-
|
|
172
|
+
| Renderer | Average wall time | P95 wall time | Average peak RSS |
|
|
173
|
+
| --- | ---: | ---: | ---: |
|
|
174
|
+
| PagePrint | 78.2 ms | 89.6 ms | 42.1 MB |
|
|
175
|
+
| PDFKit | 782.2 ms | 2,127.1 ms | 59.4 MB |
|
|
255
176
|
|
|
256
|
-
|
|
177
|
+
These numbers are illustrative, not a capacity guarantee. Rendering cost varies significantly with fonts, images, document length, and host configuration. Run the benchmark—or your own production templates—on the deployment target before sizing infrastructure.
|
|
257
178
|
|
|
258
179
|
```sh
|
|
259
|
-
|
|
180
|
+
RUNS=30 WARMUPS=3 bundle exec ruby benchmark/pdf_renderers.rb
|
|
260
181
|
```
|
|
261
182
|
|
|
262
|
-
|
|
183
|
+
See [benchmark/README.md](benchmark/README.md) for methodology and CSV output.
|
|
263
184
|
|
|
264
|
-
|
|
185
|
+
## Documentation
|
|
265
186
|
|
|
266
|
-
|
|
187
|
+
- [Configuration and API reference](docs/configuration.md)
|
|
188
|
+
- [Production guide](docs/production.md)
|
|
189
|
+
- [Benchmark methodology](benchmark/README.md)
|
|
190
|
+
- [PlutoBook feature matrix](https://github.com/plutoprint/plutobook/blob/main/FEATURES.md)
|
|
267
191
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
```sh
|
|
271
|
-
bundle exec rake package:linux
|
|
272
|
-
```
|
|
192
|
+
## Development
|
|
273
193
|
|
|
274
|
-
|
|
194
|
+
Install dependencies, compile the extension, and run the test suite:
|
|
275
195
|
|
|
276
196
|
```sh
|
|
277
|
-
bundle
|
|
197
|
+
bundle install
|
|
198
|
+
bundle exec rake
|
|
278
199
|
```
|
|
279
200
|
|
|
280
|
-
|
|
201
|
+
Use `bin/console` for an IRB session with the extension compiled and PagePrint loaded.
|
|
281
202
|
|
|
282
|
-
|
|
203
|
+
## License
|
|
283
204
|
|
|
284
|
-
|
|
205
|
+
PagePrint is available under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Configuration and API reference
|
|
2
|
+
|
|
3
|
+
PagePrint exposes two rendering methods and a small set of keyword options.
|
|
4
|
+
|
|
5
|
+
## Rendering methods
|
|
6
|
+
|
|
7
|
+
### `PagePrint.render(html, **options)`
|
|
8
|
+
|
|
9
|
+
Renders a non-empty HTML string and returns the PDF as an `ASCII-8BIT` Ruby string.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
pdf = PagePrint.render("<h1>Report</h1>")
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### `PagePrint.render_to_file(html, path, **options)`
|
|
16
|
+
|
|
17
|
+
Renders a non-empty HTML string directly to `path` and returns `true`. The destination directory must already exist and be writable.
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
PagePrint.render_to_file("<h1>Report</h1>", "/tmp/report.pdf")
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Writing directly to a file avoids keeping the finished PDF in a Ruby string and is preferable for large documents or background jobs.
|
|
24
|
+
|
|
25
|
+
## Options
|
|
26
|
+
|
|
27
|
+
Both rendering methods accept the same options.
|
|
28
|
+
|
|
29
|
+
| Option | Default | Accepted values |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| `base_url:` | `nil` outside Rails | A string or `nil` |
|
|
32
|
+
| `page_size:` | `:a4` | A page preset or custom dimensions |
|
|
33
|
+
| `margins:` | `:normal` | A margin preset or custom dimensions |
|
|
34
|
+
| `media:` | `:print` | `:print` or `:screen` |
|
|
35
|
+
| `resource_fetcher:` | Configured global fetcher or `nil` | Any callable, `nil`, or `false` |
|
|
36
|
+
| `metadata:` | `nil` | A metadata hash or `nil` |
|
|
37
|
+
|
|
38
|
+
Unknown options raise `ArgumentError`; values of the wrong type raise `TypeError`.
|
|
39
|
+
|
|
40
|
+
### Base URL
|
|
41
|
+
|
|
42
|
+
`base_url` resolves relative URLs found in the HTML:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
html = '<link rel="stylesheet" href="/assets/report.css"><h1>Report</h1>'
|
|
46
|
+
PagePrint.render(html, base_url: "https://example.test", resource_fetcher: fetcher)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The fetcher receives `https://example.test/assets/report.css`. A base URL resolves the URL; it does not enable network access.
|
|
50
|
+
|
|
51
|
+
Set an application-wide default when rendering outside Rails:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
PagePrint.configure do |config|
|
|
55
|
+
config.base_url = "https://example.test"
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
An explicit `base_url:` takes precedence over the configured value.
|
|
60
|
+
|
|
61
|
+
### Page size
|
|
62
|
+
|
|
63
|
+
Available presets:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
:a3 :a4 :a5 :b4 :b5 :letter :legal :ledger
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Custom dimensions require a width, height, and unit:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
PagePrint.render(
|
|
73
|
+
html,
|
|
74
|
+
page_size: { width: 100, height: 150, unit: :mm }
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Width and height must be greater than zero.
|
|
79
|
+
|
|
80
|
+
### Margins
|
|
81
|
+
|
|
82
|
+
Available presets:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
:none :normal :narrow :moderate :wide
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Custom margins require all four sides and a unit:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
PagePrint.render(
|
|
92
|
+
html,
|
|
93
|
+
margins: { top: 5, right: 6, bottom: 7, left: 8, unit: :mm }
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Margin values must be zero or greater.
|
|
98
|
+
|
|
99
|
+
Page sizes and margins support these units:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
:pt :pc :in :cm :mm :px
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Media type
|
|
106
|
+
|
|
107
|
+
Use `:print` to apply print styles and `:screen` to apply screen styles:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
PagePrint.render(html, media: :screen)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### PDF metadata
|
|
114
|
+
|
|
115
|
+
Metadata keys and values must be symbols and strings, respectively. A value may also be `nil`.
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
PagePrint.render(
|
|
119
|
+
html,
|
|
120
|
+
metadata: {
|
|
121
|
+
title: "Quarterly report",
|
|
122
|
+
author: "Example Ltd",
|
|
123
|
+
subject: "Q2 results",
|
|
124
|
+
keywords: "finance,quarterly",
|
|
125
|
+
creation_date: "2026-05-10T12:00:00Z",
|
|
126
|
+
modification_date: nil
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Supported keys are:
|
|
132
|
+
|
|
133
|
+
- `:title`
|
|
134
|
+
- `:author`
|
|
135
|
+
- `:subject`
|
|
136
|
+
- `:keywords`
|
|
137
|
+
- `:creation_date`
|
|
138
|
+
- `:modification_date`
|
|
139
|
+
|
|
140
|
+
Use ISO 8601 strings for creation and modification dates.
|
|
141
|
+
|
|
142
|
+
## Resource fetchers
|
|
143
|
+
|
|
144
|
+
A resource fetcher is a callable that receives a resolved URL. It must return either `nil` or a hash containing binary `content` and its `mime_type`. `text_encoding` is optional.
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
fetcher = lambda do |url|
|
|
148
|
+
case url
|
|
149
|
+
when "asset:report.css"
|
|
150
|
+
{
|
|
151
|
+
content: File.binread("app/assets/stylesheets/report.css"),
|
|
152
|
+
mime_type: "text/css",
|
|
153
|
+
text_encoding: "utf-8"
|
|
154
|
+
}
|
|
155
|
+
when "asset:logo.png"
|
|
156
|
+
{
|
|
157
|
+
content: File.binread("app/assets/images/logo.png"),
|
|
158
|
+
mime_type: "image/png"
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
PagePrint.render(html, resource_fetcher: fetcher)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Returning `nil` skips the URL. PagePrint does not fall back to an HTTP request. Exceptions raised by the fetcher are propagated to the caller.
|
|
167
|
+
|
|
168
|
+
Configure a default fetcher for every render:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
PagePrint.configure do |config|
|
|
172
|
+
config.resource_fetcher = MyResourceFetcher.new
|
|
173
|
+
end
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Pass `resource_fetcher: false` or `nil` to disable a configured fetcher for one render.
|
|
177
|
+
|
|
178
|
+
Keep fetchers narrowly scoped. Prefer an allowlist or a fixed asset map over fetching arbitrary URLs supplied by HTML.
|
|
179
|
+
|
|
180
|
+
## Rails behavior
|
|
181
|
+
|
|
182
|
+
When Rails is loaded, PagePrint installs two defaults:
|
|
183
|
+
|
|
184
|
+
1. During controller actions, the current `request.base_url` becomes the default base URL for that request.
|
|
185
|
+
2. `PagePrint::RailsResourceFetcher` resolves URLs under `/assets/` from `public/assets` or Propshaft.
|
|
186
|
+
|
|
187
|
+
An explicit `base_url:` or `resource_fetcher:` still takes precedence. To replace the Rails fetcher globally:
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
# config/initializers/page_print.rb
|
|
191
|
+
PagePrint.configure do |config|
|
|
192
|
+
config.resource_fetcher = MyResourceFetcher.new
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The built-in Rails fetcher only handles `/assets/...` URLs. Other paths are skipped.
|
|
197
|
+
|
|
198
|
+
## CSS and rendering support
|
|
199
|
+
|
|
200
|
+
PagePrint delegates HTML and CSS support to PlutoBook. It supports print-oriented features including `@page`, page counters, and running headers and footers, but it is not a full web browser. JavaScript and CSS Grid are not supported.
|
|
201
|
+
|
|
202
|
+
Consult the upstream [PlutoBook feature matrix](https://github.com/plutoprint/plutobook/blob/main/FEATURES.md) when designing or migrating templates. Test representative documents after every renderer or font change because pagination can change even when the HTML does not.
|
|
203
|
+
|
|
204
|
+
[Back to the README](../README.md)
|
data/docs/production.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Production guide
|
|
2
|
+
|
|
3
|
+
PagePrint is pre-1.0 software. Treat adoption as an application-specific engineering decision: validate the native package, representative documents, throughput, memory use, fonts, and failure behavior on the same platform used in production.
|
|
4
|
+
|
|
5
|
+
## Production readiness checklist
|
|
6
|
+
|
|
7
|
+
Before deploying PagePrint:
|
|
8
|
+
|
|
9
|
+
- [ ] Confirm the deployment platform has a native gem or can build PlutoBook from source.
|
|
10
|
+
- [ ] Render representative short, long, image-heavy, and multilingual documents in CI.
|
|
11
|
+
- [ ] Visually compare pagination, fonts, images, headers, and footers against approved fixtures.
|
|
12
|
+
- [ ] Verify every stylesheet, image, and font is available through the Rails integration or an allowlisted resource fetcher.
|
|
13
|
+
- [ ] Confirm templates do not depend on JavaScript, CSS Grid, or other unsupported browser features.
|
|
14
|
+
- [ ] Load-test with production-sized documents and the same worker/thread configuration used in deployment.
|
|
15
|
+
- [ ] Set application-level job timeouts and monitor render latency, failures, and output size.
|
|
16
|
+
- [ ] Prefer `render_to_file` for large PDFs, and clean up temporary output in both success and failure paths.
|
|
17
|
+
- [ ] Pin the gem version and test upgrades before rollout; renderer upgrades can alter layout and pagination.
|
|
18
|
+
|
|
19
|
+
## Platform support
|
|
20
|
+
|
|
21
|
+
PagePrint requires Ruby 3.2 or newer.
|
|
22
|
+
|
|
23
|
+
| Platform | Installation |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `x86_64-linux` | Native gem with vendored PlutoBook libraries; the Ruby extension is compiled against the local Ruby |
|
|
26
|
+
| `arm64-darwin` | Native gem with vendored PlutoBook libraries; the Ruby extension is compiled against the local Ruby |
|
|
27
|
+
| Other platforms | Source build using an installed PlutoBook |
|
|
28
|
+
|
|
29
|
+
The published native gems disable PlutoBook's optional curl, TurboJPEG, and WebP features to keep the bundled dependency set smaller. PagePrint uses its own Ruby resource fetcher boundary and does not expose PlutoBook's optional network fetcher.
|
|
30
|
+
|
|
31
|
+
Your production image still needs a compiler and Ruby development headers because the small PagePrint extension is compiled during installation.
|
|
32
|
+
|
|
33
|
+
### Installing on other platforms
|
|
34
|
+
|
|
35
|
+
Install PlutoBook development headers and libraries before installing the gem. On macOS with Homebrew:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
brew install plutobook pkg-config
|
|
39
|
+
bundle install
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
If `pkg-config` cannot find PlutoBook, provide explicit paths:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
gem install page_print -- \
|
|
46
|
+
--with-plutobook-include=/path/to/include \
|
|
47
|
+
--with-plutobook-lib=/path/to/lib
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Use equivalent Bundler build configuration in repeatable deployments:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
bundle config set --local build.page_print \
|
|
54
|
+
"--with-plutobook-include=/path/to/include --with-plutobook-lib=/path/to/lib"
|
|
55
|
+
bundle install
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Run a render smoke test while building the deployment artifact, rather than only checking that the gem can be required:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
bundle exec ruby -e '
|
|
62
|
+
require "page_print"
|
|
63
|
+
pdf = PagePrint.render("<h1>Deployment check</h1>")
|
|
64
|
+
abort "invalid PDF" unless pdf.start_with?("%PDF")
|
|
65
|
+
'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Security
|
|
69
|
+
|
|
70
|
+
PagePrint's resource policy is deny-by-default: an unresolved URL is not fetched over the network. Preserve that property in custom fetchers.
|
|
71
|
+
|
|
72
|
+
- Do not build a fetcher that blindly downloads arbitrary URLs. Doing so can introduce server-side request forgery (SSRF), access to cloud metadata endpoints, and unbounded downloads.
|
|
73
|
+
- Prefer a fixed asset map, an application-owned URI scheme, or strict allowlists for scheme, host, path, content type, and size.
|
|
74
|
+
- Do not treat PagePrint as a sandbox for hostile HTML. It parses complex input in native code and should run with the minimum filesystem and process permissions needed by the application.
|
|
75
|
+
- Never place secrets in HTML, asset URLs, metadata, or error logs.
|
|
76
|
+
- Validate and limit user-controlled content before inserting it into a trusted template.
|
|
77
|
+
|
|
78
|
+
PagePrint's disabled network fallback improves predictability and reduces accidental outbound access, but it is not a complete security boundary.
|
|
79
|
+
|
|
80
|
+
## Template compatibility
|
|
81
|
+
|
|
82
|
+
PagePrint is intended for static, server-rendered documents. It does not execute JavaScript. PlutoBook supports much of CSS 3 and selected CSS 4 functionality, but important gaps include CSS Grid and some advanced visual features.
|
|
83
|
+
|
|
84
|
+
Before migrating from PDFKit, `wkhtmltopdf`, or a browser renderer:
|
|
85
|
+
|
|
86
|
+
1. Inventory JavaScript, layout modules, fonts, SVG, and remote assets used by the templates.
|
|
87
|
+
2. Check the [PlutoBook feature matrix](https://github.com/plutoprint/plutobook/blob/main/FEATURES.md).
|
|
88
|
+
3. Render a fixture set with both engines.
|
|
89
|
+
4. Compare page breaks and visual output, not only whether a PDF was produced.
|
|
90
|
+
|
|
91
|
+
Use explicit `@page`, print styles, and embedded or controlled fonts. Font substitution is a common cause of different line wrapping and pagination across development and production.
|
|
92
|
+
|
|
93
|
+
## Assets and networking
|
|
94
|
+
|
|
95
|
+
For deterministic output, package document assets with the application and return them from a resource fetcher. Avoid making rendering depend on the availability or latency of another service.
|
|
96
|
+
|
|
97
|
+
Rails applications using Propshaft get a default fetcher for `/assets/...`. Outside Rails, configure a fetcher once at process startup:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
PagePrint.configure do |config|
|
|
101
|
+
config.resource_fetcher = ProductionPdfAssets.new
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
See the [resource fetcher reference](configuration.md#resource-fetchers) for the callable contract.
|
|
106
|
+
|
|
107
|
+
## Capacity and concurrency
|
|
108
|
+
|
|
109
|
+
HTML parsing, layout, and PDF writing are native CPU- and memory-intensive work. The extension releases Ruby's Global VM Lock during PlutoBook loading and writing, but Ruby callbacks such as a custom resource fetcher run with the lock.
|
|
110
|
+
|
|
111
|
+
Do not size workers from the repository benchmark alone. Measure:
|
|
112
|
+
|
|
113
|
+
- latency percentiles, not only averages;
|
|
114
|
+
- peak resident memory with concurrent renders;
|
|
115
|
+
- the largest realistic HTML, image, and PDF sizes;
|
|
116
|
+
- application behavior when a render raises or a job times out;
|
|
117
|
+
- both `render` and `render_to_file` if you expect large outputs.
|
|
118
|
+
|
|
119
|
+
Use bounded job concurrency. For background processing, let the job system retry only failures known to be transient; invalid templates and unsupported resources will fail repeatedly.
|
|
120
|
+
|
|
121
|
+
## Failure handling
|
|
122
|
+
|
|
123
|
+
PagePrint validates option types and raises Ruby exceptions for invalid input, resource fetcher failures, HTML loading failures, and PDF write failures. Treat a successful method return—and, where appropriate, a `%PDF` signature and non-zero output size—as the completion signal.
|
|
124
|
+
|
|
125
|
+
When writing temporary files:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
require "tempfile"
|
|
129
|
+
|
|
130
|
+
Tempfile.create(["invoice", ".pdf"]) do |file|
|
|
131
|
+
PagePrint.render_to_file(html, file.path)
|
|
132
|
+
deliver(file.path)
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Do not expose native error text directly to end users. Record enough context to identify the template and render options, without logging document contents that may contain personal or confidential data.
|
|
137
|
+
|
|
138
|
+
## Upgrade strategy
|
|
139
|
+
|
|
140
|
+
Pin PagePrint in the application lockfile. Before upgrading PagePrint or PlutoBook:
|
|
141
|
+
|
|
142
|
+
1. Build on every production platform.
|
|
143
|
+
2. Run the complete PDF fixture suite.
|
|
144
|
+
3. Compare visual output and page count.
|
|
145
|
+
4. Repeat a focused load test.
|
|
146
|
+
5. Roll out gradually while watching latency and render failures.
|
|
147
|
+
|
|
148
|
+
[Back to the README](../README.md)
|
data/ext/page_print/page_print.c
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
#include "ruby/encoding.h"
|
|
3
3
|
#include "ruby/thread.h"
|
|
4
4
|
#include <limits.h>
|
|
5
|
+
#include <math.h>
|
|
5
6
|
#include <stdint.h>
|
|
7
|
+
#include <string.h>
|
|
6
8
|
#include <plutobook/plutobook.h>
|
|
7
9
|
|
|
8
10
|
#if defined(__GNUC__) || defined(__clang__)
|
|
@@ -55,6 +57,7 @@ typedef struct {
|
|
|
55
57
|
|
|
56
58
|
typedef struct {
|
|
57
59
|
VALUE object;
|
|
60
|
+
VALUE error;
|
|
58
61
|
int state;
|
|
59
62
|
} pageprint_resource_fetcher_t;
|
|
60
63
|
|
|
@@ -66,10 +69,12 @@ typedef struct {
|
|
|
66
69
|
|
|
67
70
|
typedef struct {
|
|
68
71
|
VALUE output;
|
|
72
|
+
VALUE error;
|
|
69
73
|
int state;
|
|
70
74
|
} pageprint_pdf_string_output_t;
|
|
71
75
|
|
|
72
76
|
typedef struct {
|
|
77
|
+
pageprint_pdf_string_output_t *owner;
|
|
73
78
|
VALUE output;
|
|
74
79
|
const char *data;
|
|
75
80
|
unsigned int length;
|
|
@@ -102,10 +107,49 @@ typedef struct {
|
|
|
102
107
|
VALUE metadata;
|
|
103
108
|
} pageprint_apply_metadata_args_t;
|
|
104
109
|
|
|
110
|
+
typedef struct {
|
|
111
|
+
plutobook_t *book;
|
|
112
|
+
VALUE html;
|
|
113
|
+
VALUE base_url;
|
|
114
|
+
VALUE resource_fetcher;
|
|
115
|
+
VALUE metadata;
|
|
116
|
+
pageprint_resource_fetcher_t resource_fetcher_state;
|
|
117
|
+
} pageprint_book_context_t;
|
|
118
|
+
|
|
119
|
+
typedef struct {
|
|
120
|
+
pageprint_book_context_t book_context;
|
|
121
|
+
VALUE html;
|
|
122
|
+
VALUE path;
|
|
123
|
+
VALUE options;
|
|
124
|
+
} pageprint_render_to_file_context_t;
|
|
125
|
+
|
|
126
|
+
typedef struct {
|
|
127
|
+
pageprint_book_context_t book_context;
|
|
128
|
+
VALUE html;
|
|
129
|
+
VALUE options;
|
|
130
|
+
pageprint_pdf_string_output_t output;
|
|
131
|
+
} pageprint_render_context_t;
|
|
132
|
+
|
|
105
133
|
static VALUE pageprint_append_pdf_string(VALUE value);
|
|
106
134
|
static plutobook_stream_status_t pageprint_write_pdf_string(void *closure, const char *data, unsigned int length);
|
|
107
135
|
static plutobook_resource_data_t *pageprint_fetch_resource(void *closure, const char *url);
|
|
108
136
|
|
|
137
|
+
static void pageprint_reject_embedded_nul(VALUE value, const char *name)
|
|
138
|
+
{
|
|
139
|
+
if (memchr(RSTRING_PTR(value), '\0', (size_t)RSTRING_LEN(value))) {
|
|
140
|
+
rb_raise(rb_eArgError, "%s must not contain NUL bytes", name);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static VALUE pageprint_frozen_string_copy(VALUE value)
|
|
145
|
+
{
|
|
146
|
+
VALUE copy = rb_str_dup(value);
|
|
147
|
+
|
|
148
|
+
rb_obj_freeze(copy);
|
|
149
|
+
|
|
150
|
+
return copy;
|
|
151
|
+
}
|
|
152
|
+
|
|
109
153
|
static void PAGEPRINT_NORETURN pageprint_raise_plutobook_error(VALUE error_class, const char *message)
|
|
110
154
|
{
|
|
111
155
|
const char *error_message = plutobook_get_error_message();
|
|
@@ -128,6 +172,12 @@ static void PAGEPRINT_NORETURN pageprint_raise_plutobook_error_with_path(VALUE e
|
|
|
128
172
|
rb_raise(error_class, "%s %s", message, path);
|
|
129
173
|
}
|
|
130
174
|
|
|
175
|
+
static void PAGEPRINT_NORETURN pageprint_jump_callback_error(int state, VALUE error)
|
|
176
|
+
{
|
|
177
|
+
rb_set_errinfo(error);
|
|
178
|
+
rb_jump_tag(state);
|
|
179
|
+
}
|
|
180
|
+
|
|
131
181
|
static double pageprint_unit_factor_from_value(VALUE value, const char *name)
|
|
132
182
|
{
|
|
133
183
|
ID value_id;
|
|
@@ -175,6 +225,14 @@ static plutobook_page_size_t pageprint_page_size_from_value(VALUE value)
|
|
|
175
225
|
width_number = NUM2DBL(width);
|
|
176
226
|
height_number = NUM2DBL(height);
|
|
177
227
|
|
|
228
|
+
if (!isfinite(width_number)) {
|
|
229
|
+
rb_raise(rb_eArgError, "page_size width must be finite");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!isfinite(height_number)) {
|
|
233
|
+
rb_raise(rb_eArgError, "page_size height must be finite");
|
|
234
|
+
}
|
|
235
|
+
|
|
178
236
|
if (width_number <= 0) {
|
|
179
237
|
rb_raise(rb_eArgError, "page_size width must be greater than 0");
|
|
180
238
|
}
|
|
@@ -247,6 +305,10 @@ static plutobook_page_margins_t pageprint_margins_from_value(VALUE value)
|
|
|
247
305
|
bottom_number = NUM2DBL(bottom);
|
|
248
306
|
left_number = NUM2DBL(left);
|
|
249
307
|
|
|
308
|
+
if (!isfinite(top_number) || !isfinite(right_number) || !isfinite(bottom_number) || !isfinite(left_number)) {
|
|
309
|
+
rb_raise(rb_eArgError, "margins values must be finite");
|
|
310
|
+
}
|
|
311
|
+
|
|
250
312
|
if (top_number < 0 || right_number < 0 || bottom_number < 0 || left_number < 0) {
|
|
251
313
|
rb_raise(rb_eArgError, "margins values must be greater than or equal to 0");
|
|
252
314
|
}
|
|
@@ -410,11 +472,17 @@ static void *pageprint_write_pdf_without_gvl(void *ptr)
|
|
|
410
472
|
static void *pageprint_append_pdf_string_with_gvl(void *ptr)
|
|
411
473
|
{
|
|
412
474
|
pageprint_pdf_string_append_t *append = ptr;
|
|
475
|
+
pageprint_pdf_string_output_t *output = append->owner;
|
|
413
476
|
int state = 0;
|
|
414
477
|
|
|
415
478
|
rb_protect(pageprint_append_pdf_string, (VALUE)append, &state);
|
|
416
479
|
|
|
417
|
-
|
|
480
|
+
if (state && !output->state) {
|
|
481
|
+
output->state = state;
|
|
482
|
+
output->error = rb_errinfo();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return NULL;
|
|
418
486
|
}
|
|
419
487
|
|
|
420
488
|
static void *pageprint_write_pdf_stream_without_gvl(void *ptr)
|
|
@@ -492,24 +560,31 @@ static void *pageprint_call_resource_fetcher_with_gvl(void *ptr)
|
|
|
492
560
|
|
|
493
561
|
rb_protect(pageprint_call_resource_fetcher, (VALUE)args, &state);
|
|
494
562
|
|
|
495
|
-
|
|
563
|
+
if (state && !args->fetcher->state) {
|
|
564
|
+
args->fetcher->state = state;
|
|
565
|
+
args->fetcher->error = rb_errinfo();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return NULL;
|
|
496
569
|
}
|
|
497
570
|
|
|
498
571
|
static plutobook_resource_data_t *pageprint_fetch_resource(void *closure, const char *url)
|
|
499
572
|
{
|
|
500
573
|
pageprint_resource_fetcher_t *fetcher = closure;
|
|
501
574
|
pageprint_resource_fetch_args_t args;
|
|
502
|
-
|
|
575
|
+
|
|
576
|
+
if (fetcher->state) {
|
|
577
|
+
return NULL;
|
|
578
|
+
}
|
|
503
579
|
|
|
504
580
|
if (!NIL_P(fetcher->object)) {
|
|
505
581
|
args.fetcher = fetcher;
|
|
506
582
|
args.url = url;
|
|
507
583
|
args.resource = NULL;
|
|
508
584
|
|
|
509
|
-
|
|
585
|
+
rb_thread_call_with_gvl(pageprint_call_resource_fetcher_with_gvl, &args);
|
|
510
586
|
|
|
511
|
-
if (state) {
|
|
512
|
-
fetcher->state = state;
|
|
587
|
+
if (fetcher->state) {
|
|
513
588
|
plutobook_set_error_message("failed to fetch URL '%s'", url);
|
|
514
589
|
return NULL;
|
|
515
590
|
}
|
|
@@ -608,15 +683,11 @@ static pageprint_options_t pageprint_options_from_value(VALUE options)
|
|
|
608
683
|
return result;
|
|
609
684
|
}
|
|
610
685
|
|
|
611
|
-
static
|
|
686
|
+
static void pageprint_create_book_from_html(VALUE html, VALUE options, pageprint_book_context_t *context)
|
|
612
687
|
{
|
|
613
688
|
pageprint_options_t pageprint_options;
|
|
614
|
-
|
|
615
|
-
const char *html_str;
|
|
616
689
|
const char *base_url_str;
|
|
617
690
|
long html_len;
|
|
618
|
-
|
|
619
|
-
plutobook_t *book;
|
|
620
691
|
pageprint_load_html_args_t load_args;
|
|
621
692
|
|
|
622
693
|
if (!RB_TYPE_P(html, T_STRING)) {
|
|
@@ -631,45 +702,49 @@ static plutobook_t *pageprint_create_book_from_html(VALUE html, VALUE options, p
|
|
|
631
702
|
rb_raise(rb_eArgError, "html is too large");
|
|
632
703
|
}
|
|
633
704
|
|
|
705
|
+
pageprint_reject_embedded_nul(html, "html");
|
|
634
706
|
pageprint_options = pageprint_options_from_value(options);
|
|
707
|
+
context->html = pageprint_frozen_string_copy(html);
|
|
708
|
+
context->base_url = Qnil;
|
|
709
|
+
context->resource_fetcher = pageprint_options.resource_fetcher;
|
|
710
|
+
context->metadata = pageprint_options.metadata;
|
|
711
|
+
|
|
712
|
+
if (!NIL_P(pageprint_options.base_url)) {
|
|
713
|
+
pageprint_reject_embedded_nul(pageprint_options.base_url, "base_url");
|
|
714
|
+
context->base_url = pageprint_frozen_string_copy(pageprint_options.base_url);
|
|
715
|
+
}
|
|
635
716
|
|
|
636
|
-
|
|
637
|
-
html_len = RSTRING_LEN(html);
|
|
717
|
+
html_len = RSTRING_LEN(context->html);
|
|
638
718
|
|
|
639
719
|
/* 1. Create */
|
|
640
|
-
book = plutobook_create(pageprint_options.page_size, pageprint_options.margins, pageprint_options.media);
|
|
720
|
+
context->book = plutobook_create(pageprint_options.page_size, pageprint_options.margins, pageprint_options.media);
|
|
641
721
|
|
|
642
|
-
if (!book) {
|
|
722
|
+
if (!context->book) {
|
|
643
723
|
rb_raise(rb_eRuntimeError, "failed to create plutobook");
|
|
644
724
|
}
|
|
645
725
|
|
|
646
|
-
|
|
647
|
-
|
|
726
|
+
context->resource_fetcher_state.object = context->resource_fetcher;
|
|
727
|
+
context->resource_fetcher_state.error = Qnil;
|
|
728
|
+
context->resource_fetcher_state.state = 0;
|
|
648
729
|
|
|
649
|
-
plutobook_set_custom_resource_fetcher(book, pageprint_fetch_resource,
|
|
730
|
+
plutobook_set_custom_resource_fetcher(context->book, pageprint_fetch_resource, &context->resource_fetcher_state);
|
|
650
731
|
|
|
651
732
|
/* 2. Load HTML */
|
|
652
733
|
plutobook_clear_error_message();
|
|
653
734
|
|
|
654
735
|
base_url_str = "";
|
|
655
|
-
if (!NIL_P(
|
|
656
|
-
base_url_str =
|
|
736
|
+
if (!NIL_P(context->base_url)) {
|
|
737
|
+
base_url_str = RSTRING_PTR(context->base_url);
|
|
657
738
|
}
|
|
658
739
|
|
|
659
|
-
load_args.book = book;
|
|
660
|
-
load_args.html =
|
|
740
|
+
load_args.book = context->book;
|
|
741
|
+
load_args.html = RSTRING_PTR(context->html);
|
|
661
742
|
load_args.length = (int)html_len;
|
|
662
743
|
load_args.user_style = "";
|
|
663
744
|
load_args.user_script = "";
|
|
664
745
|
load_args.base_url = base_url_str;
|
|
665
746
|
load_args.ok = 0;
|
|
666
747
|
|
|
667
|
-
RB_GC_GUARD(html);
|
|
668
|
-
RB_GC_GUARD(options);
|
|
669
|
-
RB_GC_GUARD(pageprint_options.base_url);
|
|
670
|
-
RB_GC_GUARD(pageprint_options.resource_fetcher);
|
|
671
|
-
RB_GC_GUARD(pageprint_options.metadata);
|
|
672
|
-
|
|
673
748
|
rb_thread_call_without_gvl(
|
|
674
749
|
pageprint_load_html_without_gvl,
|
|
675
750
|
&load_args,
|
|
@@ -677,37 +752,41 @@ static plutobook_t *pageprint_create_book_from_html(VALUE html, VALUE options, p
|
|
|
677
752
|
NULL
|
|
678
753
|
);
|
|
679
754
|
|
|
680
|
-
|
|
681
|
-
|
|
755
|
+
RB_GC_GUARD(context->html);
|
|
756
|
+
RB_GC_GUARD(context->base_url);
|
|
682
757
|
|
|
683
|
-
|
|
684
|
-
|
|
758
|
+
if (!load_args.ok) {
|
|
759
|
+
if (context->resource_fetcher_state.state) {
|
|
760
|
+
pageprint_jump_callback_error(
|
|
761
|
+
context->resource_fetcher_state.state,
|
|
762
|
+
context->resource_fetcher_state.error
|
|
763
|
+
);
|
|
685
764
|
}
|
|
686
765
|
|
|
687
766
|
pageprint_raise_plutobook_error(rb_eRuntimeError, "failed to load HTML into plutobook");
|
|
688
767
|
}
|
|
689
768
|
|
|
690
|
-
if (
|
|
691
|
-
|
|
692
|
-
|
|
769
|
+
if (context->resource_fetcher_state.state) {
|
|
770
|
+
pageprint_jump_callback_error(
|
|
771
|
+
context->resource_fetcher_state.state,
|
|
772
|
+
context->resource_fetcher_state.error
|
|
773
|
+
);
|
|
693
774
|
}
|
|
694
775
|
|
|
695
776
|
{
|
|
696
777
|
pageprint_apply_metadata_args_t metadata_args;
|
|
697
778
|
int metadata_state = 0;
|
|
698
779
|
|
|
699
|
-
metadata_args.book = book;
|
|
700
|
-
metadata_args.metadata =
|
|
780
|
+
metadata_args.book = context->book;
|
|
781
|
+
metadata_args.metadata = context->metadata;
|
|
701
782
|
|
|
702
783
|
rb_protect(pageprint_apply_metadata_with_gvl, (VALUE)&metadata_args, &metadata_state);
|
|
784
|
+
RB_GC_GUARD(context->metadata);
|
|
703
785
|
|
|
704
786
|
if (metadata_state) {
|
|
705
|
-
plutobook_destroy(book);
|
|
706
787
|
rb_jump_tag(metadata_state);
|
|
707
788
|
}
|
|
708
789
|
}
|
|
709
|
-
|
|
710
|
-
return book;
|
|
711
790
|
}
|
|
712
791
|
|
|
713
792
|
static VALUE pageprint_append_pdf_string(VALUE value)
|
|
@@ -723,62 +802,54 @@ static plutobook_stream_status_t pageprint_write_pdf_string(void *closure, const
|
|
|
723
802
|
{
|
|
724
803
|
pageprint_pdf_string_output_t *output = closure;
|
|
725
804
|
pageprint_pdf_string_append_t append;
|
|
726
|
-
int state;
|
|
727
805
|
|
|
806
|
+
if (output->state) {
|
|
807
|
+
return PLUTOBOOK_STREAM_STATUS_WRITE_ERROR;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
append.owner = output;
|
|
728
811
|
append.output = output->output;
|
|
729
812
|
append.data = data;
|
|
730
813
|
append.length = length;
|
|
731
814
|
|
|
732
|
-
|
|
815
|
+
rb_thread_call_with_gvl(pageprint_append_pdf_string_with_gvl, &append);
|
|
733
816
|
|
|
734
|
-
if (state) {
|
|
735
|
-
output->state = state;
|
|
817
|
+
if (output->state) {
|
|
736
818
|
return PLUTOBOOK_STREAM_STATUS_WRITE_ERROR;
|
|
737
819
|
}
|
|
738
820
|
|
|
739
821
|
return PLUTOBOOK_STREAM_STATUS_SUCCESS;
|
|
740
822
|
}
|
|
741
823
|
|
|
742
|
-
static VALUE
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
const char *path_str;
|
|
748
|
-
|
|
749
|
-
plutobook_t *book;
|
|
750
|
-
pageprint_resource_fetcher_t resource_fetcher;
|
|
751
|
-
pageprint_write_pdf_args_t write_args;
|
|
752
|
-
|
|
753
|
-
rb_check_arity(argc, 2, 3);
|
|
824
|
+
static VALUE pageprint_destroy_book(VALUE value)
|
|
825
|
+
{
|
|
826
|
+
pageprint_book_context_t *context = (pageprint_book_context_t *)value;
|
|
827
|
+
plutobook_t *book = context->book;
|
|
754
828
|
|
|
755
|
-
|
|
756
|
-
path = argv[1];
|
|
757
|
-
options = argc == 3 ? argv[2] : Qnil;
|
|
829
|
+
context->book = NULL;
|
|
758
830
|
|
|
759
|
-
if (
|
|
760
|
-
|
|
831
|
+
if (book) {
|
|
832
|
+
plutobook_destroy(book);
|
|
761
833
|
}
|
|
762
834
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
}
|
|
835
|
+
return Qnil;
|
|
836
|
+
}
|
|
766
837
|
|
|
767
|
-
|
|
838
|
+
static VALUE pageprint_render_to_file_body(VALUE value)
|
|
839
|
+
{
|
|
840
|
+
pageprint_render_to_file_context_t *context = (pageprint_render_to_file_context_t *)value;
|
|
841
|
+
const char *path_str;
|
|
842
|
+
pageprint_write_pdf_args_t write_args;
|
|
768
843
|
|
|
769
|
-
|
|
844
|
+
pageprint_create_book_from_html(context->html, context->options, &context->book_context);
|
|
845
|
+
path_str = RSTRING_PTR(context->path);
|
|
770
846
|
|
|
771
847
|
plutobook_clear_error_message();
|
|
772
848
|
|
|
773
|
-
write_args.book = book;
|
|
849
|
+
write_args.book = context->book_context.book;
|
|
774
850
|
write_args.path = path_str;
|
|
775
851
|
write_args.ok = 0;
|
|
776
852
|
|
|
777
|
-
RB_GC_GUARD(html);
|
|
778
|
-
RB_GC_GUARD(options);
|
|
779
|
-
RB_GC_GUARD(resource_fetcher.object);
|
|
780
|
-
RB_GC_GUARD(path);
|
|
781
|
-
|
|
782
853
|
rb_thread_call_without_gvl(
|
|
783
854
|
pageprint_write_pdf_without_gvl,
|
|
784
855
|
&write_args,
|
|
@@ -786,10 +857,17 @@ static VALUE pageprint_render_to_file(int argc, VALUE *argv, VALUE self) {
|
|
|
786
857
|
NULL
|
|
787
858
|
);
|
|
788
859
|
|
|
789
|
-
|
|
860
|
+
RB_GC_GUARD(context->path);
|
|
861
|
+
RB_GC_GUARD(context->book_context.resource_fetcher);
|
|
862
|
+
RB_GC_GUARD(context->book_context.resource_fetcher_state.error);
|
|
863
|
+
RB_GC_GUARD(context->html);
|
|
864
|
+
RB_GC_GUARD(context->options);
|
|
790
865
|
|
|
791
|
-
if (
|
|
792
|
-
|
|
866
|
+
if (context->book_context.resource_fetcher_state.state) {
|
|
867
|
+
pageprint_jump_callback_error(
|
|
868
|
+
context->book_context.resource_fetcher_state.state,
|
|
869
|
+
context->book_context.resource_fetcher_state.error
|
|
870
|
+
);
|
|
793
871
|
}
|
|
794
872
|
|
|
795
873
|
if (!write_args.ok) {
|
|
@@ -799,36 +877,42 @@ static VALUE pageprint_render_to_file(int argc, VALUE *argv, VALUE self) {
|
|
|
799
877
|
return Qtrue;
|
|
800
878
|
}
|
|
801
879
|
|
|
802
|
-
static VALUE
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
pageprint_pdf_string_output_t output;
|
|
880
|
+
static VALUE pageprint_render_to_file(int argc, VALUE *argv, VALUE self)
|
|
881
|
+
{
|
|
882
|
+
pageprint_render_to_file_context_t context = { 0 };
|
|
806
883
|
|
|
807
|
-
|
|
808
|
-
pageprint_resource_fetcher_t resource_fetcher;
|
|
809
|
-
pageprint_write_pdf_stream_args_t write_args;
|
|
884
|
+
rb_check_arity(argc, 2, 3);
|
|
810
885
|
|
|
811
|
-
|
|
886
|
+
context.html = argv[0];
|
|
887
|
+
context.options = argc == 3 ? argv[2] : Qnil;
|
|
888
|
+
|
|
889
|
+
if (!RB_TYPE_P(argv[1], T_STRING)) {
|
|
890
|
+
rb_raise(rb_eTypeError, "path must be a String");
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (RSTRING_LEN(argv[1]) == 0) {
|
|
894
|
+
rb_raise(rb_eArgError, "path must not be empty");
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
pageprint_reject_embedded_nul(argv[1], "path");
|
|
898
|
+
context.path = pageprint_frozen_string_copy(argv[1]);
|
|
812
899
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
900
|
+
return rb_ensure(pageprint_render_to_file_body, (VALUE)&context, pageprint_destroy_book, (VALUE)&context.book_context);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
static VALUE pageprint_render_body(VALUE value)
|
|
904
|
+
{
|
|
905
|
+
pageprint_render_context_t *context = (pageprint_render_context_t *)value;
|
|
906
|
+
pageprint_write_pdf_stream_args_t write_args;
|
|
818
907
|
|
|
819
|
-
|
|
908
|
+
pageprint_create_book_from_html(context->html, context->options, &context->book_context);
|
|
820
909
|
|
|
821
910
|
plutobook_clear_error_message();
|
|
822
911
|
|
|
823
|
-
write_args.book = book;
|
|
824
|
-
write_args.output = &output;
|
|
912
|
+
write_args.book = context->book_context.book;
|
|
913
|
+
write_args.output = &context->output;
|
|
825
914
|
write_args.ok = 0;
|
|
826
915
|
|
|
827
|
-
RB_GC_GUARD(html);
|
|
828
|
-
RB_GC_GUARD(options);
|
|
829
|
-
RB_GC_GUARD(resource_fetcher.object);
|
|
830
|
-
RB_GC_GUARD(output.output);
|
|
831
|
-
|
|
832
916
|
rb_thread_call_without_gvl(
|
|
833
917
|
pageprint_write_pdf_stream_without_gvl,
|
|
834
918
|
&write_args,
|
|
@@ -836,21 +920,45 @@ static VALUE pageprint_render(int argc, VALUE *argv, VALUE self) {
|
|
|
836
920
|
NULL
|
|
837
921
|
);
|
|
838
922
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
923
|
+
RB_GC_GUARD(context->book_context.resource_fetcher);
|
|
924
|
+
RB_GC_GUARD(context->book_context.resource_fetcher_state.error);
|
|
925
|
+
RB_GC_GUARD(context->output.output);
|
|
926
|
+
RB_GC_GUARD(context->output.error);
|
|
927
|
+
RB_GC_GUARD(context->html);
|
|
928
|
+
RB_GC_GUARD(context->options);
|
|
929
|
+
|
|
930
|
+
if (context->book_context.resource_fetcher_state.state) {
|
|
931
|
+
pageprint_jump_callback_error(
|
|
932
|
+
context->book_context.resource_fetcher_state.state,
|
|
933
|
+
context->book_context.resource_fetcher_state.error
|
|
934
|
+
);
|
|
843
935
|
}
|
|
844
936
|
|
|
845
|
-
if (output.state) {
|
|
846
|
-
|
|
937
|
+
if (context->output.state) {
|
|
938
|
+
pageprint_jump_callback_error(context->output.state, context->output.error);
|
|
847
939
|
}
|
|
848
940
|
|
|
849
941
|
if (!write_args.ok) {
|
|
850
942
|
pageprint_raise_plutobook_error(rb_eRuntimeError, "failed to write PDF to string");
|
|
851
943
|
}
|
|
852
944
|
|
|
853
|
-
return output.output;
|
|
945
|
+
return context->output.output;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
static VALUE pageprint_render(int argc, VALUE *argv, VALUE self)
|
|
949
|
+
{
|
|
950
|
+
pageprint_render_context_t context = { 0 };
|
|
951
|
+
|
|
952
|
+
rb_check_arity(argc, 1, 2);
|
|
953
|
+
|
|
954
|
+
context.html = argv[0];
|
|
955
|
+
context.options = argc == 2 ? argv[1] : Qnil;
|
|
956
|
+
context.output.output = rb_str_new(NULL, 0);
|
|
957
|
+
context.output.error = Qnil;
|
|
958
|
+
context.output.state = 0;
|
|
959
|
+
rb_enc_associate_index(context.output.output, rb_ascii8bit_encindex());
|
|
960
|
+
|
|
961
|
+
return rb_ensure(pageprint_render_body, (VALUE)&context, pageprint_destroy_book, (VALUE)&context.book_context);
|
|
854
962
|
}
|
|
855
963
|
|
|
856
964
|
void Init_page_print(void) {
|
|
@@ -45,11 +45,14 @@ module PagePrint
|
|
|
45
45
|
return unless public_path
|
|
46
46
|
|
|
47
47
|
relative_path = path.delete_prefix('/')
|
|
48
|
-
|
|
48
|
+
public_path = public_path.realpath
|
|
49
|
+
file_path = public_path.join(relative_path).realpath
|
|
49
50
|
return unless inside_path?(file_path, public_path)
|
|
50
51
|
return unless file_path.file?
|
|
51
52
|
|
|
52
53
|
resource(File.binread(file_path), file_path.extname)
|
|
54
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
|
|
55
|
+
nil
|
|
53
56
|
end
|
|
54
57
|
|
|
55
58
|
def read_resolved_asset(path)
|
data/lib/page_print/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: page_print
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dino Maric
|
|
@@ -20,6 +20,8 @@ extra_rdoc_files: []
|
|
|
20
20
|
files:
|
|
21
21
|
- LICENSE
|
|
22
22
|
- README.md
|
|
23
|
+
- docs/configuration.md
|
|
24
|
+
- docs/production.md
|
|
23
25
|
- ext/page_print/extconf.rb
|
|
24
26
|
- ext/page_print/page_print.c
|
|
25
27
|
- lib/page_print.rb
|