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
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 // This is a UA widget. It runs in per-origin UA widget scope,
7 // to be loaded by UAWidgetsChild.jsm.
9 this.TextRecognitionWidget = class {
11 * @param {ShadowRoot} shadowRoot
12 * @param {Record<string, string | boolean | number>} _prefs
14 constructor(shadowRoot, _prefs) {
15 /** @type {ShadowRoot} */
16 this.shadowRoot = shadowRoot;
17 /** @type {HTMLElement} */
18 this.element = shadowRoot.host;
19 /** @type {Document} */
20 this.document = this.element.ownerDocument;
22 this.window = this.document.defaultView;
23 /** @type {ResizeObserver} */
24 this.resizeObserver = null;
25 /** @type {Map<HTMLSpanElement, DOMRect} */
26 this.spanRects = new Map();
27 /** @type {boolean} */
28 this.isInitialized = false;
29 /** @type {null | number} */
30 this.lastCanvasStyleWidth = null;
34 * Callback called by UAWidgets right after constructor.
37 this.resizeObserver = new this.window.ResizeObserver(() => {
40 this.resizeObserver.observe(this.element);
44 if (!this.shadowRoot.firstChild) {
47 this.lazilyInitialize();
49 /** @type {HTMLDivElement} */
50 const div = this.shadowRoot.firstChild;
51 const canvas = div.querySelector("canvas");
52 const spans = div.querySelectorAll("span");
54 // TODO Bug 1770438 - The <img> element does not currently let child elements be
55 // sized relative to the size of the containing <img> element. It would be better
56 // to teach the <img> element how to do this. For the prototype, do the more expensive
57 // operation of getting the bounding client rect, and handle the positioning manually.
58 const imgRect = this.element.getBoundingClientRect();
59 div.style.width = imgRect.width + "px";
60 div.style.height = imgRect.height + "px";
61 canvas.style.width = imgRect.width + "px";
62 canvas.style.height = imgRect.height + "px";
64 // The ctx is only available when redrawing the canvas. This is operation is only
65 // done when necessary, as it can be expensive.
66 /** @type {null | CanvasRenderingContext2D} */
70 // The canvas hasn't been drawn to yet.
71 this.lastCanvasStyleWidth === null ||
72 // Only redraw when the image has grown 25% larger. This percentage was chosen
73 // as it visually seemed to work well, with the canvas never appearing blurry
74 // when manually testing it.
75 imgRect.width > this.lastCanvasStyleWidth * 1.25
77 const dpr = this.window.devicePixelRatio;
78 canvas.width = imgRect.width * dpr;
79 canvas.height = imgRect.height * dpr;
80 this.lastCanvasStyleWidth = imgRect.width;
82 ctx = canvas.getContext("2d");
84 ctx.fillStyle = "#00000088";
85 ctx.fillRect(0, 0, imgRect.width, imgRect.height);
90 for (const span of spans) {
91 let spanRect = this.spanRects.get(span);
93 // This only needs to happen once.
94 spanRect = span.getBoundingClientRect();
95 this.spanRects.set(span, spanRect);
98 const points = span.dataset.points.split(",").map(p => Number(p));
99 // Use the points in the string, e.g.
100 // "0.0275349,0.14537,0.0275349,0.244662,0.176966,0.244565,0.176966,0.145273"
102 // ^ bottomleft ^ topleft ^ topright ^ bottomright
115 topLeftY = 1 - topLeftY;
116 topRightY = 1 - topRightY;
117 bottomLeftY = 1 - bottomLeftY;
118 bottomRightY = 1 - bottomRightY;
120 // Create a projection matrix to position the <span> relative to the bounds.
122 const mat4 = projectPoints(
123 spanRect.width, spanRect.height,
124 imgRect.width * topLeftX, imgRect.height * topLeftY,
125 imgRect.width * topRightX, imgRect.height * topRightY,
126 imgRect.width * bottomLeftX, imgRect.height * bottomLeftY,
127 imgRect.width * bottomRightX, imgRect.height * bottomRightY
130 span.style.transform = "matrix3d(" + mat4.join(", ") + ")";
135 imgRect.width * bottomLeftX + inset,
136 imgRect.height * bottomLeftY - inset
139 imgRect.width * topLeftX + inset,
140 imgRect.height * topLeftY + inset
143 imgRect.width * topRightX - inset,
144 imgRect.height * topRightY + inset
147 imgRect.width * bottomRightX - inset,
148 imgRect.height * bottomRightY - inset
155 // This composite operation will cut out the quads. The color is arbitrary.
156 ctx.globalCompositeOperation = "destination-out";
157 ctx.fillStyle = "#ffffff";
160 // Creating a round line will grow the selection slightly, and round the corners.
162 ctx.lineJoin = "round";
163 ctx.strokeStyle = "#ffffff";
169 this.shadowRoot.firstChild.remove();
170 this.resizeObserver.disconnect();
171 this.spanRects.clear();
175 if (this.isInitialized) {
178 this.isInitialized = true;
180 const parser = new this.window.DOMParser();
181 let parserDoc = parser.parseFromString(
182 `<div class="textrecognition" xmlns="http://www.w3.org/1999/xhtml" role="none">
183 <link rel="stylesheet" href="chrome://global/skin/media/textrecognition.css" />
185 <!-- The spans will be reattached here -->
190 this.shadowRoot.children.length !== 1 ||
191 this.shadowRoot.firstChild.tagName !== "DIV"
194 "Expected the shadowRoot to have a single div as the root element."
198 const spansDiv = this.shadowRoot.firstChild;
199 // Example layout of spansDiv:
201 // <span data-points="0.0275349,0.14537,0.0275349,0.244662,0.176966,0.244565,0.176966,0.145273">
202 // Text that has been recognized
208 this.shadowRoot.importNodeAndAppendChildAt(
210 parserDoc.documentElement,
214 this.shadowRoot.importNodeAndAppendChildAt(
215 this.shadowRoot.firstChild,
223 * A three dimensional vector.
225 * @typedef {[number, number, number]} Vec3
231 * @typedef {[number, number, number,
232 * number, number, number,
233 * number, number, number]} Matrix3
239 * @typedef {[number, number, number, number,
240 * number, number, number, number,
241 * number, number, number, number,
242 * number, number, number, number]} Matrix4
246 * Compute the adjugate matrix.
247 * https://en.wikipedia.org/wiki/Adjugate_matrix
252 function computeAdjugate(m) {
255 m[4] * m[8] - m[5] * m[7],
256 m[2] * m[7] - m[1] * m[8],
257 m[1] * m[5] - m[2] * m[4],
258 m[5] * m[6] - m[3] * m[8],
259 m[0] * m[8] - m[2] * m[6],
260 m[2] * m[3] - m[0] * m[5],
261 m[3] * m[7] - m[4] * m[6],
262 m[1] * m[6] - m[0] * m[7],
263 m[0] * m[4] - m[1] * m[3],
272 function multiplyMat3(a, b) {
274 for (let i = 0; i < 3; i++) {
275 for (let j = 0; j < 3; j++) {
277 for (let k = 0; k < 3; k++) {
278 sum += a[3 * i + k] * b[3 * k + j];
280 out[3 * i + j] = sum;
291 function multiplyMat3Vec3(m, v) {
294 m[0] * v[0] + m[1] * v[1] + m[2] * v[2],
295 m[3] * v[0] + m[4] * v[1] + m[5] * v[2],
296 m[6] * v[0] + m[7] * v[1] + m[8] * v[2],
303 function basisToPoints(x1, y1, x2, y2, x3, y3, x4, y4) {
304 /** @type {Matrix3} */
305 let mat3 = [x1, x2, x3, y1, y2, y3, 1, 1, 1];
306 let vec3 = multiplyMat3Vec3(computeAdjugate(mat3), [x4, y4, 1]);
319 * @type {(...Matrix4) => Matrix3}
322 function general2DProjection(
328 let s = basisToPoints(x1s, y1s, x2s, y2s, x3s, y3s, x4s, y4s);
329 let d = basisToPoints(x1d, y1d, x2d, y2d, x3d, y3d, x4d, y4d);
330 return multiplyMat3(d, computeAdjugate(s));
334 * Given a width and height, compute a projection matrix to points 1-4.
336 * The points (x1,y1) through (x4, y4) use the following ordering:
339 * ┌─────┐ project 1 ─────── 2
346 function projectPoints(w, h, x1, y1, x2, y2, x3, y3, x4, y4) {
348 const mat3 = general2DProjection(
355 for (let i = 0; i < 9; i++) {
356 mat3[i] = mat3[i] / mat3[8];
361 mat3[0], mat3[3], 0, mat3[6],
362 mat3[1], mat3[4], 0, mat3[7],
364 mat3[2], mat3[5], 0, mat3[8],