gkhtmltopdf 0.9.0 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bbc9b80ee14f3f1a0eae169fbd5b1b48a3a14f79bccf8c23cb445720ccf256ac
4
- data.tar.gz: 84596639fd0dfc5a82a5d9e16373d7b00652688f747a6c8e6fd71a9eb9280038
3
+ metadata.gz: 693828b2a9f004f9038b744c5d4a527a46f27ca55ad7c3d4638478aa2a747ade
4
+ data.tar.gz: 418727b131d4305641bcb66df639c961aabc73de334fd599b06d772f2b0557ba
5
5
  SHA512:
6
- metadata.gz: 587c445948f7c8552a51ad81bf98e94fb6d76d8699faa59cfbf4138633585b9a3b64cd2ad0a307723908473d7f3258434346771bc6f4921b88628f07da101b81
7
- data.tar.gz: 8d5edab4799522f1121a0fc2801fff8d5133a0cd58f4a98398f65b90b40dcabda48de29ebe442341e562b2ed27c045b7d312d01a78432bb72ef1e561057a56c2
6
+ metadata.gz: 8776f073004b0e5dba496316e4c95e18121784be2430b12f679dc900cfee41e7002befda8fe022f30739e8bdf33c76ce7c51cd625ed5e5095cc95f4eb60b4d3f
7
+ data.tar.gz: 3df53840690e009d5c39a18710867505eff22e0bbb0d6711dfc6f8cc44a73442c02f1d6f5b5c1c3ac9f5c5c327f61d377bbab9a73dafea0124ff4a20e378426b
data/CHANGELOG.md CHANGED
@@ -2,18 +2,40 @@
2
2
 
3
3
  All noteworthy changes to this project will be documented in this file.
4
4
 
5
+ ## 1.1.0 / 2026-06-29
6
+
7
+ - UA設定機能の追加
8
+ - mktmpdirの明示的な削除
9
+ - ubuntuでの動かし方とか修正
10
+
11
+ ## 1.0.0 / 2026-03-19
12
+
13
+ - Added serial processing for multiple files and URLs.
14
+ - 複数ファイル&URLの直列処理追加
15
+ - Separated the Gkhtmltopdf::Error into many.
16
+ - エラーを分離
17
+ - Add and Fixed test.
18
+ - テスト追加&修正
19
+
5
20
  ## 0.9.0 / 2026-03-13
6
21
 
7
22
  - Achieved 100% test coverage.🎉
23
+ - テストカバレッジ100%!
8
24
  - Fixed github url.
25
+ - github URL 修正
9
26
 
10
27
  ## 0.8.0 / 2026-03-10
11
28
 
12
29
  - Added parallel processing support with automatic free port checking.
30
+ - 空きポートの処理追加による並列実行追加
13
31
  - Added print options.
32
+ - プリントオプションの追加
14
33
  - Added Dockerfile (Debian compatibility verified).
34
+ - Dockerfile追加(Debian互換検証)
15
35
  - Added error message display when PATH errors occur.
36
+ - PATH関連のエラーメッセージ追加
16
37
 
17
38
  ## 0.1.0 / 2026-01-09
18
39
 
19
40
  - Initial release.
41
+ - 初期リリース
data/README.md CHANGED
@@ -5,6 +5,11 @@ Gkhtmltopdf is mean Gecko HTML to PDF converter.
5
5
  Developed as an alternative to wkhtmltopdf.
6
6
  This gem converts HTML to PDF using Firefox's Geckodriver.
7
7
 
8
+ [![Gem Version](https://badge.fury.io/rb/gkhtmltopdf.svg)](https://badge.fury.io/rb/gkhtmltopdf)
9
+ ![Gem Total Downloads](https://img.shields.io/gem/dt/gkhtmltopdf)
10
+ ![GitHub License](https://img.shields.io/github/license/fantasia-tech/gkhtmltopdf-rb)
11
+ ![Rspec](https://github.com/fantasia-tech/gkhtmltopdf-rb/actions/workflows/main.yml/badge.svg)
12
+
8
13
  ---
9
14
 
10
15
  ## How to
@@ -12,49 +17,78 @@ This gem converts HTML to PDF using Firefox's Geckodriver.
12
17
  ### 1. Install
13
18
 
14
19
  1. [Firefox](https://www.firefox.com)
15
- - for Ubuntu
16
- ```Ubuntu
17
- $ apt install -y firefox
18
- ```
19
- - for Debian
20
- ```bash
21
- $ apt install -y firefox-esr
22
- ```
20
+ - Ubuntu
21
+ > The snap does not work correctly, so please install it from the [official source](https://support.mozilla.org/en-US/kb/install-firefox-linux).
22
+ ```bash
23
+ $ wget -q https://packages.mozilla.org/apt/repo-signing-key.gpg -O- | sudo tee /etc/apt/keyrings/packages.mozilla.org.asc > /dev/null
24
+ $ echo "deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main" | tee -a /etc/apt/sources.list.d/mozilla.list > /dev/null
25
+ $ tee /etc/apt/preferences.d/mozilla > /dev/null << EOF
26
+ Package: *
27
+ Pin: origin packages.mozilla.org
28
+ Pin-Priority: 1000
29
+ EOF
30
+ $ apt install -y firefox
31
+ $ apt install -y fonts-noto # recommended
32
+ ```
33
+ - Debian
34
+ ```bash
35
+ $ apt install -y firefox-esr
36
+ $ apt install -y fonts-noto # recommended
37
+ ```
38
+
23
39
  2. [geckodriver](https://github.com/mozilla/geckodriver)
24
- - for Linux(Ubuntu / Debian)
25
- ```bash
26
- $ wget "https://github.com/mozilla/geckodriver/releases/download/v0.36.0/geckodriver-v0.36.0-linux64.tar.gz" -O /tmp/geckodriver.tar.gz
27
- $ tar -xzf /tmp/geckodriver.tar.gz -C /usr/local/bin
28
- ```
29
- 3. gem install
30
- - bundler
31
- ```bash
32
- $ bundle add gkhtmltopdf
33
- ```
34
- - other
35
- ```bash
36
- $ gem install gkhtmltopdf
37
- ```
40
+ - Linux (Ubuntu / Debian)
41
+ ```bash
42
+ $ wget "https://github.com/mozilla/geckodriver/releases/download/v0.36.0/geckodriver-v0.36.0-linux64.tar.gz" -O /tmp/geckodriver.tar.gz
43
+ $ tar -xzf /tmp/geckodriver.tar.gz -C /usr/local/bin
44
+ ```
38
45
 
39
- ---
46
+ 3. gem install
47
+ - bundler
48
+ ```bash
49
+ $ bundle add gkhtmltopdf
50
+ ```
51
+ - other
52
+ ```bash
53
+ $ gem install gkhtmltopdf
54
+ ```
40
55
 
41
56
  ### 2. Using
42
57
 
43
- #### ruby
58
+ #### Ruby
44
59
 
45
60
  > **⚠️ Security Warning for Web Frameworks (e.g., Ruby on Rails):**
46
61
  > If you are accepting URLs from untrusted users, you must implement strict SSRF protection. Do not pass user-input URLs directly without network-level isolation. Please read the [SSRF](#what-is-ssrf) section below for details.
47
62
 
48
63
  ```ruby
64
+ require 'gkhtmltopdf'
49
65
  # over network
50
66
  Gkhtmltopdf.convert('https://example.com', 'example_com.pdf')
51
67
  # local file
52
68
  Gkhtmltopdf.convert('file:///foo/bar/test.html', 'local.pdf')
53
69
  # with option (print background)
54
70
  Gkhtmltopdf.convert('https://f6a.net/oss/', 'with_bg.pdf', print_options: {background: true})
71
+ # with option (set custom user-agent)
72
+ Gkhtmltopdf.convert('https://f6a.net/oss/', 'ua.pdf', user_agent: 'YOUR USER AGENT')
55
73
  ```
56
74
 
57
- #### shell
75
+ Additionally, in version 1.0.0 we added the following syntax.
76
+ If you want to generate multiple PDFs faster, use this:
77
+
78
+ ```ruby
79
+ require 'gkhtmltopdf'
80
+ Gkhtmltopdf.open do |gkh2p|
81
+ gkh2p.save_pdf('https://example.com', 'example_com.pdf')
82
+ gkh2p.save_pdf('file:///foo/bar/test.html', 'local.pdf')
83
+ gkh2p.save_pdf('https://f6a.net/oss/', 'with_bg.pdf', print_options: {background: true})
84
+ end
85
+ # set custom user-agent
86
+ Gkhtmltopdf.open(user_agent: 'YOUR USER AGENT') do |gkh2p|
87
+ gkh2p.save_pdf('https://f6a.net/oss/', 'ua.pdf')
88
+ end
89
+ ```
90
+
91
+ #### Shell
58
92
 
59
93
  ```bash
60
94
  # over network
@@ -63,6 +97,8 @@ $ gkhtmltopdf https://example.com/ example_com.pdf
63
97
  $ gkhtmltopdf /foo/bar/test.html local.pdf
64
98
  # with option (print background)
65
99
  $ gkhtmltopdf https://f6a.net/oss/ with_bg.pdf --background
100
+ # with option (set custom user-agent)
101
+ $ gkhtmltopdf https://f6a.net/oss/ ua.pdf --user-agent "YOUR USER AGENT"
66
102
  # other option
67
103
  $ gkhtmltopdf --help
68
104
  ```
@@ -88,6 +124,39 @@ Attackers could potentially generate PDFs of internal network resources (e.g., `
88
124
 
89
125
  ---
90
126
 
127
+ ## Expected Errors
128
+
129
+ The following errors inherit `Gkhtmltopdf::Error`, so you can handle them as follows:
130
+
131
+ ```ruby
132
+ begin
133
+ Gkhtmltopdf.convert('ftp://example.com', 'example_com.pdf')
134
+ rescue Gkhtmltopdf::Error => e
135
+ puts e.class # -> Gkhtmltopdf::URLSchemeInvalid
136
+ puts e.message # -> Invalid URL scheme: (ftp)
137
+ end
138
+ ```
139
+
140
+ ### Gkhtmltopdf::PathUnresolvedError
141
+
142
+ Firefox or Geckodriver path unresolved. Follow the installation steps in [1. Install](#1-install) to set up.
143
+
144
+ ### Gkhtmltopdf::URLSchemeInvalid
145
+
146
+ Raised when the URL scheme is invalid (e.g., `ftp://`, `about://`) or the hostname is missing (e.g., `f6a.net`).
147
+
148
+ ### Gkhtmltopdf::BrowserError
149
+
150
+ Response from Firefox/Geckodriver is not as expected.
151
+
152
+ ---
153
+
154
+ ## Documents
155
+
156
+ - [ForDeveloper](/docs/ForDeveloper.md)
157
+
158
+ ---
159
+
91
160
  ## Acknowledgments & Third-Party Licenses
92
161
 
93
162
  This gem acts as a wrapper and communicates with the following external open-source tools.
data/TODO.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # TODO
2
2
 
3
- ## 完了
3
+ ## 完了 v0.9.0
4
4
 
5
5
  - [x] 並列処理対応(自動空きportチェック)
6
6
  - [x] 印刷オプションの追加
@@ -8,11 +8,29 @@
8
8
  - [x] PATHエラー時の表示追加
9
9
  - [x] 入力値検証
10
10
  - [x] テストカバレッジ100%
11
+ - [x] RubyGemsで公開
12
+ - [x] 多言語対応について
11
13
 
12
- ## 未了
14
+ ## 完了 v1.0.0
13
15
 
14
- - [ ] RubyGemsで公開
15
- - [ ] UA設定機能
16
- - [ ] ポート範囲設定
17
- - [ ] 複数ファイル&URLの直列実行による高速化
18
- - [ ] configファイルからオプションを設定
16
+ - [x] 複数ファイル&URLの直列実行による高速化
17
+ - [x] エラー処理
18
+ - [x] 起動時の待機時間オプション
19
+
20
+ ## 完了 v1.1.0
21
+
22
+ - [x] UA設定機能の追加
23
+ - [x] mktmpdirの明示的な削除
24
+
25
+ ## つぎ v1.2.0
26
+
27
+ - [ ] 設定のStruct化?(default切り出し)
28
+ - [ ] pdfをsaveせずバイナリを直接返却するメソッドの追加
29
+ - [ ] 肥大化したconvert.rbの分割( `Gkhtmltopdf::PDF` とかつくるか)
30
+
31
+ ## 検討中
32
+
33
+ - [ ] ポート範囲設定?
34
+ - [ ] YARD追加
35
+ - [ ] FireFox timeout
36
+ - [ ] configファイルからオプションを設定?
@@ -1,8 +1,13 @@
1
+ # escape=\
2
+ # syntax=docker/dockerfile:1.7
1
3
  FROM ruby:3.2-slim
2
4
 
3
5
  RUN apt-get update
4
6
  RUN apt-get install -y git wget xz-utils build-essential libyaml-dev
5
7
 
8
+ # Install Noto Fonts
9
+ RUN apt-get install -y fonts-noto
10
+
6
11
  # Install Firefox
7
12
  RUN apt-get install -y firefox-esr
8
13
 
@@ -0,0 +1,28 @@
1
+ FROM ubuntu:noble
2
+
3
+ RUN apt-get update
4
+ RUN apt-get install -y ruby3.2 bundler
5
+ RUN apt-get install -y git wget xz-utils build-essential libyaml-dev
6
+
7
+ # Install Noto Fonts
8
+ RUN apt-get install -y fonts-noto
9
+
10
+ # Install Firefox
11
+ RUN wget -q https://packages.mozilla.org/apt/repo-signing-key.gpg -O- | tee /etc/apt/keyrings/packages.mozilla.org.asc > /dev/null
12
+ RUN echo "deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main" | tee -a /etc/apt/sources.list.d/mozilla.list > /dev/null
13
+ RUN tee /etc/apt/preferences.d/mozilla > /dev/null << EOF
14
+ Package: *
15
+ Pin: origin packages.mozilla.org
16
+ Pin-Priority: 1000
17
+ EOF
18
+ RUN apt-get update
19
+ RUN apt-get install -y firefox
20
+
21
+ # Install Geckodriver
22
+ RUN wget "https://github.com/mozilla/geckodriver/releases/download/v0.36.0/geckodriver-v0.36.0-linux64.tar.gz" -O geckodriver.tar.gz
23
+ RUN tar -xzf geckodriver.tar.gz -C /usr/local/bin
24
+
25
+ COPY . /app
26
+ WORKDIR /app
27
+ RUN bundle install
28
+ CMD ["bundle", "exec", "rspec"]
@@ -0,0 +1,17 @@
1
+ # for Developer
2
+
3
+ ## Test
4
+
5
+ ```bash
6
+ $ docker build -f ./dockerfiles/Dockerfile.debian13-ruby32 . -t gkhtmltopdf-d13r32
7
+ $ docker build -f ./dockerfiles/Dockerfile.ubuntu24-ruby32 . -t gkhtmltopdf-u24r32
8
+ $ docker run --rm gkhtmltopdf-d13r32
9
+ $ docker run --rm gkhtmltopdf-u24r32
10
+ $ docker rmi gkhtmltopdf-d13r32
11
+ $ docker rmi gkhtmltopdf-u24r32
12
+ ```
13
+
14
+ ## Build
15
+
16
+ ```
17
+ ```
data/exe/gkhtmltopdf CHANGED
@@ -12,32 +12,40 @@ options = {
12
12
  parser = OptionParser.new do |opts|
13
13
  opts.banner = "Usage: gkhtmltopdf [options] <URL_OR_FILE> <OUTPUT_PDF>"
14
14
 
15
- opts.on("-O", "--orientation [PORTRAIT|LANDSCAPE]", "default: portrait") do |v|
15
+ opts.on("-O", "--orientation ORIENTATION", ["portrait", "landscape"], "ORIENTATION portrait or landscape (default: portrait)") do |v|
16
16
  options[:print_options][:orientation] = v.downcase
17
17
  end
18
18
 
19
+ opts.on("--user-agent USERAGENT", String, "Browser custom USERAGENT") do |v|
20
+ options[:user_agent] = v
21
+ end
22
+
19
23
  opts.on("--background", "Print background") do
20
24
  options[:print_options][:background] = true
21
25
  end
22
26
 
23
- opts.on("--margin-top [CM]", Float, "margin top (cm)") do |v|
27
+ opts.on("--margin-top CM", Float, "margin top CM") do |v|
24
28
  options[:print_options][:margin] ||= {}
25
29
  options[:print_options][:margin][:top] = v
26
30
  end
27
31
 
28
- opts.on("--margin-bottom [CM]", Float, "margin bottom (cm)") do |v|
32
+ opts.on("--margin-bottom CM", Float, "margin bottom CM") do |v|
29
33
  options[:print_options][:margin] ||= {}
30
34
  options[:print_options][:margin][:bottom] = v
31
35
  end
32
36
 
33
- opts.on("--firefox-path [PATH]", "Firefox custom PATH") do |v|
37
+ opts.on("--firefox-path PATH", String, "Firefox custom PATH") do |v|
34
38
  options[:firefox_path] = v
35
39
  end
36
40
 
37
- opts.on("--geckodriver-path [PATH]", "geckodriver custom PATH") do |v|
41
+ opts.on("--geckodriver-path PATH", String, "Geckodriver custom PATH") do |v|
38
42
  options[:geckodriver_path] = v
39
43
  end
40
44
 
45
+ opts.on("--launch-max-wait-time NUM", Integer, "Launch max wait time (approx: NUM * 0.1sec)") do |v|
46
+ options[:wait_time] = v
47
+ end
48
+
41
49
  opts.on("-v", "--version", "display version") do
42
50
  puts "Gkhtmltopdf version #{Gkhtmltopdf::VERSION}"
43
51
  exit
@@ -69,9 +77,10 @@ begin
69
77
  init_options = {}
70
78
  init_options[:firefox_path] = options.delete(:firefox_path) if options[:firefox_path]
71
79
  init_options[:geckodriver_path] = options.delete(:geckodriver_path) if options[:geckodriver_path]
80
+ init_options[:wait_time] = options.delete(:wait_time) if options[:wait_time]
81
+ init_options[:user_agent] = options.delete(:user_agent) if options[:user_agent]
72
82
 
73
- converter = Gkhtmltopdf::Converter.new(**init_options)
74
- converter.convert(input_url, output_path, print_options: options[:print_options])
83
+ Gkhtmltopdf.convert(input_url, output_path, print_options: options[:print_options], **init_options)
75
84
 
76
85
  puts "✅ Completed PDF generation!"
77
86
 
data/lib/errors.rb ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gkhtmltopdf
4
+ class Error < StandardError; end
5
+
6
+ class PathUnresolvedError < Error
7
+ def initialize(name)
8
+ message = "#{name} is not found. Please ensure #{name} is installed and either in your PATH or specify the path during initialization."
9
+ super(message)
10
+ end
11
+ end
12
+
13
+ class BrowserError < Error; end
14
+
15
+ class URLSchemeInvalid < Error
16
+ def initialize(url_scheme)
17
+ message = "Invalid URL scheme: (#{url_scheme})"
18
+ super(message)
19
+ end
20
+ end
21
+ end
@@ -1,40 +1,48 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'net/http'
2
4
  require 'json'
3
5
  require 'base64'
4
6
  require 'uri'
5
7
  require 'socket'
8
+ require 'tmpdir'
9
+ require 'fileutils'
6
10
 
7
11
  module Gkhtmltopdf
8
12
  class Converter
9
- def initialize(geckodriver_path: nil, firefox_path: nil, port: nil)
13
+ DEFAULT_FX_USER_AGENT = "gkhtmltopdf-rb(v#{VERSION}) by firefox and gecko".freeze
14
+
15
+ def open(geckodriver_path: nil, firefox_path: nil, wait_time: nil, port: nil, user_agent: nil)
10
16
  @geckodriver_path = resolve_geckodriver_path!(geckodriver_path)
11
17
  @firefox_path = resolve_firefox_path!(firefox_path)
12
18
  @port = port || get_free_port
13
19
  @base_url = "http://127.0.0.1:#{@port}"
20
+ @pid = spawn("#{@geckodriver_path} --port #{@port}", out: File::NULL, err: File::NULL)
21
+ wait_time ||= 20
22
+ @profile_path = gen_tmp_profile(user_agent)
23
+ wait_for_gk(wait_time)
24
+ create_session!
14
25
  end
15
26
 
16
- def convert(url, output_path, print_options: {})
17
- validate_url_scheme!(url)
18
-
19
- pid = spawn("#{@geckodriver_path} --port #{@port}", out: File::NULL, err: File::NULL)
20
- wait_for_server
21
-
22
- session_id = nil
27
+ def close
28
+ delete_session! if @session_id
29
+ delete_tmp_profile! if @profile_path
23
30
  begin
24
- session_id = create_session
25
- navigate(session_id, url)
26
-
27
- pdf_base64 = print_pdf(session_id, print_options)
28
- File.binwrite(output_path, Base64.decode64(pdf_base64))
29
- ensure
30
- delete_session(session_id) if session_id
31
- begin
32
- Process.kill('TERM', pid)
33
- Process.wait(pid)
34
- rescue Errno::ESRCH, Errno::ECHILD
35
- # nothing to do if the process is already terminated
31
+ unless @pid.nil?
32
+ Process.kill('TERM', @pid)
33
+ Process.wait(@pid)
36
34
  end
35
+ rescue Errno::ESRCH, Errno::ECHILD
36
+ # nothing to do if the process is already terminated
37
37
  end
38
+ nil
39
+ end
40
+
41
+ def save_pdf(url, output_path, print_options: {})
42
+ validate_url_scheme!(url)
43
+ navigate(url)
44
+ pdf_base64 = print_pdf(print_options)
45
+ File.binwrite(output_path, Base64.decode64(pdf_base64))
38
46
  end
39
47
 
40
48
  private
@@ -49,7 +57,7 @@ module Gkhtmltopdf
49
57
  def resolve_geckodriver_path!(provided_path)
50
58
  path = provided_path || find_default_geckodriver
51
59
  unless path
52
- raise Error, "Geckodriver is not found. Please ensure Geckodriver is installed and either in your PATH or specify the path during initialization."
60
+ raise PathUnresolvedError, 'Geckodriver'
53
61
  end
54
62
  path
55
63
  end
@@ -57,7 +65,7 @@ module Gkhtmltopdf
57
65
  def resolve_firefox_path!(provided_path)
58
66
  path = provided_path || find_default_firefox
59
67
  unless path
60
- raise Error, "Firefox is not found. Please ensure Firefox is installed and either in your PATH or specify the path during initialization."
68
+ raise PathUnresolvedError, 'Firefox'
61
69
  end
62
70
  path
63
71
  end
@@ -89,16 +97,29 @@ module Gkhtmltopdf
89
97
  common_paths.find { |path| File.executable?(path) && !File.directory?(path) }
90
98
  end
91
99
 
92
- def wait_for_server
93
- 10.times do
100
+ def wait_for_gk(num)
101
+ num.times do
94
102
  begin
95
103
  Net::HTTP.get(URI("#{@base_url}/status"))
96
104
  return
97
105
  rescue Errno::ECONNREFUSED
98
- sleep 0.2
106
+ sleep 0.1
99
107
  end
100
108
  end
101
- raise Error, "Failed to launch geckodriver (port #{@port})"
109
+ raise BrowserError, "Failed to launch geckodriver (port #{@port})"
110
+ end
111
+
112
+ def gen_tmp_profile(ua = nil)
113
+ tmp_profile_path = Dir.mktmpdir
114
+ ua ||= DEFAULT_FX_USER_AGENT
115
+ escaped_user_agent = JSON.generate(ua)
116
+ profile = []
117
+ profile << '# set gkhtmltopdf default profile'
118
+ profile << "user_pref(\"general.useragent.override\", #{escaped_user_agent});\n"
119
+ File.open(File.join(tmp_profile_path, 'user.js'), 'w') do |f|
120
+ profile.each { |line| f.puts(line) }
121
+ end
122
+ tmp_profile_path
102
123
  end
103
124
 
104
125
  def post(path, payload)
@@ -106,16 +127,15 @@ module Gkhtmltopdf
106
127
  req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
107
128
  req.body = payload.to_json
108
129
  res = Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(req) }
109
-
110
130
  begin
111
131
  JSON.parse(res.body)
112
132
  rescue JSON::ParserError
113
- raise Error, "Invalid geckodriver response (Status: #{res.code}): #{res.body}"
133
+ raise BrowserError, "Invalid json response (Status: #{res.code}): #{res.body}"
114
134
  end
115
135
  end
116
136
 
117
- def create_session
118
- firefox_options = { args: ["-headless"] }
137
+ def create_session!
138
+ firefox_options = { args: ["-headless", '--profile', @profile_path] }
119
139
  firefox_options[:binary] = @firefox_path if @firefox_path != 'firefox'
120
140
 
121
141
  payload = {
@@ -129,16 +149,16 @@ module Gkhtmltopdf
129
149
 
130
150
  response = post("/session", payload)
131
151
  value = response["value"]
132
- raise Error, "Failed to launch Firefox: #{value}" if value["error"]
152
+ raise BrowserError, "Failed to create session: #{value}" if value["error"]
133
153
 
134
- value["sessionId"]
154
+ @session_id = value["sessionId"]
135
155
  end
136
156
 
137
- def navigate(session_id, url)
138
- post("/session/#{session_id}/url", { url: url })
157
+ def navigate(url)
158
+ post("/session/#{@session_id}/url", { url: url })
139
159
  end
140
160
 
141
- def print_pdf(session_id, user_options)
161
+ def print_pdf(user_options)
142
162
  default_options = {
143
163
  background: false,
144
164
  shrinkToFit: true,
@@ -149,25 +169,30 @@ module Gkhtmltopdf
149
169
 
150
170
  payload = default_options.merge(user_options)
151
171
 
152
- response = post("/session/#{session_id}/print", payload)
172
+ response = post("/session/#{@session_id}/print", payload)
153
173
  value = response["value"]
154
- raise Error, "Failed to generate PDF: #{value}" if value["error"]
174
+ raise BrowserError, "Failed to generate PDF: #{value}" if value["error"]
155
175
 
156
176
  value
157
177
  end
158
178
 
159
- def delete_session(session_id)
160
- uri = URI("#{@base_url}/session/#{session_id}")
179
+ def delete_session!
180
+ uri = URI("#{@base_url}/session/#{@session_id}")
161
181
  req = Net::HTTP::Delete.new(uri)
162
182
  Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(req) }
183
+ @session_id = nil
184
+ end
185
+
186
+ def delete_tmp_profile!
187
+ FileUtils.remove_entry_secure(@profile_path)
163
188
  end
164
189
 
165
190
  def validate_url_scheme!(url_string)
166
191
  parsed_url = URI.parse(url_string)
167
192
  allowed_schemes = ['http', 'https', 'file']
168
- raise Error, 'URL scheme is nil' if parsed_url.scheme.nil?
193
+ raise URLSchemeInvalid, nil if parsed_url.scheme.nil?
169
194
  unless allowed_schemes.include?(parsed_url.scheme)
170
- raise Error, "Invalid URL scheme: #{parsed_url.scheme}"
195
+ raise URLSchemeInvalid, parsed_url.scheme
171
196
  end
172
197
  end
173
198
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gkhtmltopdf
4
+ class DSL
5
+ def initialize
6
+ @converter = Converter.new
7
+ end
8
+
9
+ def open(options)
10
+ @converter.open(**options)
11
+ end
12
+
13
+ def close
14
+ @converter.close
15
+ end
16
+
17
+ def save_pdf(url, output_path, print_options: {})
18
+ @converter.save_pdf(url, output_path, print_options: print_options)
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gkhtmltopdf
4
- VERSION = "0.9.0"
4
+ VERSION = '1.1.0'
5
5
  end
data/lib/gkhtmltopdf.rb CHANGED
@@ -1,13 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "gkhtmltopdf/version"
4
- require_relative "gkhtmltopdf/converter"
3
+ require_relative 'gkhtmltopdf/version'
4
+ require_relative 'gkhtmltopdf/converter'
5
+ require_relative 'gkhtmltopdf/dsl'
6
+ require_relative 'errors'
5
7
 
6
8
  module Gkhtmltopdf
7
- class Error < StandardError; end
8
-
9
- def self.convert(url, output_path, geckodriver_path: nil, firefox_path: nil, port: nil, print_options: {})
10
- converter = Converter.new(geckodriver_path: geckodriver_path, firefox_path: firefox_path, port: port)
11
- converter.convert(url, output_path, print_options: print_options)
9
+ def self.convert(url, output_path, geckodriver_path: nil, firefox_path: nil, wait_time: nil, port: nil, user_agent: nil, print_options: {})
10
+ converter = DSL.new
11
+ converter.open(geckodriver_path: geckodriver_path, firefox_path: firefox_path, wait_time: wait_time, port: port, user_agent: user_agent)
12
+ converter.save_pdf(url, output_path, print_options: print_options)
13
+ ensure
14
+ converter.close
15
+ end
16
+
17
+ def self.open(geckodriver_path: nil, firefox_path: nil, wait_time: nil, port: nil, user_agent: nil, &block)
18
+ converter = DSL.new
19
+ converter.open(geckodriver_path: geckodriver_path, firefox_path: firefox_path, wait_time: wait_time, port: port, user_agent: user_agent)
20
+ yield converter
21
+ ensure
22
+ converter.close
12
23
  end
13
24
  end
@@ -1,14 +1,40 @@
1
1
  require 'spec_helper'
2
- require 'gkhtmltopdf'
3
2
 
4
3
  RSpec.describe Gkhtmltopdf::Converter do
5
- let(:converter) { Gkhtmltopdf::Converter.allocate }
4
+ let(:converter) { Gkhtmltopdf::Converter.new }
5
+ after { converter.close }
6
+ describe '#open' do
7
+ subject { converter.open }
8
+ it {
9
+ subject
10
+ expect(converter.instance_variable_get(:@geckodriver_path)).to be_a(String)
11
+ expect(converter.instance_variable_get(:@firefox_path)).to be_a(String)
12
+ expect(converter.instance_variable_get(:@port)).to be_a(Integer)
13
+ expect(converter.instance_variable_get(:@base_url)).to be_a(String)
14
+ expect(converter.instance_variable_get(:@pid)).to be_a(Integer)
15
+ expect(converter.instance_variable_get(:@session_id)).to be_a(String)
16
+ }
17
+ end
18
+ describe '#save_pdf' do
19
+ before { converter.open }
20
+ let(:url) { "file://#{file_fixture('test.html')}" }
21
+ let(:output_path) { Dir.mktmpdir }
22
+ let(:output) { File.join(output_path, 'output.pdf') }
23
+ after { FileUtils.remove_entry_secure(output_path) }
24
+
25
+ subject { converter.save_pdf(url, output) }
26
+
27
+ it {
28
+ expect { subject }.to change { Dir.glob(output).count }.from(0).to(1)
29
+ expect(File.binread(output)).to include('/FontName')
30
+ }
31
+ end
6
32
  describe '#resolve_geckodriver_path!' do
7
33
  subject { converter.send(:resolve_geckodriver_path!, nil) }
8
34
  context 'geckodriver is not available' do
9
35
  before { allow(File).to receive(:executable?).and_return(false) }
10
36
  it 'raises an error' do
11
- expect { subject }.to raise_error(Gkhtmltopdf::Error, /\AGeckodriver is not found./)
37
+ expect { subject }.to raise_error(Gkhtmltopdf::PathUnresolvedError, /\AGeckodriver is not found./)
12
38
  end
13
39
  end
14
40
  end
@@ -18,29 +44,45 @@ RSpec.describe Gkhtmltopdf::Converter do
18
44
  context 'firefox is not available' do
19
45
  before { allow(File).to receive(:executable?).and_return(false) }
20
46
  it 'raises an error' do
21
- expect { subject }.to raise_error(Gkhtmltopdf::Error, /\AFirefox is not found./)
47
+ expect { subject }.to raise_error(Gkhtmltopdf::PathUnresolvedError, /\AFirefox is not found./)
22
48
  end
23
49
  end
24
50
  end
25
- describe '#wait_for_server' do
26
- subject { converter.send(:wait_for_server) }
51
+
52
+ describe '#wait_for_gk' do
53
+ subject { converter.send(:wait_for_gk, 0) }
27
54
  context 'fail launch geckodriver' do
28
55
  before { allow(Net::HTTP).to receive(:get).and_raise(Errno::ECONNREFUSED, 'Dummy error') }
29
56
  it 'raises an error' do
30
- expect { subject }.to raise_error(Gkhtmltopdf::Error, /\AFailed to launch geckodriver \(port \)\Z/)
57
+ expect { subject }.to raise_error(Gkhtmltopdf::BrowserError, /\AFailed to launch geckodriver \(port \)\Z/)
31
58
  end
32
59
  end
33
60
  end
61
+
34
62
  describe '#post' do
35
- let(:converter) { Gkhtmltopdf::Converter.new }
36
63
  subject { converter.send(:post, '/dummy', {test: :value}) }
37
64
  context 'Invalid json response from geckodriver' do
38
65
  before {
66
+ converter.instance_variable_set(:@base_url, 'http://test')
39
67
  allow(Net::HTTP).to receive(:start).and_return(Struct.new(:code, :body).new('200', 'invalid_json: 0123'))
40
68
  }
41
69
  it 'raises an error' do
42
- expect { subject }.to raise_error(Gkhtmltopdf::Error, 'Invalid geckodriver response (Status: 200): invalid_json: 0123')
70
+ expect { subject }.to raise_error(Gkhtmltopdf::BrowserError, 'Invalid json response (Status: 200): invalid_json: 0123')
43
71
  end
44
72
  end
45
73
  end
74
+
75
+ describe '#validate_url_scheme!' do
76
+ subject { converter.send(:validate_url_scheme!, url_string) }
77
+ let(:url_string) { 'http://f6a.net' }
78
+ it { expect { subject }.not_to raise_error }
79
+ context 'scheme is nil' do
80
+ let(:url_string) { 'f6a.net' }
81
+ it { expect { subject }.to raise_error(Gkhtmltopdf::URLSchemeInvalid, 'Invalid URL scheme: ()') }
82
+ end
83
+ context 'invalid scheme' do
84
+ let(:url_string) { 'about://version' }
85
+ it { expect { subject }.to raise_error(Gkhtmltopdf::URLSchemeInvalid, 'Invalid URL scheme: (about)') }
86
+ end
87
+ end
46
88
  end
@@ -1,15 +1,13 @@
1
1
  require 'spec_helper'
2
- require 'gkhtmltopdf'
3
- require 'tmpdir'
4
- require 'base64'
5
2
 
6
3
  RSpec.describe Gkhtmltopdf do
7
4
  describe '.convert' do
8
5
  let(:url) { 'https://f6a.net/oss/' }
9
- let(:output) { File.join(Dir.mktmpdir, 'output.pdf') }
10
- let(:hash) { {} }
6
+ let(:output_path) { Dir.mktmpdir }
7
+ let(:output) { File.join(output_path, 'output.pdf') }
8
+ after { FileUtils.remove_entry_secure(output_path) }
11
9
 
12
- subject { Gkhtmltopdf.convert(url, output, **hash) }
10
+ subject { Gkhtmltopdf.convert(url, output) }
13
11
 
14
12
  it 'successful conversion' do
15
13
  expect { subject }.not_to raise_error
@@ -18,7 +16,30 @@ RSpec.describe Gkhtmltopdf do
18
16
  context 'invalid URL' do
19
17
  let(:url) { 'ftp://example.com' }
20
18
  it 'raises an error' do
21
- expect { subject }.to raise_error(Gkhtmltopdf::Error, 'Invalid URL scheme: ftp')
19
+ expect { subject }.to raise_error(Gkhtmltopdf::URLSchemeInvalid, 'Invalid URL scheme: (ftp)')
20
+ end
21
+ end
22
+ end
23
+ describe '.open' do
24
+ let(:url) { 'https://f6a.net/oss/' }
25
+ let(:output_path) { Dir.mktmpdir }
26
+ after { FileUtils.remove_entry_secure(output_path) }
27
+
28
+ subject do
29
+ Gkhtmltopdf.open do |gk|
30
+ (1..3).each { |n| gk.save_pdf("#{url}?test=#{n}", File.join(output_path, "#{n}.pdf")) }
31
+ end
32
+ end
33
+
34
+ it 'successful conversion' do
35
+ expect { subject }.to change { Dir.glob(File.join(output_path, '*.pdf')).count }.from(0).to(3)
36
+ expect { subject }.not_to raise_error
37
+ end
38
+
39
+ context 'invalid URL' do
40
+ let(:url) { 'ftp://example.com' }
41
+ it 'raises an error' do
42
+ expect { subject }.to raise_error(Gkhtmltopdf::URLSchemeInvalid, 'Invalid URL scheme: (ftp)')
22
43
  end
23
44
  end
24
45
  end
data/spec/spec_helper.rb CHANGED
@@ -10,6 +10,15 @@ SimpleCov.start do
10
10
  )
11
11
  end
12
12
 
13
+ require 'gkhtmltopdf'
14
+ require 'tmpdir'
15
+ require 'base64'
16
+ module FileFixtureHelper
17
+ def file_fixture(filename)
18
+ File.join(File.expand_path('fixtures', __dir__), filename)
19
+ end
20
+ end
21
+
13
22
  # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
14
23
  # The generated `.rspec` file contains `--require spec_helper` which will cause
15
24
  # this file to always be loaded, without a need to explicitly require it in any
@@ -104,4 +113,6 @@ RSpec.configure do |config|
104
113
  # # test failures related to randomization by passing the same `--seed` value
105
114
  # # as the one that triggered the failure.
106
115
  # Kernel.srand config.seed
116
+
117
+ config.include FileFixtureHelper
107
118
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gkhtmltopdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kazuki Sakane
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-03-12 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: base64
@@ -79,14 +78,18 @@ files:
79
78
  - ".rspec"
80
79
  - ".ruby-version"
81
80
  - CHANGELOG.md
82
- - Dockerfile
83
81
  - LICENSE
84
82
  - README.md
85
83
  - Rakefile
86
84
  - TODO.md
85
+ - dockerfiles/Dockerfile.debian13-ruby32
86
+ - dockerfiles/Dockerfile.ubuntu24-ruby32
87
+ - docs/ForDeveloper.md
87
88
  - exe/gkhtmltopdf
89
+ - lib/errors.rb
88
90
  - lib/gkhtmltopdf.rb
89
91
  - lib/gkhtmltopdf/converter.rb
92
+ - lib/gkhtmltopdf/dsl.rb
90
93
  - lib/gkhtmltopdf/version.rb
91
94
  - spec/fixtures/test.html
92
95
  - spec/gkhtmltopdf/converter_spec.rb
@@ -98,6 +101,7 @@ licenses:
98
101
  metadata:
99
102
  homepage_uri: https://f6a.net/oss/
100
103
  source_code_uri: https://github.com/fantasia-tech/gkhtmltopdf-rb
104
+ changelog_uri: https://github.com/fantasia-tech/gkhtmltopdf-rb/blob/main/CHANGELOG.md
101
105
  post_install_message: "=====================================================================\nGkhtmltopdf
102
106
  has been installed successfully. \U0001F389\n\n⚠️ Caution\nRequired: To run this
103
107
  gem, you need to have `firefox` and `geckodriver` installed and added to your PATH.\n\ncheck
@@ -119,8 +123,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
119
123
  requirements:
120
124
  - Firefox
121
125
  - Geckodriver
122
- rubygems_version: 3.4.19
123
- signing_key:
126
+ rubygems_version: 4.0.8
124
127
  specification_version: 4
125
128
  summary: Gkhtmltopdf is mean Gecko HTML to PDF converter.
126
129
  test_files: []