diff --git a/src/printer/dxgi.rs b/src/printer/dxgi.rs index b60e48a..02c61ea 100644 --- a/src/printer/dxgi.rs +++ b/src/printer/dxgi.rs @@ -1,6 +1,7 @@ use crate::printer::PrinterDevice; use crate::ticket::PrintTicket; use crate::ticket::ToDevModeError; +use crate::utils::com::ComInitializer; use crate::utils::print_completion_source::PrintCompletionSource; use crate::utils::print_completion_source::PrintCompletionWaiter; use crate::utils::wchar; @@ -35,11 +36,8 @@ use windows::Win32::Storage::Xps::Printing::IPrintDocumentPackageTarget; use windows::Win32::Storage::Xps::Printing::IPrintDocumentPackageTargetFactory; use windows::Win32::Storage::Xps::Printing::PrintDocumentPackageTargetFactory; use windows::Win32::System::Com::CoCreateInstance; -use windows::Win32::System::Com::CoInitializeEx; -use windows::Win32::System::Com::CoUninitialize; use windows::Win32::System::Com::IConnectionPointContainer; use windows::Win32::System::Com::CLSCTX_ALL; -use windows::Win32::System::Com::COINIT_MULTITHREADED; use windows::Win32::UI::Shell::SHCreateMemStream; #[derive(Error, Debug)] @@ -85,28 +83,6 @@ pub struct DxgiPrintContext { _com_initializer: ComInitializer, } -#[non_exhaustive] -struct ComInitializer { - // Do not use empty struct. - // Empty struct is easy to misuse. - _dummy: (), -} -impl ComInitializer { - fn new() -> ComInitializer { - unsafe { - let _ = CoInitializeEx(None, COINIT_MULTITHREADED); - } - ComInitializer { _dummy: () } - } -} -impl Drop for ComInitializer { - fn drop(&mut self) { - unsafe { - CoUninitialize(); - } - } -} - impl DxgiPrintContext { /// Create a new print context, used for printing via DXGI. pub fn new( diff --git a/src/printer/pdfium.rs b/src/printer/pdfium.rs index a1fbf7c..e5093e2 100644 --- a/src/printer/pdfium.rs +++ b/src/printer/pdfium.rs @@ -3,22 +3,27 @@ use crate::printer::FilePrinter; use crate::printer::PrinterDevice; use crate::ticket::PrintTicket; use crate::ticket::ToDevModeError; +use crate::utils::emf::Emf; use crate::utils::pdfium::PdfiumCustomDocument; use crate::utils::pdfium::PdfiumGuard; use crate::utils::wchar; use scopeguard::defer; +use std::cell::Cell; use std::path::Path; use std::ptr; use std::{fs::File, mem}; use thiserror::Error; +use windows::Win32::Foundation::RECT; use windows::{ core::PCWSTR, - Win32::Graphics::Gdi::{ - CreateDCW, DeleteDC, GetDeviceCaps, SetBrushOrgEx, SetGraphicsMode, SetStretchBltMode, - SetViewportOrgEx, GET_DEVICE_CAPS_INDEX, GM_ADVANCED, HALFTONE, LOGPIXELSX, LOGPIXELSY, - PHYSICALHEIGHT, PHYSICALOFFSETX, PHYSICALOFFSETY, PHYSICALWIDTH, + Win32::{ + Graphics::Gdi::{ + CreateDCW, DeleteDC, GetDeviceCaps, SetBrushOrgEx, SetGraphicsMode, SetStretchBltMode, + GET_DEVICE_CAPS_INDEX, GM_ADVANCED, HALFTONE, LOGPIXELSX, LOGPIXELSY, PHYSICALHEIGHT, + PHYSICALOFFSETX, PHYSICALOFFSETY, PHYSICALWIDTH, + }, + Storage::Xps::{AbortDoc, EndDoc, EndPage, StartDocW, StartPage, DOCINFOW}, }, - Win32::Storage::Xps::*, }; #[derive(Error, Debug)] @@ -33,6 +38,27 @@ pub enum PdfiumPrinterError { /// Print ticket error. #[error("Print Ticker Error")] PrintTicketError(#[source] ToDevModeError), + /// StartDoc failed. + #[error("StartDocW failed (returned {0})")] + StartDocFailed(i32), + /// StartPage failed. + #[error("StartPage failed for page {0} (returned {1})")] + StartPageFailed(i32, i32), + /// EndPage failed. + #[error("EndPage failed for page {0} (returned {1})")] + EndPageFailed(i32, i32), + /// EndDoc failed. + #[error("EndDoc failed (returned {0})")] + EndDocFailed(i32), + /// PDFium failed to load the document. + #[error("PDFium failed to load the document (error {0})")] + PdfiumLoadFailed(u32), + /// EMF creation failed for a page. + #[error("Failed to create EMF for page {0}")] + EmfCreateFailed(i32), + /// EMF playback failed for a page. + #[error("Failed to playback EMF for page {0}")] + EmfPlaybackFailed(i32), } /// A printer that uses Pdfium to print PDF documents. @@ -58,6 +84,7 @@ const PRINT_DRIVER: PCWSTR = PCWSTR( impl FilePrinter for PdfiumPrinter { type Options = PrintTicket; type Error = PdfiumPrinterError; + fn print( &self, path: &Path, @@ -87,12 +114,14 @@ impl FilePrinter for PdfiumPrinter { defer! { let _ = DeleteDC(hdc_print); } + SetGraphicsMode(hdc_print, GM_ADVANCED); SetStretchBltMode(hdc_print, HALFTONE); // After setting the HALFTONE stretching mode, // an application must call the SetBrushOrgEx function to set the brush origin. // If it fails to do so, brush misalignment occurs. let _ = SetBrushOrgEx(hdc_print, 0, 0, None); + let mut doc_name = wchar::to_wide_chars(path.file_name().unwrap_or(path.as_ref())); let doc_info = DOCINFOW { cbSize: mem::size_of::() as i32, @@ -101,43 +130,104 @@ impl FilePrinter for PdfiumPrinter { lpszOutput: PCWSTR::null(), lpszDatatype: PCWSTR::null(), }; - StartDocW(hdc_print, &doc_info); - { - let _pdfium_guard = PdfiumGuard::guard(); - let mut file = File::open(path).map_err(PdfiumPrinterError::FileIOError)?; - let mut file_delegation = PdfiumCustomDocument::new(&mut file) - .map_err(PdfiumPrinterError::FileIOError)?; - let document = FPDF_LoadCustomDocument(file_delegation.as_mut(), ptr::null()); - defer! { - FPDF_CloseDocument(document); - } - let page_count = FPDF_GetPageCount(document); - for page_index in 0..page_count { - let page = FPDF_LoadPage(document, page_index); - defer! { - FPDF_ClosePage(page); - } - StartPage(hdc_print); - let get_attr = - |kind: GET_DEVICE_CAPS_INDEX| -> i32 { GetDeviceCaps(hdc_print, kind) }; - let dpi_x = get_attr(LOGPIXELSX); - let dpi_y = get_attr(LOGPIXELSY); - let page_width = FPDF_GetPageWidth(page) * dpi_x as f64 / 72.0; - let page_height = FPDF_GetPageHeight(page) * dpi_y as f64 / 72.0; - let physical_width = get_attr(PHYSICALWIDTH) as f64; - let physical_height = get_attr(PHYSICALHEIGHT) as f64; - let scale = - f64::min(physical_width / page_width, physical_height / page_height); - let w = page_width * scale; - let h = page_height * scale; - let org_x = -get_attr(PHYSICALOFFSETX); - let org_y = -get_attr(PHYSICALOFFSETY); - let _ = SetViewportOrgEx(hdc_print, org_x, org_y, None); - FPDF_RenderPage(hdc_print, page, 0, 0, w as i32, h as i32, 0, FPDF_PRINTING); - EndPage(hdc_print); + + let start_doc_ret = StartDocW(hdc_print, &doc_info); + if start_doc_ret <= 0 { + return Err(PdfiumPrinterError::StartDocFailed(start_doc_ret)); + } + + let document_completed = Cell::new(false); + defer! { + if !document_completed.get() { + let _ = AbortDoc(hdc_print); } } - EndDoc(hdc_print); + + let _pdfium_guard = PdfiumGuard::guard(); + let mut file = File::open(path).map_err(PdfiumPrinterError::FileIOError)?; + let mut file_delegation = + PdfiumCustomDocument::new(&mut file).map_err(PdfiumPrinterError::FileIOError)?; + let document = FPDF_LoadCustomDocument(file_delegation.as_mut(), ptr::null()); + if document.is_null() { + return Err(PdfiumPrinterError::PdfiumLoadFailed(FPDF_GetLastError())); + } + defer! { + FPDF_CloseDocument(document); + } + + let get_attr = |kind: GET_DEVICE_CAPS_INDEX| -> i32 { GetDeviceCaps(hdc_print, kind) }; + let page_count = FPDF_GetPageCount(document); + for page_index in 0..page_count { + let page = FPDF_LoadPage(document, page_index); + defer! { + FPDF_ClosePage(page); + } + + let start_page_ret = StartPage(hdc_print); + if start_page_ret <= 0 { + return Err(PdfiumPrinterError::StartPageFailed( + page_index, + start_page_ret, + )); + } + let dpi_x = get_attr(LOGPIXELSX); + let dpi_y = get_attr(LOGPIXELSY); + let page_std_width = FPDF_GetPageWidth(page); + let page_std_height = FPDF_GetPageHeight(page); + let page_width = (page_std_width * dpi_x as f64 / 72.0).round() as i32; + let page_height = (page_std_height * dpi_y as f64 / 72.0).round() as i32; + let emf = Emf::new( + hdc_print, + (page_std_width * 2540.0 / 72.0).round() as i32, + (page_std_height * 2540.0 / 72.0).round() as i32, + |hdc_emf| { + SetGraphicsMode(hdc_emf, GM_ADVANCED); + FPDF_RenderPage( + hdc_emf, + page, + 0, + 0, + page_width, + page_height, + 0, + FPDF_PRINTING, + ); + true + }, + ) + .map_err(|_| PdfiumPrinterError::EmfCreateFailed(page_index))?; + + let paper_width = get_attr(PHYSICALWIDTH); + let paper_height = get_attr(PHYSICALHEIGHT); + let scale = f64::min( + paper_width as f64 / page_width as f64, + paper_height as f64 / page_height as f64, + ); + let actual_width = (page_width as f64 * scale).round() as i32; + let actual_height = (page_height as f64 * scale).round() as i32; + let left = -get_attr(PHYSICALOFFSETX) + (paper_width - actual_width) / 2; + let top = -get_attr(PHYSICALOFFSETY) + (paper_height - actual_height) / 2; + let target_rect = RECT { + left, + top, + right: actual_width + left, + bottom: actual_height + top, + }; + let page_result = emf + .playback(hdc_print, target_rect) + .map_err(|_| PdfiumPrinterError::EmfPlaybackFailed(page_index)); + let end_page_ret = EndPage(hdc_print); + page_result?; + if end_page_ret <= 0 { + return Err(PdfiumPrinterError::EndPageFailed(page_index, end_page_ret)); + } + } + + let end_doc_ret = EndDoc(hdc_print); + if end_doc_ret <= 0 { + return Err(PdfiumPrinterError::EndDocFailed(end_doc_ret)); + } + document_completed.set(true); } Ok(()) } diff --git a/src/utils/com.rs b/src/utils/com.rs new file mode 100644 index 0000000..986383b --- /dev/null +++ b/src/utils/com.rs @@ -0,0 +1,23 @@ +use windows::Win32::System::Com::{CoInitializeEx, CoUninitialize, COINIT_MULTITHREADED}; + +#[non_exhaustive] +pub struct ComInitializer { + // Do not use empty struct. + // Empty struct is easy to misuse. + _dummy: (), +} +impl ComInitializer { + pub fn new() -> ComInitializer { + unsafe { + let _ = CoInitializeEx(None, COINIT_MULTITHREADED); + } + ComInitializer { _dummy: () } + } +} +impl Drop for ComInitializer { + fn drop(&mut self) { + unsafe { + CoUninitialize(); + } + } +} diff --git a/src/utils/emf.rs b/src/utils/emf.rs new file mode 100644 index 0000000..4a7a3fa --- /dev/null +++ b/src/utils/emf.rs @@ -0,0 +1,362 @@ +use crate::utils::com::ComInitializer; +use scopeguard::defer; +use std::mem; +use std::ptr; +use std::slice; +use thiserror::Error; +use windows::Win32::Graphics::Gdi::MODIFY_WORLD_TRANSFORM_MODE; +use windows::Win32::Graphics::Gdi::{CHECKJPEGFORMAT, CHECKPNGFORMAT, EMRSETWORLDTRANSFORM}; +use windows::Win32::Graphics::Gdi::{EMRMODIFYWORLDTRANSFORM, QUERYESCSUPPORT}; +use windows::Win32::Storage::Xps::ExtEscape; +use windows::{ + core::PCWSTR, + Win32::{ + Foundation::{LPARAM, RECT}, + Graphics::{ + Gdi::{ + CloseEnhMetaFile, CreateEnhMetaFileW, DeleteEnhMetaFile, EnumEnhMetaFile, + GetEnhMetaFileHeader, GetWorldTransform, ModifyWorldTransform, + PlayEnhMetaFileRecord, SetWorldTransform, StretchDIBits, BITMAPINFO, + BITMAPINFOHEADER, BI_JPEG, BI_PNG, BI_RGB, DIB_USAGE, EMRSTRETCHDIBITS, + EMR_MODIFYWORLDTRANSFORM, EMR_SETLAYOUT, EMR_SETWORLDTRANSFORM, EMR_STRETCHDIBITS, + ENHMETAHEADER, ENHMETARECORD, HANDLETABLE, HDC, HENHMETAFILE, MWT_IDENTITY, + MWT_LEFTMULTIPLY, MWT_RIGHTMULTIPLY, ROP_CODE, XFORM, + }, + Imaging::{ + CLSID_WICImagingFactory2, GUID_WICPixelFormat32bppBGRA, WICBitmapDitherTypeNone, + WICBitmapPaletteTypeMedianCut, WICDecodeMetadataCacheOnDemand, + D2D::IWICImagingFactory2, + }, + }, + System::Com::{CoCreateInstance, CLSCTX_ALL}, + }, +}; + +#[derive(Error, Debug)] +#[error("Failed to create EMF")] +pub struct EmfCreateFailedError; + +#[derive(Error, Debug)] +#[error("Failed to play EMF")] +pub struct EmfPlaybackFailedError; + +pub struct Emf { + metafile: HENHMETAFILE, +} + +impl Emf { + /// Creates a new EMF by drawing on a metafile DC. + /// All units are in .01-millimeter units. + pub unsafe fn new( + hdc: HDC, + width: i32, + height: i32, + draw: impl FnOnce(HDC) -> bool, + ) -> Result { + // in .01-millimeter units + let frame = RECT { + left: 0, + top: 0, + right: width, + bottom: height, + }; + + let emf_hdc = CreateEnhMetaFileW( + hdc, + PCWSTR::null(), + Some(ptr::addr_of!(frame)), + PCWSTR::null(), + ); + if emf_hdc.is_invalid() { + return Err(EmfCreateFailedError); + } + + if !draw(emf_hdc) { + let _ = CloseEnhMetaFile(emf_hdc); + return Err(EmfCreateFailedError); + } + + let metafile = CloseEnhMetaFile(emf_hdc); + if metafile.is_invalid() { + return Err(EmfCreateFailedError); + } + + Ok(Self { metafile }) + } + + pub unsafe fn playback( + &self, + printer_hdc: HDC, + rect: RECT, + ) -> Result<(), EmfPlaybackFailedError> { + let mut base_matrix = XFORM::default(); + if !GetWorldTransform(printer_hdc, &mut base_matrix).as_bool() { + return Err(EmfPlaybackFailedError); + } + defer! { + let _ = SetWorldTransform(printer_hdc, ptr::addr_of!(base_matrix)); + } + + let mut context = EnumerationContext { + base_matrix, + }; + + let mut header = ENHMETAHEADER::default(); + if GetEnhMetaFileHeader( + self.metafile, + mem::size_of::() as u32, + Some(&mut header), + ) != mem::size_of::() as u32 + { + return Err(EmfPlaybackFailedError); + } + if EnumEnhMetaFile( + printer_hdc, + self.metafile, + Some(Self::playback_proc), + Some((&mut context as *mut EnumerationContext).cast()), + Some(ptr::addr_of!(rect)), + ) + .as_bool() + { + return Ok(()); + } + Err(EmfPlaybackFailedError) + } + + unsafe extern "system" fn playback_proc( + hdc: HDC, + handle_table: *const HANDLETABLE, + record: *const ENHMETARECORD, + handle_count: i32, + param: LPARAM, + ) -> i32 { + let context = &mut *(param.0 as *mut EnumerationContext); + let handle_table_slice = if handle_table.is_null() || handle_count <= 0 { + &[] + } else { + slice::from_raw_parts(handle_table, handle_count as usize) + }; + match play_record(hdc, record, handle_table_slice, context) { + true => 1, + false => 0, + } + } +} + +impl Drop for Emf { + fn drop(&mut self) { + unsafe { + let _ = DeleteEnhMetaFile(self.metafile); + } + } +} + +struct EnumerationContext { + base_matrix: XFORM, +} + +unsafe fn play_record( + hdc: HDC, + record: *const ENHMETARECORD, + handle_table_slice: &[HANDLETABLE], + context: &mut EnumerationContext, +) -> bool { + match (*record).iType { + EMR_STRETCHDIBITS => { + // EMRSTRETCHDIBITS is flexible in size, so we check the size with "<" operation. + if (*record).nSize < (mem::size_of::()) as u32 { + return false; + } + play_stretch_dibits_record(hdc, record as *const EMRSTRETCHDIBITS, handle_table_slice) + } + EMR_SETWORLDTRANSFORM => { + if (*record).nSize != (mem::size_of::()) as u32 { + return false; + } + let set_world_transform = &*(record as *const EMRSETWORLDTRANSFORM); + SetWorldTransform(hdc, ptr::addr_of!(context.base_matrix)).as_bool() + && ModifyWorldTransform( + hdc, + Some(ptr::addr_of!(set_world_transform.xform)), + MWT_LEFTMULTIPLY, + ) + .as_bool() + } + EMR_MODIFYWORLDTRANSFORM => { + if (*record).nSize != (mem::size_of::()) as u32 { + return false; + } + let modify_world_transform = &*(record as *const EMRMODIFYWORLDTRANSFORM); + match modify_world_transform.iMode { + MWT_IDENTITY => { + SetWorldTransform(hdc, ptr::addr_of!(context.base_matrix)).as_bool() + } + MWT_LEFTMULTIPLY => ModifyWorldTransform( + hdc, + Some(ptr::addr_of!(modify_world_transform.xform)), + MWT_LEFTMULTIPLY, + ) + .as_bool(), + MWT_RIGHTMULTIPLY => ModifyWorldTransform( + hdc, + Some(ptr::addr_of!(modify_world_transform.xform)), + MWT_RIGHTMULTIPLY, + ) + .as_bool(), + // 4 = MWT_SET + MODIFY_WORLD_TRANSFORM_MODE(4) => { + SetWorldTransform(hdc, ptr::addr_of!(context.base_matrix)).as_bool() + && ModifyWorldTransform( + hdc, + Some(ptr::addr_of!(modify_world_transform.xform)), + MWT_LEFTMULTIPLY, + ) + .as_bool() + } + _ => false, + } + } + // Skip EMR_SETLAYOUT because some printer drivers choke on + // layout changes when replaying EMF records. + // Chromium also skips EMR_SETLAYOUT in their EMF playback implementation: + // https://github.com/chromium/chromium/blob/056cc5e47e8dd311cf345d4d83a2729691ac2610/printing/emf_win.cc#L368-L371 + EMR_SETLAYOUT => true, + _ => PlayEnhMetaFileRecord(hdc, handle_table_slice, record).as_bool(), + } +} + +unsafe fn play_stretch_dibits_record( + hdc: HDC, + sdib_record: *const EMRSTRETCHDIBITS, + handle_table_slice: &[HANDLETABLE], +) -> bool { + let record_size = (*sdib_record).emr.nSize as usize; + let bmi_offset = (*sdib_record).offBmiSrc as usize; + let bmi_len = (*sdib_record).cbBmiSrc as usize; + let bmi_end = match bmi_offset.checked_add(bmi_len) { + Some(end) => end, + None => return false, + }; + if bmi_len < mem::size_of::() || bmi_end > record_size { + return false; + } + let bitmap_header = &*(sdib_record.byte_add(bmi_offset) as *const BITMAPINFOHEADER); + let bits_offset = (*sdib_record).offBitsSrc as usize; + let bits_len = bitmap_header.biSizeImage as usize; + let bits_end = match bits_offset.checked_add(bits_len) { + Some(end) => end, + None => return false, + }; + if bits_end > record_size { + return false; + } + let bits = { + if bits_len == 0 { + &[] + } else { + slice::from_raw_parts(sdib_record.byte_add(bits_offset) as *const u8, bits_len) + } + }; + if (bitmap_header.biCompression == BI_JPEG.0 + && !dib_format_natively_supported(hdc, CHECKJPEGFORMAT, bits)) + || (bitmap_header.biCompression == BI_PNG.0 + && !dib_format_natively_supported(hdc, CHECKPNGFORMAT, bits)) + { + // Decode compressed format in application and play with StretchDIBits, to avoid EMF playback failure. + // Aligns with how Chromium plays EMF records with compressed DIBs: + // https://github.com/chromium/chromium/blob/c6e8aae4573cec06352a4748ae902831f9a1334c/printing/emf_win.cc#L276 + return match DecodedBitmap::from_compressed(bits) { + Ok(decoded) => { + StretchDIBits( + hdc, + (*sdib_record).xDest, + (*sdib_record).yDest, + (*sdib_record).cxDest, + (*sdib_record).cyDest, + (*sdib_record).xSrc, + (*sdib_record).ySrc, + (*sdib_record).cxSrc, + (*sdib_record).cySrc, + Some(decoded.pixels.as_ptr().cast()), + ptr::addr_of!(decoded.bitmap_info), + DIB_USAGE((*sdib_record).iUsageSrc), + ROP_CODE((*sdib_record).dwRop), + ) != 0 + } + Err(_) => false, + }; + } + + PlayEnhMetaFileRecord(hdc, handle_table_slice, sdib_record as *const ENHMETARECORD).as_bool() +} + +fn dib_format_natively_supported(dc: HDC, escape: u32, bits: &[u8]) -> bool { + let mut supported: [u8; 4] = [0, 0, 0, 0]; + unsafe { + if ExtEscape( + dc, + QUERYESCSUPPORT as _, + Some(escape.to_ne_bytes().as_slice()), + None, + ) > 0 + { + ExtEscape(dc, escape as _, Some(bits), Some(supported.as_mut_slice())); + } + } + supported != [0, 0, 0, 0] +} + +struct DecodedBitmap { + bitmap_info: BITMAPINFO, + pixels: Vec, +} + +impl DecodedBitmap { + unsafe fn from_compressed(bytes: &[u8]) -> windows::core::Result { + let _com = ComInitializer::new(); + let factory: IWICImagingFactory2 = + CoCreateInstance(&CLSID_WICImagingFactory2, None, CLSCTX_ALL)?; + let stream = factory.CreateStream()?; + stream.InitializeFromMemory(bytes)?; + let decoder = factory.CreateDecoderFromStream( + &stream, + ptr::null(), + WICDecodeMetadataCacheOnDemand, + )?; + let frame = decoder.GetFrame(0)?; + let converter = factory.CreateFormatConverter()?; + converter.Initialize( + &frame, + &GUID_WICPixelFormat32bppBGRA, + WICBitmapDitherTypeNone, + None, + 0.0, + WICBitmapPaletteTypeMedianCut, + )?; + + let mut width = 0u32; + let mut height = 0u32; + converter.GetSize(&mut width, &mut height)?; + let stride = width.saturating_mul(4); + let mut pixels = vec![0u8; stride.saturating_mul(height) as usize]; + converter.CopyPixels(ptr::null(), stride, &mut pixels)?; + + Ok(Self { + bitmap_info: BITMAPINFO { + bmiHeader: BITMAPINFOHEADER { + biSize: mem::size_of::() as u32, + biWidth: width as i32, + biHeight: -(height as i32), + biPlanes: 1, + biBitCount: 32, + biCompression: BI_RGB.0, + biSizeImage: pixels.len() as u32, + ..Default::default() + }, + ..Default::default() + }, + pixels, + }) + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index e005787..b4781fa 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,3 +1,5 @@ +pub(crate) mod com; +pub(crate) mod emf; #[cfg(feature = "pdfium")] pub(crate) mod pdfium; pub(crate) mod print_completion_source;