use lazy_static::lazy_static;
use log::{debug, error, warn};
use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::marker::PhantomData;
use std::os::raw::{c_char, c_int};
use std::sync::{mpsc, Arc, Mutex};
use std::{ptr, slice};
use wkhtmltox_sys::image::*;
use super::{Error, ImageOutput, Result};
enum WkhtmltoimageState {
New,
Ready,
Busy,
Dropped,
}
lazy_static! {
static ref WKHTMLTOIMAGE_STATE: Mutex<WkhtmltoimageState> = Mutex::new(WkhtmltoimageState::New);
static ref WKHTMLTOIMAGE_INIT_THREAD: usize = thread_id::get();
static ref FINISHED_CALLBACKS: Mutex<HashMap<usize, Box<dyn FnMut(i32) + 'static + Send>>> = Mutex::new(HashMap::new());
static ref ERROR_CALLBACKS: Mutex<HashMap<usize, Box<dyn FnMut(String) + 'static + Send>>> = Mutex::new(HashMap::new());
}
pub struct ImageGuard {
_private: PhantomData<*const ()>,
}
pub struct ImageGlobalSettings {
global_settings: *mut wkhtmltoimage_global_settings,
needs_delete: bool,
}
pub struct ImageConverter {
converter: *mut wkhtmltoimage_converter,
_global: ImageGlobalSettings,
}
pub fn image_init() -> Result<ImageGuard> {
let mut wk_state = WKHTMLTOIMAGE_STATE.lock().unwrap();
match *wk_state {
WkhtmltoimageState::New => {
debug!("wkhtmltoimage_init graphics=0");
let success = unsafe { wkhtmltoimage_init(0) == 1 };
if success {
*wk_state = WkhtmltoimageState::Ready;
let _ = *WKHTMLTOIMAGE_INIT_THREAD;
} else {
error!("failed to initialize wkhtmltoimage");
}
Ok(ImageGuard {
_private: PhantomData,
})
}
_ => Err(Error::IllegalInit),
}
}
impl ImageGlobalSettings {
pub fn new() -> Result<ImageGlobalSettings> {
if *WKHTMLTOIMAGE_INIT_THREAD != thread_id::get() {
return Err(Error::ThreadMismatch(
*WKHTMLTOIMAGE_INIT_THREAD,
thread_id::get(),
));
}
let mut wk_state = WKHTMLTOIMAGE_STATE.lock().unwrap();
match *wk_state {
WkhtmltoimageState::New => Err(Error::NotInitialized),
WkhtmltoimageState::Dropped => Err(Error::NotInitialized),
WkhtmltoimageState::Busy => Err(Error::Blocked),
WkhtmltoimageState::Ready => {
debug!("wkhtmltoimage_create_global_settings");
let gs = unsafe { wkhtmltoimage_create_global_settings() };
*wk_state = WkhtmltoimageState::Busy;
Ok(ImageGlobalSettings {
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!("wkhtmltoimage_set_global_setting {}='{}'", name, value);
match wkhtmltoimage_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!("wkhtmltoimage_set_global_setting returned invalid value"),
}
}
pub fn create_converter(mut self) -> ImageConverter {
debug!("wkhtmltoimage_create_converter");
let converter = unsafe { wkhtmltoimage_create_converter(self.global_settings, &0) };
self.needs_delete = false;
ImageConverter {
converter,
_global: self,
}
}
}
impl ImageConverter {
pub fn convert<'a>(self) -> Result<ImageOutput<'a>> {
let rx = self.setup_callbacks();
debug!("wkhtmltoimage_convert");
let success = unsafe { wkhtmltoimage_convert(self.converter) == 1 };
self.remove_callbacks();
if success {
let mut buf_ptr = ptr::null();
debug!("wkhtmltoimage_get_output");
unsafe {
let bytes = wkhtmltoimage_get_output(self.converter, &mut buf_ptr) as usize;
let image_slice = slice::from_raw_parts(buf_ptr, bytes);
Ok(ImageOutput {
data: image_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;
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!("wkhtmltoimage_set_finished_callback");
wkhtmltoimage_set_finished_callback(self.converter, Some(finished_callback));
debug!("wkhtmltoimage_set_error_callback");
wkhtmltoimage_set_error_callback(self.converter, Some(error_callback));
}
rx
}
}
impl Drop for ImageConverter {
fn drop(&mut self) {
debug!("wkhtmltoimage_destroy_converter");
unsafe { wkhtmltoimage_destroy_converter(self.converter) }
}
}
impl<'a> Drop for ImageOutput<'a> {
fn drop(&mut self) {
let mut wk_state = WKHTMLTOIMAGE_STATE.lock().unwrap();
debug!("wkhtmltoimage ready again");
*wk_state = WkhtmltoimageState::Ready;
}
}
impl Drop for ImageGuard {
fn drop(&mut self) {
let mut wk_state = WKHTMLTOIMAGE_STATE.lock().unwrap();
debug!("wkhtmltoimage_deinit");
let success = unsafe { wkhtmltoimage_deinit() == 1 };
*wk_state = WkhtmltoimageState::Dropped;
if !success {
warn!("Failed to deinitialize wkhtmltoimage")
}
}
}
unsafe extern "C" fn finished_callback(converter: *mut wkhtmltoimage_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 "C" fn error_callback(
converter: *mut wkhtmltoimage_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),
}
}