feat: add Chromium-style safe EMF playback

This commit is contained in:
ArcticLampyrid committed 2026-04-22 03:21:29 +08:00
1 parent a61a99ff11
commit 774996a59b
5 files changed
+518 -65

No files matched your search

+1 -25
View File
@@ -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(
+130 -40
View File
@@ -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::<DOCINFOW>() 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(())
}
+23
View File
@@ -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();
}
}
}
+362
View File
@@ -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<Self, EmfCreateFailedError> {
// 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::<ENHMETAHEADER>() as u32,
Some(&mut header),
) != mem::size_of::<ENHMETAHEADER>() 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::<EMRSTRETCHDIBITS>()) as u32 {
return false;
}
play_stretch_dibits_record(hdc, record as *const EMRSTRETCHDIBITS, handle_table_slice)
}
EMR_SETWORLDTRANSFORM => {
if (*record).nSize != (mem::size_of::<EMRSETWORLDTRANSFORM>()) 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::<EMRMODIFYWORLDTRANSFORM>()) 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::<BITMAPINFOHEADER>() || 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<u8>,
}
impl DecodedBitmap {
unsafe fn from_compressed(bytes: &[u8]) -> windows::core::Result<Self> {
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::<BITMAPINFOHEADER>() 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,
})
}
}
+2
View File
@@ -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;