palapala_pdf 0.1.10 → 0.1.12
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +69 -138
- data/doc/installing_node.md +16 -0
- data/doc/paged_css.md +167 -0
- data/examples/all.rb +9 -0
- data/examples/chrome_base_header_footer_template.html +169 -0
- data/examples/headers_and_footers.pdf +0 -0
- data/examples/headers_and_footers.rb +19 -34
- data/examples/js_based_rendering.pdf +0 -0
- data/examples/js_based_rendering.rb +4 -6
- data/examples/paged_css.pdf +0 -0
- data/examples/paged_css.rb +187 -0
- data/examples/performance_benchmark.rb +9 -21
- data/lib/palapala/chrome_process.rb +1 -1
- data/lib/palapala/pdf.rb +23 -3
- data/lib/palapala/renderer.rb +15 -0
- data/lib/palapala/version.rb +1 -1
- data/lib/palapala.rb +2 -6
- data/paged_css.pdf +0 -0
- metadata +11 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9371e3f5558b6cac6126a9f8004e48dbbf7059e3bfbd230885d66502e1e0f931
|
4
|
+
data.tar.gz: 769a77ca03fb96e858991d98c0c49f2aa3de58ba0dc2e0f33207c264969ce9c4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3458400fa50aeb4d2e672f156e8bf81f0a8d6a2384cb4b3d8624ca0cc18adc2c8a38eb905850fcb8d73ac3a72cd61525f77f02c5c1dd26d4a1f32ec87831556c
|
7
|
+
data.tar.gz: f536a148a022ebed055f349362d1f90593de300ac7972e767dc26c20db7be1ee7e1437a1195fa15e4ce5f5b40dc6e623196a64e6c0cc34c3a3fdf61dba080e4e
|
data/README.md
CHANGED
@@ -4,7 +4,9 @@
|
|
4
4
|
|
5
5
|
This project is a Ruby gem that provides functionality for generating PDF files from HTML using the Chrome browser. It allows you to easily convert HTML content into PDF documents, making it convenient for tasks such as generating reports, invoices, or any other printable documents. The gem provides a simple and intuitive API for converting HTML to PDF, and it leverages the power and flexibility of the Chrome browser's rendering engine to ensure accurate and high-quality PDF output. With this gem, you can easily integrate PDF generation capabilities into your Ruby applications.
|
6
6
|
|
7
|
-
At the core, this project leverages the
|
7
|
+
At the core, this project leverages the Chrome rendering engine, but with significantly reduced overhead and dependencies. Instead of relying on the full Grover/Puppeteer/NodeJS stack, this project uses a raw web socket to enable direct communication from Ruby to a headless Chrome or Chromium browser. This approach ensures efficieny while providing a streamlined alternative for rendering tasks without sacrificing performance or flexibility.
|
8
|
+
|
9
|
+
It leverages work from [Puppeteer](https://pptr.dev/browsers-api/) (@puppeteer/browsers) to install a local Chrome-Headless-Shell if no Chrome is running, but that requires node (npx) to be available.
|
8
10
|
|
9
11
|
This is how easy PDF generation can be in Ruby:
|
10
12
|
|
@@ -16,85 +18,30 @@ And this while having the most modern HTML/CSS/JS availlable to you: flex, grid,
|
|
16
18
|
|
17
19
|
A core goal of this project is performance, and it is designed to be exceptionally fast. By leveraging **direct communication** with a headless Chrome or Chromium browser via a **raw web socket**, the gem minimizes overhead and dependencies, enabling PDF generation at speeds that significantly outperform other solutions. Whether generating simple or complex documents, this gem ensures that your Ruby applications can handle PDF tasks efficiently and at scale.
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
-
To install the gem and add it to your application's Gemfile, execute the following command:
|
22
|
-
|
23
|
-
```
|
24
|
-
$ bundle add palapala_pdf
|
25
|
-
```
|
26
|
-
|
27
|
-
If you are not using bundler to manage dependencies, you can install the gem by running:
|
28
|
-
|
29
|
-
```
|
30
|
-
$ gem install palapala_pdf
|
31
|
-
```
|
32
|
-
|
33
|
-
Palapala PDF connects to Chrome over a web socket connection.
|
34
|
-
An external Chrome/Chromium is preferred. Start it with the following
|
35
|
-
command (9222 is the default/expected port):
|
21
|
+
[Example: paged_css.pdf](https://raw.githubusercontent.com/palapala-app/palapala_pdf/main/examples/paged_css.pdf)
|
36
22
|
|
37
|
-
|
38
|
-
/path/to/chrome --headless --disable-gpu --remote-debugging-port=9222
|
39
|
-
```
|
40
|
-
|
41
|
-
### Connecting to Chrome
|
42
|
-
|
43
|
-
Palapa PDF will go through this process
|
44
|
-
|
45
|
-
- check if a Chrome is running and exposing port 9222 (and if so, use it)
|
46
|
-
- if `Palapala.headless_chrome_path` is defined, launch Chrome as a child process using that path
|
47
|
-
- if **NPX** is avalaillable, install a **Chrome-Headless-Shell** variant locally and launch it as a child process. It will install the 'stable' version or the version identified by `Palapala.chrome_headless_shell_version` setting (or from ENV `CHROME_HEADLESS_SHELL_VERSION`).
|
48
|
-
- as a last fallback it will guess a chrome path from the detected OS and try to launch a Chrome with that
|
49
|
-
|
50
|
-
A Chrome-Headless-Shell version gives the best performance and resource useage
|
51
|
-
|
52
|
-
### Installing Chrome / Headless Chrome manually
|
53
|
-
|
54
|
-
This is easiest using npx and some tooling provided by Puppeteer. Unfortunately it depends on node/npm, but it's worth it. E.g. install a specific version like this:
|
23
|
+
## Sponsor This Project
|
55
24
|
|
56
|
-
|
57
|
-
npx @puppeteer/browsers install chrome@127.0.6533.88
|
58
|
-
````
|
25
|
+
If you find this project useful and would like to support its development, consider sponsoring or buying a coffee to help keep it going:
|
59
26
|
|
60
|
-
|
27
|
+
- **GitHub Sponsors:** [Sponsor on GitHub](https://github.com/sponsors/koenhandekyn)
|
28
|
+
- **Buy Me a Coffee:** [Buy a Coffee](https://buymeacoffee.com/koenhandekyn)
|
61
29
|
|
62
|
-
|
30
|
+
Your support is greatly appreciated and helps maintain the project!
|
63
31
|
|
64
|
-
|
65
|
-
npx @puppeteer/browsers install chrome-headless-shell@stable
|
66
|
-
```
|
32
|
+
## Installation
|
67
33
|
|
68
|
-
|
34
|
+
To install the gem and add it to your application's Gemfile, execute the following command:
|
69
35
|
|
70
36
|
```
|
71
|
-
|
72
|
-
```
|
73
|
-
|
74
|
-
*Note: Seems the august 2024 release 128.0.6613.85 is seriously performance impacted. So to avoid regression issues, it's suggested to install a specific version of Chrome, test it and stick with it. The chrome-headless-shell does not seem to suffer from this though.*
|
75
|
-
|
76
|
-
### Installing Node/NPX
|
77
|
-
|
78
|
-
Using Brew
|
79
|
-
|
80
|
-
````
|
81
|
-
brew install node
|
37
|
+
$ bundle add palapala_pdf
|
82
38
|
```
|
83
39
|
|
84
|
-
Using NVM (Node Version Manager)
|
85
|
-
|
86
|
-
````
|
87
|
-
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
|
88
|
-
source ~/.nvm/nvm.sh
|
89
|
-
nvm --version
|
90
|
-
nvm install node
|
91
|
-
````
|
92
|
-
|
93
40
|
## Usage Instructions
|
94
41
|
|
95
42
|
To create a PDF from HTML content using the `Palapala` library, follow these steps:
|
96
43
|
|
97
|
-
|
44
|
+
**Configuration from inside Ruby**
|
98
45
|
|
99
46
|
Configure the `Palapala` library with the necessary options, such as the URL for the browser and default settings like scale and format.
|
100
47
|
|
@@ -102,76 +49,82 @@ In a Rails context, this could be inside an initializer.
|
|
102
49
|
|
103
50
|
```ruby
|
104
51
|
Palapala.setup do |config|
|
105
|
-
#
|
52
|
+
# debug mode
|
106
53
|
config.debug = true
|
107
|
-
|
108
|
-
|
109
|
-
|
54
|
+
# Chrome headless shell version to use (stable, beta, dev, canary, etc.) when launching a new Chrome instance
|
55
|
+
config.chrome_headless_shell_version = :stable
|
56
|
+
# run against an external chrome/chromium or leave this out to run against a chrome that is started as a child process
|
57
|
+
config.headless_chrome_url = 'http://localhost:9222'
|
58
|
+
# path to Chrome executable
|
59
|
+
config.headless_chrome_path = '/usr/bin/google-chrome-stable'
|
60
|
+
# default options for PDF generation
|
61
|
+
config.defaults = { scale: 1 }
|
62
|
+
# extra params to pass to Chrome when launched as a child process
|
63
|
+
config.chrome_params = []
|
110
64
|
end
|
111
65
|
```
|
112
|
-
1. **Create a PDF from HTML**:
|
113
66
|
|
114
|
-
|
67
|
+
**Using environemnt variables**
|
115
68
|
|
116
69
|
```sh
|
117
|
-
|
70
|
+
CHROME_HEADLESS_SHELL_VERSION=canary ruby examples/performance_benchmark.rb
|
71
|
+
````
|
72
|
+
|
73
|
+
```sh
|
74
|
+
HEADLESS_CHROME_URL=http://192.168.1.1:9222 ruby examples/performance_benchmark.rb
|
118
75
|
```
|
119
76
|
|
120
|
-
|
77
|
+
```sh
|
78
|
+
CHROME_HEADLESS_PATH=/var/to/chrome ruby examples/performance_benchmark.rb
|
79
|
+
```
|
80
|
+
|
81
|
+
**Create a PDF from HTML**
|
82
|
+
|
83
|
+
Load palapala and create a PDF file from an HTML snippet:
|
121
84
|
|
122
85
|
```ruby
|
123
86
|
require "palapala"
|
124
87
|
Palapala::Pdf.new("<h1>Hello, world! #{Time.now}</h1>").save('hello.pdf')
|
125
88
|
```
|
126
89
|
|
127
|
-
Instantiate a new Palapala::Pdf object with your HTML content and generate the PDF binary data
|
90
|
+
Instantiate a new Palapala::Pdf object with your HTML content and generate the PDF binary data:
|
128
91
|
|
129
92
|
```ruby
|
130
93
|
require "palapala"
|
131
94
|
binary_data = Palapala::Pdf.new("<h1>Hello, world! #{Time.now}</h1>").binary_data
|
132
95
|
```
|
133
96
|
|
134
|
-
##
|
97
|
+
## Advanced Examples
|
135
98
|
|
136
|
-
|
99
|
+
- headers and footers
|
100
|
+
- paged css for paper sizes, paper margins, pages breaks, etc
|
101
|
+
- js based rendering
|
137
102
|
|
138
|
-
|
103
|
+
## Connecting to Chrome
|
139
104
|
|
140
|
-
|
141
|
-
|
142
|
-
With palapala PDF headers and footers are defined using `header_html` and `footer_html` options. These allow you to insert HTML content directly into the header or footer areas.
|
143
|
-
|
144
|
-
```ruby
|
145
|
-
Palapala::Pdf.new(
|
146
|
-
"<p>Hello world</>",
|
147
|
-
header_html: '<div style="text-align: center;">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>',
|
148
|
-
footer_html: '<div style="text-align: center;">Generated with Palapala PDF</div>',
|
149
|
-
margin: { top: "2cm", bottom: "2cm"}
|
150
|
-
).save("test.pdf")
|
151
|
-
```
|
105
|
+
Palapa PDF will go through this process
|
152
106
|
|
153
|
-
|
107
|
+
- check if a Chrome is running and exposing port 9222 (and if so, use it)
|
108
|
+
- if `Palapala.headless_chrome_path` is defined, launch Chrome as a child process using that path
|
109
|
+
- if **NPX** is avalaillable, install a **Chrome-Headless-Shell** variant locally and launch it as a child process. It will install the 'stable' version or the version identified by `Palapala.chrome_headless_shell_version` setting (or from ENV `CHROME_HEADLESS_SHELL_VERSION`).
|
110
|
+
- as a last fallback it will guess a chrome path from the detected OS and try to launch a Chrome with that
|
154
111
|
|
155
|
-
|
112
|
+
In our expreience a Chrome-Headless-Shell version gives the best performance and resource useage.
|
156
113
|
|
157
|
-
|
114
|
+
### Installing Chrome / Headless Chrome manually
|
158
115
|
|
159
|
-
|
116
|
+
This is easiest using npx and tooling provided by Puppeteer (depends on node/npm, but it's worth it). This installs chrome in a `chrome` folder in the current working dir and it outputs the path where it's installed when it's finished. Currently we'd advise for the `chrome-headless-shell` variant that is a light version meant just for this use case. The chrome-headless-shell is a minimal, headless version of the Chrome browser designed specifically for environments where you need to run Chrome without a graphical user interface (GUI). This is particularly useful in scenarios like server-side rendering, automated testing, web scraping, or any situation where you need the power of the Chrome browser engine without the overhead of displaying a UI. Headless by design, reduced size and overhead but still the same engine.
|
160
117
|
|
161
|
-
|
118
|
+
```sh
|
119
|
+
npx @puppeteer/browsers install chrome-headless-shell@stable
|
120
|
+
```
|
162
121
|
|
163
|
-
|
122
|
+
It installs to a path like this `./chrome-headless-shell/mac_arm-128.0.6613.84/chrome-headless-shell-mac-arm64/chrome-headless-shell`. As it's headless by design, it only needs one parameter:
|
164
123
|
|
165
|
-
```
|
166
|
-
|
167
|
-
<script type="text/javascript">
|
168
|
-
document.addEventListener("DOMContentLoaded", () => {
|
169
|
-
document.body.innerHTML += "<p>Current time from JS: " + new Date().toLocaleString() + "</p>";
|
170
|
-
});
|
171
|
-
</script>
|
172
|
-
<body><p>Default body text.</p></body>
|
173
|
-
</html>
|
124
|
+
```sh
|
125
|
+
./chrome-headless-shell/mac_arm-128.0.6613.84/chrome-headless-shell-mac-arm64/chrome-headless-shell --remote-debugging-port=9222
|
174
126
|
```
|
127
|
+
*Note: Seems the august 2024 release Chrome releases 128.0.6613.85 onward is seriously performance impacted for PDF generation. Chrome Headless Shell releases don't seem to suffer from this issue.
|
175
128
|
|
176
129
|
## Raw parameters (Page.printToPDF)
|
177
130
|
|
@@ -193,15 +146,6 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/palapa
|
|
193
146
|
- [Eugen Neagoe](https://github.com/eneagoe) - Thank you for your valuable input, feedback and opinions.
|
194
147
|
- [Radu Bogoevici](https://github.com/codenighter) - Thanks for test driving, and all help big and small.
|
195
148
|
|
196
|
-
## Sponsor This Project
|
197
|
-
|
198
|
-
If you find this project useful and would like to support its development, consider sponsoring or buying a coffee to help keep it going:
|
199
|
-
|
200
|
-
- **GitHub Sponsors:** [Sponsor on GitHub](https://github.com/sponsors/koenhandekyn)
|
201
|
-
- **Buy Me a Coffee:** [Buy a Coffee](https://buymeacoffee.com/koenhandekyn)
|
202
|
-
|
203
|
-
Your support is greatly appreciated and helps maintain the project!
|
204
|
-
|
205
149
|
## Findings
|
206
150
|
|
207
151
|
- For Chrome, mode headless=new seems to be slower for pdf rendering cases.
|
@@ -209,24 +153,14 @@ Your support is greatly appreciated and helps maintain the project!
|
|
209
153
|
|
210
154
|
## Primitive benchmark
|
211
155
|
|
212
|
-
On a macbook m3, the throughput for 'hello world' PDF generation can reach around
|
156
|
+
On a macbook m3, the throughput for 'hello world' PDF generation can reach around 500 to 800 docs/second when allowing for some concurrency (4 threads). As Chrome is actually also very efficient, it scales really well for complex documents also. If you run this in Rails, the concurrency is being taken care of either by the front end thread pool or by the workers and you shouldn't have to think about this. (Using an external Chrome)
|
213
157
|
|
214
158
|
Note: it renders `"Hello #{i}, world #{j}! #{Time.now}."` where i is the thread and j is the iteration counter within the thread and persists it to an SSD (which is very fast these days).
|
215
159
|
|
216
|
-
### benchmarking 20 docs: 1x20, 2x10, 4x5
|
217
|
-
|
218
160
|
```sh
|
219
|
-
c:1, n:
|
220
|
-
c:2, n:10 : Throughput =
|
221
|
-
c:4, n:
|
222
|
-
```
|
223
|
-
|
224
|
-
### benchmarking 320 docs: 1x320, 4x80, 8x40
|
225
|
-
|
226
|
-
```sh
|
227
|
-
c:1, n:320 : Throughput = 184.99 docs/sec, Total time = 1.7299 seconds
|
228
|
-
c:4, n:80 : Throughput = 302.50 docs/sec, Total time = 1.0578 seconds
|
229
|
-
c:8, n:40 : Throughput = 254.29 docs/sec, Total time = 1.2584 seconds
|
161
|
+
c:1, n:10 : Throughput = 16.76 docs/sec, Total time = 0.5968 seconds
|
162
|
+
c:2, n:10 : Throughput = 170.41 docs/sec, Total time = 0.1174 seconds
|
163
|
+
c:4, n:80 : Throughput = 579.03 docs/sec, Total time = 0.5526 seconds```
|
230
164
|
```
|
231
165
|
|
232
166
|
This is about a factor 100x faster then what you typically get with Grover and still 10x faster then with many alternatives. It's effectively that fast that you can run this for a lot of uses cases straight from e.g. your Ruby On Rails web worker in the controller on a single machine and still scale to lot's of users.
|
@@ -253,25 +187,22 @@ In this example, `pdf_data` is the binary data of the PDF file. The `filename` o
|
|
253
187
|
|
254
188
|
## Docker
|
255
189
|
|
256
|
-
|
190
|
+
TODO
|
257
191
|
|
258
|
-
|
259
|
-
Palapala.setup do |config|
|
260
|
-
config.opts = { 'no-sandbox': nil }
|
261
|
-
end
|
262
|
-
```
|
263
|
-
It has also been reported that the Chrome process repeatedly crashes when running inside a Docker container on an M1 Mac. Chrome should work as expected when deployed to a Docker container on a non-M1 Mac.
|
192
|
+
*It has also been reported that the Chrome process repeatedly crashes when running inside a Docker container on an M1 Mac. Chrome should work as expected when deployed to a Docker container on a non-M1 Mac.*
|
264
193
|
|
265
194
|
## Thread-safety
|
266
195
|
|
267
|
-
Behind the scenes, a websocket is openend and stored on Thread.current for subsequent requests. Hence, the code is
|
268
|
-
thread safe in the sense that every web socket get's a new tab in the underlying chromium and get an isolated context.
|
269
|
-
|
270
196
|
For performance reasons, the code uses a low level websocket connection that does all it's work on the curent thread
|
271
197
|
so we can avoid synchronisation penalties.
|
272
198
|
|
199
|
+
Behind the scenes, a websocket is openend and stored on Thread.current for subsequent requests. Hence, the code is
|
200
|
+
thread safe in the sense that every web socket get's a new tab in the underlying chromium and get an isolated context.
|
201
|
+
|
273
202
|
## Heroku
|
274
203
|
|
204
|
+
TODO
|
205
|
+
|
275
206
|
possible buildpacks
|
276
207
|
|
277
208
|
https://github.com/heroku/heroku-buildpack-chrome-for-testing
|
@@ -0,0 +1,16 @@
|
|
1
|
+
### Installing Node (npx)
|
2
|
+
|
3
|
+
Using Brew
|
4
|
+
|
5
|
+
```sh
|
6
|
+
brew install node
|
7
|
+
```
|
8
|
+
|
9
|
+
Using NVM (Node Version Manager)
|
10
|
+
|
11
|
+
```sh
|
12
|
+
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
|
13
|
+
source ~/.nvm/nvm.sh
|
14
|
+
nvm --version
|
15
|
+
nvm install node
|
16
|
+
```
|
data/doc/paged_css.md
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
## Paged CSS
|
2
|
+
|
3
|
+
Paged CSS is a subset of CSS designed for styling printed documents. It extends standard CSS to handle pagination, page sizes, headers, footers, and other aspects of printed content. Paged CSS is commonly used in scenarios where web content needs to be converted to PDFs or other paginated formats.
|
4
|
+
|
5
|
+
Setting page size
|
6
|
+
|
7
|
+
```css
|
8
|
+
@page {
|
9
|
+
/* set a standard page size */
|
10
|
+
size: A4 landscape;
|
11
|
+
/* Custom */
|
12
|
+
size: 8.5in 11in; /* Width x Height */
|
13
|
+
}
|
14
|
+
```
|
15
|
+
|
16
|
+
Setting page margins
|
17
|
+
|
18
|
+
```css
|
19
|
+
@page {
|
20
|
+
margin: 1in; /* 1 inch on all sides */
|
21
|
+
margin: 1in 0.5in 1in 0.5in; /* Top, Right, Bottom, Left */
|
22
|
+
}
|
23
|
+
```
|
24
|
+
|
25
|
+
Forcing a Page Break before or after an Element
|
26
|
+
|
27
|
+
```css
|
28
|
+
/* This ensures that every `h1` starts on a new page. */
|
29
|
+
h1 {
|
30
|
+
page-break-before: always;
|
31
|
+
}
|
32
|
+
/* This ensures that every `p` element ends with a page break, starting the next content on a new page. */
|
33
|
+
p {
|
34
|
+
page-break-after: always;
|
35
|
+
}
|
36
|
+
/* This prevents a table from being split across two pages. */
|
37
|
+
table {
|
38
|
+
page-break-inside: avoid;
|
39
|
+
}
|
40
|
+
```
|
41
|
+
|
42
|
+
### Headers and Footers
|
43
|
+
|
44
|
+
When using Chromium-based rendering engines, headers and footers are not controlled by the Paged CSS standard but are instead managed through specific settings in the rendering engine.
|
45
|
+
|
46
|
+
With palapala PDF headers and footers are defined using `header_template` and `footer_template` options. These allow you to insert HTML content directly into the header or footer areas.
|
47
|
+
|
48
|
+
Critical is that you specify a font-size because by default Chrome uses a very tiny font.
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
Palapala::Pdf.new(
|
52
|
+
"<p>Hello world</>",
|
53
|
+
header_template: '<div style="text-align: center; font-size: 12pt;">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>',
|
54
|
+
footer_template: '<div style="text-align: center; font-size: 12pt;">Generated with Palapala PDF</div>',
|
55
|
+
).save("test.pdf")
|
56
|
+
```
|
57
|
+
|
58
|
+
### Examples
|
59
|
+
|
60
|
+
#### Headers and Footers
|
61
|
+
|
62
|
+
TODO explain about headers and footers, font sizes, styles being independent, and how to insert current page, total pages, etc.
|
63
|
+
|
64
|
+
#### Page sizes and margins
|
65
|
+
|
66
|
+
Paged CSS, also known as @page CSS, is used to control the layout and appearance of printed documents. It allows you to define page-specific styles, such as sizes and margins, which are crucial for generating well-formatted PDFs.
|
67
|
+
|
68
|
+
You can specify the size of the page using predefined sizes or custom dimensions. Common predefined sizes include A4, A3, letter, etc. Margins can be set for the top, right, bottom, and left sides of the page. You can specify all four margins at once or individually. You can also define named pages for different sections of your document.
|
69
|
+
|
70
|
+
##### Example: Different First Page
|
71
|
+
|
72
|
+
TODO Validate
|
73
|
+
|
74
|
+
```css
|
75
|
+
@page first {
|
76
|
+
size: A4;
|
77
|
+
margin: 2in; /* Larger margin for the first page */
|
78
|
+
}
|
79
|
+
|
80
|
+
@page {
|
81
|
+
size: A4;
|
82
|
+
margin: 1in;
|
83
|
+
}
|
84
|
+
|
85
|
+
body {
|
86
|
+
counter-reset: page;
|
87
|
+
}
|
88
|
+
|
89
|
+
body:first {
|
90
|
+
page: first;
|
91
|
+
}
|
92
|
+
```
|
93
|
+
|
94
|
+
#### Page breaks
|
95
|
+
|
96
|
+
Paged CSS allows you to control how content is divided across pages when printing or generating PDFs. Page breaks are an essential part of this, as they determine where a new page starts. You can control page breaks using the `page-break-before`, `page-break-after`, and `page-break-inside` properties.
|
97
|
+
|
98
|
+
##### Page Break Properties
|
99
|
+
|
100
|
+
1. **`page-break-before`**: Forces a page break before the element.
|
101
|
+
2. **`page-break-after`**: Forces a page break after the element.
|
102
|
+
3. **`page-break-inside`**: Prevents or allows a page break inside the element.
|
103
|
+
|
104
|
+
##### Values
|
105
|
+
|
106
|
+
- `auto`: Default. Neither forces nor prevents a page break.
|
107
|
+
- `always` Always forces a page break.
|
108
|
+
- `avoid`: Avoids a page break inside the element.
|
109
|
+
- `left`: Forces a page break so that the next page is a left page.
|
110
|
+
- `right`: Forces a page break so that the next page is a right page.
|
111
|
+
|
112
|
+
##### Examples
|
113
|
+
|
114
|
+
```css
|
115
|
+
/* This ensures that every `h1` starts on a new page. */
|
116
|
+
h1 {
|
117
|
+
page-break-before: always;
|
118
|
+
}
|
119
|
+
/* This ensures that every `p` element ends with a page break, starting the next content on a new page. */
|
120
|
+
p {
|
121
|
+
page-break-after: always;
|
122
|
+
}
|
123
|
+
/* This prevents a table from being split across two pages. */
|
124
|
+
table {
|
125
|
+
page-break-inside: avoid;
|
126
|
+
}
|
127
|
+
```
|
128
|
+
|
129
|
+
##### Practical Use Cases
|
130
|
+
|
131
|
+
- **Chapter Titles**: Use `page-break-before: always;` for chapter titles to ensure each chapter starts on a new page.
|
132
|
+
- **Sections**: Use `page-break-after: always;` for sections that should end with a page break.
|
133
|
+
- **Tables and Figures**: Use `page-break-inside: avoid;` to keep tables and figures from being split across pages.
|
134
|
+
|
135
|
+
#### Tables accross Pages
|
136
|
+
|
137
|
+
TODO explain `display` property with the values `table-header-group` and `table-footer-group`
|
138
|
+
|
139
|
+
##### Example
|
140
|
+
|
141
|
+
```html
|
142
|
+
<table>
|
143
|
+
<thead>
|
144
|
+
<tr>
|
145
|
+
<th>Header 1</th>
|
146
|
+
<th>Header 2</th>
|
147
|
+
</tr>
|
148
|
+
</thead>
|
149
|
+
<tbody>
|
150
|
+
<tr>
|
151
|
+
<td>Data 1</td>
|
152
|
+
<td>Data 2</td>
|
153
|
+
</tr>
|
154
|
+
<!-- More rows -->
|
155
|
+
</tbody>
|
156
|
+
<tfoot>
|
157
|
+
<tr>
|
158
|
+
<td>Footer 1</td>
|
159
|
+
<td>Footer 2</td>
|
160
|
+
</tr>
|
161
|
+
</tfoot>
|
162
|
+
</table>
|
163
|
+
```
|
164
|
+
|
165
|
+
In this example:
|
166
|
+
- The `<thead>` section will be repeated at the top of each page.
|
167
|
+
- The `<tfoot>` section will be repeated at the bottom of each page.
|
data/examples/all.rb
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
<!--
|
2
|
+
OPTIONS AS PASSED IN THE C++ code
|
3
|
+
=================================
|
4
|
+
options.Set(kSettingHeaderFooterDate,
|
5
|
+
base::Time::Now().InMillisecondsFSinceUnixEpoch());
|
6
|
+
options.Set("width", static_cast<double>(page_size.width()));
|
7
|
+
options.Set("height", static_cast<double>(page_size.height()));
|
8
|
+
options.Set("topMargin", page_layout.margin_top);
|
9
|
+
options.Set("bottomMargin", page_layout.margin_bottom);
|
10
|
+
options.Set("leftMargin", page_layout.margin_left);
|
11
|
+
options.Set("rightMargin", page_layout.margin_right);
|
12
|
+
// `page_index` is 0-based, so 1 is added to get the page number.
|
13
|
+
options.Set("pageNumber", base::checked_cast<int>(page_index + 1));
|
14
|
+
options.Set("totalPages", base::checked_cast<int>(total_pages));
|
15
|
+
options.Set("url", params.url);
|
16
|
+
std::u16string title = source_frame.GetDocument().Title().Utf16();
|
17
|
+
options.Set("title", title.empty() ? params.title : title);
|
18
|
+
options.Set("headerTemplate", params.header_template);
|
19
|
+
options.Set("footerTemplate", params.footer_template);
|
20
|
+
options.Set("isRtl", base::i18n::IsRTL());
|
21
|
+
-->
|
22
|
+
|
23
|
+
<!doctype html>
|
24
|
+
<html>
|
25
|
+
|
26
|
+
<head>
|
27
|
+
<link rel="stylesheet" href="chrome://resources/css/text_defaults.css">
|
28
|
+
<style>
|
29
|
+
body {
|
30
|
+
display: flex;
|
31
|
+
flex-direction: column;
|
32
|
+
margin: 0;
|
33
|
+
}
|
34
|
+
|
35
|
+
#header,
|
36
|
+
#footer {
|
37
|
+
display: flex;
|
38
|
+
flex: none;
|
39
|
+
}
|
40
|
+
|
41
|
+
#header {
|
42
|
+
align-items: flex-start;
|
43
|
+
padding-top: 15pt;
|
44
|
+
}
|
45
|
+
|
46
|
+
#footer {
|
47
|
+
align-items: flex-end;
|
48
|
+
padding-bottom: 15pt;
|
49
|
+
}
|
50
|
+
|
51
|
+
#content {
|
52
|
+
flex: auto;
|
53
|
+
}
|
54
|
+
|
55
|
+
.left {
|
56
|
+
flex: none;
|
57
|
+
padding-left: 24pt;
|
58
|
+
/* csschecker-disable-line left-right */
|
59
|
+
padding-right: 6pt;
|
60
|
+
/* csschecker-disable-line left-right */
|
61
|
+
}
|
62
|
+
|
63
|
+
.center {
|
64
|
+
flex: auto;
|
65
|
+
padding-left: 24pt;
|
66
|
+
/* csschecker-disable-line left-right */
|
67
|
+
padding-right: 24pt;
|
68
|
+
/* csschecker-disable-line left-right */
|
69
|
+
text-align: center;
|
70
|
+
}
|
71
|
+
|
72
|
+
.right {
|
73
|
+
flex: none;
|
74
|
+
/* historically does not account for RTL */
|
75
|
+
padding-left: 6pt;
|
76
|
+
/* csschecker-disable-line left-right */
|
77
|
+
padding-right: 24pt;
|
78
|
+
/* csschecker-disable-line left-right */
|
79
|
+
}
|
80
|
+
|
81
|
+
.grow {
|
82
|
+
flex: auto;
|
83
|
+
}
|
84
|
+
|
85
|
+
.text {
|
86
|
+
font-size: 8pt;
|
87
|
+
overflow: hidden;
|
88
|
+
text-overflow: ellipsis;
|
89
|
+
white-space: nowrap;
|
90
|
+
}
|
91
|
+
</style>
|
92
|
+
<script>
|
93
|
+
|
94
|
+
function getComputedStyleAsFloat(style, value) {
|
95
|
+
return parseFloat(style.getPropertyValue(value).slice(0, -2));
|
96
|
+
}
|
97
|
+
|
98
|
+
function elementIntersects(element, topPos, bottomPos, leftPos, rightPos) {
|
99
|
+
const rect = element.getBoundingClientRect();
|
100
|
+
const style = window.getComputedStyle(element);
|
101
|
+
|
102
|
+
// Only consider the size of |element|, so remove the padding from |rect|.
|
103
|
+
// The padding is used for positioning.
|
104
|
+
rect.top += getComputedStyleAsFloat(style, 'padding-top');
|
105
|
+
rect.bottom -= getComputedStyleAsFloat(style, 'padding-bottom');
|
106
|
+
rect.left += getComputedStyleAsFloat(style, 'padding-left');
|
107
|
+
rect.right -= getComputedStyleAsFloat(style, 'padding-right');
|
108
|
+
return leftPos < rect.right && rightPos > rect.left && topPos < rect.bottom &&
|
109
|
+
bottomPos > rect.top;
|
110
|
+
}
|
111
|
+
|
112
|
+
function setupHeaderFooterTemplate(options) {
|
113
|
+
const body = document.querySelector('body');
|
114
|
+
const header = document.querySelector('#header');
|
115
|
+
const footer = document.querySelector('#footer');
|
116
|
+
|
117
|
+
body.style.width = `${options.width}px`;
|
118
|
+
body.style.height = `${options.height}px`;
|
119
|
+
header.style.height = `${options.topMargin}px`;
|
120
|
+
footer.style.height = `${options.bottomMargin}px`;
|
121
|
+
|
122
|
+
const topMargin = options.topMargin;
|
123
|
+
const bottomMargin = options.height - options.bottomMargin;
|
124
|
+
const leftMargin = options.leftMargin;
|
125
|
+
const rightMargin = options.width - options.rightMargin;
|
126
|
+
|
127
|
+
header.innerHTML = options['headerTemplate'] || `
|
128
|
+
<div class='date text left'></div>
|
129
|
+
<div class='title text center'></div>`;
|
130
|
+
footer.innerHTML = options['footerTemplate'] || `
|
131
|
+
<div class='url text left grow'></div>
|
132
|
+
<div class='text right'>
|
133
|
+
<span class='pageNumber'></span>/<span class='totalPages'></span>
|
134
|
+
</div>`;
|
135
|
+
|
136
|
+
const date = new Date(options.date);
|
137
|
+
const formatter =
|
138
|
+
new Intl.DateTimeFormat(
|
139
|
+
navigator.languages[0].split('@')[0],
|
140
|
+
{ dateStyle: 'short', timeStyle: 'short' });
|
141
|
+
options.date = formatter.format(date);
|
142
|
+
for (const cssClass of ['date', 'title', 'url', 'pageNumber', 'totalPages']) {
|
143
|
+
for (const element of document.querySelectorAll(`.${cssClass}`)) {
|
144
|
+
element.textContent = options[cssClass];
|
145
|
+
}
|
146
|
+
}
|
147
|
+
for (const element of document.querySelectorAll(`.text`)) {
|
148
|
+
if (options.isRtl &&
|
149
|
+
!element.classList.contains('url') &&
|
150
|
+
!element.classList.contains('title')) {
|
151
|
+
element.dir = 'rtl';
|
152
|
+
}
|
153
|
+
if (elementIntersects(element, topMargin, bottomMargin, leftMargin,
|
154
|
+
rightMargin)) {
|
155
|
+
element.style.visibility = 'hidden';
|
156
|
+
}
|
157
|
+
}
|
158
|
+
}
|
159
|
+
|
160
|
+
</script>
|
161
|
+
</head>
|
162
|
+
|
163
|
+
<body>
|
164
|
+
<div id="header"></div>
|
165
|
+
<div id="content"></div>
|
166
|
+
<div id="footer"></div>
|
167
|
+
</body>
|
168
|
+
|
169
|
+
</html>
|
Binary file
|
@@ -3,42 +3,27 @@
|
|
3
3
|
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
4
4
|
require 'palapala'
|
5
5
|
|
6
|
-
|
7
|
-
<
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
padding: 5px;
|
16
|
-
text-align: center;
|
17
|
-
vertical-align: middle;
|
18
|
-
width: 100%;
|
19
|
-
border: 1px solid black;
|
20
|
-
}
|
21
|
-
</style>
|
22
|
-
<div class="header" style="text-align: center">
|
23
|
-
Page <span class="pageNumber"></span> of <span class="totalPages"></span>
|
24
|
-
</div>
|
6
|
+
header =
|
7
|
+
'<div class="center">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>'
|
8
|
+
|
9
|
+
left_center_right = <<~HTML
|
10
|
+
<div style="display: flex; justify-content: space-between; width: 100%; margin-left: 1cm; margin-right: 1cm;">
|
11
|
+
<div style="text-align: left; flex: 1;">Left Text</div>
|
12
|
+
<div style="text-align: center; flex: 1;">Center Text</div>
|
13
|
+
<div style="text-align: right; flex: 1;">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>
|
14
|
+
</div>
|
25
15
|
HTML
|
26
16
|
|
27
|
-
|
28
|
-
|
29
|
-
# config.headless_chrome_url = 'http://localhost:9222' # run against a remote Chrome instance
|
30
|
-
# config.headless_chrome_path = '/usr/bin/google-chrome-stable' # path to Chrome executable
|
31
|
-
end
|
17
|
+
footer =
|
18
|
+
'<span>Generated at <span class="date"></span></span>'
|
32
19
|
|
33
|
-
|
34
|
-
# "<style>@page { size: A4 landscape; }</style><p>Hello world #{Time.now}</>",
|
20
|
+
Palapala::Pdf.new(
|
35
21
|
"<h1>Title</h1><p>Hello world #{Time.now}</>",
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
margin_bottom: 2).save('tmp/headers_and_footers.pdf')
|
22
|
+
header: left_center_right,
|
23
|
+
footer:,
|
24
|
+
margin_top: 1,
|
25
|
+
margin_left: 1,
|
26
|
+
margin_bottom: 1).save('headers_and_footers.pdf')
|
42
27
|
|
43
|
-
puts
|
44
|
-
`open
|
28
|
+
puts "Generated headers_and_footers.pdf"
|
29
|
+
# `open headers_and_footers.pdf`
|
Binary file
|
@@ -14,10 +14,8 @@ DOCUMENT = <<~HTML
|
|
14
14
|
</html>
|
15
15
|
HTML
|
16
16
|
|
17
|
-
Palapala.
|
18
|
-
# config.debug = true
|
19
|
-
# config.defaults = { header_template: '<div></div>', footer_template: '<div></div>' }
|
20
|
-
end
|
17
|
+
Palapala::Pdf.new(DOCUMENT).save('js_based_rendering.pdf')
|
21
18
|
|
22
|
-
|
23
|
-
|
19
|
+
puts "Generated js_based_rendering.pdf"
|
20
|
+
|
21
|
+
# `open tmp/js_based_rendering.pdf`
|
Binary file
|
@@ -0,0 +1,187 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
4
|
+
require "palapala"
|
5
|
+
|
6
|
+
long_text = (1..30).map { "Demonstrate a paragraph that is not split across pages." }.join(" ")
|
7
|
+
|
8
|
+
def table(rows)
|
9
|
+
<<~HTML
|
10
|
+
<table>
|
11
|
+
<thead>
|
12
|
+
<tr>
|
13
|
+
<th>Header 1</th>
|
14
|
+
<th>Header 2</th>
|
15
|
+
</tr>
|
16
|
+
</thead>
|
17
|
+
<tbody>
|
18
|
+
#{ (1..rows).map { |i| "<tr><td>Row #{i}, Cell 1</td><td>Row #{i}, Cell 2</td></tr>" }.join }
|
19
|
+
</tbody>
|
20
|
+
<tfoot>
|
21
|
+
<tr>
|
22
|
+
<td>Footer 1</td>
|
23
|
+
<td>Footer 2</td>
|
24
|
+
</tr>
|
25
|
+
</tfoot>
|
26
|
+
</table>
|
27
|
+
HTML
|
28
|
+
end
|
29
|
+
|
30
|
+
big_table = table(35)
|
31
|
+
small_table = table(5)
|
32
|
+
|
33
|
+
document = <<~HTML
|
34
|
+
<html>
|
35
|
+
<style>
|
36
|
+
@page {
|
37
|
+
size: A4;
|
38
|
+
margin: 2cm;
|
39
|
+
margin-top: 3cm;
|
40
|
+
margin-bottom: 3cm;
|
41
|
+
}
|
42
|
+
body, html {
|
43
|
+
margin: 0;
|
44
|
+
padding: 0;
|
45
|
+
font-family: Arial, sans-serif;
|
46
|
+
/* background-color: yellow; */
|
47
|
+
}
|
48
|
+
h1 {
|
49
|
+
page-break-before: always;
|
50
|
+
border-bottom: 1px solid black;
|
51
|
+
}
|
52
|
+
h2 {
|
53
|
+
/* keep with next */
|
54
|
+
page-break-after: avoid;
|
55
|
+
}
|
56
|
+
@page:first {
|
57
|
+
size: A4 landscape;
|
58
|
+
margin: 0; /* no margin for the first page */
|
59
|
+
padding: 0;
|
60
|
+
}
|
61
|
+
div.titlepage {
|
62
|
+
background-color: black;
|
63
|
+
color: white;
|
64
|
+
font-size: 72pt;
|
65
|
+
text-align: center;
|
66
|
+
display: flex;
|
67
|
+
justify-content: center;
|
68
|
+
align-items: center;
|
69
|
+
height: 100%;
|
70
|
+
width: 100vw;
|
71
|
+
}
|
72
|
+
table {
|
73
|
+
font-size: 10pt;
|
74
|
+
width: 100%;
|
75
|
+
border-collapse: collapse;
|
76
|
+
td, th {
|
77
|
+
border: 1px solid black;
|
78
|
+
padding: 0.5rem;
|
79
|
+
}
|
80
|
+
& thead, & tfoot {
|
81
|
+
tr {
|
82
|
+
background-color: lightgray;
|
83
|
+
& th, & td {
|
84
|
+
padding-top: 0.5rem;
|
85
|
+
padding-bottom: 0.5rem;
|
86
|
+
}
|
87
|
+
}
|
88
|
+
}
|
89
|
+
}
|
90
|
+
/* Initialize counters */
|
91
|
+
body {
|
92
|
+
counter-reset: h1Counter h2Counter;
|
93
|
+
}
|
94
|
+
/* Numbering for H1 elements */
|
95
|
+
h1 {
|
96
|
+
counter-increment: h1Counter;
|
97
|
+
counter-reset: h2Counter; /* Reset h2 counter when a new h1 appears */
|
98
|
+
}
|
99
|
+
h1::before {
|
100
|
+
content: counter(h1Counter) ". ";
|
101
|
+
/* font-weight: bold; */
|
102
|
+
}
|
103
|
+
/* Numbering for H2 elements */
|
104
|
+
h2 {
|
105
|
+
counter-increment: h2Counter;
|
106
|
+
}
|
107
|
+
h2::before {
|
108
|
+
content: counter(h1Counter) "." counter(h2Counter) " ";
|
109
|
+
/* font-weight: bold; */
|
110
|
+
}
|
111
|
+
/* named pages */
|
112
|
+
@page addendum {
|
113
|
+
size: A5;
|
114
|
+
margin: 1cm;
|
115
|
+
margin-top: 3cm;
|
116
|
+
}
|
117
|
+
.addendum {
|
118
|
+
page: addendum;
|
119
|
+
counter-reset: h1Counter h2Counter;
|
120
|
+
}
|
121
|
+
</style>
|
122
|
+
<body>
|
123
|
+
<div class="titlepage">
|
124
|
+
<c-title>Title Page</c-title>
|
125
|
+
</div>
|
126
|
+
<h1>New Section</h1>
|
127
|
+
<h2>Subsection tables</h2>
|
128
|
+
<p>This demonstrates a table with a header and footer that spans multiple pages.</p>
|
129
|
+
#{big_table}
|
130
|
+
<h2>Subsection page break inside</h2>
|
131
|
+
<p style="page-break-inside: avoid; text-align: justify">
|
132
|
+
#{long_text}
|
133
|
+
</p>
|
134
|
+
<p>Note that the section title has moved to the second page because the paragraph above was moved to the second page.</p>
|
135
|
+
<h1>New Section</h1>
|
136
|
+
<p>Page 3 content</p>
|
137
|
+
<p>A small table</p>
|
138
|
+
#{small_table}
|
139
|
+
<h2>Subsection</h2>
|
140
|
+
<p>Some content</p>
|
141
|
+
<h2>Subsection</h2>
|
142
|
+
<p>Some content</p>
|
143
|
+
<div class="addendum">
|
144
|
+
This is an addendum and the page size is A5.
|
145
|
+
Headers are starting again from 1.
|
146
|
+
<h1>Some addendum header</h1>
|
147
|
+
<h2>Subsection</h2>
|
148
|
+
<h2>Subsection</h2>
|
149
|
+
<h1>Some addendum header</h1>
|
150
|
+
</div>
|
151
|
+
</body>
|
152
|
+
</html>
|
153
|
+
HTML
|
154
|
+
|
155
|
+
def debug(color: "red")
|
156
|
+
<<~HTML
|
157
|
+
<style>
|
158
|
+
/* this is a class chrome assigns to the header, footer and content in the main template */
|
159
|
+
#header, #content, #footer {
|
160
|
+
border: 1px dotted #{color}; /* uncomment to see the areas */
|
161
|
+
}
|
162
|
+
</style>
|
163
|
+
HTML
|
164
|
+
end
|
165
|
+
|
166
|
+
def header_footer_template(debug_color: nil)
|
167
|
+
<<~HTML
|
168
|
+
#{ debug(color: debug_color) if debug_color }
|
169
|
+
<div style="font-size: 12pt;">#{yield}</div>
|
170
|
+
HTML
|
171
|
+
end
|
172
|
+
|
173
|
+
footer = header_footer_template do
|
174
|
+
"Page <span class='pageNumber'></span> of <span class='totalPages'></span>"
|
175
|
+
end
|
176
|
+
|
177
|
+
header = header_footer_template do
|
178
|
+
"Generated with Palapala PDF"
|
179
|
+
end
|
180
|
+
|
181
|
+
Palapala::Pdf.new(document,
|
182
|
+
header:,
|
183
|
+
footer:).save("paged_css.pdf")
|
184
|
+
|
185
|
+
puts "Generated paged_css.pdf"
|
186
|
+
|
187
|
+
# `open paged_css.pdf`
|
@@ -5,14 +5,10 @@ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
5
5
|
require 'benchmark'
|
6
6
|
require 'palapala'
|
7
7
|
|
8
|
-
debug = ARGV[0] == 'debug'
|
8
|
+
$debug = ARGV[0] == 'debug'
|
9
|
+
$save = ARGV[0] == 'save'
|
9
10
|
|
10
|
-
Palapala.
|
11
|
-
# config.headless_chrome_url = 'http://localhost:9222'
|
12
|
-
config.debug = debug
|
13
|
-
config.defaults.merge! scale: 0.75, format: :A4
|
14
|
-
config.chrome_headless_shell_version = 'canary'
|
15
|
-
end
|
11
|
+
Palapala.debug = $debug
|
16
12
|
|
17
13
|
# @param concurrency Number of concurrent threads
|
18
14
|
# @param iterations Number of iterations per thread
|
@@ -22,7 +18,8 @@ def benchmark(concurrency, iterations)
|
|
22
18
|
Thread.new do
|
23
19
|
iterations.times do |j|
|
24
20
|
doc = "Hello #{i}, world #{j}! #{Time.now}."
|
25
|
-
Palapala::Pdf.new(doc)
|
21
|
+
pdf = Palapala::Pdf.new(doc)
|
22
|
+
$save ? pdf.save("tmp/benchmark_#{i}_#{j}.pdf") : pdf.binary_data
|
26
23
|
end
|
27
24
|
end
|
28
25
|
end
|
@@ -32,18 +29,9 @@ def benchmark(concurrency, iterations)
|
|
32
29
|
time
|
33
30
|
end
|
34
31
|
|
35
|
-
puts
|
32
|
+
puts "Warmup..."
|
33
|
+
benchmark(1, 5)
|
34
|
+
puts "Starting benchmark..."
|
36
35
|
benchmark(1, 10)
|
37
|
-
|
38
|
-
# benchmark(1, 20)
|
39
|
-
benchmark(2, 10)
|
40
|
-
# benchmark(4, 5)
|
41
|
-
# benchmark(5, 4)
|
42
|
-
# benchmark(20, 1)
|
43
|
-
|
44
|
-
# benchmark(1, 320)
|
45
|
-
# benchmark(2, 320 / 2)
|
36
|
+
benchmark(2, 20 / 2)
|
46
37
|
benchmark(4, 320 / 4)
|
47
|
-
# benchmark(8, 320 / 8)
|
48
|
-
# benchmark(20, 2)
|
49
|
-
# benchmark(40, 1)
|
@@ -82,7 +82,7 @@ module Palapala
|
|
82
82
|
# Display the version
|
83
83
|
system("#{chrome_path} --version") if Palapala.debug
|
84
84
|
# Launch chrome-headless-shell with the --remote-debugging-port parameter
|
85
|
-
params = [ "--disable-gpu", "--remote-debugging-port=9222" ]
|
85
|
+
params = [ "--disable-gpu", "--remote-debugging-port=9222", "--remote-debugging-address=0.0.0.0" ]
|
86
86
|
params.merge!(Palapala.chrome_params) if Palapala.chrome_params
|
87
87
|
pid = if Palapala.debug
|
88
88
|
spawn(chrome_path, *params)
|
data/lib/palapala/pdf.rb
CHANGED
@@ -12,8 +12,10 @@ module Palapala
|
|
12
12
|
#
|
13
13
|
# @param content [String] the HTML content to convert to PDF
|
14
14
|
# @param footer_html [String] the HTML content for the footer
|
15
|
+
# @param footer [String] the footer content that is centered
|
15
16
|
# @param generate_tagged_pdf [Boolean] whether to generate a tagged PDF
|
16
17
|
# @param header_html [String] the HTML content for the header
|
18
|
+
# @param header [String] the header content that is centered
|
17
19
|
# @param landscape [Boolean] whether to use landscape orientation
|
18
20
|
# @param margin_bottom [Integer] the bottom margin in inches
|
19
21
|
# @param margin_left [Integer] the left margin in inches
|
@@ -27,8 +29,10 @@ module Palapala
|
|
27
29
|
# @param scale [Float] the scale of the PDF rendering
|
28
30
|
def initialize(content,
|
29
31
|
footer_template: nil,
|
32
|
+
footer: nil,
|
30
33
|
generate_tagged_pdf: nil,
|
31
34
|
header_template: nil,
|
35
|
+
header: nil,
|
32
36
|
landscape: nil,
|
33
37
|
margin_bottom: nil,
|
34
38
|
margin_left: nil,
|
@@ -42,8 +46,10 @@ module Palapala
|
|
42
46
|
scale: nil)
|
43
47
|
@content = content || raise(ArgumentError, "Content is required and can't be nil")
|
44
48
|
@opts = {}
|
45
|
-
|
46
|
-
|
49
|
+
raise(ArgumentError, "Either footer or footer_template is expected") if !footer_template.nil? && !footer.nil?
|
50
|
+
raise(ArgumentError, "Either header or header_template is expected") if !header_template.nil? && !header.nil?
|
51
|
+
@opts[:headerTemplate] = header_template || hf_template(from: header) || Palapala.defaults[:header_template]
|
52
|
+
@opts[:footerTemplate] = footer_template || hf_template(from: footer) || Palapala.defaults[:footer_template]
|
47
53
|
@opts[:pageRanges] = page_ranges || Palapala.defaults[:page_ranges]
|
48
54
|
@opts[:generateTaggedPDF] = generate_tagged_pdf || Palapala.defaults[:generate_tagged_pdf]
|
49
55
|
@opts[:paperWidth] = paper_width || Palapala.defaults[:paper_width]
|
@@ -56,11 +62,25 @@ module Palapala
|
|
56
62
|
@opts[:preferCSSPageSize] = prefer_css_page_size || Palapala.defaults[:prefer_css_page_size]
|
57
63
|
@opts[:printBackground] = print_background || Palapala.defaults[:print_background]
|
58
64
|
@opts[:scale] = scale || Palapala.defaults[:scale]
|
59
|
-
@opts[:displayHeaderFooter] = true
|
65
|
+
@opts[:displayHeaderFooter] = (@opts[:headerTemplate] || @opts[:footerTemplate]) ? true : false
|
60
66
|
@opts[:encoding] = :binary
|
61
67
|
@opts.compact!
|
62
68
|
end
|
63
69
|
|
70
|
+
def hf_template(from:)
|
71
|
+
return if from.nil?
|
72
|
+
style = <<~HTML.freeze
|
73
|
+
<style>
|
74
|
+
#header, #footer {
|
75
|
+
font-size: 10pt;
|
76
|
+
display: flex;
|
77
|
+
justify-content: center;
|
78
|
+
}
|
79
|
+
</style>
|
80
|
+
HTML
|
81
|
+
style + from
|
82
|
+
end
|
83
|
+
|
64
84
|
# Render the PDF content to a binary string.
|
65
85
|
#
|
66
86
|
# The params from the initializer are converted to the expected casing and merged with the options passed to this method.
|
data/lib/palapala/renderer.rb
CHANGED
@@ -44,6 +44,9 @@ module Palapala
|
|
44
44
|
def on_message(e)
|
45
45
|
puts "Received: #{e.data[0..64]}" if Palapala.debug
|
46
46
|
@response = JSON.parse(e.data) # Parse the JSON response
|
47
|
+
if @response["error"] # Raise an error if the response contains an error
|
48
|
+
raise "#{@response["error"]["message"]}: #{@response["error"]["data"]} (#{@response["error"]["code"]})"
|
49
|
+
end
|
47
50
|
end
|
48
51
|
|
49
52
|
# Update the current ID to the next ID (increment by 1)
|
@@ -100,8 +103,20 @@ module Palapala
|
|
100
103
|
Base64.decode64(result["data"])
|
101
104
|
end
|
102
105
|
|
106
|
+
def ping
|
107
|
+
result = send_command_and_wait_for_result("Runtime.evaluate", params: { expression: "1 + 1" })
|
108
|
+
raise "Ping failed" unless result["result"]["value"] == 2
|
109
|
+
end
|
110
|
+
|
103
111
|
def self.html_to_pdf(html, params: {})
|
104
112
|
thread_local_instance.html_to_pdf(html, params: params)
|
113
|
+
rescue StandardError
|
114
|
+
reset # Reset the renderer on error, the websocket connection might be broken
|
115
|
+
thread_local_instance.html_to_pdf(html, params: params) # Retry (once)
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.ping
|
119
|
+
thread_local_instance.ping
|
105
120
|
end
|
106
121
|
|
107
122
|
def close
|
data/lib/palapala/version.rb
CHANGED
data/lib/palapala.rb
CHANGED
@@ -27,12 +27,8 @@ module Palapala
|
|
27
27
|
attr_accessor :chrome_headless_shell_version
|
28
28
|
end
|
29
29
|
self.debug = false
|
30
|
-
self.defaults = {
|
31
|
-
|
32
|
-
footer_template: "<div></div>"
|
33
|
-
# footer_template: '<div style="text-align: center; font-size: 12pt; width: 100%;">Generated with Palapala PDF</div>'
|
34
|
-
}
|
35
|
-
self.headless_chrome_path = nil
|
30
|
+
self.defaults = { print_background: true, prefer_css_page_size: true, margin_left: 0, margin_right: 0, margin_top: 0, margin_bottom: 0 }
|
31
|
+
self.headless_chrome_path = ENV.fetch("HEADLESS_CHROME_PATH", nil)
|
36
32
|
self.headless_chrome_url = ENV.fetch("HEADLESS_CHROME_URL", "http://localhost:9222")
|
37
33
|
self.chrome_headless_shell_version = ENV.fetch("CHROME_HEADLESS_SHELL_VERSION", "stable")
|
38
34
|
end
|
data/paged_css.pdf
ADDED
Binary file
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: palapala_pdf
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.12
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Koen Handekyn
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-09-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: base64
|
@@ -56,8 +56,16 @@ files:
|
|
56
56
|
- assets/images/logo-variant2.webp
|
57
57
|
- assets/images/logo.webp
|
58
58
|
- bin/chrome-headless-server
|
59
|
+
- doc/installing_node.md
|
60
|
+
- doc/paged_css.md
|
61
|
+
- examples/all.rb
|
62
|
+
- examples/chrome_base_header_footer_template.html
|
63
|
+
- examples/headers_and_footers.pdf
|
59
64
|
- examples/headers_and_footers.rb
|
65
|
+
- examples/js_based_rendering.pdf
|
60
66
|
- examples/js_based_rendering.rb
|
67
|
+
- examples/paged_css.pdf
|
68
|
+
- examples/paged_css.rb
|
61
69
|
- examples/performance_benchmark.rb
|
62
70
|
- lib/palapala.rb
|
63
71
|
- lib/palapala/chrome_process.rb
|
@@ -66,6 +74,7 @@ files:
|
|
66
74
|
- lib/palapala/version.rb
|
67
75
|
- lib/palapala/web_socket_client.rb
|
68
76
|
- lib/palapala_pdf.rb
|
77
|
+
- paged_css.pdf
|
69
78
|
- palapala_pdf.gemspec
|
70
79
|
homepage: https://github.com/palapala-app/palapala_pdf
|
71
80
|
licenses:
|