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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 989676fa7f23af91a250034ce2b63479b27f2d2bda3bf3c56983c36497fb4723
4
- data.tar.gz: 5e18cd17dae7825eee333a11279503d136f74fc41b56eab2d5b4601fa15a5ba7
3
+ metadata.gz: ca32ba67da6a5c3cfe5c66b3eac1242ffc1068ccce7838820d4550fffbe9d73d
4
+ data.tar.gz: 90ad2b4d3612d123f562ebd43098ed110c2ccb608d058409c496ce76e455e25d
5
5
  SHA512:
6
- metadata.gz: b4574c7f4b22644193c516ffc81f66bad89ac725ebc9ffe9903924ebb99c694647d6ab203c1961f3b0f1e4182971b79cc871877bda7e79ad6266453e8597d4cd
7
- data.tar.gz: dd4aa832adacdcdcb77baa1022d04fdfe00eef0bc5dd7c3d73bda1b4355efe2770a7bd67cd7411355ca6acb75039542c7e7ebdab01e47aaa7f05b9cbc4122e9c
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 not production-ready yet. The gem is still in active testing, especially around native packaging and platform compatibility.
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
- `page_print` renders HTML strings to PDF files from Ruby using the `plutobook` library through a native C extension.
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
- ## Installation
11
+ ## Why PagePrint?
11
12
 
12
- Add PagePrint to your Gemfile:
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
- ```ruby
15
- gem "page_print"
16
- ```
15
+ That gives PagePrint a few useful properties:
17
16
 
18
- Or install from RubyGems directly:
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
- ```sh
21
- gem install page_print
22
- ```
23
+ ### How does it compare with PDFKit and Wicked PDF?
23
24
 
24
- Native gems are published for `x86_64-linux` and `arm64-darwin`. Other platforms build from source and require PlutoBook development headers and library files.
25
+ PDFKit and Wicked PDF are Ruby integrations for `wkhtmltopdf`; their rendering behavior and operational requirements come from that executable.
25
26
 
26
- ## Rails Usage
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 optimized for Rails applications using Propshaft. In Rails, PagePrint installs a default Propshaft-backed resource fetcher and uses the current request URL as the default `base_url`.
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
- Render a PDF from a controller:
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
- class PrintsController < ApplicationController
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
- Use normal Rails asset helpers in the PDF template or layout:
49
+ Then run:
50
50
 
51
- ```erb
52
- <%= stylesheet_link_tag "pdf" %>
53
- <%= image_tag "logo.png" %>
51
+ ```sh
52
+ bundle install
54
53
  ```
55
54
 
56
- During controller actions, PagePrint defaults `base_url` to `request.base_url`. The default Rails resource fetcher resolves `/assets/...` through Propshaft or `public/assets`, avoiding HTTP requests back to the Rails app.
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
- ```ruby
61
- pdf = PagePrint.render(html, base_url: "https://example.com")
62
- ```
57
+ - `x86_64-linux`
58
+ - `arm64-darwin` (Apple Silicon)
63
59
 
64
- Override the fetcher only when needed:
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
- ```ruby
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
- ## Ruby Usage
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>Hello</h1>
83
- <p>This PDF was generated by PagePrint.</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
- output_path = File.join(Dir.tmpdir, "page_print-output.pdf")
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
- To get the generated PDF as a binary string instead of writing directly to a file:
98
+ For large documents, write directly to a file instead of retaining the PDF in a Ruby string:
94
99
 
95
100
  ```ruby
96
- pdf = PagePrint.render(html, base_url: "https://example.com")
101
+ PagePrint.render_to_file(html, "invoice.pdf", page_size: :a4, margins: :normal)
97
102
  ```
98
103
 
99
- Use custom page dimensions and margins when a preset is not enough:
104
+ `render_to_file` returns `true`; `render` returns an `ASCII-8BIT` Ruby string containing the PDF.
100
105
 
101
- ```ruby
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
- You can configure default options once, for example to provide a resource fetcher used by all renders:
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
- PagePrint.configure do |config|
113
- config.resource_fetcher = lambda do |url|
114
- next unless url == "asset:pdf.css"
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
- { content: "body { font-family: sans-serif; }", mime_type: "text/css" }
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
- ## Options
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
- If `pkg-config` cannot find `plutobook`, install the gem with explicit include and library paths:
175
-
176
- ```sh
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
- Open an interactive Ruby session against the local checkout:
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
- ```sh
205
- bundle exec irb -Ilib
206
- ```
130
+ ## Assets and network access
207
131
 
208
- Then load the gem from the repo and try it:
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
- require "page_print"
212
- require "tmpdir"
213
-
214
- output_path = File.join(Dir.tmpdir, "page_print-output.pdf")
215
-
216
- PagePrint.render_to_file("<html><body><h1>Hello</h1></body></html>", output_path, page_size: :letter, margins: :narrow, media: :screen)
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
- ```sh
222
- bundle exec ruby -Ilib -e 'require "tmpdir"; require "page_print"; output_path = File.join(Dir.tmpdir, "page_print-output.pdf"); p PagePrint.render_to_file("<html><body><h1>Hello</h1></body></html>", output_path, page_size: :letter, margins: :narrow, media: :screen)'
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
- ## Running Tests
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
- ```sh
236
- bundle exec rake test
237
- ```
150
+ ## Configuration
238
151
 
239
- Or run the default rake task, which compiles the extension and then runs tests:
152
+ The common rendering options are:
240
153
 
241
- ```sh
242
- bundle exec rake
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
- ## Local Build
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
- Build and install locally:
168
+ ## Performance
248
169
 
249
- ```sh
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
- ## Releasing
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
- Publish a new version with:
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
- bin/bump 0.1.6
180
+ RUNS=30 WARMUPS=3 bundle exec ruby benchmark/pdf_renderers.rb
260
181
  ```
261
182
 
262
- That updates `lib/page_print/version.rb` and `Gemfile.lock`, commits `Release 0.1.6`, creates annotated tag `v0.1.6`, and pushes `main` plus the tag. Requires a clean worktree on `main`.
183
+ See [benchmark/README.md](benchmark/README.md) for methodology and CSV output.
263
184
 
264
- The Package workflow then builds the source and platform gems, pushes them to RubyGems (trusted publishing / OIDC, GitHub environment `release`), and creates a GitHub Release with the gem artifacts and commit notes since the previous `v*` tag.
185
+ ## Documentation
265
186
 
266
- ## Building Native Gems
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
- Build an `x86_64-linux` platform gem with a vendored PlutoBook library:
269
-
270
- ```sh
271
- bundle exec rake package:linux
272
- ```
192
+ ## Development
273
193
 
274
- Build an Apple Silicon macOS platform gem with a vendored PlutoBook library:
194
+ Install dependencies, compile the extension, and run the test suite:
275
195
 
276
196
  ```sh
277
- bundle exec rake package:darwin_arm64
197
+ bundle install
198
+ bundle exec rake
278
199
  ```
279
200
 
280
- These tasks check out PlutoBook `v0.19.0`, build it into `lib/page_print/vendor/<platform>`, and write a platform gem to `pkg/`. Platform gems compile the Ruby extension during gem install to avoid tying the gem to the build machine's Ruby version.
201
+ Use `bin/console` for an IRB session with the extension compiled and PagePrint loaded.
281
202
 
282
- Native gems bundle PlutoBook and its non-system shared library dependencies. Optional PlutoBook features for curl, TurboJPEG, and WebP are disabled to keep the bundled dependency set smaller.
203
+ ## License
283
204
 
284
- The packaging tasks expect PlutoBook's build dependencies to be installed on the build machine, including Meson, Ninja, pkg-config, Cairo, FreeType, HarfBuzz, Fontconfig, Expat, and ICU.
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)
@@ -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)
@@ -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
- return (void *)(intptr_t)state;
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
- return (void *)(intptr_t)state;
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
- int state;
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
- state = (int)(intptr_t)rb_thread_call_with_gvl(pageprint_call_resource_fetcher_with_gvl, &args);
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 plutobook_t *pageprint_create_book_from_html(VALUE html, VALUE options, pageprint_resource_fetcher_t *resource_fetcher)
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
- html_str = RSTRING_PTR(html);
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
- resource_fetcher->object = pageprint_options.resource_fetcher;
647
- resource_fetcher->state = 0;
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, resource_fetcher);
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(pageprint_options.base_url)) {
656
- base_url_str = StringValueCStr(pageprint_options.base_url);
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 = html_str;
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
- if (!load_args.ok) {
681
- plutobook_destroy(book);
755
+ RB_GC_GUARD(context->html);
756
+ RB_GC_GUARD(context->base_url);
682
757
 
683
- if (resource_fetcher->state) {
684
- rb_jump_tag(resource_fetcher->state);
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 (resource_fetcher->state) {
691
- plutobook_destroy(book);
692
- rb_jump_tag(resource_fetcher->state);
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 = pageprint_options.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
- state = (int)(intptr_t)rb_thread_call_with_gvl(pageprint_append_pdf_string_with_gvl, &append);
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 pageprint_render_to_file(int argc, VALUE *argv, VALUE self) {
743
- VALUE html;
744
- VALUE path;
745
- VALUE options;
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
- html = argv[0];
756
- path = argv[1];
757
- options = argc == 3 ? argv[2] : Qnil;
829
+ context->book = NULL;
758
830
 
759
- if (!RB_TYPE_P(path, T_STRING)) {
760
- rb_raise(rb_eTypeError, "path must be a String");
831
+ if (book) {
832
+ plutobook_destroy(book);
761
833
  }
762
834
 
763
- if (RSTRING_LEN(path) == 0) {
764
- rb_raise(rb_eArgError, "path must not be empty");
765
- }
835
+ return Qnil;
836
+ }
766
837
 
767
- path_str = StringValueCStr(path);
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
- book = pageprint_create_book_from_html(html, options, &resource_fetcher);
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
- plutobook_destroy(book);
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 (resource_fetcher.state) {
792
- rb_jump_tag(resource_fetcher.state);
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 pageprint_render(int argc, VALUE *argv, VALUE self) {
803
- VALUE html;
804
- VALUE options;
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
- plutobook_t *book;
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
- rb_check_arity(argc, 1, 2);
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
- html = argv[0];
814
- options = argc == 2 ? argv[1] : Qnil;
815
- output.output = rb_str_new(NULL, 0);
816
- output.state = 0;
817
- rb_enc_associate_index(output.output, rb_ascii8bit_encindex());
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
- book = pageprint_create_book_from_html(html, options, &resource_fetcher);
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
- plutobook_destroy(book);
840
-
841
- if (resource_fetcher.state) {
842
- rb_jump_tag(resource_fetcher.state);
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
- rb_jump_tag(output.state);
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
- file_path = public_path.join(relative_path).cleanpath
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)
@@ -1,3 +1,3 @@
1
1
  module PagePrint
2
- VERSION = '0.1.9'
2
+ VERSION = '0.1.10'
3
3
  end
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.9
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