mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 13:21:05 +00:00
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:
parent
ce07f3fba4
commit
25fdbb9aff
@ -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.
|
||||
|
@ -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]
|
||||
|
52
remote/test/browser/page/browser_printToPDF.js
Normal file
52
remote/test/browser/page/browser_printToPDF.js
Normal 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");
|
||||
}
|
Loading…
Reference in New Issue
Block a user