Skip to main content

winprint_ext/ticket/
builder.rs

1use super::{PrintTicket, DEFAULT_PRINT_TICKET_XML};
2use crate::{
3    printer::PrinterDevice,
4    utils::{stream::copy_com_stream_to_vec, wchar},
5};
6use thiserror::Error;
7use windows::{
8    core::{BSTR, PCWSTR},
9    Win32::{
10        Graphics::Printing::PrintTicket::{
11            kPTJobScope, PTCloseProvider, PTMergeAndValidatePrintTicket, PTOpenProvider,
12            HPTPROVIDER,
13        },
14        UI::Shell::SHCreateMemStream,
15    },
16};
17
18/// Represents a print ticket builder, which can be used to build a print ticket.
19pub struct PrintTicketBuilder {
20    xml: Vec<u8>,
21    provider: HPTPROVIDER,
22}
23
24#[derive(Error, Debug)]
25/// Represents an error occurred while building a print ticket.
26pub enum PrintTicketBuilderError {
27    /// Failed to open print ticket provider.
28    #[error("Failed to open print ticket provider")]
29    OpenProviderFailed(#[source] windows::core::Error),
30    /// Stream not allocated.
31    #[error("Stream not allocated")]
32    StreamNotAllocated,
33    /// Failed to merge print tickets.
34    #[error("Failed to merge print tickets")]
35    MergePrintTicketsFailed(String, #[source] windows::core::Error),
36    /// Failed to decode print ticket.
37    #[error("Failed to decode print ticket")]
38    DecodePrintTicketFailed(#[source] windows::core::Error),
39}
40
41impl PrintTicketBuilder {
42    /// Create a new print ticket builder for the given printer device.
43    pub fn new(device: &PrinterDevice) -> Result<Self, PrintTicketBuilderError> {
44        let provider = unsafe {
45            PTOpenProvider(PCWSTR(wchar::to_wide_chars(device.os_name()).as_ptr()), 1)
46                .map_err(PrintTicketBuilderError::OpenProviderFailed)?
47        };
48        Ok(Self {
49            xml: DEFAULT_PRINT_TICKET_XML.into(),
50            provider,
51        })
52    }
53
54    /// Merge the given print ticket into the current print ticket.
55    pub fn merge(&mut self, delta: impl Into<PrintTicket>) -> Result<(), PrintTicketBuilderError> {
56        unsafe {
57            let base = SHCreateMemStream(Some(self.xml.as_ref()))
58                .ok_or(PrintTicketBuilderError::StreamNotAllocated)?;
59            let delta = SHCreateMemStream(Some(delta.into().get_xml()))
60                .ok_or(PrintTicketBuilderError::StreamNotAllocated)?;
61            let result =
62                SHCreateMemStream(None).ok_or(PrintTicketBuilderError::StreamNotAllocated)?;
63            let mut error_message = BSTR::default();
64            PTMergeAndValidatePrintTicket(
65                self.provider,
66                &base,
67                &delta,
68                kPTJobScope,
69                Some(&result),
70                Some(&mut error_message),
71            )
72            .map_err(|win32_error| {
73                PrintTicketBuilderError::MergePrintTicketsFailed(
74                    error_message.to_string(),
75                    win32_error,
76                )
77            })?;
78            copy_com_stream_to_vec(&mut self.xml, &result)
79                .map_err(PrintTicketBuilderError::DecodePrintTicketFailed)?;
80        }
81        Ok(())
82    }
83
84    /// Build the print ticket.
85    pub fn build(mut self) -> Result<PrintTicket, PrintTicketBuilderError> {
86        let xml = std::mem::take(&mut self.xml);
87        Ok(PrintTicket { xml })
88    }
89}
90
91impl Drop for PrintTicketBuilder {
92    fn drop(&mut self) {
93        unsafe {
94            let _ = PTCloseProvider(self.provider);
95        }
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::PrintTicketBuilder;
102    use crate::{
103        test_utils::null_device,
104        ticket::{PrintTicket, PrintTicketBuilderError},
105    };
106
107    #[test]
108    fn merge_simple_ticket() {
109        let device = null_device::thread_local();
110        let mut builder = PrintTicketBuilder::new(&device).unwrap();
111        let delta = r#"<psf:PrintTicket xmlns:psf="http://schemas.microsoft.com/windows/2003/08/printing/printschemaframework" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" version="1" xmlns:psk="http://schemas.microsoft.com/windows/2003/08/printing/printschemakeywords">
112    <psf:Feature name="psk:PageMediaSize">
113		<psf:Option name="psk:NorthAmericaTabloid">
114			<psf:ScoredProperty name="psk:MediaSizeWidth">
115				<psf:Value xsi:type="xsd:integer">279400</psf:Value>
116			</psf:ScoredProperty>
117			<psf:ScoredProperty name="psk:MediaSizeHeight">
118				<psf:Value xsi:type="xsd:integer">431800</psf:Value>
119			</psf:ScoredProperty>
120			<psf:Property name="psk:DisplayName">
121				<psf:Value xsi:type="xsd:string">Tabloid</psf:Value>
122			</psf:Property>
123		</psf:Option>
124	</psf:Feature>
125</psf:PrintTicket>"#;
126        builder.merge(PrintTicket::from_xml(delta)).unwrap();
127        let _ticket = builder.build().unwrap();
128    }
129
130    #[test]
131    fn merge_invalid_ticket() {
132        let device = null_device::thread_local();
133        let mut builder = PrintTicketBuilder::new(&device).unwrap();
134        let delta = r#"This is not a valid print ticket"#;
135        let result = builder.merge(PrintTicket::from_xml(delta));
136        assert!(matches!(
137            result,
138            Err(PrintTicketBuilderError::MergePrintTicketsFailed(..))
139        ));
140    }
141}