Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,8 @@ source_set("electron_lib") {

if (enable_printing) {
sources += [
"shell/browser/printing/print_to_pdf.cc",
"shell/browser/printing/print_to_pdf.h",
"shell/browser/printing/print_view_manager_electron.cc",
"shell/browser/printing/print_view_manager_electron.h",
"shell/browser/printing/printing_utils.cc",
Expand Down
6 changes: 6 additions & 0 deletions docs/api/structures/print-to-pdf-margins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# PrintToPDFMargins Object

* `top` number (optional) - Top margin in inches. Defaults to 1cm (~0.4 inches).
* `bottom` number (optional) - Bottom margin in inches. Defaults to 1cm (~0.4 inches).
* `left` number (optional) - Left margin in inches. Defaults to 1cm (~0.4 inches).
* `right` number (optional) - Right margin in inches. Defaults to 1cm (~0.4 inches).
15 changes: 15 additions & 0 deletions docs/api/structures/print-to-pdf-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# PrintToPDFOptions Object

* `landscape` boolean (optional) - Paper orientation.`true` for landscape, `false` for portrait. Defaults to false.
* `displayHeaderFooter` boolean (optional) - Whether to display header and footer. Defaults to false.
* `printBackground` boolean (optional) - Whether to print background graphics. Defaults to false.
* `scale` number(optional) - Scale of the webpage rendering. Defaults to 1.
* `pageSize` string | Size (optional) - Specify page size of the generated PDF. Can be `A0`, `A1`, `A2`, `A3`,
`A4`, `A5`, `A6`, `Legal`, `Letter`, `Tabloid`, `Ledger`, or an Object containing `height` and `width` in inches. Defaults to `Letter`.
* `margins` [PrintToPDFMargins](print-to-pdf-margins.md?inline) (optional)
* `pageRanges` string (optional) - Page ranges to print, e.g., '1-5, 8, 11-13'. Defaults to the empty string, which means print all pages.
* `headerTemplate` string (optional) - HTML template for the print header. Should be valid HTML markup with following classes used to inject printing values into them: `date` (formatted print date), `title` (document title), `url` (document location), `pageNumber` (current page number) and `totalPages` (total pages in the document). For example, `<span class=title></span>` would generate span containing the title.
* `footerTemplate` string (optional) - HTML template for the print footer. Should use the same format as the `headerTemplate`.
* `preferCSSPageSize` boolean (optional) - Whether or not to prefer page size as defined by css. Defaults to false, in which case the content will be scaled to fit the paper size.
* `generateTaggedPDF` boolean (optional) _Experimental_ - Whether or not to generate a tagged (accessible) PDF. Defaults to false. As this property is experimental, the generated PDF may not adhere fully to PDF/UA and WCAG standards.
* `generateDocumentOutline` boolean (optional) _Experimental_ - Whether or not to generate a PDF document outline from content headers. Defaults to false.
19 changes: 1 addition & 18 deletions docs/api/web-contents.md
Original file line number Diff line number Diff line change
Expand Up @@ -1864,24 +1864,7 @@ win.webContents.print(options, (success, errorType) => {

#### `contents.printToPDF(options)`

* `options` Object
* `landscape` boolean (optional) - Paper orientation.`true` for landscape, `false` for portrait. Defaults to false.
* `displayHeaderFooter` boolean (optional) - Whether to display header and footer. Defaults to false.
* `printBackground` boolean (optional) - Whether to print background graphics. Defaults to false.
* `scale` number(optional) - Scale of the webpage rendering. Defaults to 1.
* `pageSize` string | Size (optional) - Specify page size of the generated PDF. Can be `A0`, `A1`, `A2`, `A3`,
`A4`, `A5`, `A6`, `Legal`, `Letter`, `Tabloid`, `Ledger`, or an Object containing `height` and `width` in inches. Defaults to `Letter`.
* `margins` Object (optional)
* `top` number (optional) - Top margin in inches. Defaults to 1cm (~0.4 inches).
* `bottom` number (optional) - Bottom margin in inches. Defaults to 1cm (~0.4 inches).
* `left` number (optional) - Left margin in inches. Defaults to 1cm (~0.4 inches).
* `right` number (optional) - Right margin in inches. Defaults to 1cm (~0.4 inches).
* `pageRanges` string (optional) - Page ranges to print, e.g., '1-5, 8, 11-13'. Defaults to the empty string, which means print all pages.
* `headerTemplate` string (optional) - HTML template for the print header. Should be valid HTML markup with following classes used to inject printing values into them: `date` (formatted print date), `title` (document title), `url` (document location), `pageNumber` (current page number) and `totalPages` (total pages in the document). For example, `<span class=title></span>` would generate span containing the title.
* `footerTemplate` string (optional) - HTML template for the print footer. Should use the same format as the `headerTemplate`.
* `preferCSSPageSize` boolean (optional) - Whether or not to prefer page size as defined by css. Defaults to false, in which case the content will be scaled to fit the paper size.
* `generateTaggedPDF` boolean (optional) _Experimental_ - Whether or not to generate a tagged (accessible) PDF. Defaults to false. As this property is experimental, the generated PDF may not adhere fully to PDF/UA and WCAG standards.
* `generateDocumentOutline` boolean (optional) _Experimental_ - Whether or not to generate a PDF document outline from content headers. Defaults to false.
* `options` [PrintToPDFOptions](structures/print-to-pdf-options.md?inline)

Returns `Promise<Buffer>` - Resolves with the generated PDF data.

Expand Down
51 changes: 51 additions & 0 deletions docs/api/web-frame-main.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,57 @@ When executed on a video media element, copies the frame at (x, y) to the clipbo

When executed on a video media element, shows a save dialog and saves the frame at (x, y) to disk.

#### `frame.printToPDF(options)`

<!--
```YAML history
added:
- pr-url: https://github.com/electron/electron/pull/52439
```
-->

* `options` [PrintToPDFOptions](structures/print-to-pdf-options.md?inline)

Comment thread
MarshallOfSound marked this conversation as resolved.
Returns `Promise<Buffer>` - Resolves with the generated PDF data.

Prints the frame's web page as PDF.

Unlike [`webContents.printToPDF`](web-contents.md#contentsprinttopdfoptions),
this method prints only the contents of the frame it is called on. This can be
used to print an individual `<iframe>` from the main process.

The `landscape` will be ignored if `@page` CSS at-rule is used in the web page.

An example of printing an iframe to PDF:

```js
const { app, BrowserWindow } = require('electron')

const fs = require('node:fs')
const os = require('node:os')
const path = require('node:path')

app.whenReady().then(() => {
const win = new BrowserWindow()
win.loadFile('page-with-iframe.html')

win.webContents.on('did-finish-load', () => {
const pdfPath = path.join(os.homedir(), 'Desktop', 'iframe.pdf')
const iframe = win.webContents.mainFrame.frames[0]
iframe.printToPDF({}).then(data => {
fs.writeFile(pdfPath, data, (error) => {
if (error) throw error
console.log(`Wrote PDF successfully to ${pdfPath}`)
})
}).catch(error => {
console.log(`Failed to write PDF to ${pdfPath}: `, error)
})
})
})
```

See [Page.printToPdf](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF) for more information.

### Instance Properties

#### `frame.ipc` _Readonly_
Expand Down
19 changes: 1 addition & 18 deletions docs/api/webview-tag.md
Original file line number Diff line number Diff line change
Expand Up @@ -596,24 +596,7 @@ Prints `webview`'s web page. Same as `webContents.print([options])`.

### `<webview>.printToPDF(options)`

* `options` Object
* `landscape` boolean (optional) - Paper orientation.`true` for landscape, `false` for portrait. Defaults to false.
* `displayHeaderFooter` boolean (optional) - Whether to display header and footer. Defaults to false.
* `printBackground` boolean (optional) - Whether to print background graphics. Defaults to false.
* `scale` number(optional) - Scale of the webpage rendering. Defaults to 1.
* `pageSize` string | Size (optional) - Specify page size of the generated PDF. Can be `A0`, `A1`, `A2`, `A3`,
`A4`, `A5`, `A6`, `Legal`, `Letter`, `Tabloid`, `Ledger`, or an Object containing `height` and `width` in inches. Defaults to `Letter`.
* `margins` Object (optional)
* `top` number (optional) - Top margin in inches. Defaults to 1cm (~0.4 inches).
* `bottom` number (optional) - Bottom margin in inches. Defaults to 1cm (~0.4 inches).
* `left` number (optional) - Left margin in inches. Defaults to 1cm (~0.4 inches).
* `right` number (optional) - Right margin in inches. Defaults to 1cm (~0.4 inches).
* `pageRanges` string (optional) - Page ranges to print, e.g., '1-5, 8, 11-13'. Defaults to the empty string, which means print all pages.
* `headerTemplate` string (optional) - HTML template for the print header. Should be valid HTML markup with following classes used to inject printing values into them: `date` (formatted print date), `title` (document title), `url` (document location), `pageNumber` (current page number) and `totalPages` (total pages in the document). For example, `<span class=title></span>` would generate span containing the title.
* `footerTemplate` string (optional) - HTML template for the print footer. Should use the same format as the `headerTemplate`.
* `preferCSSPageSize` boolean (optional) - Whether or not to prefer page size as defined by css. Defaults to false, in which case the content will be scaled to fit the paper size.
* `generateTaggedPDF` boolean (optional) _Experimental_ - Whether or not to generate a tagged (accessible) PDF. Defaults to false. As this property is experimental, the generated PDF may not adhere fully to PDF/UA and WCAG standards.
* `generateDocumentOutline` boolean (optional) _Experimental_ - Whether or not to generate a PDF document outline from content headers. Defaults to false.
* `options` [PrintToPDFOptions](structures/print-to-pdf-options.md?inline)

Returns `Promise<Uint8Array>` - Resolves with the generated PDF data.

Expand Down
3 changes: 3 additions & 0 deletions filenames.auto.gni
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ auto_filenames = {
"docs/api/structures/post-body.md",
"docs/api/structures/preload-script-registration.md",
"docs/api/structures/preload-script.md",
"docs/api/structures/print-to-pdf-margins.md",
"docs/api/structures/print-to-pdf-options.md",
"docs/api/structures/printer-info.md",
"docs/api/structures/process-memory-info.md",
"docs/api/structures/process-metric.md",
Expand Down Expand Up @@ -655,6 +657,7 @@ auto_filenames = {
"lib/browser/ipc-main-internal.ts",
"lib/browser/message-port-main.ts",
"lib/browser/parse-features-string.ts",
"lib/browser/print-to-pdf.ts",
"lib/browser/rpc-server.ts",
"lib/browser/web-view-events.ts",
"lib/common/api/module-list.ts",
Expand Down
95 changes: 2 additions & 93 deletions lib/browser/api/web-contents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import { IpcMainImpl } from '@electron/internal/browser/ipc-main-impl';
import * as ipcMainUtils from '@electron/internal/browser/ipc-main-internal-utils';
import { parseFeatures } from '@electron/internal/browser/parse-features-string';
import { printToPDF } from '@electron/internal/browser/print-to-pdf';
import * as deprecate from '@electron/internal/common/deprecate';
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';

Expand All @@ -20,11 +21,6 @@ import * as url from 'url';
// eslint-disable-next-line no-unused-expressions
session;

let nextId = 0;
const getNextId = function () {
return ++nextId;
};

// Stock page sizes
const PDFPageSizes: Record<string, ElectronInternal.MediaSize> = {
Letter: {
Expand Down Expand Up @@ -90,20 +86,6 @@ const PDFPageSizes: Record<string, ElectronInternal.MediaSize> = {
}
} as const;

const paperFormats: Record<string, ElectronInternal.PageSize> = {
letter: { width: 8.5, height: 11 },
legal: { width: 8.5, height: 14 },
tabloid: { width: 11, height: 17 },
ledger: { width: 17, height: 11 },
a0: { width: 33.1, height: 46.8 },
a1: { width: 23.4, height: 33.1 },
a2: { width: 16.54, height: 23.4 },
a3: { width: 11.7, height: 16.54 },
a4: { width: 8.27, height: 11.7 },
a5: { width: 5.83, height: 8.27 },
a6: { width: 4.13, height: 5.83 }
} as const;

// The minimum micron size Chromium accepts is that where:
// Per printing/units.h:
// * kMicronsPerInch - Length of an inch in 0.001mm unit.
Expand Down Expand Up @@ -199,81 +181,8 @@ WebContents.prototype.executeJavaScriptInIsolatedWorld = async function (worldId
);
};

function checkType<T>(value: T, type: 'number' | 'boolean' | 'string' | 'object', name: string): T {
// eslint-disable-next-line valid-typeof
if (typeof value !== type) {
throw new TypeError(`${name} must be a ${type}`);
}

return value;
}

function parsePageSize(pageSize: string | ElectronInternal.PageSize) {
if (typeof pageSize === 'string') {
const format = paperFormats[pageSize.toLowerCase()];
if (!format) {
throw new Error(`Invalid pageSize ${pageSize}`);
}

return { paperWidth: format.width, paperHeight: format.height };
} else if (typeof pageSize === 'object') {
if (typeof pageSize.width !== 'number' || typeof pageSize.height !== 'number') {
throw new TypeError('width and height properties are required for pageSize');
}

return { paperWidth: pageSize.width, paperHeight: pageSize.height };
} else {
throw new TypeError('pageSize must be a string or an object');
}
}

// Translate the options of printToPDF.

const printToPDFQueues = new WeakMap<Electron.WebContents, Promise<unknown>>();
WebContents.prototype.printToPDF = async function (options) {
const margins = checkType(options.margins ?? {}, 'object', 'margins');
const pageSize = parsePageSize(options.pageSize ?? 'letter');

const { top, bottom, left, right } = margins;
const validHeight = [top, bottom].every((u) => u === undefined || u <= pageSize.paperHeight);
const validWidth = [left, right].every((u) => u === undefined || u <= pageSize.paperWidth);

if (!validHeight || !validWidth) {
throw new Error('margins must be less than or equal to pageSize');
}

const printSettings = {
requestID: getNextId(),
landscape: checkType(options.landscape ?? false, 'boolean', 'landscape'),
displayHeaderFooter: checkType(options.displayHeaderFooter ?? false, 'boolean', 'displayHeaderFooter'),
headerTemplate: checkType(options.headerTemplate ?? '', 'string', 'headerTemplate'),
footerTemplate: checkType(options.footerTemplate ?? '', 'string', 'footerTemplate'),
printBackground: checkType(options.printBackground ?? false, 'boolean', 'printBackground'),
scale: checkType(options.scale ?? 1.0, 'number', 'scale'),
marginTop: checkType(margins.top ?? 0.4, 'number', 'margins.top'),
marginBottom: checkType(margins.bottom ?? 0.4, 'number', 'margins.bottom'),
marginLeft: checkType(margins.left ?? 0.4, 'number', 'margins.left'),
marginRight: checkType(margins.right ?? 0.4, 'number', 'margins.right'),
pageRanges: checkType(options.pageRanges ?? '', 'string', 'pageRanges'),
preferCSSPageSize: checkType(options.preferCSSPageSize ?? false, 'boolean', 'preferCSSPageSize'),
generateTaggedPDF: checkType(options.generateTaggedPDF ?? false, 'boolean', 'generateTaggedPDF'),
generateDocumentOutline: checkType(options.generateDocumentOutline ?? false, 'boolean', 'generateDocumentOutline'),
...pageSize
};

if (!this._printToPDF) {
throw new Error('Printing feature is disabled');
}

const prev = printToPDFQueues.get(this) ?? Promise.resolve();
const next = prev.catch(() => {}).then(() => this._printToPDF(printSettings));
printToPDFQueues.set(this, next);
next
.finally(() => {
if (printToPDFQueues.get(this) === next) printToPDFQueues.delete(this);
})
.catch(() => {});
return next;
return printToPDF(this, options);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we simplify PrintToPDFTarget by always directing to the frame-level API?

Suggested change
return printToPDF(this, options);
return this.mainFrame.printToPDF(options);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not quite equivalent. WebContents::PrintToPDF doesn't print the primary main frame, it prints GetRenderFrameHostToUse(web_contents()) (printing_utils.cc:111) which redirects to the PDF plugin child frame when the contents is showing the PDF viewer (OOPIF or full-page plugin) and to the focused frame if it has a selection. Routing through this.mainFrame would drop that, so printToPDF() on a loaded .pdf would print the extension embedder page instead of the document. And I don't think we want that redirect inside WebFrameMain::PrintToPDF since a frame-scoped API should print the frame you asked for.

That's why both entry points funnel into printToPDF(target, options) on the JS side and PrintFrameToPDF(rfh, settings) in C++. PrintToPDFTarget is just the shim so the option translation + queueing isn't duplicated.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense and I do like that both are able to reuse the JS printToPDF method.

If my understanding is correct, an <iframe> embedding a PDF would not return the actual PDF's byte data in the result of frame.printToPDF. I'm a little confused about what the behavior might be which may imply it'd be worth adding a test to confirm expectations. An explicit call out in the docs might also be worth clarifying.

Unlike webContents.printToPDF, this method prints only the contents of the frame it is called on.

If this already implies the expected behavior to what I mentioned above, then maybe that's fine too.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

printToPDF on an iframe with a PDF would print the PDF but in a scroll container. You'd need to do frameWithPDF.frames[0] basically to get the actual PDF iframe

};

// TODO(codebytere): deduplicate argument sanitization by moving rest of
Expand Down
5 changes: 5 additions & 0 deletions lib/browser/api/web-frame-main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { IpcMainImpl } from '@electron/internal/browser/ipc-main-impl';
import { MessagePortMain } from '@electron/internal/browser/message-port-main';
import { printToPDF } from '@electron/internal/browser/print-to-pdf';

const { WebFrameMain, fromId, fromFrameToken } = process._linkedBinding('electron_browser_web_frame_main');

Expand Down Expand Up @@ -35,6 +36,10 @@ WebFrameMain.prototype._sendInternal = function (channel, ...args) {
}
};

WebFrameMain.prototype.printToPDF = async function (options) {
return printToPDF(this, options);
};

WebFrameMain.prototype.postMessage = function (...args) {
if (Array.isArray(args[2])) {
args[2] = args[2].map((o) => (o instanceof MessagePortMain ? o._internalPort : o));
Expand Down
Loading