summaryrefslogtreecommitdiffstats
path: root/browser/components/screenshots/ScreenshotsUtils.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/screenshots/ScreenshotsUtils.sys.mjs')
-rw-r--r--browser/components/screenshots/ScreenshotsUtils.sys.mjs993
1 files changed, 993 insertions, 0 deletions
diff --git a/browser/components/screenshots/ScreenshotsUtils.sys.mjs b/browser/components/screenshots/ScreenshotsUtils.sys.mjs
new file mode 100644
index 0000000000..68e4f896bf
--- /dev/null
+++ b/browser/components/screenshots/ScreenshotsUtils.sys.mjs
@@ -0,0 +1,993 @@
+/* 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/. */
+
+import { getFilename } from "chrome://browser/content/screenshots/fileHelpers.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const SCREENSHOTS_LAST_SCREENSHOT_METHOD_PREF =
+ "screenshots.browser.component.last-screenshot-method";
+const SCREENSHOTS_LAST_SAVED_METHOD_PREF =
+ "screenshots.browser.component.last-saved-method";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ Downloads: "resource://gre/modules/Downloads.sys.mjs",
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyServiceGetters(lazy, {
+ AlertsService: ["@mozilla.org/alerts-service;1", "nsIAlertsService"],
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "SCREENSHOTS_LAST_SAVED_METHOD",
+ SCREENSHOTS_LAST_SAVED_METHOD_PREF,
+ "download"
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "SCREENSHOTS_LAST_SCREENSHOT_METHOD",
+ SCREENSHOTS_LAST_SCREENSHOT_METHOD_PREF,
+ "visible"
+);
+
+ChromeUtils.defineLazyGetter(lazy, "screenshotsLocalization", () => {
+ return new Localization(["browser/screenshots.ftl"], true);
+});
+
+// The max dimension for a canvas is 32,767 https://searchfox.org/mozilla-central/rev/f40d29a11f2eb4685256b59934e637012ea6fb78/gfx/cairo/cairo/src/cairo-image-surface.c#62.
+// The max number of pixels for a canvas is 472,907,776 pixels (i.e., 22,528 x 20,992) https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size
+// We have to limit screenshots to these dimensions otherwise it will cause an error.
+export const MAX_CAPTURE_DIMENSION = 32766;
+export const MAX_CAPTURE_AREA = 472907776;
+export const MAX_SNAPSHOT_DIMENSION = 1024;
+
+export class ScreenshotsComponentParent extends JSWindowActorParent {
+ async receiveMessage(message) {
+ let region, title;
+ let browser = message.target.browsingContext.topFrameElement;
+ // ignore message from child actors with no associated browser element
+ if (!browser) {
+ return;
+ }
+ if (
+ ScreenshotsUtils.getUIPhase(browser) == UIPhases.CLOSED &&
+ !ScreenshotsUtils.browserToScreenshotsState.has(browser)
+ ) {
+ // We've already exited or never opened and there's no UI or state that could
+ // handle this message. We additionally check for screenshot-state to ensure we
+ // don't ignore an overlay message when there is no current selection - which
+ // otherwise looks like the UIPhases.CLOSED state.
+ return;
+ }
+ switch (message.name) {
+ case "Screenshots:CancelScreenshot":
+ let { reason } = message.data;
+ ScreenshotsUtils.cancel(browser, reason);
+ break;
+ case "Screenshots:CopyScreenshot":
+ ScreenshotsUtils.closePanel(browser);
+ ({ region } = message.data);
+ await ScreenshotsUtils.copyScreenshotFromRegion(region, browser);
+ ScreenshotsUtils.exit(browser);
+ break;
+ case "Screenshots:DownloadScreenshot":
+ ScreenshotsUtils.closePanel(browser);
+ ({ title, region } = message.data);
+ await ScreenshotsUtils.downloadScreenshotFromRegion(
+ title,
+ region,
+ browser
+ );
+ ScreenshotsUtils.exit(browser);
+ break;
+ case "Screenshots:OverlaySelection":
+ ScreenshotsUtils.setPerBrowserState(browser, {
+ hasOverlaySelection: message.data.hasSelection,
+ });
+ break;
+ case "Screenshots:ShowPanel":
+ ScreenshotsUtils.openPanel(browser);
+ break;
+ case "Screenshots:HidePanel":
+ ScreenshotsUtils.closePanel(browser);
+ break;
+ }
+ }
+
+ didDestroy() {
+ // When restoring a crashed tab the browser is null
+ let browser = this.browsingContext.topFrameElement;
+ if (browser) {
+ ScreenshotsUtils.exit(browser);
+ }
+ }
+}
+
+export class ScreenshotsHelperParent extends JSWindowActorParent {
+ receiveMessage(message) {
+ switch (message.name) {
+ case "ScreenshotsHelper:GetElementRectFromPoint":
+ let cxt = BrowsingContext.get(message.data.bcId);
+ return cxt.currentWindowGlobal
+ .getActor("ScreenshotsHelper")
+ .sendQuery("ScreenshotsHelper:GetElementRectFromPoint", message.data);
+ }
+ return null;
+ }
+}
+
+export const UIPhases = {
+ CLOSED: 0, // nothing showing
+ INITIAL: 1, // panel and overlay showing
+ OVERLAYSELECTION: 2, // something selected in the overlay
+ PREVIEW: 3, // preview dialog showing
+};
+
+export var ScreenshotsUtils = {
+ browserToScreenshotsState: new WeakMap(),
+ initialized: false,
+ methodsUsed: {},
+
+ /**
+ * Figures out which of various states the screenshots UI is in, for the given browser.
+ * @param browser The selected browser
+ * @returns One of the `UIPhases` constants
+ */
+ getUIPhase(browser) {
+ let perBrowserState = this.browserToScreenshotsState.get(browser);
+ if (perBrowserState?.previewDialog) {
+ return UIPhases.PREVIEW;
+ }
+ const buttonsPanel = this.panelForBrowser(browser);
+ if (buttonsPanel && !buttonsPanel.hidden) {
+ return UIPhases.INITIAL;
+ }
+ if (perBrowserState?.hasOverlaySelection) {
+ return UIPhases.OVERLAYSELECTION;
+ }
+ return UIPhases.CLOSED;
+ },
+
+ resetMethodsUsed() {
+ this.methodsUsed = { fullpage: 0, visible: 0 };
+ },
+
+ initialize() {
+ if (!this.initialized) {
+ if (
+ !Services.prefs.getBoolPref(
+ "screenshots.browser.component.enabled",
+ false
+ )
+ ) {
+ return;
+ }
+ this.resetMethodsUsed();
+ Services.telemetry.setEventRecordingEnabled("screenshots", true);
+ Services.obs.addObserver(this, "menuitem-screenshot");
+ this.initialized = true;
+ if (Cu.isInAutomation) {
+ Services.obs.notifyObservers(null, "screenshots-component-initialized");
+ }
+ }
+ },
+
+ uninitialize() {
+ if (this.initialized) {
+ Services.obs.removeObserver(this, "menuitem-screenshot");
+ this.initialized = false;
+ }
+ },
+
+ handleEvent(event) {
+ // Escape should cancel and exit
+ if (event.type === "keydown" && event.key === "Escape") {
+ let browser = event.view.gBrowser.selectedBrowser;
+ this.cancel(browser, "escape");
+ }
+ },
+
+ observe(subj, topic, data) {
+ let { gBrowser } = subj;
+ let browser = gBrowser.selectedBrowser;
+
+ switch (topic) {
+ case "menuitem-screenshot": {
+ const uiPhase = this.getUIPhase(browser);
+ if (uiPhase !== UIPhases.CLOSED) {
+ // toggle from already-open to closed
+ this.cancel(browser, data);
+ return;
+ }
+ this.start(browser, data);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Notify screenshots when screenshot command is used.
+ * @param window The current window the screenshot command was used.
+ * @param type The type of screenshot taken. Used for telemetry.
+ */
+ notify(window, type) {
+ if (Services.prefs.getBoolPref("screenshots.browser.component.enabled")) {
+ Services.obs.notifyObservers(
+ window.event.currentTarget.ownerGlobal,
+ "menuitem-screenshot",
+ type
+ );
+ } else {
+ Services.obs.notifyObservers(null, "menuitem-screenshot-extension", type);
+ }
+ },
+
+ /**
+ * Creates/gets and returns a Screenshots actor.
+ *
+ * @param browser The current browser.
+ * @returns JSWindowActor The screenshot actor.
+ */
+ getActor(browser) {
+ let actor = browser.browsingContext.currentWindowGlobal.getActor(
+ "ScreenshotsComponent"
+ );
+ return actor;
+ },
+
+ /**
+ * Show the Screenshots UI and start the capture flow
+ * @param browser The current browser.
+ * @param reason [string] Optional reason string passed along when recording telemetry events
+ */
+ start(browser, reason = "") {
+ const uiPhase = this.getUIPhase(browser);
+ switch (uiPhase) {
+ case UIPhases.CLOSED:
+ this.captureFocusedElement(browser, "previousFocusRef");
+ this.showPanelAndOverlay(browser, reason);
+ break;
+ case UIPhases.INITIAL:
+ // nothing to do, panel & overlay are already open
+ break;
+ case UIPhases.PREVIEW: {
+ this.closeDialogBox(browser);
+ this.showPanelAndOverlay(browser, reason);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Exit the Screenshots UI for the given browser
+ * Closes any of open UI elements (preview dialog, panel, overlay) and cleans up internal state.
+ * @param browser The current browser.
+ */
+ exit(browser) {
+ this.captureFocusedElement(browser, "currentFocusRef");
+ this.closeDialogBox(browser);
+ this.closePanel(browser);
+ this.closeOverlay(browser);
+ this.resetMethodsUsed();
+ this.attemptToRestoreFocus(browser);
+
+ this.browserToScreenshotsState.delete(browser);
+ if (Cu.isInAutomation) {
+ Services.obs.notifyObservers(null, "screenshots-exit");
+ }
+ },
+
+ /**
+ * Cancel/abort the screenshots operation for the given browser
+ *
+ * @param browser The current browser.
+ */
+ cancel(browser, reason) {
+ this.recordTelemetryEvent("canceled", reason, {});
+ this.exit(browser);
+ },
+
+ /**
+ * Update internal UI state associated with the given browser
+ *
+ * @param browser The current browser.
+ * @param nameValues {object} An object with one or more named property values
+ */
+ setPerBrowserState(browser, nameValues = {}) {
+ if (!this.browserToScreenshotsState.has(browser)) {
+ // we should really have this state already, created when the preview dialog was opened
+ this.browserToScreenshotsState.set(browser, {});
+ }
+ let perBrowserState = this.browserToScreenshotsState.get(browser);
+ Object.assign(perBrowserState, nameValues);
+ },
+
+ /**
+ * Attempt to place focus on the element that had focus before screenshots UI was shown
+ *
+ * @param browser The current browser.
+ */
+ attemptToRestoreFocus(browser) {
+ const document = browser.ownerDocument;
+ const window = browser.ownerGlobal;
+
+ const doFocus = () => {
+ // Move focus it back to where it was previously.
+ prevFocus.setAttribute("refocused-by-panel", true);
+ try {
+ let fm = Services.focus;
+ fm.setFocus(prevFocus, fm.FLAG_NOSCROLL);
+ } catch (e) {
+ prevFocus.focus();
+ }
+ prevFocus.removeAttribute("refocused-by-panel");
+ let focusedElement;
+ try {
+ focusedElement = document.commandDispatcher.focusedElement;
+ if (!focusedElement) {
+ focusedElement = document.activeElement;
+ }
+ } catch (ex) {
+ focusedElement = document.activeElement;
+ }
+ };
+
+ let perBrowserState = this.browserToScreenshotsState.get(browser) || {};
+ let prevFocus = perBrowserState.previousFocusRef?.get();
+ let currentFocus = perBrowserState.currentFocusRef?.get();
+ delete perBrowserState.currentFocusRef;
+
+ // Avoid changing focus if focus changed during exit - perhaps exit was caused
+ // by a user action which resulted in focus moving
+ let nowFocus;
+ try {
+ nowFocus = document.commandDispatcher.focusedElement;
+ } catch (e) {
+ nowFocus = document.activeElement;
+ }
+ if (nowFocus && nowFocus != currentFocus) {
+ return;
+ }
+
+ let dialog = this.getDialog(browser);
+ let panel = this.panelForBrowser(browser);
+
+ if (prevFocus) {
+ // Try to restore focus
+ try {
+ if (document.commandDispatcher.focusedWindow != window) {
+ // Focus has already been set to a different window
+ return;
+ }
+ } catch (ex) {}
+
+ if (!currentFocus) {
+ doFocus();
+ return;
+ }
+ while (currentFocus) {
+ if (
+ (dialog && currentFocus == dialog) ||
+ (panel && currentFocus == panel) ||
+ currentFocus == browser
+ ) {
+ doFocus();
+ return;
+ }
+ currentFocus = currentFocus.parentNode;
+ if (
+ currentFocus &&
+ currentFocus.nodeType == currentFocus.DOCUMENT_FRAGMENT_NODE &&
+ currentFocus.host
+ ) {
+ // focus was in a shadowRoot, we'll try the host",
+ currentFocus = currentFocus.host;
+ }
+ }
+ doFocus();
+ }
+ },
+
+ /**
+ * Set a flag so we don't try to exit when preview dialog next closes.
+ *
+ * @param browser The current browser.
+ * @param reason [string] Optional reason string passed along when recording telemetry events
+ */
+ scheduleRetry(browser, reason) {
+ let perBrowserState = this.browserToScreenshotsState.get(browser);
+ if (!perBrowserState?.closedPromise) {
+ console.warn(
+ "Expected perBrowserState with a closedPromise for the preview dialog"
+ );
+ return;
+ }
+ this.setPerBrowserState(browser, { exitOnPreviewClose: false });
+ perBrowserState?.closedPromise.then(() => {
+ this.start(browser, reason);
+ });
+ },
+
+ /**
+ * Open the tab dialog for preview
+ *
+ * @param browser The current browser
+ */
+ async openPreviewDialog(browser) {
+ let dialogBox = browser.ownerGlobal.gBrowser.getTabDialogBox(browser);
+ let { dialog, closedPromise } = await dialogBox.open(
+ `chrome://browser/content/screenshots/screenshots.html?browsingContextId=${browser.browsingContext.id}`,
+ {
+ features: "resizable=no",
+ sizeTo: "available",
+ allowDuplicateDialogs: false,
+ },
+ browser
+ );
+
+ this.setPerBrowserState(browser, {
+ previewDialog: dialog,
+ exitOnPreviewClose: true,
+ closedPromise: closedPromise.finally(() => {
+ this.onDialogClose(browser);
+ }),
+ });
+ return dialog;
+ },
+
+ /**
+ * Take a weak-reference to whatever element currently has focus and associate it with
+ * the UI state for this browser.
+ *
+ * @param browser The current browser.
+ * @param {string} stateRefName The property name for this element reference.
+ */
+ captureFocusedElement(browser, stateRefName) {
+ let document = browser.ownerDocument;
+ let focusedElement;
+ try {
+ focusedElement = document.commandDispatcher.focusedElement;
+ if (!focusedElement) {
+ focusedElement = document.activeElement;
+ }
+ } catch (ex) {
+ focusedElement = document.activeElement;
+ }
+ this.setPerBrowserState(browser, {
+ [stateRefName]: Cu.getWeakReference(focusedElement),
+ });
+ },
+
+ /**
+ * Returns the buttons panel for the given browser
+ * @param browser The current browser
+ * @returns The buttons panel
+ */
+ panelForBrowser(browser) {
+ return browser.ownerDocument.getElementById("screenshotsPagePanel");
+ },
+
+ /**
+ * Create the buttons container from its template, for this browser
+ * @param browser The current browser
+ * @returns The buttons panel
+ */
+ createPanelForBrowser(browser) {
+ let buttonsPanel = this.panelForBrowser(browser);
+ if (!buttonsPanel) {
+ let doc = browser.ownerDocument;
+ let template = doc.getElementById("screenshotsPagePanelTemplate");
+ let fragmentClone = template.content.cloneNode(true);
+ buttonsPanel = fragmentClone.firstElementChild;
+ template.replaceWith(buttonsPanel);
+
+ let anchor = browser.ownerDocument.querySelector("#navigator-toolbox");
+ anchor.appendChild(buttonsPanel);
+ }
+
+ return this.panelForBrowser(browser);
+ },
+
+ /**
+ * Open the buttons panel.
+ * @param browser The current browser
+ */
+ openPanel(browser) {
+ let buttonsPanel = this.panelForBrowser(browser);
+ if (!buttonsPanel.hidden) {
+ return;
+ }
+ buttonsPanel.hidden = false;
+ buttonsPanel.ownerDocument.addEventListener("keydown", this);
+
+ buttonsPanel
+ .querySelector("screenshots-buttons")
+ .focusButton(lazy.SCREENSHOTS_LAST_SCREENSHOT_METHOD);
+ },
+
+ /**
+ * Close the panel
+ * @param browser The current browser
+ */
+ closePanel(browser) {
+ let buttonsPanel = this.panelForBrowser(browser);
+ if (!buttonsPanel) {
+ return;
+ }
+ buttonsPanel.hidden = true;
+ buttonsPanel.ownerDocument.removeEventListener("keydown", this);
+ },
+
+ /**
+ * If the buttons panel exists and is open we will hide both the panel
+ * and the overlay. If the overlay is showing, we will hide the overlay.
+ * Otherwise create or display the buttons.
+ * @param browser The current browser.
+ */
+ async showPanelAndOverlay(browser, data) {
+ let actor = this.getActor(browser);
+ actor.sendAsyncMessage("Screenshots:ShowOverlay");
+ this.createPanelForBrowser(browser);
+ this.recordTelemetryEvent("started", data, {});
+ this.openPanel(browser);
+ },
+
+ /**
+ * Close the overlay UI, and clear out internal state if there was an overlay selection
+ * The overlay lives in the child document; so although closing is actually async, we assume success.
+ * @param browser The current browser.
+ */
+ closeOverlay(browser, options = {}) {
+ let actor = this.getActor(browser);
+ actor?.sendAsyncMessage("Screenshots:HideOverlay", options);
+
+ if (this.browserToScreenshotsState.has(browser)) {
+ this.setPerBrowserState(browser, {
+ hasOverlaySelection: false,
+ });
+ }
+ },
+
+ /**
+ * Gets the screenshots dialog box
+ * @param browser The selected browser
+ * @returns Screenshots dialog box if it exists otherwise null
+ */
+ getDialog(browser) {
+ let currTabDialogBox = browser.tabDialogBox;
+ let browserContextId = browser.browsingContext.id;
+ if (currTabDialogBox) {
+ currTabDialogBox.getTabDialogManager();
+ let manager = currTabDialogBox.getTabDialogManager();
+ let dialogs = manager.hasDialogs && manager.dialogs;
+ if (dialogs.length) {
+ for (let dialog of dialogs) {
+ if (
+ dialog._openedURL.endsWith(
+ `browsingContextId=${browserContextId}`
+ ) &&
+ dialog._openedURL.includes("screenshots.html")
+ ) {
+ return dialog;
+ }
+ }
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Closes the dialog box it it exists
+ * @param browser The selected browser
+ */
+ closeDialogBox(browser) {
+ let perBrowserState = this.browserToScreenshotsState.get(browser);
+ if (perBrowserState?.previewDialog) {
+ perBrowserState.previewDialog.close();
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Callback fired when the preview dialog window closes
+ * Will exit the screenshots UI if the `exitOnPreviewClose` flag is set for this browser
+ * @param browser The associated browser
+ */
+ onDialogClose(browser) {
+ let perBrowserState = this.browserToScreenshotsState.get(browser);
+ if (!perBrowserState) {
+ return;
+ }
+ delete perBrowserState.previewDialog;
+ if (perBrowserState?.exitOnPreviewClose) {
+ this.exit(browser);
+ }
+ },
+
+ /**
+ * Gets the screenshots button if it is visible, otherwise it will get the
+ * element that the screenshots button is nested under. If the screenshots
+ * button doesn't exist then we will default to the navigator toolbox.
+ * @param browser The selected browser
+ * @returns The anchor element for the ConfirmationHint
+ */
+ getWidgetAnchor(browser) {
+ let window = browser.ownerGlobal;
+ let widgetGroup = window.CustomizableUI.getWidget("screenshot-button");
+ let widget = widgetGroup?.forWindow(window);
+ let anchor = widget?.anchor;
+
+ // Check if the anchor exists and is visible
+ if (
+ !anchor ||
+ !anchor.isConnected ||
+ !window.isElementVisible(anchor.parentNode)
+ ) {
+ // Use the hamburger button if the screenshots button isn't available
+ anchor = browser.ownerDocument.getElementById("PanelUI-menu-button");
+ }
+ return anchor;
+ },
+
+ /**
+ * Indicate that the screenshot has been copied via ConfirmationHint.
+ * @param browser The selected browser
+ */
+ showCopiedConfirmationHint(browser) {
+ let anchor = this.getWidgetAnchor(browser);
+
+ browser.ownerGlobal.ConfirmationHint.show(
+ anchor,
+ "confirmation-hint-screenshot-copied"
+ );
+ },
+
+ /**
+ * Gets the full page bounds from the screenshots child actor.
+ * @param browser The current browser.
+ * @returns { object }
+ * Contains the full page bounds from the screenshots child actor.
+ */
+ fetchFullPageBounds(browser) {
+ let actor = this.getActor(browser);
+ return actor.sendQuery("Screenshots:getFullPageBounds");
+ },
+
+ /**
+ * Gets the visible bounds from the screenshots child actor.
+ * @param browser The current browser.
+ * @returns { object }
+ * Contains the visible bounds from the screenshots child actor.
+ */
+ fetchVisibleBounds(browser) {
+ let actor = this.getActor(browser);
+ return actor.sendQuery("Screenshots:getVisibleBounds");
+ },
+
+ showAlertMessage(title, message) {
+ lazy.AlertsService.showAlertNotification(null, title, message);
+ },
+
+ /**
+ * The max dimension of any side of a canvas is 32767 and the max canvas area is
+ * 124925329. If the width or height is greater or equal to 32766 we will crop the
+ * screenshot to the max width. If the area is still too large for the canvas
+ * we will adjust the height so we can successfully capture the screenshot.
+ * @param {Object} rect The dimensions of the screenshot. The rect will be
+ * modified in place
+ */
+ cropScreenshotRectIfNeeded(rect) {
+ let cropped = false;
+ let width = rect.width * rect.devicePixelRatio;
+ let height = rect.height * rect.devicePixelRatio;
+
+ if (width > MAX_CAPTURE_DIMENSION) {
+ width = MAX_CAPTURE_DIMENSION;
+ cropped = true;
+ }
+ if (height > MAX_CAPTURE_DIMENSION) {
+ height = MAX_CAPTURE_DIMENSION;
+ cropped = true;
+ }
+ if (width * height > MAX_CAPTURE_AREA) {
+ height = Math.floor(MAX_CAPTURE_AREA / width);
+ cropped = true;
+ }
+
+ rect.width = Math.floor(width / rect.devicePixelRatio);
+ rect.height = Math.floor(height / rect.devicePixelRatio);
+ rect.right = rect.left + rect.width;
+ rect.bottom = rect.top + rect.height;
+
+ if (cropped) {
+ let [errorTitle, errorMessage] =
+ lazy.screenshotsLocalization.formatMessagesSync([
+ { id: "screenshots-too-large-error-title" },
+ { id: "screenshots-too-large-error-details" },
+ ]);
+ this.showAlertMessage(errorTitle.value, errorMessage.value);
+ this.recordTelemetryEvent("failed", "screenshot_too_large", null);
+ }
+ },
+
+ /**
+ * Open and add screenshot-ui to the dialog box and then take the screenshot
+ * @param browser The current browser.
+ * @param type The type of screenshot taken.
+ */
+ async doScreenshot(browser, type) {
+ this.closePanel(browser);
+ this.closeOverlay(browser, { doNotResetMethods: true });
+
+ let dialog = await this.openPreviewDialog(browser);
+ await dialog._dialogReady;
+ let screenshotsUI =
+ dialog._frame.contentDocument.createElement("screenshots-ui");
+ dialog._frame.contentDocument.body.appendChild(screenshotsUI);
+
+ screenshotsUI.focusButton(lazy.SCREENSHOTS_LAST_SAVED_METHOD);
+
+ let rect;
+ let lastUsedMethod;
+ if (type === "full_page") {
+ rect = await this.fetchFullPageBounds(browser);
+ lastUsedMethod = "fullpage";
+ } else {
+ rect = await this.fetchVisibleBounds(browser);
+ lastUsedMethod = "visible";
+ }
+
+ Services.prefs.setStringPref(
+ SCREENSHOTS_LAST_SCREENSHOT_METHOD_PREF,
+ lastUsedMethod
+ );
+ this.methodsUsed[lastUsedMethod] += 1;
+ this.recordTelemetryEvent("selected", type, {});
+ return this.takeScreenshot(browser, dialog, rect);
+ },
+
+ /**
+ * Take the screenshot and add the image to the dialog box
+ * @param browser The current browser.
+ * @param dialog The dialog box to show the screenshot preview.
+ * @param rect DOMRect containing bounds of the screenshot.
+ */
+ async takeScreenshot(browser, dialog, rect) {
+ let canvas = await this.createCanvas(rect, browser);
+
+ let newImg = dialog._frame.contentDocument.createElement("img");
+ let url = canvas.toDataURL();
+
+ newImg.id = "placeholder-image";
+
+ newImg.src = url;
+ dialog._frame.contentDocument
+ .getElementById("preview-image-div")
+ .appendChild(newImg);
+
+ if (Cu.isInAutomation) {
+ Services.obs.notifyObservers(null, "screenshots-preview-ready");
+ }
+ },
+
+ /**
+ * Creates a canvas and draws a snapshot of the screenshot on the canvas
+ * @param region The bounds of screenshots
+ * @param browser The current browser
+ * @returns The canvas
+ */
+ async createCanvas(region, browser) {
+ region.left = Math.round(region.left);
+ region.right = Math.round(region.right);
+ region.top = Math.round(region.top);
+ region.bottom = Math.round(region.bottom);
+ region.width = Math.round(region.right - region.left);
+ region.height = Math.round(region.bottom - region.top);
+
+ this.cropScreenshotRectIfNeeded(region);
+
+ let { devicePixelRatio } = region;
+
+ let browsingContext = BrowsingContext.get(browser.browsingContext.id);
+
+ let canvas = browser.ownerDocument.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "html:canvas"
+ );
+ let context = canvas.getContext("2d");
+
+ canvas.width = region.width * devicePixelRatio;
+ canvas.height = region.height * devicePixelRatio;
+
+ for (
+ let startLeft = region.left;
+ startLeft < region.right;
+ startLeft += MAX_SNAPSHOT_DIMENSION
+ ) {
+ for (
+ let startTop = region.top;
+ startTop < region.bottom;
+ startTop += MAX_SNAPSHOT_DIMENSION
+ ) {
+ let height =
+ startTop + MAX_SNAPSHOT_DIMENSION > region.bottom
+ ? region.bottom - startTop
+ : MAX_SNAPSHOT_DIMENSION;
+ let width =
+ startLeft + MAX_SNAPSHOT_DIMENSION > region.right
+ ? region.right - startLeft
+ : MAX_SNAPSHOT_DIMENSION;
+ let rect = new DOMRect(startLeft, startTop, width, height);
+
+ let snapshot = await browsingContext.currentWindowGlobal.drawSnapshot(
+ rect,
+ devicePixelRatio,
+ "rgb(255,255,255)"
+ );
+
+ context.drawImage(
+ snapshot,
+ (startLeft - region.left) * devicePixelRatio,
+ (startTop - region.top) * devicePixelRatio,
+ width * devicePixelRatio,
+ height * devicePixelRatio
+ );
+
+ snapshot.close();
+ }
+ }
+
+ return canvas;
+ },
+
+ /**
+ * Copy the screenshot
+ * @param region The bounds of the screenshots
+ * @param browser The current browser
+ */
+ async copyScreenshotFromRegion(region, browser) {
+ let canvas = await this.createCanvas(region, browser);
+ let url = canvas.toDataURL();
+
+ await this.copyScreenshot(url, browser, {
+ object: "overlay_copy",
+ });
+ },
+
+ /**
+ * Copy the image to the clipboard
+ * This is called from the preview dialog
+ * @param dataUrl The image data
+ * @param browser The current browser
+ * @param data Telemetry data
+ */
+ async copyScreenshot(dataUrl, browser, data) {
+ // Guard against missing image data.
+ if (!dataUrl) {
+ return;
+ }
+
+ const imageTools = Cc["@mozilla.org/image/tools;1"].getService(
+ Ci.imgITools
+ );
+
+ const base64Data = dataUrl.replace("data:image/png;base64,", "");
+
+ const image = atob(base64Data);
+ const imgDecoded = imageTools.decodeImageFromBuffer(
+ image,
+ image.length,
+ "image/png"
+ );
+
+ const transferable = Cc[
+ "@mozilla.org/widget/transferable;1"
+ ].createInstance(Ci.nsITransferable);
+ transferable.init(null);
+ transferable.addDataFlavor("image/png");
+ transferable.setTransferData("image/png", imgDecoded);
+
+ Services.clipboard.setData(
+ transferable,
+ null,
+ Services.clipboard.kGlobalClipboard
+ );
+
+ this.showCopiedConfirmationHint(browser);
+
+ let extra = await this.getActor(browser).sendQuery(
+ "Screenshots:GetMethodsUsed"
+ );
+ this.recordTelemetryEvent("copy", data.object, {
+ ...extra,
+ ...this.methodsUsed,
+ });
+ this.resetMethodsUsed();
+
+ Services.prefs.setStringPref(SCREENSHOTS_LAST_SAVED_METHOD_PREF, "copy");
+ },
+
+ /**
+ * Download the screenshot
+ * @param title The title of the current page
+ * @param region The bounds of the screenshot
+ * @param browser The current browser
+ */
+ async downloadScreenshotFromRegion(title, region, browser) {
+ let canvas = await this.createCanvas(region, browser);
+ let dataUrl = canvas.toDataURL();
+
+ await this.downloadScreenshot(title, dataUrl, browser, {
+ object: "overlay_download",
+ });
+ },
+
+ /**
+ * Download the screenshot
+ * This is called from the preview dialog
+ * @param title The title of the current page or null and getFilename will get the title
+ * @param dataUrl The image data
+ * @param browser The current browser
+ * @param data Telemetry data
+ */
+ async downloadScreenshot(title, dataUrl, browser, data) {
+ // Guard against missing image data.
+ if (!dataUrl) {
+ return;
+ }
+
+ let filename = await getFilename(title, browser);
+
+ const targetFile = new lazy.FileUtils.File(filename);
+
+ // Create download and track its progress.
+ try {
+ const download = await lazy.Downloads.createDownload({
+ source: dataUrl,
+ target: targetFile,
+ });
+
+ let isPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(
+ browser.ownerGlobal
+ );
+ const list = await lazy.Downloads.getList(
+ isPrivate ? lazy.Downloads.PRIVATE : lazy.Downloads.PUBLIC
+ );
+ // add the download to the download list in the Downloads list in the Browser UI
+ list.add(download);
+
+ // Await successful completion of the save via the download manager
+ await download.start();
+ } catch (ex) {}
+
+ let extra = await this.getActor(browser).sendQuery(
+ "Screenshots:GetMethodsUsed"
+ );
+ this.recordTelemetryEvent("download", data.object, {
+ ...extra,
+ ...this.methodsUsed,
+ });
+ this.resetMethodsUsed();
+
+ Services.prefs.setStringPref(
+ SCREENSHOTS_LAST_SAVED_METHOD_PREF,
+ "download"
+ );
+ },
+
+ recordTelemetryEvent(type, object, args) {
+ if (args) {
+ for (let key of Object.keys(args)) {
+ args[key] = args[key].toString();
+ }
+ }
+ Services.telemetry.recordEvent("screenshots", type, object, null, args);
+ },
+};