Bug 1599994 - [remote] Implement Page.printToPDF. r=remote-protocol-reviewers,ato,maja_zf

Differential Revision: https://phabricator.services.mozilla.com/D55961

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Henrik Skupin 2019-12-16 16:19:04 +00:00
parent ce07f3fba4
commit 25fdbb9aff
3 changed files with 267 additions and 1 deletions

View File

@ -6,6 +6,11 @@
var EXPORTED_SYMBOLS = ["Page"];
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
const { clearInterval, setInterval } = ChromeUtils.import(
"resource://gre/modules/Timer.jsm"
);
const { DialogHandler } = ChromeUtils.import(
"chrome://remote/content/domains/parent/page/DialogHandler.jsm"
);
@ -15,6 +20,9 @@ const { Domain } = ChromeUtils.import(
const { UnsupportedError } = ChromeUtils.import(
"chrome://remote/content/Error.jsm"
);
const { streamRegistry } = ChromeUtils.import(
"chrome://remote/content/domains/parent/IO.jsm"
);
const { TabManager } = ChromeUtils.import(
"chrome://remote/content/TabManager.jsm"
);
@ -22,6 +30,14 @@ const { WindowManager } = ChromeUtils.import(
"chrome://remote/content/WindowManager.jsm"
);
const PRINT_MAX_SCALE_VALUE = 2.0;
const PRINT_MIN_SCALE_VALUE = 0.1;
const PDF_TRANSFER_MODES = {
base64: "ReturnAsBase64",
stream: "ReturnAsStream",
};
class Page extends Domain {
constructor(session) {
super(session);
@ -243,6 +259,203 @@ class Page extends Domain {
await this._dialogHandler.handleJavaScriptDialog({ accept, promptText });
}
/**
* Print page as PDF.
*
* @param {Object} options
* @param {boolean=} options.displayHeaderFooter
* Display header and footer. Defaults to false.
* @param {string=} options.footerTemplate (not supported)
* HTML template for the print footer.
* @param {string=} options.headerTemplate (not supported)
* HTML template for the print header. Should use the same format
* as the footerTemplate.
* @param {boolean=} options.ignoreInvalidPageRanges
* Whether to silently ignore invalid but successfully parsed page ranges,
* such as '3-2'. Defaults to false.
* @param {boolean=} options.landscape
* Paper orientation. Defaults to false.
* @param {number=} options.marginBottom
* Bottom margin in inches. Defaults to 1cm (~0.4 inches).
* @param {number=} options.marginLeft
* Left margin in inches. Defaults to 1cm (~0.4 inches).
* @param {number=} options.marginRight
* Right margin in inches. Defaults to 1cm (~0.4 inches).
* @param {number=} options.marginTop
* Top margin in inches. Defaults to 1cm (~0.4 inches).
* @param {string=} options.pageRanges (not supported)
* Paper ranges to print, e.g., '1-5, 8, 11-13'.
* Defaults to the empty string, which means print all pages.
* @param {number=} options.paperHeight
* Paper height in inches. Defaults to 11 inches.
* @param {number=} options.paperWidth
* Paper width in inches. Defaults to 8.5 inches.
* @param {boolean=} options.preferCSSPageSize
* 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.
* @param {boolean=} options.printBackground
* Print background graphics. Defaults to false.
* @param {number=} options.scale
* Scale of the webpage rendering. Defaults to 1.
* @param {string=} options.transferMode
* Return as base64-encoded string (ReturnAsBase64),
* or stream (ReturnAsStream). Defaults to ReturnAsBase64.
*
* @return {Promise<{data:string, stream:string}>
* Based on the transferMode setting data is a base64-encoded string,
* or stream is a handle to a OS.File stream.
*/
async printToPDF(options) {
const {
displayHeaderFooter = false,
// Bug 1601570 - Implement templates for header and footer
// headerTemplate = "",
// footerTemplate = "",
landscape = false,
marginBottom = 0.39,
marginLeft = 0.39,
marginRight = 0.39,
marginTop = 0.39,
// Bug 1601571 - Implement handling of page ranges
// TODO: pageRanges = "",
// TODO: ignoreInvalidPageRanges = false,
paperHeight = 11.0,
paperWidth = 8.5,
preferCSSPageSize = false,
printBackground = false,
scale = 1.0,
transferMode = PDF_TRANSFER_MODES.base64,
} = options;
if (marginBottom < 0) {
throw new TypeError("marginBottom is negative");
}
if (marginLeft < 0) {
throw new TypeError("marginLeft is negative");
}
if (marginRight < 0) {
throw new TypeError("marginRight is negative");
}
if (marginTop < 0) {
throw new TypeError("marginTop is negative");
}
if (scale < PRINT_MIN_SCALE_VALUE || scale > PRINT_MAX_SCALE_VALUE) {
throw new TypeError("scale is outside [0.1 - 2] range");
}
if (paperHeight <= 0) {
throw new TypeError("paperHeight is zero or negative");
}
if (paperWidth <= 0) {
throw new TypeError("paperWidth is zero or negative");
}
// Create a unique filename for the temporary PDF file
const basePath = OS.Path.join(OS.Constants.Path.tmpDir, "remote-agent.pdf");
const { file, path: filePath } = await OS.File.openUnique(basePath);
await file.close();
const psService = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(
Ci.nsIPrintSettingsService
);
const printSettings = psService.newPrintSettings;
printSettings.isInitializedFromPrinter = true;
printSettings.isInitializedFromPrefs = true;
printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
printSettings.printerName = "";
printSettings.printSilent = true;
printSettings.printToFile = true;
printSettings.showPrintProgress = false;
printSettings.toFileName = filePath;
printSettings.paperSizeUnit = Ci.nsIPrintSettings.kPaperSizeInches;
printSettings.paperWidth = paperWidth;
printSettings.paperHeight = paperHeight;
printSettings.marginBottom = marginBottom;
printSettings.marginLeft = marginLeft;
printSettings.marginRight = marginRight;
printSettings.marginTop = marginTop;
printSettings.printBGColors = printBackground;
printSettings.printBGImages = printBackground;
printSettings.scaling = scale;
printSettings.shrinkToFit = preferCSSPageSize;
if (!displayHeaderFooter) {
printSettings.headerStrCenter = "";
printSettings.headerStrLeft = "";
printSettings.headerStrRight = "";
printSettings.footerStrCenter = "";
printSettings.footerStrLeft = "";
printSettings.footerStrRight = "";
}
if (landscape) {
printSettings.orientation = Ci.nsIPrintSettings.kLandscapeOrientation;
}
await new Promise(resolve => {
// Bug 1603739 - With e10s enabled the WebProgressListener states
// STOP too early, which means the file hasn't been completely written.
const waitForFileWritten = () => {
const DELAY_CHECK_FILE_COMPLETELY_WRITTEN = 100;
let lastSize = 0;
const timerId = setInterval(async () => {
const fileInfo = await OS.File.stat(filePath);
if (lastSize > 0 && fileInfo.size == lastSize) {
clearInterval(timerId);
resolve();
}
lastSize = fileInfo.size;
}, DELAY_CHECK_FILE_COMPLETELY_WRITTEN);
};
const printProgressListener = {
onStateChange(webProgress, request, flags, status) {
if (
flags & Ci.nsIWebProgressListener.STATE_STOP &&
flags & Ci.nsIWebProgressListener.STATE_IS_NETWORK
) {
waitForFileWritten();
}
},
QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener]),
};
const { tab } = this.session.target;
tab.linkedBrowser.print(
tab.linkedBrowser.outerWindowID,
printSettings,
printProgressListener
);
});
const fp = await OS.File.open(filePath);
const retval = { data: null, stream: null };
if (transferMode == PDF_TRANSFER_MODES.stream) {
retval.stream = streamRegistry.add(fp);
} else {
// return all data as a base64 encoded string
let bytes;
try {
bytes = await fp.read();
} finally {
fp.close();
await OS.File.remove(filePath);
}
// Each UCS2 character has an upper byte of 0 and a lower byte matching
// the binary data
retval.data = btoa(String.fromCharCode.apply(null, bytes));
}
return retval;
}
/**
* Emit the proper CDP event javascriptDialogOpening when a javascript dialog
* opens for the current target.

View File

@ -17,6 +17,7 @@ support-files =
[browser_javascriptDialog_otherTarget.js]
[browser_javascriptDialog_prompt.js]
[browser_lifecycleEvent.js]
[browser_scriptToEvaluateOnNewDocument.js]
[browser_printToPDF.js]
[browser_reload.js]
[browser_runtimeEvents.js]
[browser_scriptToEvaluateOnNewDocument.js]

View File

@ -0,0 +1,52 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const DOC = toDataURL("<div style='background-color: green'>Hello world</div>");
add_task(async function transferModes({ IO, Page }) {
await loadURL(DOC);
// as base64 encoded data
const base64 = await Page.printToPDF({ transferMode: "ReturnAsBase64" });
is(base64.stream, null, "No stream handle is returned");
ok(!!base64.data, "Base64 encoded data is returned");
verifyPDF(atob(base64.data).trimEnd());
// defaults to base64 encoded data
const defaults = await Page.printToPDF();
is(defaults.stream, null, "By default no stream handle is returned");
ok(!!defaults.data, "By default base64 encoded data is returned");
verifyPDF(atob(defaults.data).trimEnd());
// unknown transfer modes default to base64
const fallback = await Page.printToPDF({ transferMode: "ReturnAsFoo" });
is(fallback.stream, null, "Unknown mode doesn't return a stream");
ok(!!fallback.data, "Unknown mode defaults to base64 encoded data");
verifyPDF(atob(fallback.data).trimEnd());
// as stream handle
const stream = await Page.printToPDF({ transferMode: "ReturnAsStream" });
ok(!!stream.stream, "Stream handle is returned");
is(stream.data, null, "No base64 encoded data is returned");
let streamData = "";
while (true) {
const { data, base64Encoded, eof } = await IO.read({
handle: stream.stream,
});
streamData += base64Encoded ? atob(data) : data;
if (eof) {
await IO.close({ handle: stream.stream });
break;
}
}
verifyPDF(streamData.trimEnd());
});
function verifyPDF(data) {
is(data.slice(0, 5), "%PDF-", "Decoded data starts with the PDF signature");
is(data.slice(-5), "%%EOF", "Decoded data ends with the EOF flag");
}