wry icon indicating copy to clipboard operation
wry copied to clipboard

enhance: Add API to allow print options

Open mildred opened this issue 1 year ago • 9 comments

Implement global print options with WebKitGtk2 support and reuse MacOS support from #1259

Previous failed attempt for Windows was #1167

This could allow a print API in Tauri: https://github.com/tauri-apps/tauri/issues/4917

The specific goal I have is to generate PDF files from WebViews in Tauri.

I took the approach of specifying options in an array as it allows to only pass some options of interest and not the full list, and it also allows some platform to support some options and not others. this would be easier to have the feature developped and some options might not be relevant everywhere.

mildred avatar Jul 13 '24 12:07 mildred

Currently there is an issue with printing PDF on webkit. It shows lpr: Error - No default destination.

mildred avatar Jul 13 '24 22:07 mildred

It seems there is easy way to print to PDF in WebkitGtk: https://bugs.webkit.org/show_bug.cgi?id=212814

mildred avatar Jul 13 '24 22:07 mildred

Perhaps there is a way to make the PDF generation work by using the PDF printer by name, but there is a problem with i18n in that case as the printer name can change...

I'm not sure how to continue there. Still, it's possible to have the printing options without the PDF generation if that can be of use to other people (it seems it would be looking at https://github.com/tauri-apps/tauri/issues/4917)

mildred avatar Jul 17 '24 10:07 mildred

On windows, you could use the WebView2 function PrintToPdf. It seems as if the used webview-com crate provides an interface to this function.

nros avatar Jul 22 '24 14:07 nros

Here's a workable POC for the Windows to print to PDF. I'm unfamiliar with win32 API, it would be nice if this one can give you some ideas.

pub fn print(&self) -> Result<()> {
  // self.eval(
  //   "window.print()",
  //   None::<Box<dyn FnOnce(String) + Send + 'static>>,
  // )
  unsafe {
    if let Ok(webview7) = self.webview.cast::<ICoreWebView2_7>() {
      if let Ok(env) = self.env.cast::<ICoreWebView2Environment6>() {
        let printsettings = env.CreatePrintSettings().unwrap();
        let _ = webview7.PrintToPdf(
          PCWSTR::from_raw(HSTRING::from("C:\\Users\\Jason\\Downloads\\webview2.pdf").as_ptr()),
          &printsettings,
          &PrintToPdfCompletedHandler::create(Box::new(move |_hresult, _is_success| {
            dbg!(_hresult, _is_success);
            Ok(())
          })),
        );
      }
    }

    Ok(())
  }
}

pewsheen avatar Jul 22 '24 15:07 pewsheen

It seems possible to print to PDF using the print to file printer, and it's possible to discover this printer as webkitgtk does it already, but in a private part of its code...

https://github.com/WebKit/WebKit/blob/c289534e80934cfed8a0f32ac13a3cf70beb0f7f/Source/WebKit/UIProcess/API/gtk/WebKitPrintOperation.cpp#L423-L440

Just below you have the code that uses the "print to file printer" and generates a file

Ideally, in wry, we should be able to run identical code and fetch the printer name to feed it to the printer settings. Here is the code that selects the printer from the printer settings in WebkitGtk (and if not found selects the default printer)

https://github.com/WebKit/WebKit/blob/c289534e80934cfed8a0f32ac13a3cf70beb0f7f/Source/WebKit/UIProcess/API/gtk/WebKitPrintOperation.cpp#L836C28-L836C56

Up until now, I was not able to select correctly the file printer, I could trace the function calls with ltrace -e 'gtk_print*+gtk_page*' but the gtk_print_settings_get_printer function returned NULL.

What's tricky is that the gtk_enumerate_printers is not available to the gtk crate. And the gtk crate is unmaintained. However, the gtk4 crate has the function, but that's not the gtk version wry is built against... :(

Also, trying to use extern C bindings for those gtk functions could work but the G_OBJECT_TYPE_NAME call to get the printer backend name is a C macro and is not available to FFI or extern C :( GObject is tricky and I don't see how without the C proprocessor I can manage to get the type name of a GObject pointer... Else, this would have been doable with something like:

use gtk;

pub struct Printer { }
pub struct PrinterBackend { }
struct UserData {
  printer_name: &str
}

extern "C" {
    // pub fn wry_gtk_find_file_printer(settings: &gtk::PrintSettings) -> &str;
    pub fn gtk_enumerate_printers(cb: Fn(&Printer, &UserData) -> bool, userdata: &UserData, destroy: &UserData, wait: bool);
    pub fn gtk_printer_get_backend(printer: &Printer) -> &PrinterBackend;
    pub fn gtk_printer_get_name(printer: &Printer) -> &str;
    fn g_type_name(backend: &PrinterBackend) -> &str;
}

pub fn find_file_printer() -> String {
  let userdata: UserData;
  unsafe {
    gtk_enumerate_printers(|printer, userdata| {
      let backend = gtk_printer_get_backend(printer);
      let type_name = g_type_name(backend);
      if (type_name == "GtkPrintBackendFile") {
        userdata.printer_name = gtk_printer_get_name(printer);
      }
    }, &userdata, null, true);
  }
  return userdata.printer_name;
}

I'll probably continue investigation later...

mildred avatar Jul 23 '24 21:07 mildred

As for Win32, I have no means of testing

mildred avatar Jul 23 '24 21:07 mildred

Another problem... with this I found that the printers have all an empty name :(

use gtk;
use std::ptr;

pub struct Printer { }
pub struct PrinterBackend { }
struct UserData {
  printer_name: String
}

extern "C" {
    // pub fn wry_gtk_find_file_printer(settings: &gtk::PrintSettings) -> &str;
    pub fn gtk_enumerate_printers(cb: extern "C" fn(&Printer, &UserData) -> bool, userdata: &UserData, destroy: *const i32, wait: bool);
    pub fn gtk_printer_get_backend(printer: &Printer) -> &PrinterBackend;
    pub fn gtk_printer_get_name(printer: &Printer) -> &str;
    fn g_type_name(backend: &PrinterBackend) -> &str;
}

extern "C" fn printer_enumerate(printer: &Printer, _userdata: &UserData) -> bool {
    unsafe {
        // let backend = gtk_printer_get_backend(printer);
        //let type_name = g_type_name(backend);
        //if type_name == "GtkPrintBackendFile" {
        //  userdata.printer_name = gtk_printer_get_name(printer).to_string();
        //  return true; // stop iteration
        //}
        let printer_name = gtk_printer_get_name(printer);
        println!("Printer: {printer_name}");
        return false; // continue iteration
    }
}

pub fn find_file_printer() -> String {
    let userdata: UserData = UserData { printer_name: "".to_string() };
    unsafe {
        let cb = printer_enumerate as extern "C" fn(&Printer, &UserData) -> bool;
        gtk_enumerate_printers(cb, &userdata, ptr::null(), true);
    }
    return userdata.printer_name;
}

mildred avatar Jul 23 '24 21:07 mildred

What I'll probably be doing is to set up a HTTP server and open the browser to localhost on that server and let the user use the print dialog himself... But this PR is still interesting to get a print API rolling...

mildred avatar Jul 23 '24 21:07 mildred