Fix printing on Mac desktop (#5920)

# Description of Changes
Fix #5164 

As I mentioned on the bug
https://github.com/Stirling-Tools/Stirling-PDF/issues/5164#issuecomment-4045170827,
it's impossible to print on Mac currently because
`iframe.contentWindow?.print()` silently does nothing in Tauri on Mac,
but [it seems unlikely that this will be
fixed](https://github.com/tauri-apps/tauri/issues/13451#issuecomment-4048075861).

Instead, I've linked directly to the Mac `PDFKit` framework in Rust to
use its printing functionality instead of Safari's. I believe that
`PDFKit` is what `Preview.app` is using and the print UI that it
generates seems to perform identically, so this should solve the issue
on Mac. Hopefully one day the TS iframe print API will be fixed and
we'll be able to get rid of this code, or [there'll be an official Tauri
plugin for printing which we can use
instead](https://github.com/tauri-apps/plugins-workspace/issues/293).

This implementation should be entirely Mac-specific. Windows & Linux
will continue to use their TS printing (which comes from EmbedPDF)
unless we have a good reason to change them to use a native solution as
well.
This commit is contained in:
James Brunton
2026-03-16 10:49:45 +00:00
committed by GitHub
parent f384e765fb
commit 971321fb19
12 changed files with 248 additions and 4 deletions

View File

@@ -685,7 +685,11 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
<ExportAPIBridge />
<BookmarkAPIBridge />
<AttachmentAPIBridge />
<PrintAPIBridge />
<PrintAPIBridge
file={file}
url={pdfUrl}
fileName={fileName}
/>
<DocumentPermissionsAPIBridge />
<DocumentReadyWrapper
fallback={

View File

@@ -3,10 +3,16 @@ import { usePrintCapability } from '@embedpdf/plugin-print/react';
import { useViewer } from '@app/contexts/ViewerContext';
import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady';
export interface PrintAPIBridgeProps {
file?: File | Blob;
url?: string | null;
fileName?: string;
}
/**
* Connects the PDF print plugin to the shared ViewerContext.
*/
export function PrintAPIBridge() {
export function PrintAPIBridge(_props: PrintAPIBridgeProps) {
const { provides: print } = usePrintCapability();
const { registerBridge } = useViewer();
const documentReady = useDocumentReady();

View File

@@ -0,0 +1,44 @@
import { useEffect } from 'react';
import { usePrintCapability } from '@embedpdf/plugin-print/react';
import { useViewer } from '@app/contexts/ViewerContext';
import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady';
import { printPdfNatively } from '@app/services/nativePrintService';
import { DesktopOs, getDesktopOs } from '@app/services/platformService';
import { PrintAPIBridgeProps } from "@core/components/viewer/PrintAPIBridge";
export function PrintAPIBridge({ file, url, fileName }: PrintAPIBridgeProps) {
const { provides: print } = usePrintCapability();
const { registerBridge } = useViewer();
const documentReady = useDocumentReady();
useEffect(() => {
if (documentReady) {
registerBridge('print', {
state: {},
api: {
print: () => {
void (async () => {
// macOS desktop uses a native print path because Tauri/WKWebView does not
// reliably support iframe-based PDF printing yet:
// https://github.com/tauri-apps/tauri/issues/13451#issuecomment-4045138142
if ((await getDesktopOs()) === DesktopOs.Mac) {
await printPdfNatively(file, url, fileName);
return;
}
print?.print?.();
})().catch((error) => {
console.error('[Desktop Print] Print failed', error);
});
},
}
});
}
return () => {
registerBridge('print', null);
};
}, [documentReady, file, fileName, print, registerBridge, url]);
return null;
}

View File

@@ -0,0 +1,56 @@
import { invoke } from "@tauri-apps/api/core";
function sanitizeFileName(fileName: string) {
const cleaned = fileName.replace(/[^A-Za-z0-9._-]+/g, "_");
if (!cleaned.toLowerCase().endsWith(".pdf")) {
return `${cleaned || "document"}.pdf`;
}
return cleaned || "document.pdf";
}
async function resolvePdfSource(file?: File | Blob, url?: string | null) {
if (file) {
return file;
}
if (!url) {
return null;
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to load PDF for native print (${response.status})`);
}
return response.blob();
}
export async function printPdfNatively(file?: File | Blob, url?: string | null, fileName = "document.pdf") {
const source = await resolvePdfSource(file, url);
if (!source) {
throw new Error("No PDF source available for native print");
}
const { tempDir, join } = await import("@tauri-apps/api/path");
const { remove, writeFile } = await import("@tauri-apps/plugin-fs");
const tempPath = await join(
await tempDir(),
`stirling-print-${crypto.randomUUID()}-${sanitizeFileName(fileName)}`,
);
await writeFile(tempPath, new Uint8Array(await source.arrayBuffer()));
try {
await invoke("print_pdf_file_native", {
filePath: tempPath,
title: fileName,
});
} finally {
try {
await remove(tempPath);
} catch (error) {
console.warn("[Desktop Print] Failed to clean up temp print file", error);
}
}
}

View File

@@ -0,0 +1,18 @@
import { invoke } from "@tauri-apps/api/core";
export enum DesktopOs {
Mac = "macos",
Windows = "windows",
Linux = "linux",
Unknown = "unknown",
}
let desktopOsPromise: Promise<DesktopOs> | null = null;
export async function getDesktopOs() {
if (!desktopOsPromise) {
desktopOsPromise = invoke<DesktopOs>("get_desktop_os");
}
return desktopOsPromise;
}