diff --git a/chrome/content/zotero/actors/ActorManager.jsm b/chrome/content/zotero/actors/ActorManager.jsm
index b2e4dc9ee8..4cbba710d0 100644
--- a/chrome/content/zotero/actors/ActorManager.jsm
+++ b/chrome/content/zotero/actors/ActorManager.jsm
@@ -34,3 +34,16 @@ ChromeUtils.registerWindowActor("FeedAbstract", {
 	},
 	messageManagerGroups: ["feedAbstract"]
 });
+
+ChromeUtils.registerWindowActor("ZoteroPrint", {
+	parent: {
+		moduleURI: "chrome://zotero/content/actors/ZoteroPrintParent.jsm"
+	},
+	child: {
+		moduleURI: "chrome://zotero/content/actors/ZoteroPrintChild.jsm",
+		events: {
+			pageshow: {}
+		}
+	}
+});
+
diff --git a/chrome/content/zotero/actors/ZoteroPrintChild.jsm b/chrome/content/zotero/actors/ZoteroPrintChild.jsm
new file mode 100644
index 0000000000..d6ab50dac8
--- /dev/null
+++ b/chrome/content/zotero/actors/ZoteroPrintChild.jsm
@@ -0,0 +1,26 @@
+var EXPORTED_SYMBOLS = ["ZoteroPrintChild"];
+
+
+class ZoteroPrintChild extends JSWindowActorChild {
+	actorCreated() {
+		Cu.exportFunction(
+			() => new this.contentWindow.Promise(
+				(resolve, reject) => this._sendZoteroPrint().then(resolve, reject)
+			),
+			this.contentWindow,
+			{ defineAs: "zoteroPrint" }
+		);
+	}
+
+	async handleEvent(event) {
+		switch (event.type) {
+			case "pageshow": {
+				// We just need this to trigger actor creation
+			}
+		}
+	}
+
+	async _sendZoteroPrint() {
+		await this.sendQuery("zoteroPrint");
+	}
+}
diff --git a/chrome/content/zotero/actors/ZoteroPrintParent.jsm b/chrome/content/zotero/actors/ZoteroPrintParent.jsm
new file mode 100644
index 0000000000..21512bde49
--- /dev/null
+++ b/chrome/content/zotero/actors/ZoteroPrintParent.jsm
@@ -0,0 +1,51 @@
+var EXPORTED_SYMBOLS = ["ZoteroPrintParent"];
+
+const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+	Services: "resource://gre/modules/Services.jsm",
+});
+
+ChromeUtils.defineESModuleGetters(this, {
+	Zotero: "chrome://zotero/content/zotero.mjs",
+});
+
+class ZoteroPrintParent extends JSWindowActorParent {
+	async receiveMessage({ name, data }) {
+		switch (name) {
+			case "zoteroPrint": {
+				await this.zoteroPrint(data || {});
+			}
+		}
+	}
+
+	/**
+	 * A custom print function to work around Zotero 7 printing issues
+	 * @param {Object} [options]
+	 * @param {Object} [options.overrideSettings] PrintUtils.getPrintSettings() settings to override
+	 * @returns {Promise<void>}
+	 */
+	async zoteroPrint(options = {}) {
+		let win = Zotero.getMainWindow();
+		if (win) {
+			let { PrintUtils } = win;
+			let settings = PrintUtils.getPrintSettings("", false);
+			Object.assign(settings, options.overrideSettings || {});
+			let doPrint = await PrintUtils.handleSystemPrintDialog(
+				this.browsingContext.topChromeWindow, false, settings
+			);
+			if (doPrint) {
+				let printPromise = this.browsingContext.print(settings);
+				// An ugly hack to close the browser window that has a static clone
+				// of the content that is being printed. Without this, the window
+				// will be open while transferring the content into system print queue,
+				// which can take time for large PDF files
+				let win = Services.wm.getMostRecentWindow("navigator:browser");
+				if (win?.document?.getElementById('statuspanel')) {
+					win.close();
+				}
+				await printPromise;
+			}
+		}
+	}
+}
diff --git a/chrome/content/zotero/xpcom/reader.js b/chrome/content/zotero/xpcom/reader.js
index c872a636df..1e7c02a319 100644
--- a/chrome/content/zotero/xpcom/reader.js
+++ b/chrome/content/zotero/xpcom/reader.js
@@ -162,29 +162,6 @@ class ReaderInstance {
 
 		await this._waitForReader();
 
-		// A custom print function to work around Zotero 7 printing issues
-		this._iframeWindow.wrappedJSObject.zoteroPrint = async () => {
-			let win = Zotero.getMainWindow();
-			if (win) {
-				let { PrintUtils } = win;
-				let settings = PrintUtils.getPrintSettings("", false);
-				let doPrint = await PrintUtils.handleSystemPrintDialog(
-					this._iframeWindow.browsingContext.topChromeWindow, false, settings
-				);
-				if (doPrint) {
-					this._iframeWindow.browsingContext.print(settings);
-					// An ugly hack to close the browser window that has a static clone
-					// of the content that is being printed. Without this, the window
-					// will be open while transferring the content into system print queue,
-					// which can take time for large PDF files
-					let win = Services.wm.getMostRecentWindow("navigator:browser");
-					if (win?.document?.getElementById('statuspanel')) {
-						win.close();
-					}
-				}
-			}
-		};
-
 		this._iframeWindow.addEventListener('customEvent', (event) => {
 			let data = event.detail.wrappedJSObject;
 			let append = data.append;