bidi2pdf 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: 3d7fa3c853f53e21a110cadb25fcd66bf97b890eb00663f90e73e8ee9d1ce07e
4
- data.tar.gz: 5900568c47f526e9b00d95a15f8293abeaee88f6c0f323bc6e79cdcb3aba38ef
3
+ metadata.gz: 134f35256652da3e5e6c8e383e97c98779a1e2665798dbfa5d133bc85130b8cc
4
+ data.tar.gz: dd106af3e757d26ba3d935bbe54d0249402280a84f1b1a8e27855d19b4cd155c
5
5
  SHA512:
6
- metadata.gz: 9ecffa81a6358c413dd24b1cef48c3b2282518f5934710558b2ea834a362b198d5a1e3f01a2b3eb8ed163dedffaae61fe2bb0c5f3edec5e3850ad375daf10304
7
- data.tar.gz: 0b9d4b19f9d2d01a8babfe2a13c4256b9f93d34f2fcdc1ea2942fb780ce4ad02962050f1adffd9d2c5731e2e6c6d1dd83deaa3d31b348157197c982701a8995b
6
+ metadata.gz: 15d9df827cefb697a44f78945f5fc20866319c7fc6a403f5ad58817f239deca9857fc0923e0a7faf582dac6d7f0770236310cca8bd58aab138ab6499749c6d37
7
+ data.tar.gz: '09b043e8da1b79a4cc95c708c32afa78b00fa3f6edbedd2e0dacfdd562aff1a385be50f36cc78951a771b1b63c775eac11980dec07d1998476b9b247d6114377'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,4 @@
1
1
  <!-- generated by git-cliff start -->
2
-
3
2
  # Changelog
4
3
 
5
4
  All notable changes to this project will be documented in this file.
@@ -7,10 +6,65 @@ All notable changes to this project will be documented in this file.
7
6
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
8
7
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
9
8
 
10
- [unreleased]: https://github.com/dieter-medium/bidi2pdf/compare/v0.1.8..HEAD
9
+ [unreleased]: https://github.com///compare/v0.1.10..HEAD
11
10
 
12
11
  <!-- generated by git-cliff end -->
13
12
 
13
+ ## [0.1.10] - 2025-06-18
14
+
15
+ ### 💄 Style
16
+
17
+ - Improve readability of conditional statements
18
+
19
+ ### 📝 Docs
20
+
21
+ - Update README with Quick Start section
22
+ - Add Table of Contents and architecture diagram
23
+ - Add high level overview diagram to README
24
+ - Update example URLs in README for clarity
25
+
26
+ ### 🔄 Changed
27
+
28
+ - Merge pull request #19 from dieter-medium/enhance-docs
29
+ - Merge pull request #18 from dieter-medium/enhance-docs
30
+ - Merge pull request #13 from dieter-medium/dependabot/bundler/main/base64-0.3.0
31
+ - Merge branch 'main' into dependabot/bundler/main/base64-0.3.0
32
+ - Merge pull request #14 from dieter-medium/dependabot/bundler/main/rake-13.3.0
33
+ - Merge branch 'main' into dependabot/bundler/main/rake-13.3.0
34
+ - Merge pull request #12 from dieter-medium/dependabot/bundler/main/rspec-3.13.1
35
+ - Merge pull request #11 from dieter-medium/dependabot/bundler/main/rubocop-1.75.7
36
+ - Merge branch 'main' into dependabot/bundler/main/rubocop-1.75.7
37
+ - Merge pull request #10 from dieter-medium/dependabot/bundler/main/json-2.12.2
38
+ - Merge branch 'main' into dependabot/bundler/main/json-2.12.2
39
+ - Merge pull request #9 from dieter-medium/dependabot/bundler/main/diff-lcs-1.6.2
40
+ - Merge pull request #7 from dieter-medium/dependabot/bundler/main/rubocop-1.75.6
41
+ - Merge pull request #8 from dieter-medium/dependabot/bundler/main/json-2.12.0
42
+ - Merge pull request #6 from dieter-medium/dependabot/bundler/main/rbs-3.9.4
43
+ - Merge pull request #5 from dieter-medium/dependabot/bundler/main/chromedriver-binary-0.1.3
44
+ - Merge pull request #4 from dieter-medium/add-more-default-chrome-args
45
+ - Merge pull request #3 from dieter-medium/make-configuration-rw
46
+ - Merge pull request #2 from dieter-medium/add-test-helpers
47
+ - Merge pull request #1 from dieter-medium/testing-vips
48
+
49
+ ### 🔧 Build
50
+
51
+ - Bump rspec from 3.13.0 to 3.13.1
52
+ - Bump rake from 13.2.1 to 13.3.0
53
+ - Update base64 requirement from ~> 0.2.0 to >= 0.2, < 0.4
54
+ - Bump rubocop from 1.75.3 to 1.76.2
55
+ - Bump diff-lcs from 1.6.1 to 1.6.2
56
+ - Bump json from 2.10.2 to 2.12.2
57
+ - Bump rbs from 3.9.2 to 3.9.4
58
+ - Bump chromedriver-binary from 0.1.2 to 0.1.3
59
+
60
+ ### 🚀 Added
61
+
62
+ - Add CDP session handling for PDF generation
63
+ - Expand default Chrome arguments for sessions
64
+ - Add configuration setter for TestHelpers
65
+ - Add test helpers for PDF testing and handling
66
+ - Add image extraction and similarity checking
67
+
14
68
  ## [0.1.9] - 2025-05-04
15
69
 
16
70
  ### 🎨 Refactored
@@ -200,8 +254,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
200
254
 
201
255
  - Initial release
202
256
 
257
+ ## Changelog
203
258
 
204
- - [unreleased](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.9..HEAD)
259
+ - [unreleased](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.10..HEAD)
260
+ - [0.1.10](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.9..V0.1.10)
205
261
  - [0.1.9](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.8..v0.1.9)
206
262
  - [0.1.8](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.7..v0.1.8)
207
263
  - [0.1.7](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.6..v0.1.7)
data/README.md CHANGED
@@ -14,6 +14,23 @@ Bidi2pdf gives you **precision, flexibility, and full control**.
14
14
 
15
15
  ---
16
16
 
17
+ ## 📚 Table of Contents
18
+
19
+ 1. [Key Features](#key-features)
20
+ 2. [Quick Start](#quick-start)
21
+ 3. [Why BiDi?](#why-bidi-instead-of-cdp)
22
+ 4. [Installation](#installation)
23
+ 5. [CLI Usage](#cli-usage)
24
+ 6. [Library API](#library-api)
25
+ 7. [Architecture](#architecture)
26
+ 8. [Docker](#docker)
27
+ 9. [Configuration Options](#configuration-options)
28
+ 10. [Rails Integration](#rails-integration)
29
+ 11. [Test Helpers](#test-helpers)
30
+ 12. [Development](#development)
31
+ 13. [Contributing](#contributing)
32
+ 14. [License](#license)
33
+
17
34
  ## ✨ Key Features
18
35
 
19
36
  ✅ **One-liner CLI** – From URL to PDF in a single command
@@ -27,6 +44,25 @@ Bidi2pdf gives you **precision, flexibility, and full control**.
27
44
 
28
45
  ---
29
46
 
47
+ ## ⚡ Quick Start
48
+
49
+ Get up and running in three easy steps:
50
+
51
+ ```bash
52
+ # 1. Install the gem (system-wide)
53
+ gem install bidi2pdf
54
+
55
+ # 2. Render any page to PDF
56
+ bidi2pdf render --url https://example.com --output example.pdf
57
+
58
+ # 3. Open the PDF (macOS shown; use xdg-open on Linux)
59
+ open example.pdf
60
+ ```
61
+
62
+ > **Bundler users** – Add it to your project with `bundle add bidi2pdf`.
63
+
64
+ ---
65
+
30
66
  ## 🚀 Installation
31
67
 
32
68
  ### Bundler
@@ -54,14 +90,14 @@ gem install bidi2pdf
54
90
  ### Command-line
55
91
 
56
92
  ```bash
57
- bidi2pdf render --url https://example.com --output example.pdf
93
+ bidi2pdf render --url https://example.com/invoice/14432423 --output example.pdf
58
94
  ```
59
95
 
60
96
  ### Advanced CLI Options
61
97
 
62
98
  ```bash
63
99
  bidi2pdf render \
64
- --url https://example.com \
100
+ --url https://example.com/invoice/14432423 \
65
101
  --output example.pdf \
66
102
  --cookie session=abc123 \
67
103
  --header X-API-KEY=token \
@@ -81,7 +117,7 @@ bidi2pdf render \
81
117
  require 'bidi2pdf'
82
118
 
83
119
  launcher = Bidi2pdf::Launcher.new(
84
- url: 'https://example.com',
120
+ url: 'https://example.com/invoice/14432423',
85
121
  output: 'example.pdf',
86
122
  cookies: { 'session' => 'abc123' },
87
123
  headers: { 'X-API-KEY' => 'token' },
@@ -99,7 +135,7 @@ launcher.launch
99
135
  require "bidi2pdf"
100
136
 
101
137
  Bidi2pdf::DSL.with_tab(headless: true) do |tab|
102
- tab.navigate_to("https://example.com")
138
+ tab.navigate_to("https://example.com/invoice/14432423")
103
139
  tab.wait_until_network_idle
104
140
  tab.print("example.pdf")
105
141
  end
@@ -145,7 +181,7 @@ tab.basic_auth(url_patterns: [{ type: "pattern", protocol: "https", hostname: "e
145
181
  username: "username", password: "secret")
146
182
 
147
183
  # 4. Render PDF
148
- tab.navigate_to "https://example.com"
184
+ tab.navigate_to "https://example.com/invoice/14432423"
149
185
 
150
186
  # Alternative: send html code to the browser
151
187
  # tab.render_html_content("<html>...</html>")
@@ -176,6 +212,48 @@ session.close
176
212
 
177
213
  ---
178
214
 
215
+ ## 🌐 Architecture
216
+
217
+ ```mermaid
218
+ %%{ init: {
219
+ "theme": "base",
220
+ "themeVariables": {
221
+ "primaryColor": "#E0E7FF",
222
+ "secondaryColor":"#FEF9C3",
223
+ "edgeLabelBackground":"#FFFFFF",
224
+ "fontSize":"14px",
225
+ "nodeBorderRadius":"6"
226
+ }
227
+ }
228
+ }%%
229
+ flowchart LR
230
+ %% ----- Ruby side ---------
231
+ A["fa:fa-gem Ruby Application"]
232
+ B["fa:fa-gem bidi2pdf<br/>Library"]
233
+ %% ----Chrome environment -----------
234
+ subgraph C["fa:fa-chrome Chrome Environment"]
235
+ direction TB
236
+ C1["fa:fa-chrome Local Chrome<br/>(sub-process)"]
237
+ C2["fa:fa-docker Docker Chrome<br/>(remote)"]
238
+ end
239
+
240
+ D[[PDF File]]
241
+ %% ---- Data / control flows ------
242
+ A -- " HTML / URL + JS / CSS " --> B
243
+ B -- " WebDriver BiDi " --> C1
244
+ B -- " WebDriver BiDi " --> C2
245
+ C1 -- " PDF bytes " --> B
246
+ C2 -- " PDF bytes " --> B
247
+ B -- " PDF " --> D
248
+ %% --- Optional extra styling classes (for future tweaks) ---
249
+ classDef ruby fill:#E0E7FF,stroke:#6366F1,color:#1E1B4B;
250
+ classDef chrome fill:#FEF9C3,stroke:#F59E0B,color:#78350F;
251
+ class A,B ruby;
252
+ class C1,C2 chrome;
253
+ ```
254
+
255
+ ---
256
+
179
257
  ## 🐳 Docker Support
180
258
 
181
259
  ### 🛠️ Build & Run Locally
@@ -191,7 +269,7 @@ docker build -t bidi2pdf -f docker/Dockerfile .
191
269
  docker run -it --rm \
192
270
  -v ./output:/reports \
193
271
  bidi2pdf \
194
- bidi2pdf render --url=https://example.com --output /reports/example.pdf
272
+ bidi2pdf render --url=https://example.com/invoice/14432423 --output /reports/example.pdf
195
273
 
196
274
  ```
197
275
 
@@ -203,7 +281,7 @@ Grab it directly from [Docker Hub](https://hub.docker.com/r/dieters877565/bidi2p
203
281
  docker run -it --rm \
204
282
  -v ./output:/reports \
205
283
  dieters877565/bidi2pdf:main-slim \
206
- bidi2pdf render --url=https://example.com --output /reports/example.pdf
284
+ bidi2pdf render --url=https://example.com/invoice/14432423 --output /reports/example.pdf
207
285
  ```
208
286
 
209
287
  ✅ Tip: Mount your local directory (e.g. ./output) to /reports in the container to easily access the generated PDFs.
@@ -271,6 +349,67 @@ visit: [https://github.com/dieter-medium/bidi2pdf-rails](https://github.com/diet
271
349
 
272
350
  ---
273
351
 
352
+ ## 🧪 Test Helpers
353
+
354
+ Bidi2pdf provides a suite of RSpec helpers (activated with `pdf: true`) to
355
+ simplify PDF-related testing:
356
+
357
+ ### SpecPathsHelper
358
+
359
+ – `spec_dir` → returns your spec directory
360
+ – `tmp_dir` → returns your tmp directory
361
+ – `tmp_file(*parts)` → builds a tmp file path
362
+ – `random_tmp_dir(*dirs, prefix:)` → builds a random tmp directory
363
+
364
+ - `fixture_file(*parts)` → returns the path to a fixture file
365
+
366
+ ### PdfFileHelper
367
+
368
+ – `with_pdf_debug(pdf_data) { |data| … }` → on failure, writes PDF to disk
369
+ – `store_pdf_file(pdf_data, filename_prefix = "test")` → saves PDF and returns path
370
+
371
+ ### Rspec Matchers
372
+
373
+ - `have_pdf_page_count` → checks if the PDF has a specific number of pages
374
+ - `match_pdf_text` → checks if the PDF equals a specific text, after stripping whitespace and normalizing characters
375
+ - `contains_pdf_text` → checks if the PDF contains a specific text, after stripping whitespace and normalizing
376
+ characters, supporting regex
377
+ - `contains_pdf_image` → checks if the PDF contains a specific image
378
+
379
+ ### ChromedriverContainer
380
+
381
+ `require "bidi2pdf/test_helpers/testcontainers"` you can use the `chromedriver_container` helper to
382
+ start a ChromeDriver container for your tests. This is useful if you don't want to run ChromeDriver locally
383
+ or if you want to ensure a clean environment for your tests.
384
+
385
+ This also provides the helper methods:
386
+
387
+ - `session_url` → returns the session URL for the ChromeDriver container
388
+ - `chromedriver_container` → returns the Testcontainers container object
389
+ - `create_session` -> creates a `Bidi2pdf::Bidi::Session` object for the ChromeDriver container
390
+
391
+ With the environment variable `DISABLE_CHROME_SANDBOX` set to `true`, the container will run Chrome without
392
+ the sandbox. This is useful for CI environments where the sandbox may cause issues.
393
+
394
+ #### Example
395
+
396
+ ```ruby
397
+ require "bidi2pdf/test_helpers"
398
+ require "bidi2pdf/test_helpers/images" # <= for image matching, requires lib-vips
399
+ require "bidi2pdf/test_helpers/testcontainers" # <= requires testcontainers gem
400
+
401
+ RSpec.describe "PDF generation", :pdf, :chromedriver do
402
+ it "generates a PDF with the correct content" do
403
+ pdf_data = generate_pdf("https://example.com/invoice/14432423")
404
+ expect(pdf_data).to have_pdf_page_count(1)
405
+ expect(pdf_data).to match_pdf_text("Hello, world!")
406
+ expect(pdf_data).to contain_pdf_image(fixture_file("logo.png"))
407
+ end
408
+ end
409
+ ```
410
+
411
+ ---
412
+
274
413
  ## 🛠 Development
275
414
 
276
415
  ```bash
@@ -39,6 +39,7 @@ module Bidi2pdf
39
39
 
40
40
  private
41
41
 
42
+ # rubocop:disable Naming/PredicateMethod
42
43
  def handled_bad_credentials(navigation_id, network_id, url)
43
44
  return false unless network_ids.include?(network_id)
44
45
 
@@ -55,6 +56,8 @@ module Bidi2pdf
55
56
 
56
57
  true
57
58
  end
59
+
60
+ # rubocop:enable Naming/PredicateMethod
58
61
  end
59
62
  end
60
63
  end
@@ -371,13 +371,13 @@ module Bidi2pdf
371
371
  # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
372
372
  def print(outputfile = nil, print_options: { background: true }, &block)
373
373
  Bidi2pdf.notification_service.instrument("print.bidi2pdf") do |instrumentation_payload|
374
- cmd = Bidi2pdf::Bidi::Commands::BrowsingContextPrint.new context: browsing_context_id, print_options: print_options
374
+ cmd, extractor = build_command_and_extractor(print_options)
375
375
 
376
376
  instrumentation_payload[:cmd] = cmd
377
377
 
378
378
  client.send_cmd_and_wait(cmd) do |response|
379
379
  if response["result"]
380
- pdf_base64 = response["result"]["data"]
380
+ pdf_base64 = extractor.call response
381
381
 
382
382
  instrumentation_payload[:pdf_base64] = pdf_base64
383
383
 
@@ -404,6 +404,36 @@ module Bidi2pdf
404
404
 
405
405
  private
406
406
 
407
+ def build_command_and_extractor(print_options)
408
+ cmd_type = (print_options.delete(:cmd_type) || :bidi).to_sym
409
+
410
+ if cmd_type == :bidi
411
+ cmd = Bidi2pdf::Bidi::Commands::BrowsingContextPrint.new(
412
+ context: browsing_context_id,
413
+ print_options: print_options
414
+ )
415
+ extractor = ->(response) { response.dig("result", "data") }
416
+ else
417
+ cmd = Bidi2pdf::Bidi::Commands::PagePrint.new(
418
+ cdp_session: cdp_session,
419
+ print_options: print_options
420
+ )
421
+ extractor = ->(response) { response.dig("result", "result", "data") }
422
+ end
423
+
424
+ [cmd, extractor]
425
+ end
426
+
427
+ def cdp_session
428
+ @cdp_session ||= begin
429
+ cmd = Bidi2pdf::Bidi::Commands::CdpGetSession.new context: browsing_context_id
430
+ client.send_cmd_and_wait(cmd) do |response|
431
+ Bidi2pdf.logger.debug "CDP session: #{response.inspect}"
432
+ response["result"]["session"]
433
+ end
434
+ end
435
+ end
436
+
407
437
  def navigate_with_listeners(url)
408
438
  register_event_listeners
409
439
 
@@ -479,7 +509,7 @@ module Bidi2pdf
479
509
  const script = document.createElement('script');
480
510
  script.type = 'text/javascript';
481
511
 
482
- #{content ? "script.text = #{content.to_json};" : ""}
512
+ #{"script.text = #{content.to_json};" if content}
483
513
 
484
514
  script.addEventListener(
485
515
  'error',
@@ -489,12 +519,12 @@ module Bidi2pdf
489
519
  {once: true},
490
520
  );
491
521
 
492
- #{id ? "script.id = '#{id}';" : ""}
522
+ #{"script.id = '#{id}';" if id}
493
523
  #{js_src_part}
494
524
 
495
525
  document.head.appendChild(script);
496
526
 
497
- #{url ? "" : "resolve(script);"}
527
+ #{"resolve(script);" unless url}
498
528
  });
499
529
  JS
500
530
  end
@@ -515,7 +545,7 @@ module Bidi2pdf
515
545
  link.type = 'text/css';
516
546
  link.href = '#{url}';
517
547
  #{" "}
518
- #{id ? "link.id = '#{id}';" : ""}
548
+ #{"link.id = '#{id}';" if id}
519
549
  #{" "}
520
550
  link.addEventListener(
521
551
  'load',
@@ -544,9 +574,9 @@ module Bidi2pdf
544
574
  const style = document.createElement('style');
545
575
  style.type = 'text/css';
546
576
  #{" "}
547
- #{id ? "style.id = '#{id}';" : ""}
577
+ #{"style.id = '#{id}';" if id}
548
578
  #{" "}
549
- #{content ? "style.textContent = #{content.to_json};" : ""}
579
+ #{"style.textContent = #{content.to_json};" if content}
550
580
  #{" "}
551
581
  document.head.appendChild(style);
552
582
  resolve(style);
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ module Bidi
5
+ module Commands
6
+ class CdpGetSession
7
+ include Base
8
+
9
+ def initialize(context:)
10
+ @context = context
11
+ end
12
+
13
+ def params = { context: @context }
14
+
15
+ def method_name
16
+ "goog:cdp.getSession"
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "print_parameters_validator"
4
+
5
+ module Bidi2pdf
6
+ module Bidi
7
+ module Commands
8
+ class PagePrint
9
+ include Base
10
+
11
+ def initialize(cdp_session:, print_options:)
12
+ @cdp_session = cdp_session
13
+ @print_options = print_options || { background: true }
14
+
15
+ PrintParametersValidator.validate!(@print_options)
16
+
17
+ return unless @print_options[:page]&.key?(:format)
18
+
19
+ @print_options[:page] = Bidi2pdf.translate_paper_format @print_options[:page][:format]
20
+ end
21
+
22
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
23
+ def params
24
+ {
25
+ # https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF
26
+ method: "Page.printToPDF",
27
+ session: @cdp_session,
28
+ params: {
29
+ "printBackground" => @print_options[:background],
30
+
31
+ "marginTop" => cm_to_inch(@print_options.dig(:margin, :top) || 0),
32
+ "marginBottom" => cm_to_inch(@print_options.dig(:margin, :bottom) || 0),
33
+ "marginLeft" => cm_to_inch(@print_options.dig(:margin, :left) || 0),
34
+ "marginRight" => cm_to_inch(@print_options.dig(:margin, :right) || 0),
35
+ "landscape" => (@print_options[:orientation] || "portrait").to_sym == :landscape,
36
+
37
+ "paperWidth" => cm_to_inch(@print_options.dig(:page, :width)),
38
+ "paperHeight" => cm_to_inch(@print_options.dig(:page, :height)),
39
+ "pageRanges" => page_ranges_to_string(@print_options[:pageRanges]),
40
+ "scale" => @print_options[:scale] || 1.0,
41
+
42
+ "displayHeaderFooter" => @print_options[:display_header_footer],
43
+ "headerTemplate" => @print_options[:header_template] || "",
44
+ "footerTemplate" => @print_options[:footer_template] || "",
45
+
46
+ "preferCSSPageSize" => @print_options.fetch(:prefer_css_page_size, true),
47
+
48
+ "generateTaggedPDF" => @print_options.fetch(:generate_tagged_pdf, false),
49
+ "generateDocumentOutline" => @print_options.fetch(:generate_document_outline, false),
50
+
51
+ transferMode: "ReturnAsBase64"
52
+ }.compact
53
+ }
54
+ end
55
+
56
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
57
+
58
+ def method_name
59
+ "goog:cdp.sendCommand"
60
+ end
61
+
62
+ private
63
+
64
+ # rubocop:disable Naming/MethodParameterName
65
+ def cm_to_inch(cm)
66
+ return nil if cm.nil?
67
+
68
+ cm.to_f / 2.54
69
+ end
70
+
71
+ # rubocop:enable Naming/MethodParameterName
72
+
73
+ # rubocop:disable Metrics/CyclomaticComplexity
74
+ def page_ranges_to_string(input)
75
+ return nil if input.nil? || input.empty?
76
+
77
+ segments = input.map do |entry|
78
+ case entry
79
+ when Integer
80
+ entry.to_s
81
+ when String
82
+ raise ArgumentError, "Invalid page entry: #{entry.inspect}" unless entry =~ /\A\d+(-\d+)?\z/
83
+
84
+ entry
85
+ else
86
+ raise ArgumentError, "Unsupported page entry type: #{entry.class}"
87
+ end
88
+ end
89
+
90
+ # dedupe, sort by numeric start, and join
91
+ segments
92
+ .uniq
93
+ .sort_by { |seg| seg.split("-", 2).first.to_i }
94
+ .join(",")
95
+ end
96
+
97
+ # rubocop:enable Metrics/CyclomaticComplexity
98
+ end
99
+ end
100
+ end
101
+ end
@@ -37,6 +37,7 @@ module Bidi2pdf
37
37
  @params = params
38
38
  end
39
39
 
40
+ # rubocop:disable Naming/PredicateMethod
40
41
  def validate!
41
42
  raise ArgumentError, "params must be a Hash" unless @params.is_a?(Hash)
42
43
 
@@ -51,6 +52,8 @@ module Bidi2pdf
51
52
  true
52
53
  end
53
54
 
55
+ # rubocop:enable Naming/PredicateMethod
56
+
54
57
  private
55
58
 
56
59
  def validate_boolean(key)
@@ -79,7 +82,7 @@ module Bidi2pdf
79
82
  def validate_page_ranges
80
83
  return unless @params.key?(:pageRanges)
81
84
  unless @params[:pageRanges].is_a?(Array) &&
82
- @params[:pageRanges].all? { |v| v.is_a?(Integer) || v.is_a?(String) }
85
+ @params[:pageRanges].all? { |v| v.is_a?(Integer) || v.is_a?(String) }
83
86
  raise ArgumentError, ":pageRanges must be an array of integers or strings"
84
87
  end
85
88
  end
@@ -18,6 +18,8 @@ module Bidi2pdf
18
18
  require_relative "commands/browsing_context_close"
19
19
  require_relative "commands/browsing_context_navigate"
20
20
  require_relative "commands/browsing_context_print"
21
+ require_relative "commands/cdp_get_session"
22
+ require_relative "commands/page_print"
21
23
  require_relative "commands/session_subscribe"
22
24
  require_relative "commands/session_end"
23
25
  require_relative "commands/cancel_auth"
@@ -17,6 +17,7 @@ module Bidi2pdf
17
17
  @connection_latch.count_down
18
18
  end
19
19
 
20
+ # rubocop:disable Naming/PredicateMethod
20
21
  def wait_until_open(timeout:)
21
22
  return true if @connected
22
23
 
@@ -26,6 +27,8 @@ module Bidi2pdf
26
27
 
27
28
  true
28
29
  end
30
+
31
+ # rubocop:enable Naming/PredicateMethod
29
32
  end
30
33
  end
31
34
  end
@@ -32,7 +32,38 @@ module Bidi2pdf
32
32
  SUBSCRIBE_EVENTS = %w[script].freeze
33
33
 
34
34
  # Default Chrome arguments for the session.
35
- DEFAULT_CHROME_ARGS = %w[--disable-gpu --disable-popup-blocking --disable-hang-monitor].freeze
35
+ DEFAULT_CHROME_ARGS = [
36
+ "--allow-pre-commit-input", # Allow pre-commit input for form fields
37
+ "--disable-dev-shm-usage", # Disable /dev/shm usage; use /tmp instead
38
+ "--disable-gpu", # Disable GPU hardware acceleration; force software rendering
39
+ "--disable-popup-blocking", # Allow all pop-ups; bypass built-in popup blocker
40
+ "--disable-hang-monitor", # Disable “Page Unresponsive” / “Aw, Snap!” dialogs on hangs
41
+ "--disable-background-networking", # Turn off speculative/periodic network requests (DNS prefetch, Safe Browsing updates, etc.)
42
+ "--disable-background-timer-throttling", # Prevent JS timers from being throttled in background tabs
43
+ "--disable-client-side-phishing-detection", # Disable built-in phishing checks; rely only on server-side detection
44
+ "--disable-component-extensions-with-background-pages", # Block component extensions that run persistent background pages (PDF viewer, Translate, etc.)
45
+ "--disable-crash-reporter", # Disable crash-report uploads and UI
46
+ "--disable-default-apps", # Stop installation of Chrome’s default apps on a fresh profile
47
+ "--disable-infobars", # Suppress “Chrome is being controlled by automated test software” infobar (and similar)
48
+ "--disable-ipc-flooding-protection", # Turn off defenses against too-many IPC messages from renderers
49
+ "--disable-prompt-on-repost", # Skip “Confirm Form Resubmission” dialogs on page reloads after POST
50
+ "--disable-renderer-backgrounding", # Keep background tab renderers at full priority
51
+ "--disable-search-engine-choice-screen", # Skip first-run search engine selection UI
52
+ "--disable-sync", # Turn off all Google account sync (bookmarks, passwords, etc.)
53
+ "--enable-automation", # Expose WebDriver hooks (navigator.webdriver) for automation frameworks
54
+ "--export-tagged-pdf", # When printing to PDF, include tagged structure for accessibility
55
+ "--force-color-profile=srgb", # Force rendering to use the sRGB color profile
56
+ "--generate-pdf-document-outline", # Auto-generate PDF bookmarks/outlines from HTML headings, not supported by chrome/chromium https://issues.chromium.org/issues/41387522#comment48
57
+ "--metrics-recording-only", # Collect UMA metrics locally but never upload them
58
+ "--no-first-run", # Skip the “Welcome” or “What’s New” screens on fresh profiles
59
+ "--password-store=basic", # Use Chrome’s basic (in-profile) password storage vs. OS vault
60
+ "--use-mock-keychain", # On macOS, use a fake keychain for testing (don’t touch the real one)
61
+ "--disable-backgrounding-occluded-windows", # Prevent fully-occluded windows from being treated as background
62
+ "--disable-breakpad", # Disable the Breakpad crash-reporting library entirely
63
+ "--enable-features=PdfOopif", # Enable out-of-process iframe (OOPIF) architecture for PDF rendering
64
+ "--disable-features=Translate,AcceptCHFrame,MediaRouter,OptimizationHints,ProcessPerSiteUpToMainFrameThreshold,IsolateSandboxedIframes",
65
+ "--disable-extensions about:blank"
66
+ ].freeze
36
67
 
37
68
  # @return [URI] The URI of the session.
38
69
  attr_reader :session_uri
@@ -209,7 +240,7 @@ module Bidi2pdf
209
240
  # @return [Hash] The session request payload.
210
241
  def session_request
211
242
  session_chrome_args = chrome_args.dup
212
- session_chrome_args << "--headless" if @headless
243
+ session_chrome_args << "--headless=new" if @headless
213
244
 
214
245
  {
215
246
  "capabilities" => {