Backed out 3 changesets (bug 1884623) for causing multiple failures CLOSED TREE
[gecko.git] / toolkit / content / widgets / textrecognition.js
blob887d5767707c8cea93b1abbd0039889ac8509257
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/. */
4 "use strict";
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 {
10   /**
11    * @param {ShadowRoot} shadowRoot
12    * @param {Record<string, string | boolean | number>} _prefs
13    */
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;
21     /** @type {Window} */
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;
31   }
33   /*
34    * Callback called by UAWidgets right after constructor.
35    */
36   onsetup() {
37     this.resizeObserver = new this.window.ResizeObserver(() => {
38       this.positionSpans();
39     });
40     this.resizeObserver.observe(this.element);
41   }
43   positionSpans() {
44     if (!this.shadowRoot.firstChild) {
45       return;
46     }
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} */
67     let ctx = null;
69     if (
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
76     ) {
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");
83       ctx.scale(dpr, dpr);
84       ctx.fillStyle = "#00000088";
85       ctx.fillRect(0, 0, imgRect.width, imgRect.height);
87       ctx.beginPath();
88     }
90     for (const span of spans) {
91       let spanRect = this.spanRects.get(span);
92       if (!spanRect) {
93         // This only needs to happen once.
94         spanRect = span.getBoundingClientRect();
95         this.spanRects.set(span, spanRect);
96       }
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"
101       //  0         1       2         3        4        5        6        7
102       //  ^ bottomleft      ^ topleft          ^ topright        ^ bottomright
103       let [
104         bottomLeftX,
105         bottomLeftY,
106         topLeftX,
107         topLeftY,
108         topRightX,
109         topRightY,
110         bottomRightX,
111         bottomRightY,
112       ] = points;
114       // Invert the Y.
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.
121       // prettier-ignore
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
128       );
130       span.style.transform = "matrix3d(" + mat4.join(", ") + ")";
132       if (ctx) {
133         const inset = 3;
134         ctx.moveTo(
135           imgRect.width * bottomLeftX + inset,
136           imgRect.height * bottomLeftY - inset
137         );
138         ctx.lineTo(
139           imgRect.width * topLeftX + inset,
140           imgRect.height * topLeftY + inset
141         );
142         ctx.lineTo(
143           imgRect.width * topRightX - inset,
144           imgRect.height * topRightY + inset
145         );
146         ctx.lineTo(
147           imgRect.width * bottomRightX - inset,
148           imgRect.height * bottomRightY - inset
149         );
150         ctx.closePath();
151       }
152     }
154     if (ctx) {
155       // This composite operation will cut out the quads. The color is arbitrary.
156       ctx.globalCompositeOperation = "destination-out";
157       ctx.fillStyle = "#ffffff";
158       ctx.fill();
160       // Creating a round line will grow the selection slightly, and round the corners.
161       ctx.lineWidth = 10;
162       ctx.lineJoin = "round";
163       ctx.strokeStyle = "#ffffff";
164       ctx.stroke();
165     }
166   }
168   teardown() {
169     this.shadowRoot.firstChild.remove();
170     this.resizeObserver.disconnect();
171     this.spanRects.clear();
172   }
174   lazilyInitialize() {
175     if (this.isInitialized) {
176       return;
177     }
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" />
184         <canvas />
185         <!-- The spans will be reattached here -->
186       </div>`,
187       "application/xml"
188     );
189     if (
190       this.shadowRoot.children.length !== 1 ||
191       this.shadowRoot.firstChild.tagName !== "DIV"
192     ) {
193       throw new Error(
194         "Expected the shadowRoot to have a single div as the root element."
195       );
196     }
198     const spansDiv = this.shadowRoot.firstChild;
199     // Example layout of spansDiv:
200     // <div>
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
203     //   </span>
204     //   ...
205     // </div>
206     spansDiv.remove();
208     this.shadowRoot.importNodeAndAppendChildAt(
209       this.shadowRoot,
210       parserDoc.documentElement,
211       true /* deep */
212     );
214     this.shadowRoot.importNodeAndAppendChildAt(
215       this.shadowRoot.firstChild,
216       spansDiv,
217       true /* deep */
218     );
219   }
223  * A three dimensional vector.
225  * @typedef {[number, number, number]} Vec3
226  */
229  * A 3x3 matrix.
231  * @typedef {[number, number, number,
232  *            number, number, number,
233  *            number, number, number]} Matrix3
234  */
237  * A 4x4 matrix.
239  * @typedef {[number, number, number, number,
240  *            number, number, number, number,
241  *            number, number, number, number,
242  *            number, number, number, number]} Matrix4
243  */
246  * Compute the adjugate matrix.
247  * https://en.wikipedia.org/wiki/Adjugate_matrix
249  * @param {Matrix3} m
250  * @returns {Matrix3}
251  */
252 function computeAdjugate(m) {
253   // prettier-ignore
254   return [
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],
264   ];
268  * @param {Matrix3} a
269  * @param {Matrix3} b
270  * @returns {Matrix3}
271  */
272 function multiplyMat3(a, b) {
273   let out = [];
274   for (let i = 0; i < 3; i++) {
275     for (let j = 0; j < 3; j++) {
276       let sum = 0;
277       for (let k = 0; k < 3; k++) {
278         sum += a[3 * i + k] * b[3 * k + j];
279       }
280       out[3 * i + j] = sum;
281     }
282   }
283   return out;
287  * @param {Matrix3} m
288  * @param {Vec3} v
289  * @returns {Vec3}
290  */
291 function multiplyMat3Vec3(m, v) {
292   // prettier-ignore
293   return [
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],
297   ];
301  * @returns {Matrix3}
302  */
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]);
307   // prettier-ignore
308   return multiplyMat3(
309     mat3,
310     [
311       vec3[0], 0,       0,
312       0,       vec3[1], 0,
313       0,       0,       vec3[2]
314     ]
315   );
319  * @type {(...Matrix4) => Matrix3}
320  */
321 // prettier-ignore
322 function general2DProjection(
323   x1s, y1s, x1d, y1d,
324   x2s, y2s, x2d, y2d,
325   x3s, y3s, x3d, y3d,
326   x4s, y4s, x4d, y4d
327 ) {
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:
338  *         w
339  *      ┌─────┐      project     1 ─────── 2
340  *    h │     │       -->        │        /
341  *      └─────┘                  │       /
342  *                               3 ──── 4
344  * @returns {Matrix4}
345  */
346 function projectPoints(w, h, x1, y1, x2, y2, x3, y3, x4, y4) {
347   // prettier-ignore
348   const mat3 = general2DProjection(
349     0, 0, x1, y1,
350     w, 0, x2, y2,
351     0, h, x3, y3,
352     w, h, x4, y4
353   );
355   for (let i = 0; i < 9; i++) {
356     mat3[i] = mat3[i] / mat3[8];
357   }
359   // prettier-ignore
360   return [
361     mat3[0], mat3[3], 0, mat3[6],
362     mat3[1], mat3[4], 0, mat3[7],
363     0,       0,       1, 0,
364     mat3[2], mat3[5], 0, mat3[8],
365   ];