Repository: electron/electron
Pull request: fix: `webContents.printToPDF()` with cross-process subframes

#### Description of Change

Closes https://github.com/electron/electron/issues/36588.

Fixes an issue where `webContents.printToPDF()` didn't work as expected with cross-process subframes (e.g. iframes).

<details><summary>Before</summary>
<p>

[electron-printtopdf-iframe.pdf](https://github.com/user-attachments/files/19427812/electron-printtopdf-iframe.pdf)

</p>
</details> 

<details><summary>After</summary>
<p>

[electron-printtopdf-iframe.pdf](https://github.com/user-attachments/files/19427799/electron-printtopdf-iframe.pdf)

</p>
</details> 

Tested with https://gist.github.com/4de3b99eb855215fc5b7812cbff09f5c.

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included and stakeholders cc'd
- [x] `npm test` passes
- [x] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md)
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: Fixed an issue where `webContents.printToPDF()` didn't work as expected with cross-process subframes.


Changed files:
filename: shell/browser/api/electron_api_web_contents.cc
patch: @@ -176,6 +176,7 @@
 
 #if BUILDFLAG(ENABLE_PRINTING)
 #include "chrome/browser/printing/print_view_manager_base.h"
+#include "components/printing/browser/print_composite_client.h"
 #include "components/printing/browser/print_manager_utils.h"
 #include "components/printing/browser/print_to_pdf/pdf_print_result.h"
 #include "components/printing/browser/print_to_pdf/pdf_print_utils.h"
@@ -1023,6 +1024,7 @@ void WebContents::InitWithWebContents(
 
 #if BUILDFLAG(ENABLE_PRINTING)
   PrintViewManagerElectron::CreateForWebContents(web_contents.get());
+  printing::CreateCompositeClientIfNeeded(web_contents.get(), GetUserAgent());
 #endif
 
   // Determine whether the WebContents is offscreen.
@@ -1983,6 +1985,17 @@ void WebContents::DraggableRegionsChanged(
   draggable_region_ = DraggableRegionsToSkRegion(regions);
 }
 
+void WebContents::PrintCrossProcessSubframe(
+    content::WebContents* web_contents,
+    const gfx::Rect& rect,
+    int document_cookie,
+    content::RenderFrameHost* subframe_host) const {
+  if (auto* client =
+          printing::PrintCompositeClient::FromWebContents(web_contents)) {
+    client->PrintCrossProcessSubframe(rect, document_cookie, subframe_host);
+  }
+}
+
 SkRegion* WebContents::draggable_region() {
   return g_disable_draggable_regions ? nullptr : draggable_region_.get();
 }

filename: shell/browser/api/electron_api_web_contents.h
patch: @@ -585,6 +585,11 @@ class WebContents final : public ExclusiveAccessContext,
   void DraggableRegionsChanged(
       const std::vector<blink::mojom::DraggableRegionPtr>& regions,
       content::WebContents* contents) override;
+  void PrintCrossProcessSubframe(
+      content::WebContents* web_contents,
+      const gfx::Rect& rect,
+      int document_cookie,
+      content::RenderFrameHost* subframe_host) const override;
 
   // content::WebContentsObserver:
   void BeforeUnloadFired(bool proceed) override;

filename: shell/browser/extensions/electron_extensions_api_client.cc
patch: @@ -90,6 +90,7 @@ void ElectronExtensionsAPIClient::AttachWebContentsHelpers(
     content::WebContents* web_contents) const {
 #if BUILDFLAG(ENABLE_PRINTING)
   electron::PrintViewManagerElectron::CreateForWebContents(web_contents);
+  printing::CreateCompositeClientIfNeeded(web_contents, std::string());
 #endif
 
   extensions::ElectronExtensionWebContentsObserver::CreateForWebContents(

filename: spec/api-web-contents-spec.ts
patch: @@ -2440,6 +2440,7 @@ describe('webContents module', () => {
   });
 
   ifdescribe(features.isPrintingEnabled())('printToPDF()', () => {
+    let server: http.Server | null;
     const readPDF = async (data: any) => {
       const tmpDir = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'e-spec-printtopdf-'));
       const pdfPath = path.resolve(tmpDir, 'test.pdf');
@@ -2483,7 +2484,12 @@ describe('webContents module', () => {
       });
     });
 
-    afterEach(closeAllWindows);
+    afterEach(() => {
+      closeAllWindows();
+      if (server) {
+        server.close();
+      }
+    });
 
     it('rejects on incorrectly typed parameters', async () => {
       const badTypes = {
@@ -2644,6 +2650,32 @@ describe('webContents module', () => {
       expect(pdfInfo.markInfo).to.be.null();
     });
 
+    it('can print same-origin iframes', async () => {
+      await w.loadFile(path.join(__dirname, 'fixtures', 'api', 'print-to-pdf-same-origin.html'));
+
+      const data = await w.webContents.printToPDF({});
+      const pdfInfo = await readPDF(data);
+      expect(containsText(pdfInfo.textContent, /Virtual member functions/)).to.be.true();
+    });
+
+    // TODO(codebytere): OOPIF printing is disabled on Linux at the moment due to crashes.
+    ifit(process.platform !== 'linux')('can print cross-origin iframes', async () => {
+      server = http.createServer((_, res) => {
+        res.writeHead(200);
+        res.end(`
+          <title>cross-origin iframe</title>
+          <p>This page is displayed in an iframe.</p>
+        `);
+      });
+      const { port } = await listen(server);
+
+      await w.loadURL(`data:text/html,<iframe src="http://localhost:${port}"></iframe>`);
+
+      const data = await w.webContents.printToPDF({});
+      const pdfInfo = await readPDF(data);
+      expect(containsText(pdfInfo.textContent, /This page is displayed in an iframe./)).to.be.true();
+    });
+
     it('can generate tag data for PDFs', async () => {
       await w.loadFile(path.join(__dirname, 'fixtures', 'api', 'print-to-pdf-small.html'));
 

filename: spec/fixtures/api/print-to-pdf-same-origin.html
patch: @@ -0,0 +1,11 @@
+<html>
+
+<head>
+  <title>Your Title Here</title>
+</head>
+
+<body style="background: green;">
+  <iframe src="../pages/content.html"</iframe>
+</body>
+
+</html>