-
Notifications
You must be signed in to change notification settings - Fork 17.4k
feat: add webFrameMain.printToPDF() #52439
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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). |
| 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. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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'; | ||||||
|
|
||||||
|
|
@@ -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: { | ||||||
|
|
@@ -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. | ||||||
|
|
@@ -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); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we simplify
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not quite equivalent. That's why both entry points funnel into
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 If my understanding is correct, an
If this already implies the expected behavior to what I mentioned above, then maybe that's fine too.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
| }; | ||||||
|
|
||||||
| // TODO(codebytere): deduplicate argument sanitization by moving rest of | ||||||
|
|
||||||
Uh oh!
There was an error while loading. Please reload this page.