1use std::path::PathBuf;
4use std::sync::mpsc;
5use std::time::{Duration, Instant};
6use webview2_com::{
7 wait_with_pump, CreateCoreWebView2ControllerCompletedHandler,
8 CreateCoreWebView2EnvironmentCompletedHandler, NavigationCompletedEventHandler,
9 PermissionRequestedEventHandler, PrintToPdfCompletedHandler,
10};
11use windows::core::{Interface, PCWSTR, PWSTR};
12use windows::Win32::Foundation::{E_POINTER, E_UNEXPECTED, FALSE, HWND};
13use windows::Win32::System::Com::{
14 CoInitializeEx, CoTaskMemFree, CoUninitialize, COINIT_APARTMENTTHREADED,
15};
16use windows::Win32::UI::WindowsAndMessaging::*;
17
18use webview2_com::Microsoft::Web::WebView2::Win32::*;
19
20#[derive(Debug, Clone)]
21pub struct PageSizeInfo {
22 pub display_name: String,
23 pub width_inches: f64,
24 pub height_inches: f64,
25}
26
27pub struct HtmlToPdfParams {
28 pub url: String,
29 pub output_path: PathBuf,
30 pub paper_size: Option<PageSizeInfo>,
31 pub orientation: Option<String>,
32 pub user_data_folder: Option<PathBuf>,
33}
34
35fn map_orientation(input: Option<&str>) -> COREWEBVIEW2_PRINT_ORIENTATION {
36 match input.map(|s| s.trim().to_lowercase()).as_deref() {
37 Some("landscape") => COREWEBVIEW2_PRINT_ORIENTATION_LANDSCAPE,
38 _ => COREWEBVIEW2_PRINT_ORIENTATION_PORTRAIT,
39 }
40}
41
42struct HwndGuard(HWND);
43
44impl Drop for HwndGuard {
45 fn drop(&mut self) {
46 if !self.0.is_invalid() {
47 unsafe {
48 let _ = DestroyWindow(self.0);
49 }
50 }
51 }
52}
53
54fn wait_with_pump_timeout<T>(
55 rx: mpsc::Receiver<T>,
56 timeout: Duration,
57 context: &str,
58) -> Result<T, String> {
59 let deadline = Instant::now() + timeout;
60 loop {
61 match rx.recv_timeout(Duration::from_millis(100)) {
62 Ok(val) => return Ok(val),
63 Err(mpsc::RecvTimeoutError::Timeout) => {
64 if Instant::now() >= deadline {
65 return Err(format!(
66 "{}: timed out after {}s",
67 context,
68 timeout.as_secs()
69 ));
70 }
71 unsafe {
72 let mut msg = std::mem::zeroed();
73 while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).into() {
74 let _ = TranslateMessage(&msg);
75 DispatchMessageW(&msg);
76 }
77 }
78 }
79 Err(mpsc::RecvTimeoutError::Disconnected) => {
80 return Err(format!("{}: channel disconnected", context));
81 }
82 }
83 }
84}
85
86unsafe fn create_hidden_window() -> Result<HWND, String> {
87 let class_name = windows::core::w!("Static");
88 let hwnd = CreateWindowExW(
89 WINDOW_EX_STYLE(0),
90 class_name,
91 windows::core::w!(""),
92 WINDOW_STYLE(0),
93 0,
94 0,
95 0,
96 0,
97 Some(HWND_MESSAGE),
98 None,
99 None,
100 None,
101 )
102 .map_err(|e| format!("CreateWindowExW failed: {}", e))?;
103 Ok(hwnd)
104}
105
106fn default_user_data_folder() -> PathBuf {
107 std::env::var_os("LOCALAPPDATA")
108 .map(PathBuf::from)
109 .unwrap_or_else(std::env::temp_dir)
110 .join("winprint-kit")
111 .join("webview2_print")
112}
113
114pub fn webview2_available() -> bool {
115 unsafe {
116 let mut version = PWSTR::null();
117 let result =
118 GetAvailableCoreWebView2BrowserVersionString(PCWSTR::null(), &mut version);
119 let ok = result.is_ok() && !version.is_null();
120 if ok {
121 let _ = CoTaskMemFree(Some(version.0 as *const _));
122 }
123 ok
124 }
125}
126
127pub fn html_to_pdf(params: HtmlToPdfParams) -> Result<PathBuf, String> {
128 unsafe {
129 CoInitializeEx(None, COINIT_APARTMENTTHREADED)
130 .ok()
131 .map_err(|e| format!("CoInitializeEx failed: {}", e))?;
132 }
133 let result = (|| {
134 if !webview2_available() {
135 return Err(
136 "WebView2 Runtime is not installed. HTML printing requires WebView2 Runtime."
137 .to_string(),
138 );
139 }
140 html_to_pdf_inner(params)
141 })();
142 unsafe {
143 CoUninitialize();
144 }
145 result
146}
147
148fn html_to_pdf_inner(params: HtmlToPdfParams) -> Result<PathBuf, String> {
149 let env_timeout = Duration::from_secs(10);
150 let nav_timeout = Duration::from_secs(15);
151 let pdf_timeout = Duration::from_secs(45);
152
153 unsafe {
154 let hwnd = create_hidden_window()?;
155 let _hwnd_guard = HwndGuard(hwnd);
156 let user_data_folder = params
157 .user_data_folder
158 .clone()
159 .unwrap_or_else(default_user_data_folder);
160 std::fs::create_dir_all(&user_data_folder)
161 .map_err(|e| format!("Failed to create WebView2 user data folder: {}", e))?;
162 let user_data_wide: Vec<u16> = user_data_folder
163 .to_str()
164 .ok_or_else(|| "WebView2 user data folder contains invalid UTF-8".to_string())?
165 .encode_utf16()
166 .chain(std::iter::once(0))
167 .collect();
168 let (tx, rx) = mpsc::channel();
169 let env_handler = CreateCoreWebView2EnvironmentCompletedHandler::create(Box::new(
170 move |error_code, env| {
171 let result: Result<ICoreWebView2Environment, webview2_com::Error> = (|| {
172 error_code?;
173 env.ok_or_else(|| {
174 webview2_com::Error::WindowsError(windows::core::Error::from(E_POINTER))
175 })
176 })(
177 );
178 tx.send(result)
179 .map_err(|_| windows::core::Error::from(E_UNEXPECTED))
180 },
181 ));
182
183 CreateCoreWebView2EnvironmentWithOptions(
184 PCWSTR::null(),
185 PCWSTR(user_data_wide.as_ptr()),
186 None,
187 &env_handler,
188 )
189 .map_err(|e| format!("CreateCoreWebView2EnvironmentWithOptions failed: {}", e))?;
190
191 let environment = wait_with_pump(rx)
192 .map_err(|e| format!("wait_with_pump outer: {}", e))?
193 .map_err(|e| format!("wait_with_pump inner: {}", e))?;
194
195 let (tx, rx) = mpsc::channel();
196 let controller_handler = CreateCoreWebView2ControllerCompletedHandler::create(Box::new(
197 move |error_code, controller| {
198 let result: Result<ICoreWebView2Controller, webview2_com::Error> = (|| {
199 error_code?;
200 controller.ok_or_else(|| {
201 webview2_com::Error::WindowsError(windows::core::Error::from(E_POINTER))
202 })
203 })(
204 );
205 tx.send(result)
206 .map_err(|_| windows::core::Error::from(E_UNEXPECTED))
207 },
208 ));
209
210 environment
211 .CreateCoreWebView2Controller(hwnd, &controller_handler)
212 .map_err(|e| format!("CreateCoreWebView2Controller failed: {}", e))?;
213
214 let controller = wait_with_pump_timeout(rx, env_timeout, "Controller creation")?
215 .map_err(|e| format!("Controller creation callback error: {}", e))?;
216
217 let webview = controller
218 .CoreWebView2()
219 .map_err(|e| format!("Failed to get CoreWebView2: {}", e))?;
220
221 let settings = webview
222 .Settings()
223 .map_err(|e| format!("Failed to get settings: {}", e))?;
224 settings
225 .SetIsScriptEnabled(FALSE.into())
226 .map_err(|e| format!("Failed to disable JavaScript: {}", e))?;
227
228 settings
229 .SetAreDefaultScriptDialogsEnabled(FALSE.into())
230 .map_err(|e| format!("Failed to disable script dialogs: {}", e))?;
231
232 let perm_handler =
233 PermissionRequestedEventHandler::create(Box::new(move |_sender, args| {
234 if let Some(args) = args {
235 let _ = args.SetState(COREWEBVIEW2_PERMISSION_STATE_DENY);
236 }
237 Ok(())
238 }));
239 let mut perm_token = 0i64;
240 webview
241 .add_PermissionRequested(&perm_handler, &mut perm_token)
242 .map_err(|e| format!("Failed to add PermissionRequested handler: {}", e))?;
243
244 let (tx, rx) = mpsc::channel();
245 let nav_handler =
246 NavigationCompletedEventHandler::create(Box::new(move |_sender, args| {
247 let mut success = windows::core::BOOL(0);
248 if let Some(ref a) = args {
249 let _ = a.IsSuccess(&mut success);
250 }
251 let _ = tx.send(success.0 != 0);
252 Ok(())
253 }));
254 let mut nav_token = 0i64;
255 webview
256 .add_NavigationCompleted(&nav_handler, &mut nav_token)
257 .map_err(|e| format!("Failed to add NavigationCompleted handler: {}", e))?;
258
259 let url_wide: Vec<u16> = params
260 .url
261 .encode_utf16()
262 .chain(std::iter::once(0))
263 .collect();
264 webview
265 .Navigate(PCWSTR(url_wide.as_ptr()))
266 .map_err(|e| format!("Failed to navigate: {}", e))?;
267
268 let nav_success = wait_with_pump_timeout(rx, nav_timeout, "Navigation")?;
269 let _ = webview.remove_NavigationCompleted(nav_token);
270
271 if !nav_success {
272 return Err(format!("Navigation failed for URL: {}", params.url));
273 }
274
275 let env6 = environment
276 .cast::<ICoreWebView2Environment6>()
277 .map_err(|e| format!("Environment does not support PrintSettings: {}", e))?;
278
279 let print_settings = env6
280 .CreatePrintSettings()
281 .map_err(|e| format!("CreatePrintSettings failed: {}", e))?;
282
283 print_settings
284 .SetShouldPrintHeaderAndFooter(FALSE.into())
285 .map_err(|e| format!("SetShouldPrintHeaderAndFooter failed: {}", e))?;
286
287 print_settings
288 .SetOrientation(map_orientation(params.orientation.as_deref()))
289 .map_err(|e| format!("SetOrientation failed: {}", e))?;
290
291 if let Some(ref paper) = params.paper_size {
292 print_settings
293 .SetPageWidth(paper.width_inches)
294 .map_err(|e| format!("SetPageWidth failed: {}", e))?;
295 print_settings
296 .SetPageHeight(paper.height_inches)
297 .map_err(|e| format!("SetPageHeight failed: {}", e))?;
298 }
299
300 let webview7 = webview
301 .cast::<ICoreWebView2_7>()
302 .map_err(|e| format!("CoreWebView2 does not support ICoreWebView2_7: {}", e))?;
303
304 let output_path_str = params
305 .output_path
306 .to_str()
307 .ok_or_else(|| "Output path contains invalid UTF-8".to_string())?;
308 let output_path_wide: Vec<u16> = output_path_str
309 .encode_utf16()
310 .chain(std::iter::once(0))
311 .collect();
312
313 let (tx, rx) = mpsc::channel();
314 let pdf_handler =
315 PrintToPdfCompletedHandler::create(Box::new(move |error_code, success| {
316 let result: Result<bool, webview2_com::Error> = (|| {
317 error_code?;
318 Ok(success.into())
319 })();
320 tx.send(result)
321 .map_err(|_| windows::core::Error::from(E_UNEXPECTED))
322 }));
323
324 webview7
325 .PrintToPdf(
326 PCWSTR(output_path_wide.as_ptr()),
327 &print_settings,
328 &pdf_handler,
329 )
330 .map_err(|e| format!("PrintToPdf call failed: {}", e))?;
331
332 let pdf_success = wait_with_pump_timeout(rx, pdf_timeout, "PrintToPdf")?
333 .map_err(|e| format!("PrintToPdf callback error: {}", e))?;
334
335 if !pdf_success {
336 return Err("PrintToPdf failed".to_string());
337 }
338
339 let _ = webview.remove_PermissionRequested(perm_token);
340
341 Ok(params.output_path)
342 }
343}