Bug 1604506 - Add print command to marionette, r=marionette-reviewers,whimboo,ato
authorJames Graham <james@hoppipolla.co.uk>
Fri, 20 Dec 2019 09:33:59 +0000
changeset 508011 d684a6d51c8a7ec353277003011c18fdac7df903
parent 508010 a3ea83c0bad8cd60b744e0ddf82d3ee7dbf6a8fe
child 508012 658f179110b3a01b1250a457766a2443a49380e2
push id103744
push userjames@hoppipolla.co.uk
push dateFri, 20 Dec 2019 09:54:01 +0000
treeherderautoland@658f179110b3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarionette-reviewers, whimboo, ato
bugs1604506, 1599994
milestone73.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1604506 - Add print command to marionette, r=marionette-reviewers,whimboo,ato Add a WebDriver:Print command to marionette, following the proposed WebDriver spec at https://github.com/w3c/webdriver/pull/1468 The implementation is largely the same as that added to the remote agent in Bug 1599994. Depends on D57471 Differential Revision: https://phabricator.services.mozilla.com/D57472
testing/marionette/driver.js
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -1,16 +1,20 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 /* global XPCNativeWrapper */
 
+const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { clearInterval, setInterval } = ChromeUtils.import(
+  "resource://gre/modules/Timer.jsm"
+);
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 
 const { accessibility } = ChromeUtils.import(
   "chrome://marionette/content/accessibility.js"
 );
 const { Addon } = ChromeUtils.import("chrome://marionette/content/addon.js");
@@ -3671,16 +3675,195 @@ GeckoDriver.prototype.teardownReftest = 
       "Called reftest:teardown before reftest:start"
     );
   }
 
   this._reftest.abort();
   this._reftest = null;
 };
 
+/**
+ * Print page as PDF.
+ *
+ * @param {boolean=} landscape
+ *     Paper orientation. Defaults to false.
+ * @param {number=} margin.bottom
+ *     Bottom margin in cm. Defaults to 1cm (~0.4 inches).
+ * @param {number=} margin.left
+ *     Left margin in cm. Defaults to 1cm (~0.4 inches).
+ * @param {number=} margin.right
+ *     Right margin in cm. Defaults to 1cm (~0.4 inches).
+ * @param {number=} margin.top
+ *     Top margin in cm. Defaults to 1cm (~0.4 inches).
+ * @param {string=} 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=} page.height
+ *     Paper height in cm. Defaults to US letter height (11 inches / 27.94cm)
+ * @param {number=} page.width
+ *     Paper width in cm. Defaults to US letter width (8.5 inches / 21.59cm)
+ * @param {boolean=} shrinkToFit
+ *     Whether or not to override page size as defined by CSS.
+ *     Defaults to true, in which case the content will be scaled
+ *     to fit the paper size.
+ * @param {boolean=} printBackground
+ *     Print background graphics. Defaults to false.
+ * @param {number=} scale
+ *     Scale of the webpage rendering. Defaults to 1.
+ *
+ * @return {string}
+ *     Base64 encoded PDF representing printed document
+ */
+GeckoDriver.prototype.print = async function(cmd) {
+  const printMaxScaleValue = 2.0;
+  const printMinScaleValue = 0.1;
+  const letterPaperSizeCm = {
+    width: 21.59,
+    height: 27.94,
+  };
+
+  assert.content(this.context);
+  assert.open(this.getCurrentWindow());
+  await this._handleUserPrompts();
+
+  const {
+    landscape = false,
+    margin = {
+      top: 1,
+      bottom: 1,
+      left: 1,
+      right: 1,
+    },
+    page = letterPaperSizeCm,
+    shrinkToFit = true,
+    printBackground = false,
+    scale = 1.0,
+  } = cmd.parameters;
+
+  for (let prop of ["top", "bottom", "left", "right"]) {
+    assert.positiveNumber(
+      margin[prop],
+      pprint`margin.${prop} is not a positive number`
+    );
+  }
+  for (let prop of ["width", "height"]) {
+    assert.positiveNumber(
+      page[prop],
+      pprint`page.${prop} is not a positive number`
+    );
+  }
+  assert.positiveNumber(scale, `scale ${scale} is not a positive number`);
+  assert.that(
+    s => s >= printMinScaleValue && scale <= printMaxScaleValue,
+    `scale ${scale} is outside the range ${printMinScaleValue}-${printMaxScaleValue}`
+  )(scale);
+  assert.boolean(shrinkToFit);
+  assert.boolean(landscape);
+  assert.boolean(printBackground);
+
+  // Create a unique filename for the temporary PDF file
+  const basePath = OS.Path.join(OS.Constants.Path.tmpDir, "marionette.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
+  );
+
+  let cmToInches = cm => cm / 2.54;
+  const printSettings = psService.newPrintSettings;
+  printSettings.isInitializedFromPrinter = true;
+  printSettings.isInitializedFromPrefs = true;
+  printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
+  printSettings.printerName = "marionette";
+  printSettings.printSilent = true;
+  printSettings.printToFile = true;
+  printSettings.showPrintProgress = false;
+  printSettings.toFileName = filePath;
+
+  // Setting the paperSizeUnit to kPaperSizeMillimeters doesn't work on mac
+  printSettings.paperSizeUnit = Ci.nsIPrintSettings.kPaperSizeInches;
+  printSettings.paperWidth = cmToInches(page.width);
+  printSettings.paperHeight = cmToInches(page.height);
+
+  printSettings.marginBottom = cmToInches(margin.bottom);
+  printSettings.marginLeft = cmToInches(margin.left);
+  printSettings.marginRight = cmToInches(margin.right);
+  printSettings.marginTop = cmToInches(margin.top);
+
+  printSettings.printBGColors = printBackground;
+  printSettings.printBGImages = printBackground;
+  printSettings.scaling = scale;
+  printSettings.shrinkToFit = shrinkToFit;
+
+  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]),
+    };
+    let linkedBrowser = this.curBrowser.tab.linkedBrowser;
+    linkedBrowser.print(
+      linkedBrowser.outerWindowID,
+      printSettings,
+      printProgressListener
+    );
+  });
+
+  const fp = await OS.File.open(filePath);
+
+  // 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
+  return {
+    value: btoa(String.fromCharCode.apply(null, bytes)),
+  };
+};
+
 GeckoDriver.prototype.commands = {
   // Marionette service
   "Marionette:AcceptConnections": GeckoDriver.prototype.acceptConnections,
   "Marionette:GetContext": GeckoDriver.prototype.getContext,
   "Marionette:GetScreenOrientation": GeckoDriver.prototype.getScreenOrientation,
   "Marionette:GetWindowType": GeckoDriver.prototype.getWindowType,
   "Marionette:Quit": GeckoDriver.prototype.quit,
   "Marionette:SetContext": GeckoDriver.prototype.setContext,
@@ -3751,16 +3934,17 @@ GeckoDriver.prototype.commands = {
   "WebDriver:IsElementEnabled": GeckoDriver.prototype.isElementEnabled,
   "WebDriver:IsElementSelected": GeckoDriver.prototype.isElementSelected,
   "WebDriver:MinimizeWindow": GeckoDriver.prototype.minimizeWindow,
   "WebDriver:MaximizeWindow": GeckoDriver.prototype.maximizeWindow,
   "WebDriver:Navigate": GeckoDriver.prototype.get,
   "WebDriver:NewSession": GeckoDriver.prototype.newSession,
   "WebDriver:NewWindow": GeckoDriver.prototype.newWindow,
   "WebDriver:PerformActions": GeckoDriver.prototype.performActions,
+  "WebDriver:Print": GeckoDriver.prototype.print,
   "WebDriver:Refresh": GeckoDriver.prototype.refresh,
   "WebDriver:ReleaseActions": GeckoDriver.prototype.releaseActions,
   "WebDriver:SendAlertText": GeckoDriver.prototype.sendKeysToDialog,
   "WebDriver:SetTimeouts": GeckoDriver.prototype.setTimeouts,
   "WebDriver:SetWindowRect": GeckoDriver.prototype.setWindowRect,
   "WebDriver:SwitchToFrame": GeckoDriver.prototype.switchToFrame,
   "WebDriver:SwitchToParentFrame": GeckoDriver.prototype.switchToParentFrame,
   "WebDriver:SwitchToShadowRoot": GeckoDriver.prototype.switchToShadowRoot,