sghtmltopdf 0.4.0 → 0.5.1
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 +4 -4
- data/Cargo.lock +14 -14
- data/README.md +27 -2
- data/ext/sghtmltopdf/Cargo.toml +14 -8
- data/ext/sghtmltopdf/extconf.rb +7 -7
- data/ext/sghtmltopdf/src/callback_sink.rs +89 -90
- data/ext/sghtmltopdf/src/errors.rs +27 -25
- data/ext/sghtmltopdf/src/gvl.rs +34 -34
- data/ext/sghtmltopdf/src/lib.rs +53 -56
- data/lib/sghtmltopdf/configuration.rb +17 -17
- data/lib/sghtmltopdf/options.rb +37 -40
- data/lib/sghtmltopdf/railtie.rb +5 -5
- data/lib/sghtmltopdf/renderer.rb +22 -22
- data/lib/sghtmltopdf/server_client.rb +18 -19
- data/lib/sghtmltopdf/version.rb +1 -1
- data/lib/sghtmltopdf.rb +33 -34
- metadata +1 -1
data/ext/sghtmltopdf/src/gvl.rs
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
//! GVL(Global VM Lock)
|
|
1
|
+
//! Releasing the GVL (Global VM Lock).
|
|
2
2
|
//!
|
|
3
|
-
//!
|
|
3
|
+
//! The GVL is released during rendering so Puma's other threads are not blocked.
|
|
4
4
|
|
|
5
5
|
use std::ffi::c_void;
|
|
6
6
|
use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe};
|
|
7
7
|
|
|
8
|
-
/// GVL
|
|
8
|
+
/// Release the GVL and run `func`.
|
|
9
9
|
///
|
|
10
|
-
/// #
|
|
10
|
+
/// # Why the `Send` bound is needed
|
|
11
11
|
///
|
|
12
|
-
/// magnus
|
|
13
|
-
///
|
|
14
|
-
///
|
|
15
|
-
///
|
|
12
|
+
/// magnus caches the GVL state in a thread local on the assumption that "there is no API
|
|
13
|
+
/// that releases the GVL" (*assumed not to change because there's currently no api to
|
|
14
|
+
/// unlock* in `magnus::api`), so `Ruby::get()` returns `Ok` even after we release it here.
|
|
15
|
+
/// Touching Ruby through the handle it returns would be UB.
|
|
16
16
|
///
|
|
17
|
-
/// `Send
|
|
18
|
-
/// (`*mut ()`)
|
|
19
|
-
///
|
|
20
|
-
///
|
|
21
|
-
///
|
|
17
|
+
/// Imposing a `Send` bound makes capturing a magnus value (`NonNull<RBasic>`) or a `Ruby`
|
|
18
|
+
/// handle (`*mut ()`) a compile error, both being `!Send`.
|
|
19
|
+
/// Calling `Ruby::get()` again inside the closure is not something the types can prevent,
|
|
20
|
+
/// but the only things called in the released region are `sghtmltopdf_core` functions, and
|
|
21
|
+
/// the core knows nothing about Ruby, so it is unreachable.
|
|
22
22
|
///
|
|
23
|
-
/// #
|
|
23
|
+
/// # Interruption
|
|
24
24
|
///
|
|
25
|
-
/// UBF(unblock function)
|
|
26
|
-
///
|
|
27
|
-
#[allow(dead_code)] //
|
|
25
|
+
/// The UBF (unblock function) is `None`, meaning uninterruptible. Interruption through
|
|
26
|
+
/// `Kernel#trap` or Ctrl-C is outside the initial scope.
|
|
27
|
+
#[allow(dead_code)] // used by the future GVL-releasing implementation
|
|
28
28
|
pub fn without_gvl<F, R>(func: F) -> R
|
|
29
29
|
where
|
|
30
30
|
F: FnOnce() -> R + Send,
|
|
@@ -41,9 +41,9 @@ where
|
|
|
41
41
|
R: Send,
|
|
42
42
|
{
|
|
43
43
|
let state = unsafe { &mut *(arg as *mut State<F, R>) };
|
|
44
|
-
let func = state.func.take().expect("
|
|
45
|
-
//
|
|
46
|
-
// GVL
|
|
44
|
+
let func = state.func.take().expect("the callback was called twice");
|
|
45
|
+
// A panic crossing the FFI boundary aborts the process, so it is caught here and
|
|
46
|
+
// resumed on the Rust side after the GVL is reacquired.
|
|
47
47
|
state.result = Some(catch_unwind(AssertUnwindSafe(func)));
|
|
48
48
|
std::ptr::null_mut()
|
|
49
49
|
}
|
|
@@ -60,26 +60,26 @@ where
|
|
|
60
60
|
std::ptr::null_mut(),
|
|
61
61
|
);
|
|
62
62
|
}
|
|
63
|
-
match state.result.expect("
|
|
63
|
+
match state.result.expect("the callback was never run") {
|
|
64
64
|
Ok(value) => value,
|
|
65
65
|
Err(panic) => resume_unwind(panic),
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/// GVL
|
|
69
|
+
/// Reacquire the GVL and run `func`. Call only from inside [`without_gvl`].
|
|
70
70
|
///
|
|
71
|
-
/// `without_gvl
|
|
72
|
-
///
|
|
71
|
+
/// Unlike `without_gvl` it imposes no `Send` bound, this being a region where the GVL is
|
|
72
|
+
/// held and Ruby may be touched.
|
|
73
73
|
///
|
|
74
|
-
/// #
|
|
74
|
+
/// # What the caller must observe (libruby's constraints)
|
|
75
75
|
///
|
|
76
|
-
/// * `func
|
|
77
|
-
///
|
|
78
|
-
/// `rb_gc_register_address
|
|
76
|
+
/// * Do not return a Ruby object from `func`. Returning one puts it outside the GC's scope
|
|
77
|
+
/// once the GVL is released again, and it will not be marked. To carry a value back, put
|
|
78
|
+
/// it in a slot registered with `rb_gc_register_address`
|
|
79
79
|
/// (`callback_sink::ValueSlot`)
|
|
80
|
-
/// * `func
|
|
81
|
-
///
|
|
82
|
-
/// (magnus
|
|
80
|
+
/// * Do not let `func` throw an exception. A longjmp jumping over this function is undefined
|
|
81
|
+
/// behaviour. Every Ruby call must be wrapped in the equivalent of `rb_protect`
|
|
82
|
+
/// (magnus's `Proc::call` uses `protect` internally, so it can be used directly)
|
|
83
83
|
pub fn with_gvl<F, R>(func: F) -> R
|
|
84
84
|
where
|
|
85
85
|
F: FnOnce() -> R,
|
|
@@ -94,8 +94,8 @@ where
|
|
|
94
94
|
F: FnOnce() -> R,
|
|
95
95
|
{
|
|
96
96
|
let state = unsafe { &mut *(arg as *mut State<F, R>) };
|
|
97
|
-
let func = state.func.take().expect("
|
|
98
|
-
//
|
|
97
|
+
let func = state.func.take().expect("the callback was called twice");
|
|
98
|
+
// A panic crossing a libruby frame aborts the process.
|
|
99
99
|
state.result = Some(catch_unwind(AssertUnwindSafe(func)));
|
|
100
100
|
std::ptr::null_mut()
|
|
101
101
|
}
|
|
@@ -107,7 +107,7 @@ where
|
|
|
107
107
|
unsafe {
|
|
108
108
|
rb_sys::rb_thread_call_with_gvl(Some(call::<F, R>), &mut state as *mut _ as *mut c_void);
|
|
109
109
|
}
|
|
110
|
-
match state.result.expect("
|
|
110
|
+
match state.result.expect("the callback was never run") {
|
|
111
111
|
Ok(value) => value,
|
|
112
112
|
Err(panic) => resume_unwind(panic),
|
|
113
113
|
}
|
data/ext/sghtmltopdf/src/lib.rs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
//! Ruby
|
|
1
|
+
//! The Ruby extension's entry point.
|
|
2
2
|
//!
|
|
3
|
-
//!
|
|
4
|
-
//!
|
|
5
|
-
//!
|
|
3
|
+
//! This layer is kept thin. Assembling the option argument list (argv) is done on the Ruby
|
|
4
|
+
//! side, and this merely runs the argv it receives through the same parser as the CLI and
|
|
5
|
+
//! the HTTP server, and renders.
|
|
6
6
|
|
|
7
7
|
mod callback_sink;
|
|
8
8
|
mod errors;
|
|
@@ -13,81 +13,75 @@ use std::path::PathBuf;
|
|
|
13
13
|
|
|
14
14
|
use magnus::rb_sys::AsRawValue;
|
|
15
15
|
use magnus::{block::Proc, function, prelude::*, Error, RString, Ruby};
|
|
16
|
-
use sghtmltopdf_core::
|
|
17
|
-
use sghtmltopdf_core::render_stack;
|
|
18
|
-
use sghtmltopdf_core::sink::{FileSink, MemorySink};
|
|
16
|
+
use sghtmltopdf_core::{with_render_stack, ConvertError, Converter, FileSink};
|
|
19
17
|
|
|
20
18
|
use callback_sink::{pump_to_block, BlockSlot, PendingUnwind, ValueSlot};
|
|
21
19
|
|
|
22
|
-
/// HTML
|
|
20
|
+
/// Convert HTML and return the PDF bytes.
|
|
23
21
|
fn render(html: RString, argv: Vec<String>) -> Result<RString, Error> {
|
|
24
|
-
let ruby = Ruby::get().expect("GVL
|
|
25
|
-
// GVL
|
|
26
|
-
//
|
|
22
|
+
let ruby = Ruby::get().expect("it should be called while holding the GVL");
|
|
23
|
+
// Copy to the Rust side before releasing the GVL. Ruby objects cannot be touched while
|
|
24
|
+
// it is released, so an `RString` cannot be carried in.
|
|
27
25
|
let html = unsafe { html.as_slice() }.to_vec();
|
|
28
26
|
errors::catch_panic(&ruby, move || render_inner(html, argv))
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
fn render_inner(html: Vec<u8>, argv: Vec<String>) -> Result<RString, Error> {
|
|
32
|
-
let ruby = Ruby::get().expect("GVL
|
|
33
|
-
let
|
|
30
|
+
let ruby = Ruby::get().expect("it should be called while holding the GVL");
|
|
31
|
+
let converter = Converter::from_args(argv).map_err(|e| errors::to_ruby(&ruby, e))?;
|
|
34
32
|
|
|
35
|
-
// GVL
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
33
|
+
// Release the GVL and then move onto a thread with a stack allocated specifically for
|
|
34
|
+
// rendering. A Ruby thread's machine stack is only 1MiB by default and cannot survive the
|
|
35
|
+
// recursion of layout and drawing (see the module docs of `callback_sink`).
|
|
36
|
+
// This path never calls back into Ruby, so it can be moved as-is.
|
|
39
37
|
let pdf = gvl::without_gvl(move || {
|
|
40
|
-
|
|
41
|
-
convert::render_to_memory(&args, &fonts, Cursor::new(html), MemorySink::new())
|
|
42
|
-
})
|
|
38
|
+
with_render_stack(move || converter.render_to_vec(Cursor::new(html)))
|
|
43
39
|
})
|
|
44
40
|
.map_err(|e| errors::to_ruby(&ruby, e))?;
|
|
45
41
|
|
|
46
42
|
Ok(ruby.str_from_slice(&pdf))
|
|
47
43
|
}
|
|
48
44
|
|
|
49
|
-
/// HTML
|
|
45
|
+
/// Convert HTML and write it to `path`.
|
|
50
46
|
///
|
|
51
|
-
///
|
|
52
|
-
/// (
|
|
53
|
-
///
|
|
47
|
+
/// The destination is decided by [`FileSink`]
|
|
48
|
+
/// (it writes to a temporary file and renames only on success, so a failure part-way through
|
|
49
|
+
/// leaves no broken PDF).
|
|
54
50
|
fn render_to_file(html: RString, argv: Vec<String>, path: String) -> Result<(), Error> {
|
|
55
|
-
let ruby = Ruby::get().expect("GVL
|
|
51
|
+
let ruby = Ruby::get().expect("it should be called while holding the GVL");
|
|
56
52
|
let html = unsafe { html.as_slice() }.to_vec();
|
|
57
53
|
errors::catch_panic(&ruby, move || render_to_file_inner(html, argv, path))
|
|
58
54
|
}
|
|
59
55
|
|
|
60
56
|
fn render_to_file_inner(html: Vec<u8>, argv: Vec<String>, path: String) -> Result<(), Error> {
|
|
61
|
-
let ruby = Ruby::get().expect("GVL
|
|
62
|
-
let
|
|
57
|
+
let ruby = Ruby::get().expect("it should be called while holding the GVL");
|
|
58
|
+
let converter = Converter::from_args(argv).map_err(|e| errors::to_ruby(&ruby, e))?;
|
|
63
59
|
|
|
64
60
|
let path = PathBuf::from(path);
|
|
65
61
|
let sink = FileSink::create(&path).map_err(|e| {
|
|
66
62
|
errors::to_ruby(
|
|
67
63
|
&ruby,
|
|
68
|
-
|
|
64
|
+
ConvertError::Input(format!("failed to create {}: {e}", path.display())),
|
|
69
65
|
)
|
|
70
66
|
})?;
|
|
71
67
|
|
|
72
|
-
gvl::without_gvl(move ||
|
|
73
|
-
|
|
74
|
-
})
|
|
75
|
-
.map_err(|e| errors::to_ruby(&ruby, e))?;
|
|
68
|
+
gvl::without_gvl(move || with_render_stack(move || converter.render(Cursor::new(html), sink)))
|
|
69
|
+
.map_err(|e| errors::to_ruby(&ruby, e))?;
|
|
76
70
|
Ok(())
|
|
77
71
|
}
|
|
78
72
|
|
|
79
|
-
/// HTML
|
|
73
|
+
/// Convert HTML and hand the settled PDF bytes to `block` in `chunk_size` pieces.
|
|
80
74
|
///
|
|
81
|
-
///
|
|
82
|
-
///
|
|
83
|
-
///
|
|
75
|
+
/// The GVL is released during rendering and reacquired only for the moment the block is
|
|
76
|
+
/// called. If the block throws an exception, that exception is propagated to the caller
|
|
77
|
+
/// unchanged (the engine unwinds through its ordinary error path).
|
|
84
78
|
fn render_each(
|
|
85
79
|
html: RString,
|
|
86
80
|
argv: Vec<String>,
|
|
87
81
|
block: Proc,
|
|
88
82
|
chunk_size: usize,
|
|
89
83
|
) -> Result<(), Error> {
|
|
90
|
-
let ruby = Ruby::get().expect("GVL
|
|
84
|
+
let ruby = Ruby::get().expect("it should be called while holding the GVL");
|
|
91
85
|
let html = unsafe { html.as_slice() }.to_vec();
|
|
92
86
|
errors::catch_panic(&ruby, move || {
|
|
93
87
|
render_each_inner(html, argv, block, chunk_size)
|
|
@@ -100,11 +94,11 @@ fn render_each_inner(
|
|
|
100
94
|
block: Proc,
|
|
101
95
|
chunk_size: usize,
|
|
102
96
|
) -> Result<(), Error> {
|
|
103
|
-
let ruby = Ruby::get().expect("GVL
|
|
104
|
-
let
|
|
97
|
+
let ruby = Ruby::get().expect("it should be called while holding the GVL");
|
|
98
|
+
let converter = Converter::from_args(argv).map_err(|e| errors::to_ruby(&ruby, e))?;
|
|
105
99
|
|
|
106
|
-
//
|
|
107
|
-
// (
|
|
100
|
+
// The block has to survive across the GVL-released region, so it is registered with the
|
|
101
|
+
// GC (a value pushed onto the stack after the release is outside the conservative GC's scan).
|
|
108
102
|
let block = ValueSlot::new(block.as_raw());
|
|
109
103
|
let mut pending = PendingUnwind::default();
|
|
110
104
|
|
|
@@ -112,47 +106,50 @@ fn render_each_inner(
|
|
|
112
106
|
let slot = BlockSlot::new(&block);
|
|
113
107
|
let pending = &mut pending;
|
|
114
108
|
gvl::without_gvl(move || {
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
109
|
+
// Rendering runs on a thread with a dedicated stack, and only the settled chunks
|
|
110
|
+
// come back here. Calling the block (that is, reacquiring the GVL) has to happen
|
|
111
|
+
// on this thread, the one that released it, so it is left to `pump_to_block`.
|
|
118
112
|
pump_to_block(slot, pending, chunk_size, move |sink| {
|
|
119
|
-
|
|
113
|
+
converter.render(Cursor::new(html), sink)
|
|
120
114
|
})
|
|
121
115
|
})
|
|
122
116
|
};
|
|
123
117
|
drop(block);
|
|
124
118
|
|
|
125
|
-
//
|
|
126
|
-
// (`Sink::Error
|
|
127
|
-
// `break
|
|
128
|
-
// Rust
|
|
119
|
+
// An interruption from the block is propagated in preference to whatever error the engine
|
|
120
|
+
// returns (`Sink::Error` is fixed to `io::Error`, so the reason rides on this instead).
|
|
121
|
+
// A non-local exit such as `break` calls `rb_jump_tag` inside `into_error` and never
|
|
122
|
+
// returns, so the Rust-side values are all dropped before it is called.
|
|
129
123
|
if pending.is_pending() {
|
|
130
124
|
drop(result);
|
|
131
125
|
return Err(pending
|
|
132
126
|
.into_error()
|
|
133
|
-
.expect("is_pending
|
|
127
|
+
.expect("with is_pending true, an interruption is present"));
|
|
134
128
|
}
|
|
135
129
|
result.map_err(|e| errors::to_ruby(&ruby, e))
|
|
136
130
|
}
|
|
137
131
|
|
|
138
|
-
/// core
|
|
132
|
+
/// For confirming we can link against the core (a connectivity check).
|
|
139
133
|
fn core_version() -> String {
|
|
140
134
|
env!("CARGO_PKG_VERSION").to_string()
|
|
141
135
|
}
|
|
142
136
|
|
|
143
|
-
/// core
|
|
137
|
+
/// Really call one of the core's symbols to confirm the link.
|
|
144
138
|
fn default_page_size() -> String {
|
|
145
|
-
let settings = sghtmltopdf_core::
|
|
139
|
+
let settings = sghtmltopdf_core::PageSettings::default();
|
|
146
140
|
format!("{}x{}", settings.size.width, settings.size.height)
|
|
147
141
|
}
|
|
148
142
|
|
|
149
|
-
/// GVL
|
|
150
|
-
///
|
|
143
|
+
/// For confirming it can run with the GVL released. That other Ruby threads make progress
|
|
144
|
+
/// during the release is checked by a test on the Ruby side.
|
|
151
145
|
fn sleep_without_gvl(ms: u64) {
|
|
152
146
|
gvl::without_gvl(|| std::thread::sleep(std::time::Duration::from_millis(ms)));
|
|
153
147
|
}
|
|
154
148
|
|
|
155
|
-
|
|
149
|
+
// The exported symbol has to match the `.so` file name, which stays `sghtmltopdf`. Without
|
|
150
|
+
// this the name would come from the package (`sghtmltopdf-ruby`) and Ruby would look for an
|
|
151
|
+
// `Init_sghtmltopdf` that does not exist.
|
|
152
|
+
#[magnus::init(name = "sghtmltopdf")]
|
|
156
153
|
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
157
154
|
let module = ruby.define_module("Sghtmltopdf")?;
|
|
158
155
|
errors::define(ruby, module)?;
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Sghtmltopdf
|
|
4
|
-
#
|
|
4
|
+
# The global default options.
|
|
5
5
|
#
|
|
6
6
|
# Sghtmltopdf.configure do |c|
|
|
7
7
|
# c.page_size = "A4"
|
|
8
8
|
# c.gothic_font = "/path/to/NotoSansJP-Regular.ttf"
|
|
9
9
|
# end
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
# (
|
|
11
|
+
# A value set here can be overridden by an argument to `render`/`render_to_file`
|
|
12
|
+
# (merged in the order global, then call-time).
|
|
13
13
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
14
|
+
# Key names are not validated. The option definitions live in one place on the Rust side
|
|
15
|
+
# (`cli/options.rs`), so an unknown key makes clap raise a `UsageError` at render time.
|
|
16
16
|
class Configuration
|
|
17
17
|
def initialize(options = {})
|
|
18
18
|
@options = {}
|
|
19
|
-
#
|
|
20
|
-
# (@defaults)
|
|
21
|
-
#
|
|
19
|
+
# Values set explicitly (@options) are kept separately from the defaults injected by
|
|
20
|
+
# the Railtie and others (@defaults). Reads always prefer @options, so nothing depends
|
|
21
|
+
# on the order the initialisers run in.
|
|
22
22
|
@defaults = {}
|
|
23
23
|
options.each { |key, value| self[key] = value }
|
|
24
24
|
end
|
|
@@ -32,30 +32,30 @@ module Sghtmltopdf
|
|
|
32
32
|
@options[Options.canonical_key(key)] = value
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
# @param with_defaults [Boolean]
|
|
36
|
-
# HTTP
|
|
37
|
-
# (`base_url
|
|
38
|
-
#
|
|
35
|
+
# @param with_defaults [Boolean] whether to include the injected defaults.
|
|
36
|
+
# Set it to `false` when delegating to the HTTP server: the Rails-oriented defaults
|
|
37
|
+
# (`base_url` and `allow`) exist for local file resolution and are keys server mode
|
|
38
|
+
# cannot take from a request
|
|
39
39
|
def to_h(with_defaults: true)
|
|
40
40
|
with_defaults ? @defaults.merge(@options) : @options.dup
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
#
|
|
44
|
-
#
|
|
43
|
+
# Inject the defaults. Used by the Railtie to set the Rails-oriented defaults.
|
|
44
|
+
# They are weaker than explicitly set values (`[]=` wins regardless of order).
|
|
45
45
|
def apply_defaults(defaults)
|
|
46
46
|
defaults.each { |key, value| @defaults[Options.canonical_key(key)] = value }
|
|
47
47
|
self
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
# `c.page_size = "A4"
|
|
50
|
+
# Accepts both `c.page_size = "A4"` and `c.page_size`.
|
|
51
51
|
def method_missing(name, *args)
|
|
52
52
|
key = name.to_s
|
|
53
53
|
if key.end_with?("=")
|
|
54
|
-
raise ArgumentError, "#{name}
|
|
54
|
+
raise ArgumentError, "#{name} takes one argument" unless args.size == 1
|
|
55
55
|
|
|
56
56
|
self[key.chomp("=")] = args.first
|
|
57
57
|
else
|
|
58
|
-
raise ArgumentError, "#{name}
|
|
58
|
+
raise ArgumentError, "#{name} takes no arguments" unless args.empty?
|
|
59
59
|
|
|
60
60
|
self[key]
|
|
61
61
|
end
|
data/lib/sghtmltopdf/options.rb
CHANGED
|
@@ -3,44 +3,42 @@
|
|
|
3
3
|
require "uri"
|
|
4
4
|
|
|
5
5
|
module Sghtmltopdf
|
|
6
|
-
#
|
|
6
|
+
# Converts an options hash into:
|
|
7
7
|
#
|
|
8
|
-
# *
|
|
9
|
-
# * HTTP
|
|
8
|
+
# * the CLI argument list (argv) passed to the native extension ... [.to_argv]
|
|
9
|
+
# * the query string passed to HTTP server mode ... [.to_query]
|
|
10
10
|
module Options
|
|
11
|
-
# 入力は常に標準入力を表す`-`を置く(実際のバイト列はFFIで直接渡すため
|
|
12
|
-
# 読まれない)。出力先はRust側のSinkが決めるので、ここもダミーの`-`。
|
|
13
|
-
# `-`入力のときCLIは`--output`を必須にするため、省略はできない。
|
|
14
|
-
ARGV_PREFIX = ["sghtmltopdf", "-", "--output", "-"].freeze
|
|
15
11
|
|
|
16
|
-
# Ruby
|
|
17
|
-
#
|
|
12
|
+
# The keys interpreted on the Ruby side alone. They are not conversion options, so they
|
|
13
|
+
# appear in neither the argv nor the query.
|
|
18
14
|
TRANSPORT_KEYS = %i[server_url server_open_timeout server_read_timeout chunk_size].freeze
|
|
19
15
|
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
16
|
+
# Alias keys (values are the canonical names). The CLI accepts `--allow` as an
|
|
17
|
+
# alias of `--allow-path`, but the Ruby side must not carry both keys around.
|
|
18
|
+
# If the defaults use one key and the call uses the other, a hash merge does
|
|
19
|
+
# not override: both end up in argv (repeating the same flag means "union",
|
|
20
|
+
# not "replace").
|
|
25
21
|
ALIAS_KEYS = {allow: :allow_path}.freeze
|
|
26
22
|
|
|
27
23
|
module_function
|
|
28
24
|
|
|
29
|
-
#
|
|
25
|
+
# Map an alias key to its canonical name.
|
|
30
26
|
def canonical_key(key)
|
|
31
27
|
key = key.to_sym
|
|
32
28
|
ALIAS_KEYS.fetch(key, key)
|
|
33
29
|
end
|
|
34
30
|
|
|
35
|
-
#
|
|
31
|
+
# Normalize all keys of a hash.
|
|
36
32
|
def canonicalize(options)
|
|
37
33
|
options.to_h { |key, value| [canonical_key(key), value] }
|
|
38
34
|
end
|
|
39
35
|
|
|
40
|
-
# @param options [Hash] Ruby
|
|
41
|
-
# @return [Array<String>]
|
|
36
|
+
# @param options [Hash] the Ruby options hash
|
|
37
|
+
# @return [Array<String>] the conversion options passed to the core's `Converter`.
|
|
38
|
+
# The HTML and the destination travel over FFI, so there is no program name, input
|
|
39
|
+
# path or `--output` here.
|
|
42
40
|
def to_argv(options)
|
|
43
|
-
argv =
|
|
41
|
+
argv = []
|
|
44
42
|
each_pair(options) do |name, value|
|
|
45
43
|
argv.push("--#{name}")
|
|
46
44
|
argv.push(value) unless value.nil?
|
|
@@ -48,18 +46,18 @@ module Sghtmltopdf
|
|
|
48
46
|
argv
|
|
49
47
|
end
|
|
50
48
|
|
|
51
|
-
# @param options [Hash] Ruby
|
|
52
|
-
# @return [String] `POST /pdf
|
|
49
|
+
# @param options [Hash] the Ruby options hash
|
|
50
|
+
# @return [String] the query string for `POST /pdf` (with no leading `?`)
|
|
53
51
|
def to_query(options)
|
|
54
52
|
parts = []
|
|
55
53
|
each_pair(options) do |name, value|
|
|
56
|
-
#
|
|
54
|
+
# A valueless flag becomes just the key (the server treats no value as true).
|
|
57
55
|
parts << (value.nil? ? escape(name) : "#{escape(name)}=#{escape(value)}")
|
|
58
56
|
end
|
|
59
57
|
parts.join("&")
|
|
60
58
|
end
|
|
61
59
|
|
|
62
|
-
#
|
|
60
|
+
# Convert one key and value into an argv fragment.
|
|
63
61
|
#
|
|
64
62
|
# page_size: "A4" → ["--page-size", "A4"]
|
|
65
63
|
# grayscale: true → ["--grayscale"]
|
|
@@ -69,8 +67,8 @@ module Sghtmltopdf
|
|
|
69
67
|
pairs_for(key, value).flat_map { |name, arg| arg.nil? ? ["--#{name}"] : ["--#{name}", arg] }
|
|
70
68
|
end
|
|
71
69
|
|
|
72
|
-
#
|
|
73
|
-
#
|
|
70
|
+
# Turn one key and value into a list of "flag name and value" pairs. A pair whose value is
|
|
71
|
+
# `nil` is a flag taking no value (`--toc` and the like).
|
|
74
72
|
def pairs_for(key, value)
|
|
75
73
|
name = flag_name(key)
|
|
76
74
|
return font_pairs(value) if name == "font"
|
|
@@ -78,26 +76,25 @@ module Sghtmltopdf
|
|
|
78
76
|
case value
|
|
79
77
|
when nil, false then []
|
|
80
78
|
when true then [[name, nil]]
|
|
81
|
-
#
|
|
79
|
+
# An array means the same option repeated. The same rule applies to each element.
|
|
82
80
|
when Array then value.flat_map { |element| pairs_for(key, element) }
|
|
83
81
|
when Hash
|
|
84
|
-
# wicked_pdf
|
|
85
|
-
# CLI
|
|
86
|
-
#
|
|
87
|
-
#
|
|
82
|
+
# Nesting such as wicked_pdf's `margin: {top: 10}` is not accepted. There is no
|
|
83
|
+
# corresponding CLI flag, and flattening it mechanically would let even misspelled
|
|
84
|
+
# keys through silently. When migrating, use the correspondence table in the
|
|
85
|
+
# migration guide to rewrite them. A unitless number is read as mm, as in wicked_pdf (`cli/units.rs`).
|
|
88
86
|
example = value.keys.first
|
|
89
87
|
raise ArgumentError,
|
|
90
|
-
"#{key}
|
|
91
|
-
"
|
|
92
|
-
"#{":
|
|
88
|
+
"a Hash cannot be passed for #{key} (only :font takes a path and an index). " \
|
|
89
|
+
"Give nested options as flat keys" \
|
|
90
|
+
"#{": for example #{key}_#{example}: \"...\"" if example}"
|
|
93
91
|
else [[name, value.to_s]]
|
|
94
92
|
end
|
|
95
93
|
end
|
|
96
94
|
|
|
97
|
-
# `--font
|
|
98
|
-
# `ArgMatches#indices_of
|
|
99
|
-
#
|
|
100
|
-
# 必ず対応する`--font`の直後へ置く。
|
|
95
|
+
# `--font` and `--font-index` are paired by their order of appearance (the CLI ties each
|
|
96
|
+
# `--font-index` to "the last `--font` before it" via `ArgMatches#indices_of`).
|
|
97
|
+
# So a face index always goes immediately after its own `--font`.
|
|
101
98
|
#
|
|
102
99
|
# font: "a.ttf" → ["--font", "a.ttf"]
|
|
103
100
|
# font: {path: "a.ttc", index: 1} → ["--font", "a.ttc", "--font-index", "1"]
|
|
@@ -113,7 +110,7 @@ module Sghtmltopdf
|
|
|
113
110
|
when Array then value.flat_map { |element| font_pairs(element) }
|
|
114
111
|
when Hash
|
|
115
112
|
path = value[:path] || value["path"]
|
|
116
|
-
raise ArgumentError, "font
|
|
113
|
+
raise ArgumentError, "a font Hash needs a path: #{value.inspect}" if path.nil?
|
|
117
114
|
|
|
118
115
|
index = value[:index] || value["index"]
|
|
119
116
|
pairs = [["font", path.to_s]]
|
|
@@ -123,12 +120,12 @@ module Sghtmltopdf
|
|
|
123
120
|
end
|
|
124
121
|
end
|
|
125
122
|
|
|
126
|
-
# `:page_size`
|
|
123
|
+
# `:page_size` becomes `page-size`.
|
|
127
124
|
def flag_name(key)
|
|
128
125
|
key.to_s.tr("_", "-")
|
|
129
126
|
end
|
|
130
127
|
|
|
131
|
-
#
|
|
128
|
+
# Enumerate only the conversion options, as pairs, in the order they were given.
|
|
132
129
|
def each_pair(options, &block)
|
|
133
130
|
options.each do |key, value|
|
|
134
131
|
key = canonical_key(key)
|
data/lib/sghtmltopdf/railtie.rb
CHANGED
|
@@ -28,12 +28,12 @@ module Sghtmltopdf
|
|
|
28
28
|
defaults
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
#
|
|
32
|
-
# `config.assets.paths
|
|
33
|
-
# `propshaft.append_assets_path`)
|
|
31
|
+
# Read in `after_initialize`, not inside the initializer. The pipeline fills
|
|
32
|
+
# `config.assets.paths` in its own initializer (for Propshaft,
|
|
33
|
+
# `propshaft.append_assets_path`), and that one runs later.
|
|
34
34
|
#
|
|
35
|
-
# `config/initializers
|
|
36
|
-
#
|
|
35
|
+
# This runs after `config/initializers`, but `apply_defaults` is always
|
|
36
|
+
# weaker than explicitly set values, so it never overrides user settings.
|
|
37
37
|
initializer "sghtmltopdf.defaults" do |app|
|
|
38
38
|
app.config.after_initialize do
|
|
39
39
|
Sghtmltopdf.config.apply_defaults(Sghtmltopdf::Railtie.default_options(app))
|