1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
use wkhtmltox_sys::pdf::*;
use std::{ptr, slice};
use std::collections::HashMap;
use std::ffi::{CString, CStr};
use std::os::raw::{c_char, c_int};
use std::sync::{Arc, Mutex, mpsc};
use std::marker::PhantomData;
use thread_id;
use super::{Result, Error, PdfOutput};
enum WkhtmltopdfState {
New,
Ready,
Busy,
Dropped,
}
lazy_static! {
static ref WKHTMLTOPDF_STATE: Mutex<WkhtmltopdfState> = Mutex::new(WkhtmltopdfState::New);
static ref WKHTMLTOPDF_INIT_THREAD: usize = thread_id::get();
static ref FINISHED_CALLBACKS: Mutex<HashMap<usize, Box<FnMut(i32) + 'static + Send>>> = Mutex::new(HashMap::new());
static ref ERROR_CALLBACKS: Mutex<HashMap<usize, Box<FnMut(String) + 'static + Send>>> = Mutex::new(HashMap::new());
}
pub struct PdfGuard {
_private: PhantomData<*const ()>
}
pub struct PdfGlobalSettings {
global_settings: *mut wkhtmltopdf_global_settings,
needs_delete: bool,
}
pub struct PdfObjectSettings {
object_settings: *mut wkhtmltopdf_object_settings,
needs_delete: bool,
}
pub struct PdfConverter {
converter: *mut wkhtmltopdf_converter,
_global: PdfGlobalSettings,
}
pub fn pdf_init() -> Result<PdfGuard> {
let mut wk_state = WKHTMLTOPDF_STATE.lock().unwrap();
match *wk_state {
WkhtmltopdfState::New => {
debug!("wkhtmltopdf_init graphics=0");
let success = unsafe {
wkhtmltopdf_init(0) == 1
};
if success {
*wk_state = WkhtmltopdfState::Ready;
let _ = *WKHTMLTOPDF_INIT_THREAD;
} else {
error!("failed to initialize wkhtmltopdf");
}
Ok(PdfGuard{ _private: PhantomData })
}
_ => Err(Error::IllegalInit)
}
}
impl PdfGlobalSettings {
pub fn new() -> Result<PdfGlobalSettings> {
if *WKHTMLTOPDF_INIT_THREAD != thread_id::get() {
return Err(Error::ThreadMismatch(*WKHTMLTOPDF_INIT_THREAD, thread_id::get()))
}
let mut wk_state = WKHTMLTOPDF_STATE.lock().unwrap();
match *wk_state {
WkhtmltopdfState::New => Err(Error::NotInitialized),
WkhtmltopdfState::Dropped => Err(Error::NotInitialized),
WkhtmltopdfState::Busy => Err(Error::Blocked),
WkhtmltopdfState::Ready => {
debug!("wkhtmltopdf_create_global_settings");
let gs = unsafe { wkhtmltopdf_create_global_settings() };
*wk_state = WkhtmltopdfState::Busy;
Ok(PdfGlobalSettings {
global_settings: gs,
needs_delete: true,
})
}
}
}
pub unsafe fn set(&mut self, name: &str, value: &str) -> Result<()> {
let c_name = CString::new(name).expect("setting name may not contain interior null bytes");
let c_value = CString::new(value).expect("setting value may not contain interior null bytes");
debug!("wkhtmltopdf_set_global_setting {}='{}'", name, value);
match wkhtmltopdf_set_global_setting(self.global_settings, c_name.as_ptr(), c_value.as_ptr()) {
0 => Err(Error::GlobalSettingFailure(name.into(), value.into())),
1 => Ok(()),
_ => unreachable!("wkhtmltopdf_set_global_setting returned invalid value"),
}
}
pub fn create_converter(mut self) -> PdfConverter {
debug!("wkhtmltopdf_create_converter");
let converter = unsafe { wkhtmltopdf_create_converter(self.global_settings) };
self.needs_delete = false;
PdfConverter {
converter: converter,
_global: self,
}
}
}
impl PdfConverter {
pub fn add_page_object(&mut self, mut pdf_object: PdfObjectSettings, page: &str) {
unsafe { pdf_object.set("page", page).expect("Failed to set 'page' setting"); }
debug!("wkhtmltopdf_add_object data=NULL");
unsafe {
wkhtmltopdf_add_object(self.converter, pdf_object.object_settings, ptr::null());
};
pdf_object.needs_delete = false;
}
pub fn add_html_object(&mut self, mut pdf_object: PdfObjectSettings, html: &str) {
let c_html = CString::new(html).expect("null byte found");
debug!("wkhtmltopdf_add_object data=&html");
unsafe {
wkhtmltopdf_add_object(self.converter, pdf_object.object_settings, c_html.as_ptr());
};
pdf_object.needs_delete = false;
}
pub fn convert<'a>(self) -> Result<PdfOutput<'a>> {
let rx = self.setup_callbacks();
debug!("wkhtmltopdf_convert");
let success = unsafe { wkhtmltopdf_convert(self.converter) == 1 };
self.remove_callbacks();
if success {
let mut buf_ptr = ptr::null();
debug!("wkhtmltopdf_get_output");
unsafe {
let bytes = wkhtmltopdf_get_output(self.converter, &mut buf_ptr) as usize;
let pdf_slice = slice::from_raw_parts(buf_ptr, bytes);
Ok(PdfOutput{ data: pdf_slice, _converter: self })
}
} else {
match rx.recv().expect("sender disconnected") {
Ok(_) => unreachable!("failed without errors"),
Err(err) => Err(err)
}
}
}
fn remove_callbacks(&self) {
let id = self.converter as usize;
let _ = ERROR_CALLBACKS.lock().unwrap().remove(&id);
let _ = FINISHED_CALLBACKS.lock().unwrap().remove(&id);
}
fn setup_callbacks(&self) -> mpsc::Receiver<Result<()>> {
let (tx, rx) = mpsc::channel();
let errors = Arc::new(Mutex::new(Vec::new()));
let tx_finished = tx.clone();
let errors_finished = errors.clone();
let on_finished = move |i| {
let errors = errors_finished.lock().unwrap();
let res = match i {
1 => Ok(()),
_ => Err(Error::ConversionFailed(errors.join(", "))),
};
let _ = tx_finished.send(res);
};
let on_error = move |err| {
let mut errors = errors.lock().unwrap();
errors.push(err);
};
{
let id = self.converter as usize;
let mut finished_callbacks = FINISHED_CALLBACKS.lock().unwrap();
finished_callbacks.insert(id, Box::new(on_finished));
let mut error_callbacks = ERROR_CALLBACKS.lock().unwrap();
error_callbacks.insert(id, Box::new(on_error));
}
unsafe {
debug!("wkhtmltopdf_set_finished_callback");
wkhtmltopdf_set_finished_callback(self.converter, Some(finished_callback));
debug!("wkhtmltopdf_set_error_callback");
wkhtmltopdf_set_error_callback(self.converter, Some(error_callback));
}
rx
}
}
impl PdfObjectSettings {
pub fn new() -> PdfObjectSettings {
debug!("wkhtmltopdf_create_object_settings");
PdfObjectSettings {
object_settings: unsafe { wkhtmltopdf_create_object_settings() },
needs_delete: true,
}
}
pub unsafe fn set(&mut self, name: &str, value: &str) -> Result<()> {
let c_name = CString::new(name).expect("setting name may not contain interior null bytes");
let c_value = CString::new(value).expect("setting value may not contain interior null bytes");
debug!("wkhtmltopdf_set_object_setting {}='{}'", name, value);
match wkhtmltopdf_set_object_setting(self.object_settings, c_name.as_ptr(), c_value.as_ptr()) {
0 => Err(Error::ObjectSettingFailure(name.into(), value.into())),
1 => Ok(()),
_ => unreachable!("wkhtmltopdf_set_object_setting returned invalid value"),
}
}
}
impl Drop for PdfGlobalSettings {
fn drop(&mut self) {
if self.needs_delete {
debug!("wkhtmltopdf_destroy_global_settings");
unsafe { wkhtmltopdf_destroy_global_settings(self.global_settings); }
}
}
}
impl Drop for PdfConverter {
fn drop(&mut self) {
debug!("wkhtmltopdf_destroy_converter");
unsafe { wkhtmltopdf_destroy_converter(self.converter) }
}
}
impl Drop for PdfObjectSettings {
fn drop(&mut self) {
if self.needs_delete {
debug!("wkhtmltopdf_destroy_object_settings");
unsafe { wkhtmltopdf_destroy_object_settings(self.object_settings); }
}
}
}
impl <'a> Drop for PdfOutput<'a> {
fn drop(&mut self) {
let mut wk_state = WKHTMLTOPDF_STATE.lock().unwrap();
debug!("wkhtmltopdf ready again");
*wk_state = WkhtmltopdfState::Ready;
}
}
impl Drop for PdfGuard {
fn drop(&mut self) {
let mut wk_state = WKHTMLTOPDF_STATE.lock().unwrap();
debug!("wkhtmltopdf_deinit");
let success = unsafe { wkhtmltopdf_deinit() == 1 };
*wk_state = WkhtmltopdfState::Dropped;
if !success {
warn!("Failed to deinitialize wkhtmltopdf")
}
}
}
unsafe extern fn finished_callback(converter: *mut wkhtmltopdf_converter, val: c_int) {
let id = converter as usize;
{
let mut callbacks = FINISHED_CALLBACKS.lock().unwrap();
if let Some(mut cb) = callbacks.remove(&id) {
cb(val as i32);
}
}
}
unsafe extern fn error_callback(converter: *mut wkhtmltopdf_converter, msg_ptr: *const c_char) {
let cstr = CStr::from_ptr(msg_ptr);
let mut callbacks = ERROR_CALLBACKS.lock().unwrap();
let id = converter as usize;
let msg = cstr.to_string_lossy().into_owned();
match callbacks.get_mut(&id) {
Some(cb) => cb(msg),
None => println!("No callback for error: {}", msg),
}
}