Capture.sys.mjs (6231B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 Log: "chrome://remote/content/shared/Log.sys.mjs", 9 }); 10 11 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get()); 12 13 const CONTEXT_2D = "2d"; 14 const BG_COLOUR = "rgb(255,255,255)"; 15 const MAX_CANVAS_DIMENSION = 32767; 16 const MAX_CANVAS_AREA = 472907776; 17 const XHTML_NS = "http://www.w3.org/1999/xhtml"; 18 19 /** 20 * Provides primitives to capture screenshots. 21 * 22 * @namespace 23 */ 24 export const capture = {}; 25 26 capture.Format = { 27 Base64: 0, 28 Hash: 1, 29 }; 30 31 /** 32 * Draw a rectangle off the framebuffer. 33 * 34 * @param {DOMWindow} win 35 * The DOM window used for the framebuffer, and providing the interfaces 36 * for creating an HTMLCanvasElement. 37 * @param {BrowsingContext} browsingContext 38 * The BrowsingContext from which the snapshot should be taken. 39 * @param {number} left 40 * The left, X axis offset of the rectangle. 41 * @param {number} top 42 * The top, Y axis offset of the rectangle. 43 * @param {number} width 44 * The width dimension of the rectangle to paint. 45 * @param {number} height 46 * The height dimension of the rectangle to paint. 47 * @param {object=} options 48 * @param {HTMLCanvasElement=} options.canvas 49 * Optional canvas to reuse for the screenshot. 50 * @param {number=} options.flags 51 * Optional integer representing flags to pass to drawWindow; these 52 * are defined on CanvasRenderingContext2D. 53 * @param {number=} options.dX 54 * Horizontal offset between the browser window and content area. Defaults to 0. 55 * @param {number=} options.dY 56 * Vertical offset between the browser window and content area. Defaults to 0. 57 * @param {boolean=} options.readback 58 * If true, read back a snapshot of the pixel data currently in the 59 * compositor/window. Defaults to false. 60 * 61 * @returns {HTMLCanvasElement} 62 * The canvas on which the selection from the window's framebuffer 63 * has been painted on. 64 */ 65 capture.canvas = async function ( 66 win, 67 browsingContext, 68 left, 69 top, 70 width, 71 height, 72 { canvas = null, flags = null, dX = 0, dY = 0, readback = false } = {} 73 ) { 74 // FIXME(bug 1761032): This looks a bit sketchy, overrideDPPX doesn't 75 // influence rendering... 76 const scale = browsingContext.overrideDPPX || win.devicePixelRatio; 77 78 let canvasHeight = height * scale; 79 let canvasWidth = width * scale; 80 81 // Cap the screenshot size for width and height at 2^16 pixels, 82 // which is the maximum allowed canvas size. Higher dimensions will 83 // trigger exceptions in Gecko. 84 if (canvasWidth > MAX_CANVAS_DIMENSION) { 85 lazy.logger.warn( 86 "Limiting screen capture width to maximum allowed " + 87 MAX_CANVAS_DIMENSION + 88 " pixels" 89 ); 90 width = Math.floor(MAX_CANVAS_DIMENSION / scale); 91 canvasWidth = width * scale; 92 } 93 94 if (canvasHeight > MAX_CANVAS_DIMENSION) { 95 lazy.logger.warn( 96 "Limiting screen capture height to maximum allowed " + 97 MAX_CANVAS_DIMENSION + 98 " pixels" 99 ); 100 height = Math.floor(MAX_CANVAS_DIMENSION / scale); 101 canvasHeight = height * scale; 102 } 103 104 // If the area is larger, reduce the height to keep the full width. 105 if (canvasWidth * canvasHeight > MAX_CANVAS_AREA) { 106 lazy.logger.warn( 107 "Limiting screen capture area to maximum allowed " + 108 MAX_CANVAS_AREA + 109 " pixels" 110 ); 111 height = Math.floor(MAX_CANVAS_AREA / (canvasWidth * scale)); 112 canvasHeight = height * scale; 113 } 114 115 if (canvas === null) { 116 canvas = win.document.createElementNS(XHTML_NS, "canvas"); 117 canvas.width = canvasWidth; 118 canvas.height = canvasHeight; 119 } 120 121 const ctx = canvas.getContext(CONTEXT_2D); 122 123 if (readback) { 124 if (flags === null) { 125 flags = 126 ctx.DRAWWINDOW_DRAW_CARET | 127 ctx.DRAWWINDOW_DRAW_VIEW | 128 ctx.DRAWWINDOW_USE_WIDGET_LAYERS; 129 } 130 131 // drawWindow doesn't take scaling into account. 132 ctx.scale(scale, scale); 133 ctx.drawWindow(win, left + dX, top + dY, width, height, BG_COLOUR, flags); 134 } else { 135 let rect = new DOMRect(left, top, width, height); 136 let snapshot = await browsingContext.currentWindowGlobal.drawSnapshot( 137 rect, 138 scale, 139 BG_COLOUR 140 ); 141 142 ctx.drawImage(snapshot, 0, 0); 143 144 // Bug 1574935 - Huge dimensions can trigger an OOM because multiple copies 145 // of the bitmap will exist in memory. Force the removal of the snapshot 146 // because it is no longer needed. 147 snapshot.close(); 148 } 149 150 return canvas; 151 }; 152 153 /** 154 * Encode the contents of an HTMLCanvasElement to a Base64 encoded string. 155 * 156 * @param {HTMLCanvasElement} canvas 157 * The canvas to encode. 158 * @param {string} type 159 * The image format to output such as `image/png`. 160 * @param {number} encoderOptions 161 * A number between 0 and 1 representing the image quality. Defaults to `1.0`. 162 * 163 * @returns {string} 164 * A Base64 encoded string. 165 */ 166 capture.toBase64 = function (canvas, type, encoderOptions = 1.0) { 167 let u = canvas.toDataURL(type, encoderOptions); 168 return u.substring(u.indexOf(",") + 1); 169 }; 170 171 /** 172 * Hash the contents of an HTMLCanvasElement to a SHA-256 hex digest. 173 * 174 * @param {HTMLCanvasElement} canvas 175 * The canvas to encode. 176 * 177 * @returns {string} 178 * A hex digest of the SHA-256 hash of the base64 encoded string. 179 */ 180 capture.toHash = function (canvas) { 181 let u = capture.toBase64(canvas, "image/png"); 182 let buffer = new TextEncoder().encode(u); 183 return crypto.subtle.digest("SHA-256", buffer).then(hash => hex(hash)); 184 }; 185 186 /** 187 * Convert buffer into to hex. 188 * 189 * @param {ArrayBuffer} buffer 190 * The buffer containing the data to convert to hex. 191 * 192 * @returns {string} 193 * A hex digest of the input buffer. 194 */ 195 function hex(buffer) { 196 let hexCodes = []; 197 let view = new DataView(buffer); 198 for (let i = 0; i < view.byteLength; i += 4) { 199 let value = view.getUint32(i); 200 let stringValue = value.toString(16); 201 let padding = "00000000"; 202 let paddedValue = (padding + stringValue).slice(-padding.length); 203 hexCodes.push(paddedValue); 204 } 205 return hexCodes.join(""); 206 }