tano-native 0.10.0

Tano local filesystem, search and desktop host core
Documentation
use base64::Engine;
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PrintSettings {
    pub paper: String,
    pub landscape: bool,
    pub copies: u32,
    pub page_ranges: String,
    pub color: bool,
    pub duplex: String,
    pub margins: f64,
    pub scale: f64,
    pub backgrounds: bool,
    pub headers: bool,
    pub printer: String,
}

impl PrintSettings {
    fn validate(&self) -> Result<(), String> {
        if !["a4", "a3", "a5", "letter", "legal"].contains(&self.paper.as_str())
            || !(1..=999).contains(&self.copies)
            || !self.margins.is_finite()
            || !(0.0..=40.0).contains(&self.margins)
            || !self.scale.is_finite()
            || !(0.1..=2.0).contains(&self.scale)
            || !["simplex", "long", "short"].contains(&self.duplex.as_str())
            || self.printer.contains('\0')
            || self.printer.len() > 4096
        {
            return Err("打印设置无效".into());
        }
        if !self.page_ranges.is_empty() {
            for part in self.page_ranges.split(',') {
                let bounds: Vec<_> = part.trim().split('-').collect();
                let number = |value: &str| {
                    value
                        .trim()
                        .parse::<u32>()
                        .ok()
                        .filter(|value| *value > 0 && *value <= 100000)
                };
                if bounds.is_empty()
                    || bounds.len() > 2
                    || number(bounds[0]).is_none()
                    || (bounds.len() == 2
                        && (number(bounds[1]).is_none() || number(bounds[0]) > number(bounds[1])))
                {
                    return Err("页码范围无效,请填写如 1-5,8,11-13".into());
                }
            }
        }
        Ok(())
    }
    fn size(&self) -> (f64, f64) {
        match self.paper.as_str() {
            "a3" => (297.0, 420.0),
            "a5" => (148.0, 210.0),
            "letter" => (215.9, 279.4),
            "legal" => (215.9, 355.6),
            _ => (210.0, 297.0),
        }
    }
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Printers {
    native_settings: bool,
    default_printer: String,
    printers: Vec<String>,
}

fn ensure_print_window(window: &tauri::WebviewWindow) -> Result<(), String> {
    if window.label().starts_with("print-") {
        Ok(())
    } else {
        Err("只能在打印窗口中执行此操作".into())
    }
}

#[tauri::command]
pub async fn document_print_printers(window: tauri::WebviewWindow) -> Result<Printers, String> {
    ensure_print_window(&window)?;
    #[cfg(windows)]
    return tauri::async_runtime::spawn_blocking(windows_print::printers)
        .await
        .map_err(|error| error.to_string())?;
    #[cfg(not(windows))]
    Ok(Printers {
        native_settings: false,
        default_printer: String::new(),
        printers: vec![],
    })
}

#[tauri::command]
pub async fn document_print_preview(
    window: tauri::WebviewWindow,
    settings: PrintSettings,
) -> Result<String, String> {
    ensure_print_window(&window)?;
    settings.validate()?;
    #[cfg(windows)]
    {
        let (sender, receiver) = tokio::sync::oneshot::channel();
        window
            .with_webview(move |webview| {
                let result = windows_print::preview(webview, settings, sender);
                if let Err(error) = result {
                    eprintln!("Print preview failed: {error}");
                }
            })
            .map_err(|error| error.to_string())?;
        let bytes = receiver
            .await
            .map_err(|_| "无法生成打印预览".to_string())??;
        return Ok(base64::engine::general_purpose::STANDARD.encode(bytes));
    }
    #[cfg(not(windows))]
    Err("当前平台请使用系统打印".into())
}

#[tauri::command]
pub async fn document_print_submit(
    window: tauri::WebviewWindow,
    settings: PrintSettings,
) -> Result<(), String> {
    ensure_print_window(&window)?;
    settings.validate()?;
    #[cfg(windows)]
    {
        let (sender, receiver) = tokio::sync::oneshot::channel();
        window
            .with_webview(move |webview| {
                let result = windows_print::submit(webview, settings, sender);
                if let Err(error) = result {
                    eprintln!("Printing failed: {error}");
                }
            })
            .map_err(|error| error.to_string())?;
        return receiver.await.map_err(|_| "打印任务未完成".to_string())?;
    }
    #[cfg(not(windows))]
    Err("当前平台请使用系统打印".into())
}

#[cfg(windows)]
mod windows_print {
    use super::*;
    use std::ptr::{null, null_mut};
    use webview2_com::{
        Microsoft::Web::WebView2::Win32::*, PrintCompletedHandler, PrintToPdfStreamCompletedHandler,
    };
    use windows::{
        core::{Interface, HSTRING},
        Win32::System::Com::{IStream, STATFLAG_NONAME, STATSTG, STREAM_SEEK_SET},
    };
    use windows_sys::Win32::Graphics::Printing::*;

    unsafe fn wide(pointer: *const u16) -> String {
        if pointer.is_null() {
            return String::new();
        }
        let mut length = 0;
        while length < 32768 && unsafe { *pointer.add(length) } != 0 {
            length += 1;
        }
        String::from_utf16_lossy(unsafe { std::slice::from_raw_parts(pointer, length) })
    }
    pub fn printers() -> Result<Printers, String> {
        unsafe {
            let mut default_size = 0;
            GetDefaultPrinterW(null_mut(), &mut default_size);
            let mut default_name = vec![0u16; default_size as usize];
            let default_printer = if !default_name.is_empty()
                && GetDefaultPrinterW(default_name.as_mut_ptr(), &mut default_size) != 0
            {
                wide(default_name.as_ptr())
            } else {
                String::new()
            };
            let mut needed = 0;
            let mut count = 0;
            let flags = PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS;
            EnumPrintersW(flags, null(), 4, null_mut(), 0, &mut needed, &mut count);
            if needed == 0 {
                return Ok(Printers {
                    native_settings: true,
                    default_printer,
                    printers: vec![],
                });
            }
            let mut buffer = vec![0usize; (needed as usize).div_ceil(std::mem::size_of::<usize>())];
            if EnumPrintersW(
                flags,
                null(),
                4,
                buffer.as_mut_ptr().cast(),
                needed,
                &mut needed,
                &mut count,
            ) == 0
            {
                return Err(std::io::Error::last_os_error().to_string());
            }
            let entries = std::slice::from_raw_parts(
                buffer.as_ptr().cast::<PRINTER_INFO_4W>(),
                count as usize,
            );
            let mut printers: Vec<_> = entries
                .iter()
                .map(|entry| wide(entry.pPrinterName))
                .filter(|name| !name.is_empty())
                .collect();
            printers.sort();
            printers.dedup();
            Ok(Printers {
                native_settings: true,
                default_printer,
                printers,
            })
        }
    }
    unsafe fn settings(
        webview: &tauri::webview::PlatformWebview,
        input: &PrintSettings,
        job: bool,
    ) -> windows::core::Result<ICoreWebView2PrintSettings> {
        unsafe {
            let environment: ICoreWebView2Environment6 = webview.environment().cast()?;
            let settings = environment.CreatePrintSettings()?;
            let (width, height) = input.size();
            settings.SetOrientation(if input.landscape {
                COREWEBVIEW2_PRINT_ORIENTATION_LANDSCAPE
            } else {
                COREWEBVIEW2_PRINT_ORIENTATION_PORTRAIT
            })?;
            settings.SetPageWidth(width / 25.4)?;
            settings.SetPageHeight(height / 25.4)?;
            settings.SetScaleFactor(input.scale)?;
            settings.SetMarginTop(input.margins / 25.4)?;
            settings.SetMarginBottom(input.margins / 25.4)?;
            settings.SetMarginLeft(input.margins / 25.4)?;
            settings.SetMarginRight(input.margins / 25.4)?;
            settings.SetShouldPrintBackgrounds(input.backgrounds)?;
            settings.SetShouldPrintHeaderAndFooter(input.headers)?;
            let extended: ICoreWebView2PrintSettings2 = settings.cast()?;
            extended.SetPageRanges(&HSTRING::from(&input.page_ranges))?;
            extended.SetColorMode(if input.color {
                COREWEBVIEW2_PRINT_COLOR_MODE_COLOR
            } else {
                COREWEBVIEW2_PRINT_COLOR_MODE_GRAYSCALE
            })?;
            if job {
                extended.SetPrinterName(&HSTRING::from(&input.printer))?;
                extended.SetCopies(input.copies as i32)?;
                extended.SetDuplex(match input.duplex.as_str() {
                    "long" => COREWEBVIEW2_PRINT_DUPLEX_TWO_SIDED_LONG_EDGE,
                    "short" => COREWEBVIEW2_PRINT_DUPLEX_TWO_SIDED_SHORT_EDGE,
                    _ => COREWEBVIEW2_PRINT_DUPLEX_ONE_SIDED,
                })?;
            }
            Ok(settings)
        }
    }
    unsafe fn stream_bytes(stream: &IStream) -> Result<Vec<u8>, String> {
        unsafe {
            let mut stat = STATSTG::default();
            stream
                .Stat(&mut stat, STATFLAG_NONAME)
                .map_err(|error| error.to_string())?;
            if stat.cbSize > 64 * 1024 * 1024 {
                return Err("打印预览过大,请减少单次打印页数".into());
            }
            stream
                .Seek(0, STREAM_SEEK_SET, None)
                .map_err(|error| error.to_string())?;
            let mut bytes = vec![0u8; stat.cbSize as usize];
            let mut offset = 0;
            while offset < bytes.len() {
                let mut read = 0;
                stream
                    .Read(
                        bytes.as_mut_ptr().add(offset).cast(),
                        (bytes.len() - offset) as u32,
                        Some(&mut read),
                    )
                    .ok()
                    .map_err(|error| error.to_string())?;
                if read == 0 {
                    return Err("打印预览数据不完整".into());
                }
                offset += read as usize;
            }
            Ok(bytes)
        }
    }
    pub fn preview(
        webview: tauri::webview::PlatformWebview,
        input: PrintSettings,
        sender: tokio::sync::oneshot::Sender<Result<Vec<u8>, String>>,
    ) -> windows::core::Result<()> {
        unsafe {
            let settings = settings(&webview, &input, false)?;
            let view: ICoreWebView2_16 = webview.controller().CoreWebView2()?.cast()?;
            let callback =
                PrintToPdfStreamCompletedHandler::create(Box::new(move |result, stream| {
                    let output = result
                        .map_err(|error| error.to_string())
                        .and_then(|_| stream.ok_or_else(|| "无法生成打印预览".to_string()))
                        .and_then(|stream| stream_bytes(&stream));
                    let _ = sender.send(output);
                    Ok(())
                }));
            view.PrintToPdfStream(&settings, &callback)
        }
    }
    pub fn submit(
        webview: tauri::webview::PlatformWebview,
        input: PrintSettings,
        sender: tokio::sync::oneshot::Sender<Result<(), String>>,
    ) -> windows::core::Result<()> {
        unsafe {
            let settings = settings(&webview, &input, true)?;
            let view: ICoreWebView2_16 = webview.controller().CoreWebView2()?.cast()?;
            let callback = PrintCompletedHandler::create(Box::new(move |result, status| {
                let output = result.map_err(|error| error.to_string()).and_then(|_| {
                    if status == COREWEBVIEW2_PRINT_STATUS_SUCCEEDED {
                        Ok(())
                    } else if status == COREWEBVIEW2_PRINT_STATUS_PRINTER_UNAVAILABLE {
                        Err("所选打印机不可用,请检查连接或选择其他打印机".into())
                    } else {
                        Err("打印失败,请检查打印机和打印设置".into())
                    }
                });
                let _ = sender.send(output);
                Ok(())
            }));
            view.Print(&settings, &callback)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    fn input() -> PrintSettings {
        PrintSettings {
            paper: "a4".into(),
            landscape: false,
            copies: 1,
            page_ranges: "1-5,8,11-13".into(),
            color: true,
            duplex: "simplex".into(),
            margins: 16.0,
            scale: 1.0,
            backgrounds: true,
            headers: false,
            printer: "Printer".into(),
        }
    }
    #[test]
    fn validates_settings_before_contacting_any_printer() {
        let mut value = input();
        assert!(value.validate().is_ok());
        for range in ["0", "5-2", "1-", "1,,2", "1-2-3", "a"] {
            value.page_ranges = range.into();
            assert!(value.validate().is_err());
        }
        value = input();
        value.copies = 0;
        assert!(value.validate().is_err());
        value = input();
        value.scale = f64::NAN;
        assert!(value.validate().is_err());
        assert_eq!(input().size(), (210.0, 297.0));
    }
}