weasy_pdf 0.1.0
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 +7 -0
- data/CHANGELOG.md +41 -0
- data/LICENSE +21 -0
- data/README.md +281 -0
- data/lib/weasy_pdf/command_builder.rb +23 -0
- data/lib/weasy_pdf/configuration.rb +62 -0
- data/lib/weasy_pdf/middleware.rb +69 -0
- data/lib/weasy_pdf/pdf_helper.rb +91 -0
- data/lib/weasy_pdf/railtie.rb +23 -0
- data/lib/weasy_pdf/renderer.rb +217 -0
- data/lib/weasy_pdf/version.rb +5 -0
- data/lib/weasy_pdf/view_helpers/assets.rb +113 -0
- data/lib/weasy_pdf/view_helpers/vite_assets.rb +110 -0
- data/lib/weasy_pdf.rb +78 -0
- data/sig/configuration.rbs +16 -0
- data/sig/renderer.rbs +10 -0
- data/sig/weasy_pdf.rbs +40 -0
- metadata +151 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7ce26a8ece90c530b6b5066c8b472213772024754487d1c499eaa176ea998303
|
|
4
|
+
data.tar.gz: f67004ea643989f581f5247016e4f324629fa6a250f02cc2cd2075ee119d74cf
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: da3107e0b8a6d0b35f2217f4c96bf2ec412a8a6616de8b0d1c93fac805e92d6fbab6f902ba14e09f4745d2c5acae9bfa25c9c057f630464e04e9d5fbc2c2f080
|
|
7
|
+
data.tar.gz: 274b8f9f21837733e59926205339b19a8196595d07a1708d4f510c68564d0801d202e846b15a8a9b6a96f4adda8a29a6caa783f07b83024772d01f6795b0af90
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-04-18
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Initial release — drop-in replacement for Wicked PDF using WeasyPrint as backend
|
|
15
|
+
- `WeasyPDF.new` shortcut for direct instantiation; `WeasyPDF::Renderer` for explicit usage
|
|
16
|
+
- `WeasyPDF::Renderer` generates PDFs from HTML strings, files, and URLs
|
|
17
|
+
- Native vite_ruby support: CDN in production, inlined CSS in development, disk-read in CI
|
|
18
|
+
- Automatic Rails integration via Railtie (no ApplicationController changes needed)
|
|
19
|
+
- Rack middleware `WeasyPDF::Middleware` for transparent `.pdf` URL conversion
|
|
20
|
+
- `[page]`/`[topage]` token mapping to CSS `counter(page)`/`counter(pages)`
|
|
21
|
+
- Configuration via `WeasyPDF.configure` with `attr_accessor` + hash-style `[]`/`[]=`
|
|
22
|
+
- All wkhtmltopdf-only options accepted silently for zero-friction migration
|
|
23
|
+
- `weasy_pdf_stylesheet_link_tag`, `weasy_pdf_image_tag`, `weasy_pdf_asset_path`,
|
|
24
|
+
`weasy_pdf_asset_base64`, `weasy_pdf_url_base64` view helpers
|
|
25
|
+
- `header:`/`footer:` with `:left`/`:center`/`:right` text via CSS `@page` margin boxes
|
|
26
|
+
- `header: { html: ... }` raises `WeasyPDF::Error` immediately with a migration hint
|
|
27
|
+
|
|
28
|
+
### Development tooling
|
|
29
|
+
|
|
30
|
+
- StandardRB for linting (zero-config)
|
|
31
|
+
- SimpleCov with branch coverage (`bundle exec rake test` reports it)
|
|
32
|
+
- RBS signatures for `WeasyPDF`, `Renderer`, `Configuration` (sig/)
|
|
33
|
+
- CI matrix: Ruby 3.2/3.3/3.4 × Rails 7.1/7.2/8.0/8.1
|
|
34
|
+
- `bin/console` for interactive gem development
|
|
35
|
+
|
|
36
|
+
### Not included (intentional divergence from Wicked PDF)
|
|
37
|
+
|
|
38
|
+
- JavaScript helpers (`wicked_pdf_javascript_*`) — WeasyPrint does not execute JS
|
|
39
|
+
- Webpacker/Shakapacker pack helpers — no `*_pack_tag` equivalents
|
|
40
|
+
- HTML template headers/footers — not possible via WeasyPrint CLI; raises with a clear error
|
|
41
|
+
- `ostruct` runtime dependency — uses `attr_accessor` instead
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Diego Enjamio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# weasy_pdf
|
|
2
|
+
|
|
3
|
+
Generate PDFs from Rails views using [WeasyPrint](https://weasyprint.org/) — modern CSS, no JavaScript, no Chromium.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
gem 'weasy_pdf'
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
> Migrating from `wicked_pdf`? Jump to [Migration](#migration-from-wicked-pdf) — typically a one-line Gemfile change plus a `sed` over your views.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Ruby >= 3.2
|
|
14
|
+
- Rails >= 7.1
|
|
15
|
+
- WeasyPrint installed on the system:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
apt install weasyprint # Debian/Ubuntu
|
|
19
|
+
brew install weasyprint # macOS
|
|
20
|
+
pip install weasyprint # latest version
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
class InvoicesController < ApplicationController
|
|
27
|
+
def show
|
|
28
|
+
@invoice = Invoice.find(params[:id])
|
|
29
|
+
render pdf: "invoice_#{@invoice.number}", template: 'invoices/show'
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
No initializer, no `ApplicationController` changes — the Railtie wires everything automatically.
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
Optional — sensible defaults are used if you skip this:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# config/initializers/weasy_pdf.rb
|
|
42
|
+
WeasyPDF.configure do |config|
|
|
43
|
+
config.default_options = {
|
|
44
|
+
page_size: 'A4',
|
|
45
|
+
margin_top: '15mm',
|
|
46
|
+
media_type: 'print',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# base_url prepends to relative URLs in the rendered HTML when no asset
|
|
50
|
+
# can be resolved from disk. Useful in development.
|
|
51
|
+
config.base_url = Rails.env.development? ? 'http://localhost:3000' : nil
|
|
52
|
+
|
|
53
|
+
config.timeout = 60
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
### Controllers
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
render pdf: 'invoice',
|
|
63
|
+
template: 'invoices/show',
|
|
64
|
+
layout: 'pdf',
|
|
65
|
+
page_size: 'A4',
|
|
66
|
+
margin: { top: 15, bottom: 15 },
|
|
67
|
+
header: { right: '[page] of [topage]', font_size: 9 },
|
|
68
|
+
show_as_html: params.key?('debug')
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Jobs / Mailers
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
pdf = render_to_string pdf: 'invoice', template: 'invoices/show', layout: 'pdf'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Direct instantiation
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
WeasyPDF.new.pdf_from_string('<h1>Hello</h1>')
|
|
81
|
+
WeasyPDF.new.pdf_from_html_file('/path/to/file.html')
|
|
82
|
+
WeasyPDF.new.pdf_from_url('https://example.com')
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### PDF layout
|
|
86
|
+
|
|
87
|
+
```erb
|
|
88
|
+
<!DOCTYPE html>
|
|
89
|
+
<html>
|
|
90
|
+
<head>
|
|
91
|
+
<%= weasy_pdf_stylesheet_link_tag 'pdf' %>
|
|
92
|
+
</head>
|
|
93
|
+
<body>
|
|
94
|
+
<%= weasy_pdf_image_tag 'logo.png', width: 150 %>
|
|
95
|
+
<%= yield %>
|
|
96
|
+
</body>
|
|
97
|
+
</html>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Asset helpers
|
|
101
|
+
|
|
102
|
+
```erb
|
|
103
|
+
<%= weasy_pdf_stylesheet_link_tag 'pdf' %>
|
|
104
|
+
<%= weasy_pdf_image_tag 'logo.png', width: 150 %>
|
|
105
|
+
<%= weasy_pdf_asset_path 'logo.png' %>
|
|
106
|
+
<%= weasy_pdf_asset_base64 'logo.png' %>
|
|
107
|
+
<%= weasy_pdf_url_base64 'https://example.com/img.png' %>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Asset support
|
|
111
|
+
|
|
112
|
+
**Vite-first** (auto-detected when `vite_ruby` is present) with fallback to precompiled assets from `public/`.
|
|
113
|
+
|
|
114
|
+
Each asset is resolved in this order:
|
|
115
|
+
|
|
116
|
+
1. **Vite manifest** (if `vite_ruby` loaded):
|
|
117
|
+
|
|
118
|
+
| Environment | Behavior |
|
|
119
|
+
|---|---|
|
|
120
|
+
| Production CDN | `<link href="https://cdn.../pdf-abc.css">` — WeasyPrint downloads it |
|
|
121
|
+
| Dev + Vite server running | Inlines CSS via HTTP from localhost:5173 |
|
|
122
|
+
| No Vite server (CI/test) | Reads from `public/vite/assets/` |
|
|
123
|
+
|
|
124
|
+
2. **Disk fallback** — searches `public/vite/assets/`, `public/assets/`, `public/`.
|
|
125
|
+
3. **`base_url` fallback** — prepends `base_url` to the asset path.
|
|
126
|
+
4. **Returns input unchanged** — logs a warning so misconfigurations are visible.
|
|
127
|
+
|
|
128
|
+
## Headers and footers
|
|
129
|
+
|
|
130
|
+
CSS `@page` margin boxes — text and counters only:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
render pdf: 'report',
|
|
134
|
+
header: { left: 'Acme', center: 'Confidential', right: '[page] of [topage]' },
|
|
135
|
+
footer: { center: '[page]', font_size: 8 }
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
| Token | CSS output |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `[page]` | `counter(page)` |
|
|
141
|
+
| `[topage]` | `counter(pages)` |
|
|
142
|
+
| `[section]`, `[title]`, `[date]`, `[time]`, `[webpage]` | Removed — no CSS equivalent |
|
|
143
|
+
|
|
144
|
+
For complex headers (logos, styled HTML), use [WeasyPrint running elements](https://weasyprint.readthedocs.io/en/stable/tutorial.html#headers-and-footers):
|
|
145
|
+
|
|
146
|
+
```css
|
|
147
|
+
.pdf-header { position: running(header); }
|
|
148
|
+
@page { @top-center { content: element(header); } }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```erb
|
|
152
|
+
<div class="pdf-header">
|
|
153
|
+
<img src="<%= weasy_pdf_asset_path('logo.png') %>" height="30">
|
|
154
|
+
<span>Invoice #<%= @invoice.number %></span>
|
|
155
|
+
</div>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Page sizes and orientation
|
|
159
|
+
|
|
160
|
+
`page_size:` accepts any [CSS Paged Media](https://www.w3.org/TR/css-page-3/#page-size-prop) keyword:
|
|
161
|
+
|
|
162
|
+
| Family | Sizes |
|
|
163
|
+
|---|---|
|
|
164
|
+
| ISO A | `A0` … `A10` (default: **A4**) |
|
|
165
|
+
| ISO B | `B0` … `B10` |
|
|
166
|
+
| ISO C | `C0` … `C10` |
|
|
167
|
+
| US | `Letter`, `Legal`, `Ledger` |
|
|
168
|
+
| JIS | `JIS-B0` … `JIS-B5` |
|
|
169
|
+
|
|
170
|
+
Custom dimensions (numbers default to mm; strings pass through):
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
render pdf: 'receipt', page_width: 80, page_height: 200 # 80mm × 200mm
|
|
174
|
+
render pdf: 'badge', page_width: '4in', page_height: '6in' # any CSS unit
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Orientation:
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
render pdf: 'report', page_size: 'A4', orientation: 'Landscape'
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`page_width` / `page_height` take precedence over `page_size` / `orientation`.
|
|
184
|
+
|
|
185
|
+
## Rack Middleware
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
# config/application.rb
|
|
189
|
+
config.middleware.use WeasyPDF::Middleware
|
|
190
|
+
config.middleware.use WeasyPDF::Middleware, { page_size: 'A4' }, only: [/^\/invoices/]
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Requests to `/invoices/1.pdf` are transparently rendered as PDF.
|
|
194
|
+
|
|
195
|
+
## Limitations
|
|
196
|
+
|
|
197
|
+
| | |
|
|
198
|
+
|---|---|
|
|
199
|
+
| JavaScript execution | Not supported — WeasyPrint is JS-free. Render server-side first. |
|
|
200
|
+
| HTML template headers/footers | Not supported — raises `WeasyPDF::Error` pointing to running elements |
|
|
201
|
+
| Webpacker / Shakapacker `*_pack_tag` helpers | Not supported — no equivalents |
|
|
202
|
+
| `pdf_from_url` | Basic HTTP only — no auth, no JS rendering |
|
|
203
|
+
| `outline`, `dpi`, `proxy`, `cookie`, `extra` | Accepted silently (migration compat), not passed to WeasyPrint |
|
|
204
|
+
|
|
205
|
+
## Migration from Wicked PDF
|
|
206
|
+
|
|
207
|
+
> Not migrating from `wicked_pdf`? You can stop reading here.
|
|
208
|
+
|
|
209
|
+
### Why migrate
|
|
210
|
+
|
|
211
|
+
| | wicked_pdf | weasy_pdf |
|
|
212
|
+
|---|---|---|
|
|
213
|
+
| Backend | wkhtmltopdf (archived 2023) | WeasyPrint (active) |
|
|
214
|
+
| CSS engine | WebKit 2013 — no flexbox/grid | Modern CSS |
|
|
215
|
+
| RAM per worker | ~75–200MB + known leaks | ~35–80MB, clean exit |
|
|
216
|
+
| Vite | Manual workarounds | Native (auto-detected) |
|
|
217
|
+
| Runtime deps | `activesupport`, `ostruct` | `activesupport` |
|
|
218
|
+
|
|
219
|
+
### Steps
|
|
220
|
+
|
|
221
|
+
**1. Gemfile**
|
|
222
|
+
|
|
223
|
+
```diff
|
|
224
|
+
- gem 'wicked_pdf'
|
|
225
|
+
- gem 'wkhtmltopdf-binary'
|
|
226
|
+
+ gem 'weasy_pdf'
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**2. Initializer**
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
mv config/initializers/wicked_pdf.rb config/initializers/weasy_pdf.rb
|
|
233
|
+
sed -i 's/WickedPdf/WeasyPDF/g' config/initializers/weasy_pdf.rb
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**3. Bulk-rename helpers in views**
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
find app/views -name '*.erb' | xargs sed -i 's/wicked_pdf_/weasy_pdf_/g'
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**4. Delete dead helper calls**
|
|
243
|
+
|
|
244
|
+
After step 3 your views may contain `weasy_pdf_javascript_*` or `weasy_pdf_*_pack_tag` calls
|
|
245
|
+
that **don't exist** in weasy_pdf and would raise `NoMethodError` at render time:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
grep -rE 'weasy_pdf_(javascript_|.*_pack_)' app/views
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
WeasyPrint doesn't execute JavaScript and Webpacker isn't supported — removing them is correct.
|
|
252
|
+
|
|
253
|
+
**5. HTML template headers**
|
|
254
|
+
|
|
255
|
+
`header: { html: { template: ... } }` now raises `WeasyPDF::Error` with a migration hint.
|
|
256
|
+
Replace with [running elements](#headers-and-footers).
|
|
257
|
+
|
|
258
|
+
**6. Controllers — no changes**
|
|
259
|
+
|
|
260
|
+
`render pdf:`, `render_to_string pdf:`, all PDF options work identically.
|
|
261
|
+
|
|
262
|
+
**7. Dockerfile**
|
|
263
|
+
|
|
264
|
+
```dockerfile
|
|
265
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
266
|
+
weasyprint fonts-dejavu-core fonts-liberation \
|
|
267
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Performance differences
|
|
271
|
+
|
|
272
|
+
Neither backend is universally faster — pick by workload:
|
|
273
|
+
|
|
274
|
+
- **Long-running Sidekiq workers** → weasy_pdf (no RAM leaks)
|
|
275
|
+
- **High-concurrency web requests** → weasy_pdf (lower per-process RAM)
|
|
276
|
+
- **Documents using flexbox / grid / modern CSS** → weasy_pdf (wkhtmltopdf can't render them)
|
|
277
|
+
- **One-shot CLI on simple HTML** → roughly comparable, document complexity dominates
|
|
278
|
+
|
|
279
|
+
## License
|
|
280
|
+
|
|
281
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WeasyPDF
|
|
4
|
+
# Pure value transformation: binary + resolved options → shell command Array.
|
|
5
|
+
# No global state — callers resolve configuration (base_url, etc.) before
|
|
6
|
+
# passing options in, so this class is testable without WeasyPDF.configuration.
|
|
7
|
+
class CommandBuilder
|
|
8
|
+
def initialize(binary, options = {})
|
|
9
|
+
@binary = binary
|
|
10
|
+
@options = options
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def build(input_path, output_path)
|
|
14
|
+
cmd = [@binary]
|
|
15
|
+
cmd += ["--encoding", @options[:encoding]] if @options[:encoding]
|
|
16
|
+
cmd += ["--zoom", @options[:zoom].to_s] if @options[:zoom] && @options[:zoom] != 1
|
|
17
|
+
cmd += ["--media-type", @options[:media_type]] if @options[:media_type]
|
|
18
|
+
cmd += ["--base-url", @options[:base_url].to_s] if @options[:base_url]
|
|
19
|
+
Array(@options[:stylesheets]).each { |s| cmd += ["--stylesheet", s] }
|
|
20
|
+
cmd + [input_path, output_path]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WeasyPDF
|
|
4
|
+
class Configuration
|
|
5
|
+
DEFAULTS = {
|
|
6
|
+
page_size: "A4",
|
|
7
|
+
orientation: "Portrait",
|
|
8
|
+
margin_top: "10mm",
|
|
9
|
+
margin_bottom: "10mm",
|
|
10
|
+
margin_left: "10mm",
|
|
11
|
+
margin_right: "10mm",
|
|
12
|
+
encoding: "utf-8",
|
|
13
|
+
zoom: 1,
|
|
14
|
+
media_type: "print" # WeasyPrint defaults to 'screen'; 'print' activates @media print rules
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
BINARY_CANDIDATES = %w[
|
|
18
|
+
/usr/local/bin/weasyprint
|
|
19
|
+
/usr/bin/weasyprint
|
|
20
|
+
/opt/weasyprint/bin/weasyprint
|
|
21
|
+
/opt/homebrew/bin/weasyprint
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
attr_accessor :exe_path, :default_options, :base_url, :timeout, :temp_path
|
|
25
|
+
|
|
26
|
+
def initialize
|
|
27
|
+
@exe_path = find_binary
|
|
28
|
+
@default_options = DEFAULTS.dup
|
|
29
|
+
@base_url = nil
|
|
30
|
+
@timeout = 60
|
|
31
|
+
@temp_path = default_temp_path
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def [](key)
|
|
35
|
+
public_send(key)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def []=(key, value)
|
|
39
|
+
public_send(:"#{key}=", value)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def find_binary
|
|
45
|
+
BINARY_CANDIDATES.each { |path| return path if File.executable?(path) }
|
|
46
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |dir|
|
|
47
|
+
full = File.join(dir, "weasyprint")
|
|
48
|
+
return full if File.executable?(full)
|
|
49
|
+
end
|
|
50
|
+
"weasyprint"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def default_temp_path
|
|
54
|
+
if defined?(Rails)
|
|
55
|
+
Rails.root.join("tmp", "weasy_pdf")
|
|
56
|
+
else
|
|
57
|
+
require "pathname"
|
|
58
|
+
Pathname.new("/tmp/weasy_pdf")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WeasyPDF
|
|
4
|
+
# Rack middleware that converts HTML responses to PDF when the URL ends in .pdf.
|
|
5
|
+
#
|
|
6
|
+
# Usage in config/application.rb:
|
|
7
|
+
# config.middleware.use WeasyPDF::Middleware
|
|
8
|
+
# config.middleware.use WeasyPDF::Middleware, { page_size: 'A4' }, { only: [/^\/invoices/] }
|
|
9
|
+
class Middleware
|
|
10
|
+
def initialize(app, options = {}, conditions = {})
|
|
11
|
+
@app = app
|
|
12
|
+
@options = options
|
|
13
|
+
@conditions = conditions
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(env)
|
|
17
|
+
request = Rack::Request.new(env)
|
|
18
|
+
render_as_pdf?(request) ? serve_pdf(env, request) : @app.call(env)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def render_as_pdf?(request)
|
|
24
|
+
request.path.end_with?(".pdf") && path_allowed?(request.path)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def path_allowed?(path)
|
|
28
|
+
if @conditions[:only]
|
|
29
|
+
Array(@conditions[:only]).any? { |pattern| path.match?(pattern) }
|
|
30
|
+
elsif @conditions[:except]
|
|
31
|
+
Array(@conditions[:except]).none? { |pattern| path.match?(pattern) }
|
|
32
|
+
else
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def serve_pdf(env, request)
|
|
38
|
+
html_env = env.merge(
|
|
39
|
+
"PATH_INFO" => request.path.sub(/\.pdf\z/, ""),
|
|
40
|
+
"HTTP_ACCEPT" => "text/html",
|
|
41
|
+
"QUERY_STRING" => request.query_string
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
status, headers, body = @app.call(html_env)
|
|
45
|
+
return [status, headers, body] unless status == 200
|
|
46
|
+
|
|
47
|
+
html = body.respond_to?(:body) ? body.body : body.join
|
|
48
|
+
|
|
49
|
+
begin
|
|
50
|
+
pdf = WeasyPDF::Renderer.new(@options).pdf_from_string(html)
|
|
51
|
+
[
|
|
52
|
+
200,
|
|
53
|
+
{
|
|
54
|
+
"Content-Type" => "application/pdf",
|
|
55
|
+
"Content-Disposition" => "inline; filename=\"#{pdf_filename(request.path)}\"",
|
|
56
|
+
"Content-Length" => pdf.bytesize.to_s
|
|
57
|
+
},
|
|
58
|
+
[pdf]
|
|
59
|
+
]
|
|
60
|
+
rescue WeasyPDF::Error => e
|
|
61
|
+
[500, {"Content-Type" => "text/plain"}, ["WeasyPDF error: #{e.message}"]]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def pdf_filename(path)
|
|
66
|
+
"#{File.basename(path, ".pdf")}.pdf"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WeasyPDF
|
|
4
|
+
module PdfHelper
|
|
5
|
+
def self.prepended(base)
|
|
6
|
+
# Guard prevents after_action from being registered twice when PdfHelper
|
|
7
|
+
# is prepended to both ActionController::Base and a concrete subclass.
|
|
8
|
+
return unless base == ActionController::Base
|
|
9
|
+
|
|
10
|
+
base.class_eval do
|
|
11
|
+
after_action :clean_temp_files
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def render(*args)
|
|
16
|
+
if args.first.is_a?(Hash) && args.first.key?(:pdf)
|
|
17
|
+
render_with_weasy_pdf(args.first)
|
|
18
|
+
else
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def render_to_string(*args)
|
|
24
|
+
if args.first.is_a?(Hash) && args.first.key?(:pdf)
|
|
25
|
+
render_to_string_with_weasy_pdf(args.first)
|
|
26
|
+
else
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
RAILS_RENDER_KEYS = %i[
|
|
32
|
+
template layout file inline locals formats handlers assigns
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
def render_with_weasy_pdf(options)
|
|
36
|
+
options = options.dup
|
|
37
|
+
|
|
38
|
+
filename = options.delete(:pdf) || "document"
|
|
39
|
+
show_as_html = options.delete(:show_as_html)
|
|
40
|
+
disposition = options.delete(:disposition) || "inline"
|
|
41
|
+
|
|
42
|
+
return render(options.slice(*RAILS_RENDER_KEYS).merge(content_type: "text/html")) if show_as_html
|
|
43
|
+
|
|
44
|
+
pdf = make_pdf(options)
|
|
45
|
+
|
|
46
|
+
send_data pdf,
|
|
47
|
+
filename: "#{filename}.pdf",
|
|
48
|
+
type: "application/pdf",
|
|
49
|
+
disposition: disposition
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def render_to_string_with_weasy_pdf(options)
|
|
53
|
+
make_pdf(options.except(:pdf, :disposition, :show_as_html))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def make_pdf(options)
|
|
59
|
+
validate_header_footer!(options)
|
|
60
|
+
|
|
61
|
+
render_opts = options.slice(*RAILS_RENDER_KEYS).merge(formats: [:html])
|
|
62
|
+
html = render_to_string(**render_opts)
|
|
63
|
+
pdf_options = options.except(*RAILS_RENDER_KEYS)
|
|
64
|
+
|
|
65
|
+
WeasyPDF::Renderer.new(pdf_options).pdf_from_string(html)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Wicked PDF supported rendering an ERB template or a URL as the header/footer,
|
|
69
|
+
# compositing it as a separate PDF page. WeasyPrint only supports CSS @page margin
|
|
70
|
+
# boxes, which accept text and CSS counters — not arbitrary HTML.
|
|
71
|
+
# Raise early so the caller knows to use :left/:center/:right strings instead.
|
|
72
|
+
def validate_header_footer!(options)
|
|
73
|
+
%i[header footer].each do |hf|
|
|
74
|
+
next unless options[hf].is_a?(Hash) && options[hf][:html].is_a?(Hash)
|
|
75
|
+
|
|
76
|
+
raise WeasyPDF::Error,
|
|
77
|
+
"#{hf}: :html templates and URLs are not supported. " \
|
|
78
|
+
"WeasyPrint uses CSS @page margin boxes, not separate HTML pages. " \
|
|
79
|
+
"Use :left, :center, or :right with plain text and [page]/[topage] counters. " \
|
|
80
|
+
"For complex layouts, see WeasyPrint running elements: " \
|
|
81
|
+
"https://weasyprint.readthedocs.io/en/stable/tutorial.html#headers-and-footers"
|
|
82
|
+
end
|
|
83
|
+
options
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def clean_temp_files
|
|
87
|
+
# No-op — kept for API compatibility with Wicked PDF.
|
|
88
|
+
# Renderer cleans its own tempfiles via ensure blocks.
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WeasyPDF
|
|
4
|
+
class Railtie < Rails::Railtie
|
|
5
|
+
initializer "weasy_pdf.pdf_helper" do
|
|
6
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
7
|
+
prepend WeasyPDF::PdfHelper
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
initializer "weasy_pdf.view_helpers" do
|
|
12
|
+
ActiveSupport.on_load(:action_view) do
|
|
13
|
+
include WeasyPDF::ViewHelpers::Assets
|
|
14
|
+
include WeasyPDF::ViewHelpers::ViteAssets if defined?(ViteRuby)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
initializer "weasy_pdf.mime_type" do
|
|
19
|
+
Mime::Type.register("application/pdf", :pdf) \
|
|
20
|
+
unless Mime::Type.lookup_by_extension(:pdf)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|