Repository: electron/electron
Pull request: fix: blank page when printing pdf

Backport of #43309

See that PR for details.


Notes: Fixed an issue where calling `window.print()`, `webContents.print()` or `printToPDF` from an existing PDF document resulted in a blank page. 


Changed files:
filename: shell/browser/api/electron_api_web_contents.cc
patch: @@ -2906,11 +2906,7 @@ void WebContents::OnGetDeviceNameToUse(
   if (!print_view_manager)
     return;
 
-  auto* focused_frame = web_contents()->GetFocusedFrame();
-  auto* rfh = focused_frame && focused_frame->HasSelection()
-                  ? focused_frame
-                  : web_contents()->GetPrimaryMainFrame();
-
+  content::RenderFrameHost* rfh = GetRenderFrameHostToUse(web_contents());
   print_view_manager->PrintNow(rfh, std::move(print_settings),
                                std::move(print_callback));
 }
@@ -3107,12 +3103,13 @@ v8::Local<v8::Promise> WebContents::PrintToPDF(const base::Value& settings) {
   auto generate_document_outline =
       settings.GetDict().FindBool("generateDocumentOutline");
 
+  content::RenderFrameHost* rfh = GetRenderFrameHostToUse(web_contents());
   absl::variant<printing::mojom::PrintPagesParamsPtr, std::string>
       print_pages_params = print_to_pdf::GetPrintPagesParams(
-          web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL(),
-          landscape, display_header_footer, print_background, scale,
-          paper_width, paper_height, margin_top, margin_bottom, margin_left,
-          margin_right, std::make_optional(header_template),
+          rfh->GetLastCommittedURL(), landscape, display_header_footer,
+          print_background, scale, paper_width, paper_height, margin_top,
+          margin_bottom, margin_left, margin_right,
+          std::make_optional(header_template),
           std::make_optional(footer_template), prefer_css_page_size,
           generate_tagged_pdf, generate_document_outline);
 
@@ -3132,8 +3129,7 @@ v8::Local<v8::Promise> WebContents::PrintToPDF(const base::Value& settings) {
       absl::get<printing::mojom::PrintPagesParamsPtr>(print_pages_params));
   params->params->document_cookie = unique_id.value_or(0);
 
-  manager->PrintToPdf(web_contents()->GetPrimaryMainFrame(), page_ranges,
-                      std::move(params),
+  manager->PrintToPdf(rfh, page_ranges, std::move(params),
                       base::BindOnce(&WebContents::OnPDFCreated, GetWeakPtr(),
                                      std::move(promise)));
 

filename: shell/browser/printing/printing_utils.cc
patch: @@ -10,12 +10,20 @@
 #include "base/task/task_traits.h"
 #include "base/task/thread_pool.h"
 #include "chrome/browser/browser_process.h"
+#include "components/pdf/browser/pdf_frame_util.h"
 #include "content/public/browser/browser_thread.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/web_contents.h"
 #include "electron/buildflags/buildflags.h"
+#include "pdf/pdf_features.h"
 #include "printing/backend/print_backend.h"  // nogncheck
 #include "printing/units.h"
 #include "shell/common/thread_restrictions.h"
 
+#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
+#include "extensions/browser/guest_view/mime_handler_view/mime_handler_view_guest.h"
+#endif
+
 #if BUILDFLAG(IS_MAC)
 #include <ApplicationServices/ApplicationServices.h>
 #endif
@@ -68,6 +76,49 @@ bool IsDeviceNameValid(const std::u16string& device_name) {
 #endif
 }
 
+// Duplicated from chrome/browser/printing/print_view_manager_common.cc
+content::RenderFrameHost* GetFullPagePlugin(content::WebContents* contents) {
+  content::RenderFrameHost* full_page_plugin = nullptr;
+#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
+  contents->ForEachRenderFrameHostWithAction(
+      [&full_page_plugin](content::RenderFrameHost* rfh) {
+        auto* guest_view =
+            extensions::MimeHandlerViewGuest::FromRenderFrameHost(rfh);
+        if (guest_view && guest_view->is_full_page_plugin()) {
+          DCHECK_EQ(guest_view->GetGuestMainFrame(), rfh);
+          full_page_plugin = rfh;
+          return content::RenderFrameHost::FrameIterationAction::kStop;
+        }
+        return content::RenderFrameHost::FrameIterationAction::kContinue;
+      });
+#endif  // BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
+  return full_page_plugin;
+}
+
+// Pick the right RenderFrameHost based on the WebContents.
+// Modified from chrome/browser/printing/print_view_manager_common.cc
+content::RenderFrameHost* GetRenderFrameHostToUse(
+    content::WebContents* contents) {
+#if BUILDFLAG(ENABLE_PDF_VIEWER)
+  // Pick the plugin frame host if `contents` is a PDF viewer guest. If using
+  // OOPIF PDF viewer, pick the PDF extension frame host.
+  content::RenderFrameHost* full_page_pdf_embedder_host =
+      chrome_pdf::features::IsOopifPdfEnabled()
+          ? pdf_frame_util::FindFullPagePdfExtensionHost(contents)
+          : GetFullPagePlugin(contents);
+  content::RenderFrameHost* pdf_rfh = pdf_frame_util::FindPdfChildFrame(
+      full_page_pdf_embedder_host ? full_page_pdf_embedder_host
+                                  : contents->GetPrimaryMainFrame());
+  if (pdf_rfh) {
+    return pdf_rfh;
+  }
+#endif  // BUILDFLAG(ENABLE_PDF)
+  auto* focused_frame = contents->GetFocusedFrame();
+  return (focused_frame && focused_frame->HasSelection())
+             ? focused_frame
+             : contents->GetPrimaryMainFrame();
+}
+
 std::pair<std::string, std::u16string> GetDeviceNameToUse(
     const std::u16string& device_name) {
 #if BUILDFLAG(IS_WIN)

filename: shell/browser/printing/printing_utils.h
patch: @@ -14,6 +14,11 @@ namespace gfx {
 class Size;
 }
 
+namespace content {
+class RenderFrameHost;
+class WebContents;
+}  // namespace content
+
 namespace electron {
 
 // This function returns the per-platform default printer's DPI.
@@ -24,6 +29,9 @@ gfx::Size GetDefaultPrinterDPI(const std::u16string& device_name);
 // sanity checking of device_name validity and so will crash on invalid names.
 bool IsDeviceNameValid(const std::u16string& device_name);
 
+content::RenderFrameHost* GetRenderFrameHostToUse(
+    content::WebContents* contents);
+
 // This function returns a validated device name.
 // If the user passed one to webContents.print(), we check that it's valid and
 // return it or fail if the network doesn't recognize it. If the user didn't

filename: spec/api-web-contents-spec.ts
patch: @@ -2126,6 +2126,10 @@ describe('webContents module', () => {
   ifdescribe(features.isPrintingEnabled())('printToPDF()', () => {
     let w: BrowserWindow;
 
+    const containsText = (items: any[], text: RegExp) => {
+      return items.some(({ str }: { str: string }) => str.match(text));
+    };
+
     beforeEach(() => {
       w = new BrowserWindow({
         show: false,
@@ -2248,7 +2252,7 @@ describe('webContents module', () => {
     });
 
     it('with custom header and footer', async () => {
-      await w.loadFile(path.join(__dirname, 'fixtures', 'api', 'print-to-pdf-small.html'));
+      await w.loadFile(path.join(fixturesPath, 'api', 'print-to-pdf-small.html'));
 
       const data = await w.webContents.printToPDF({
         displayHeaderFooter: true,
@@ -2261,11 +2265,8 @@ describe('webContents module', () => {
 
       const { items } = await page.getTextContent();
 
-      // Check that generated PDF contains a header.
-      const containsText = (text: RegExp) => items.some(({ str }: { str: string }) => str.match(text));
-
-      expect(containsText(/I'm a PDF header/)).to.be.true();
-      expect(containsText(/I'm a PDF footer/)).to.be.true();
+      expect(containsText(items, /I'm a PDF header/)).to.be.true();
+      expect(containsText(items, /I'm a PDF footer/)).to.be.true();
     });
 
     it('in landscape mode', async () => {
@@ -2317,6 +2318,25 @@ describe('webContents module', () => {
         Suspects: false
       });
     });
+
+    it('from an existing pdf document', async () => {
+      const pdfPath = path.join(fixturesPath, 'cat.pdf');
+      await w.loadFile(pdfPath);
+
+      // TODO(codebytere): the PDF plugin is not always ready immediately
+      // after the document is loaded, so we need to wait for it to be ready.
+      // We should find a better way to do this.
+      await setTimeout(3000);
+
+      const data = await w.webContents.printToPDF({});
+      const doc = await pdfjs.getDocument(data).promise;
+      expect(doc.numPages).to.equal(2);
+
+      const page = await doc.getPage(1);
+
+      const { items } = await page.getTextContent();
+      expect(containsText(items, /Cat: The Ideal Pet/)).to.be.true();
+    });
   });
 
   describe('PictureInPicture video', () => {