driver.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963
  1. /* Copyright 2012 Mozilla Foundation
  2. *
  3. * Licensed under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License.
  5. * You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software
  10. * distributed under the License is distributed on an "AS IS" BASIS,
  11. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. * See the License for the specific language governing permissions and
  13. * limitations under the License.
  14. */
  15. /* globals pdfjsLib, pdfjsViewer */
  16. const {
  17. AnnotationLayer,
  18. AnnotationMode,
  19. createPromiseCapability,
  20. getDocument,
  21. GlobalWorkerOptions,
  22. PixelsPerInch,
  23. renderTextLayer,
  24. shadow,
  25. XfaLayer,
  26. } = pdfjsLib;
  27. const { GenericL10n, NullL10n, parseQueryString, SimpleLinkService } =
  28. pdfjsViewer;
  29. const WAITING_TIME = 100; // ms
  30. const CMAP_URL = "/build/generic/web/cmaps/";
  31. const CMAP_PACKED = true;
  32. const STANDARD_FONT_DATA_URL = "/build/generic/web/standard_fonts/";
  33. const IMAGE_RESOURCES_PATH = "/web/images/";
  34. const VIEWER_CSS = "../build/components/pdf_viewer.css";
  35. const VIEWER_LOCALE = "en-US";
  36. const WORKER_SRC = "../build/generic/build/pdf.worker.js";
  37. const RENDER_TASK_ON_CONTINUE_DELAY = 5; // ms
  38. const SVG_NS = "http://www.w3.org/2000/svg";
  39. const md5FileMap = new Map();
  40. function loadStyles(styles) {
  41. const promises = [];
  42. for (const file of styles) {
  43. promises.push(
  44. fetch(file)
  45. .then(response => {
  46. if (!response.ok) {
  47. throw new Error(response.statusText);
  48. }
  49. return response.text();
  50. })
  51. .catch(reason => {
  52. throw new Error(`Error fetching style (${file}): ${reason}`);
  53. })
  54. );
  55. }
  56. return Promise.all(promises);
  57. }
  58. function writeSVG(svgElement, ctx) {
  59. // We need to have UTF-8 encoded XML.
  60. const svg_xml = unescape(
  61. encodeURIComponent(new XMLSerializer().serializeToString(svgElement))
  62. );
  63. return new Promise((resolve, reject) => {
  64. const img = new Image();
  65. img.src = "data:image/svg+xml;base64," + btoa(svg_xml);
  66. img.onload = function () {
  67. ctx.drawImage(img, 0, 0);
  68. resolve();
  69. };
  70. img.onerror = function (e) {
  71. reject(new Error(`Error rasterizing SVG: ${e}`));
  72. };
  73. });
  74. }
  75. async function inlineImages(node, silentErrors = false) {
  76. const promises = [];
  77. for (const image of node.getElementsByTagName("img")) {
  78. const url = image.src;
  79. promises.push(
  80. fetch(url)
  81. .then(response => {
  82. if (!response.ok) {
  83. throw new Error(response.statusText);
  84. }
  85. return response.blob();
  86. })
  87. .then(blob => {
  88. return new Promise((resolve, reject) => {
  89. const reader = new FileReader();
  90. reader.onload = () => {
  91. resolve(reader.result);
  92. };
  93. reader.onerror = reject;
  94. reader.readAsDataURL(blob);
  95. });
  96. })
  97. .then(dataUrl => {
  98. return new Promise((resolve, reject) => {
  99. image.onload = resolve;
  100. image.onerror = evt => {
  101. if (silentErrors) {
  102. resolve();
  103. return;
  104. }
  105. reject(evt);
  106. };
  107. image.src = dataUrl;
  108. });
  109. })
  110. .catch(reason => {
  111. throw new Error(`Error inlining image (${url}): ${reason}`);
  112. })
  113. );
  114. }
  115. await Promise.all(promises);
  116. }
  117. async function convertCanvasesToImages(annotationCanvasMap, outputScale) {
  118. const results = new Map();
  119. const promises = [];
  120. for (const [key, canvas] of annotationCanvasMap) {
  121. promises.push(
  122. new Promise(resolve => {
  123. canvas.toBlob(blob => {
  124. const image = document.createElement("img");
  125. image.onload = function () {
  126. image.style.width = Math.floor(image.width / outputScale) + "px";
  127. resolve();
  128. };
  129. results.set(key, image);
  130. image.src = URL.createObjectURL(blob);
  131. });
  132. })
  133. );
  134. }
  135. await Promise.all(promises);
  136. return results;
  137. }
  138. class Rasterize {
  139. /**
  140. * For the reference tests, the full content of the various layers must be
  141. * visible. To achieve this, we load the common styles as used by the viewer
  142. * and extend them with a set of overrides to make all elements visible.
  143. *
  144. * Note that we cannot simply use `@import` to import the common styles in
  145. * the overrides file because the browser does not resolve that when the
  146. * styles are inserted via XHR. Therefore, we load and combine them here.
  147. */
  148. static get annotationStylePromise() {
  149. const styles = [VIEWER_CSS, "./annotation_layer_builder_overrides.css"];
  150. return shadow(this, "annotationStylePromise", loadStyles(styles));
  151. }
  152. static get textStylePromise() {
  153. const styles = [VIEWER_CSS, "./text_layer_test.css"];
  154. return shadow(this, "textStylePromise", loadStyles(styles));
  155. }
  156. static get xfaStylePromise() {
  157. const styles = [VIEWER_CSS, "./xfa_layer_builder_overrides.css"];
  158. return shadow(this, "xfaStylePromise", loadStyles(styles));
  159. }
  160. static createContainer(viewport) {
  161. const svg = document.createElementNS(SVG_NS, "svg:svg");
  162. svg.setAttribute("width", `${viewport.width}px`);
  163. svg.setAttribute("height", `${viewport.height}px`);
  164. const foreignObject = document.createElementNS(SVG_NS, "svg:foreignObject");
  165. foreignObject.setAttribute("x", "0");
  166. foreignObject.setAttribute("y", "0");
  167. foreignObject.setAttribute("width", `${viewport.width}px`);
  168. foreignObject.setAttribute("height", `${viewport.height}px`);
  169. const style = document.createElement("style");
  170. foreignObject.append(style);
  171. const div = document.createElement("div");
  172. foreignObject.append(div);
  173. return { svg, foreignObject, style, div };
  174. }
  175. static async annotationLayer(
  176. ctx,
  177. viewport,
  178. outputScale,
  179. annotations,
  180. annotationCanvasMap,
  181. page,
  182. imageResourcesPath,
  183. renderForms = false,
  184. l10n = NullL10n
  185. ) {
  186. try {
  187. const { svg, foreignObject, style, div } = this.createContainer(viewport);
  188. div.className = "annotationLayer";
  189. const [common, overrides] = await this.annotationStylePromise;
  190. style.textContent =
  191. `${common}\n${overrides}\n` +
  192. `:root { --scale-factor: ${viewport.scale} }`;
  193. const annotationViewport = viewport.clone({ dontFlip: true });
  194. const annotationImageMap = await convertCanvasesToImages(
  195. annotationCanvasMap,
  196. outputScale
  197. );
  198. // Rendering annotation layer as HTML.
  199. const parameters = {
  200. viewport: annotationViewport,
  201. div,
  202. annotations,
  203. page,
  204. linkService: new SimpleLinkService(),
  205. imageResourcesPath,
  206. renderForms,
  207. annotationCanvasMap: annotationImageMap,
  208. };
  209. AnnotationLayer.render(parameters);
  210. await l10n.translate(div);
  211. // Inline SVG images from text annotations.
  212. await inlineImages(div);
  213. foreignObject.append(div);
  214. svg.append(foreignObject);
  215. await writeSVG(svg, ctx);
  216. } catch (reason) {
  217. throw new Error(`Rasterize.annotationLayer: "${reason?.message}".`);
  218. }
  219. }
  220. static async textLayer(ctx, viewport, textContent) {
  221. try {
  222. const { svg, foreignObject, style, div } = this.createContainer(viewport);
  223. div.className = "textLayer";
  224. // Items are transformed to have 1px font size.
  225. svg.setAttribute("font-size", 1);
  226. const [common, overrides] = await this.textStylePromise;
  227. style.textContent =
  228. `${common}\n${overrides}\n` +
  229. `:root { --scale-factor: ${viewport.scale} }`;
  230. // Rendering text layer as HTML.
  231. const task = renderTextLayer({
  232. textContentSource: textContent,
  233. container: div,
  234. viewport,
  235. });
  236. await task.promise;
  237. svg.append(foreignObject);
  238. await writeSVG(svg, ctx);
  239. } catch (reason) {
  240. throw new Error(`Rasterize.textLayer: "${reason?.message}".`);
  241. }
  242. }
  243. static async xfaLayer(
  244. ctx,
  245. viewport,
  246. xfaHtml,
  247. fontRules,
  248. annotationStorage,
  249. isPrint
  250. ) {
  251. try {
  252. const { svg, foreignObject, style, div } = this.createContainer(viewport);
  253. const [common, overrides] = await this.xfaStylePromise;
  254. style.textContent = `${common}\n${overrides}\n${fontRules}`;
  255. // Rendering XFA layer as HTML.
  256. XfaLayer.render({
  257. viewport: viewport.clone({ dontFlip: true }),
  258. div,
  259. xfaHtml,
  260. annotationStorage,
  261. linkService: new SimpleLinkService(),
  262. intent: isPrint ? "print" : "display",
  263. });
  264. // Some unsupported type of images (e.g. tiff) lead to errors.
  265. await inlineImages(div, /* silentErrors = */ true);
  266. svg.append(foreignObject);
  267. await writeSVG(svg, ctx);
  268. } catch (reason) {
  269. throw new Error(`Rasterize.xfaLayer: "${reason?.message}".`);
  270. }
  271. }
  272. }
  273. /**
  274. * @typedef {Object} DriverOptions
  275. * @property {HTMLSpanElement} inflight - Field displaying the number of
  276. * inflight requests.
  277. * @property {HTMLInputElement} disableScrolling - Checkbox to disable
  278. * automatic scrolling of the output container.
  279. * @property {HTMLPreElement} output - Container for all output messages.
  280. * @property {HTMLDivElement} end - Container for a completion message.
  281. */
  282. class Driver {
  283. /**
  284. * @param {DriverOptions} options
  285. */
  286. constructor(options) {
  287. // Configure the global worker options.
  288. GlobalWorkerOptions.workerSrc = WORKER_SRC;
  289. this._l10n = new GenericL10n(VIEWER_LOCALE);
  290. // Set the passed options
  291. this.inflight = options.inflight;
  292. this.disableScrolling = options.disableScrolling;
  293. this.output = options.output;
  294. this.end = options.end;
  295. // Set parameters from the query string
  296. const params = parseQueryString(window.location.search.substring(1));
  297. this.browser = params.get("browser");
  298. this.manifestFile = params.get("manifestfile");
  299. this.delay = params.get("delay") | 0;
  300. this.inFlightRequests = 0;
  301. this.testFilter = JSON.parse(params.get("testfilter") || "[]");
  302. this.xfaOnly = params.get("xfaonly") === "true";
  303. // Create a working canvas
  304. this.canvas = document.createElement("canvas");
  305. }
  306. run() {
  307. window.onerror = (message, source, line, column, error) => {
  308. this._info(
  309. "Error: " +
  310. message +
  311. " Script: " +
  312. source +
  313. " Line: " +
  314. line +
  315. " Column: " +
  316. column +
  317. " StackTrace: " +
  318. error
  319. );
  320. };
  321. this._info("User agent: " + navigator.userAgent);
  322. this._log(`Harness thinks this browser is ${this.browser}\n`);
  323. this._log('Fetching manifest "' + this.manifestFile + '"... ');
  324. if (this.delay > 0) {
  325. this._log("\nDelaying for " + this.delay + " ms...\n");
  326. }
  327. // When gathering the stats the numbers seem to be more reliable
  328. // if the browser is given more time to start.
  329. setTimeout(async () => {
  330. const response = await fetch(this.manifestFile);
  331. if (!response.ok) {
  332. throw new Error(response.statusText);
  333. }
  334. this._log("done\n");
  335. this.manifest = await response.json();
  336. if (this.testFilter?.length || this.xfaOnly) {
  337. this.manifest = this.manifest.filter(item => {
  338. if (this.testFilter.includes(item.id)) {
  339. return true;
  340. }
  341. if (this.xfaOnly && item.enableXfa) {
  342. return true;
  343. }
  344. return false;
  345. });
  346. }
  347. this.currentTask = 0;
  348. this._nextTask();
  349. }, this.delay);
  350. }
  351. /**
  352. * A debugging tool to log to the terminal while tests are running.
  353. * XXX: This isn't currently referenced, but it's useful for debugging so
  354. * do not remove it.
  355. *
  356. * @param {string} msg - The message to log, it will be prepended with the
  357. * current PDF ID if there is one.
  358. */
  359. log(msg) {
  360. let id = this.browser;
  361. const task = this.manifest[this.currentTask];
  362. if (task) {
  363. id += `-${task.id}`;
  364. }
  365. this._info(`${id}: ${msg}`);
  366. }
  367. _nextTask() {
  368. let failure = "";
  369. this._cleanup().then(() => {
  370. if (this.currentTask === this.manifest.length) {
  371. this._done();
  372. return;
  373. }
  374. const task = this.manifest[this.currentTask];
  375. task.round = 0;
  376. task.pageNum = task.firstPage || 1;
  377. task.stats = { times: [] };
  378. task.enableXfa = task.enableXfa === true;
  379. const prevFile = md5FileMap.get(task.md5);
  380. if (prevFile) {
  381. if (task.file !== prevFile) {
  382. this._nextPage(
  383. task,
  384. `The "${task.file}" file is identical to the previously used "${prevFile}" file.`
  385. );
  386. return;
  387. }
  388. } else {
  389. md5FileMap.set(task.md5, task.file);
  390. }
  391. // Support *linked* test-cases for the other suites, e.g. unit- and
  392. // integration-tests, without needing to run them as reference-tests.
  393. if (task.type === "other") {
  394. this._log(`Skipping file "${task.file}"\n`);
  395. if (!task.link) {
  396. this._nextPage(task, 'Expected "other" test-case to be linked.');
  397. return;
  398. }
  399. this.currentTask++;
  400. this._nextTask();
  401. return;
  402. }
  403. this._log('Loading file "' + task.file + '"\n');
  404. const absoluteUrl = new URL(task.file, window.location).href;
  405. try {
  406. let xfaStyleElement = null;
  407. if (task.enableXfa) {
  408. // Need to get the font definitions to inject them in the SVG.
  409. // So we create this element and those definitions will be
  410. // appended in font_loader.js.
  411. xfaStyleElement = document.createElement("style");
  412. document.documentElement
  413. .getElementsByTagName("head")[0]
  414. .append(xfaStyleElement);
  415. }
  416. const loadingTask = getDocument({
  417. url: absoluteUrl,
  418. password: task.password,
  419. cMapUrl: CMAP_URL,
  420. cMapPacked: CMAP_PACKED,
  421. standardFontDataUrl: STANDARD_FONT_DATA_URL,
  422. disableRange: task.disableRange,
  423. disableAutoFetch: !task.enableAutoFetch,
  424. pdfBug: true,
  425. useSystemFonts: task.useSystemFonts,
  426. useWorkerFetch: task.useWorkerFetch,
  427. enableXfa: task.enableXfa,
  428. styleElement: xfaStyleElement,
  429. });
  430. let promise = loadingTask.promise;
  431. if (task.save) {
  432. if (!task.annotationStorage) {
  433. promise = Promise.reject(
  434. new Error("Missing `annotationStorage` entry.")
  435. );
  436. } else {
  437. promise = loadingTask.promise.then(async doc => {
  438. for (const [key, value] of Object.entries(
  439. task.annotationStorage
  440. )) {
  441. doc.annotationStorage.setValue(key, value);
  442. }
  443. const data = await doc.saveDocument();
  444. await loadingTask.destroy();
  445. delete task.annotationStorage;
  446. return getDocument(data).promise;
  447. });
  448. }
  449. }
  450. promise.then(
  451. async doc => {
  452. if (task.enableXfa) {
  453. task.fontRules = "";
  454. for (const rule of xfaStyleElement.sheet.cssRules) {
  455. task.fontRules += rule.cssText + "\n";
  456. }
  457. }
  458. task.pdfDoc = doc;
  459. task.optionalContentConfigPromise = doc.getOptionalContentConfig();
  460. if (task.optionalContent) {
  461. const entries = Object.entries(task.optionalContent),
  462. optionalContentConfig = await task.optionalContentConfigPromise;
  463. for (const [id, visible] of entries) {
  464. optionalContentConfig.setVisibility(id, visible);
  465. }
  466. }
  467. this._nextPage(task, failure);
  468. },
  469. err => {
  470. failure = "Loading PDF document: " + err;
  471. this._nextPage(task, failure);
  472. }
  473. );
  474. return;
  475. } catch (e) {
  476. failure = "Loading PDF document: " + this._exceptionToString(e);
  477. }
  478. this._nextPage(task, failure);
  479. });
  480. }
  481. _cleanup() {
  482. // Clear out all the stylesheets since a new one is created for each font.
  483. while (document.styleSheets.length > 0) {
  484. const styleSheet = document.styleSheets[0];
  485. while (styleSheet.cssRules.length > 0) {
  486. styleSheet.deleteRule(0);
  487. }
  488. styleSheet.ownerNode.remove();
  489. }
  490. const body = document.body;
  491. while (body.lastChild !== this.end) {
  492. body.lastChild.remove();
  493. }
  494. const destroyedPromises = [];
  495. // Wipe out the link to the pdfdoc so it can be GC'ed.
  496. for (let i = 0; i < this.manifest.length; i++) {
  497. if (this.manifest[i].pdfDoc) {
  498. destroyedPromises.push(this.manifest[i].pdfDoc.destroy());
  499. delete this.manifest[i].pdfDoc;
  500. }
  501. }
  502. return Promise.all(destroyedPromises);
  503. }
  504. _exceptionToString(e) {
  505. if (typeof e !== "object") {
  506. return String(e);
  507. }
  508. if (!("message" in e)) {
  509. return JSON.stringify(e);
  510. }
  511. return e.message + ("stack" in e ? " at " + e.stack.split("\n")[0] : "");
  512. }
  513. _getLastPageNumber(task) {
  514. if (!task.pdfDoc) {
  515. return task.firstPage || 1;
  516. }
  517. return task.lastPage || task.pdfDoc.numPages;
  518. }
  519. _nextPage(task, loadError) {
  520. let failure = loadError || "";
  521. let ctx;
  522. if (!task.pdfDoc) {
  523. const dataUrl = this.canvas.toDataURL("image/png");
  524. this._sendResult(dataUrl, task, failure).then(() => {
  525. this._log(
  526. "done" + (failure ? " (failed !: " + failure + ")" : "") + "\n"
  527. );
  528. this.currentTask++;
  529. this._nextTask();
  530. });
  531. return;
  532. }
  533. if (task.pageNum > this._getLastPageNumber(task)) {
  534. if (++task.round < task.rounds) {
  535. this._log(" Round " + (1 + task.round) + "\n");
  536. task.pageNum = task.firstPage || 1;
  537. } else {
  538. this.currentTask++;
  539. this._nextTask();
  540. return;
  541. }
  542. }
  543. if (task.skipPages && task.skipPages.includes(task.pageNum)) {
  544. this._log(
  545. " Skipping page " + task.pageNum + "/" + task.pdfDoc.numPages + "...\n"
  546. );
  547. task.pageNum++;
  548. this._nextPage(task);
  549. return;
  550. }
  551. if (!failure) {
  552. try {
  553. this._log(
  554. " Loading page " + task.pageNum + "/" + task.pdfDoc.numPages + "... "
  555. );
  556. ctx = this.canvas.getContext("2d", { alpha: false });
  557. task.pdfDoc.getPage(task.pageNum).then(
  558. page => {
  559. // Default to creating the test images at the devices pixel ratio,
  560. // unless the test explicitly specifies an output scale.
  561. const outputScale = task.outputScale || window.devicePixelRatio;
  562. let viewport = page.getViewport({
  563. scale: PixelsPerInch.PDF_TO_CSS_UNITS,
  564. });
  565. // Restrict the test from creating a canvas that is too big.
  566. const MAX_CANVAS_PIXEL_DIMENSION = 4096;
  567. const largestDimension = Math.max(viewport.width, viewport.height);
  568. if (
  569. Math.floor(largestDimension * outputScale) >
  570. MAX_CANVAS_PIXEL_DIMENSION
  571. ) {
  572. const rescale = MAX_CANVAS_PIXEL_DIMENSION / largestDimension;
  573. viewport = viewport.clone({
  574. scale: PixelsPerInch.PDF_TO_CSS_UNITS * rescale,
  575. });
  576. }
  577. const pixelWidth = Math.floor(viewport.width * outputScale);
  578. const pixelHeight = Math.floor(viewport.height * outputScale);
  579. task.viewportWidth = Math.floor(viewport.width);
  580. task.viewportHeight = Math.floor(viewport.height);
  581. task.outputScale = outputScale;
  582. this.canvas.width = pixelWidth;
  583. this.canvas.height = pixelHeight;
  584. this.canvas.style.width = Math.floor(viewport.width) + "px";
  585. this.canvas.style.height = Math.floor(viewport.height) + "px";
  586. this._clearCanvas();
  587. const transform =
  588. outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : null;
  589. // Initialize various `eq` test subtypes, see comment below.
  590. let renderAnnotations = false,
  591. renderForms = false,
  592. renderPrint = false,
  593. renderXfa = false,
  594. annotationCanvasMap = null,
  595. pageColors = null;
  596. if (task.annotationStorage) {
  597. const entries = Object.entries(task.annotationStorage),
  598. docAnnotationStorage = task.pdfDoc.annotationStorage;
  599. for (const [key, value] of entries) {
  600. docAnnotationStorage.setValue(key, value);
  601. }
  602. }
  603. let textLayerCanvas, annotationLayerCanvas, annotationLayerContext;
  604. let initPromise;
  605. if (task.type === "text") {
  606. // Using a dummy canvas for PDF context drawing operations
  607. textLayerCanvas = this.textLayerCanvas;
  608. if (!textLayerCanvas) {
  609. textLayerCanvas = document.createElement("canvas");
  610. this.textLayerCanvas = textLayerCanvas;
  611. }
  612. textLayerCanvas.width = pixelWidth;
  613. textLayerCanvas.height = pixelHeight;
  614. const textLayerContext = textLayerCanvas.getContext("2d");
  615. textLayerContext.clearRect(
  616. 0,
  617. 0,
  618. textLayerCanvas.width,
  619. textLayerCanvas.height
  620. );
  621. textLayerContext.scale(outputScale, outputScale);
  622. // The text builder will draw its content on the test canvas
  623. initPromise = page
  624. .getTextContent({
  625. includeMarkedContent: true,
  626. })
  627. .then(function (textContent) {
  628. return Rasterize.textLayer(
  629. textLayerContext,
  630. viewport,
  631. textContent
  632. );
  633. });
  634. } else {
  635. textLayerCanvas = null;
  636. // We fetch the `eq` specific test subtypes here, to avoid
  637. // accidentally changing the behaviour for other types of tests.
  638. renderAnnotations = !!task.annotations;
  639. renderForms = !!task.forms;
  640. renderPrint = !!task.print;
  641. renderXfa = !!task.enableXfa;
  642. pageColors = task.pageColors || null;
  643. // Render the annotation layer if necessary.
  644. if (renderAnnotations || renderForms || renderXfa) {
  645. // Create a dummy canvas for the drawing operations.
  646. annotationLayerCanvas = this.annotationLayerCanvas;
  647. if (!annotationLayerCanvas) {
  648. annotationLayerCanvas = document.createElement("canvas");
  649. this.annotationLayerCanvas = annotationLayerCanvas;
  650. }
  651. annotationLayerCanvas.width = pixelWidth;
  652. annotationLayerCanvas.height = pixelHeight;
  653. annotationLayerContext = annotationLayerCanvas.getContext("2d");
  654. annotationLayerContext.clearRect(
  655. 0,
  656. 0,
  657. annotationLayerCanvas.width,
  658. annotationLayerCanvas.height
  659. );
  660. annotationLayerContext.scale(outputScale, outputScale);
  661. if (!renderXfa) {
  662. // The annotation builder will draw its content
  663. // on the canvas.
  664. initPromise = page.getAnnotations({ intent: "display" });
  665. annotationCanvasMap = new Map();
  666. } else {
  667. initPromise = page.getXfa().then(function (xfaHtml) {
  668. return Rasterize.xfaLayer(
  669. annotationLayerContext,
  670. viewport,
  671. xfaHtml,
  672. task.fontRules,
  673. task.pdfDoc.annotationStorage,
  674. task.renderPrint
  675. );
  676. });
  677. }
  678. } else {
  679. annotationLayerCanvas = null;
  680. initPromise = Promise.resolve();
  681. }
  682. }
  683. const renderContext = {
  684. canvasContext: ctx,
  685. viewport,
  686. optionalContentConfigPromise: task.optionalContentConfigPromise,
  687. annotationCanvasMap,
  688. pageColors,
  689. transform,
  690. };
  691. if (renderForms) {
  692. renderContext.annotationMode = task.annotationStorage
  693. ? AnnotationMode.ENABLE_STORAGE
  694. : AnnotationMode.ENABLE_FORMS;
  695. } else if (renderPrint) {
  696. if (task.annotationStorage) {
  697. renderContext.annotationMode = AnnotationMode.ENABLE_STORAGE;
  698. }
  699. renderContext.intent = "print";
  700. }
  701. const completeRender = error => {
  702. // if text layer is present, compose it on top of the page
  703. if (textLayerCanvas) {
  704. ctx.save();
  705. ctx.globalCompositeOperation = "screen";
  706. ctx.fillStyle = "rgb(128, 255, 128)"; // making it green
  707. ctx.fillRect(0, 0, pixelWidth, pixelHeight);
  708. ctx.restore();
  709. ctx.drawImage(textLayerCanvas, 0, 0);
  710. }
  711. // If we have annotation layer, compose it on top of the page.
  712. if (annotationLayerCanvas) {
  713. ctx.drawImage(annotationLayerCanvas, 0, 0);
  714. }
  715. if (page.stats) {
  716. // Get the page stats *before* running cleanup.
  717. task.stats = page.stats;
  718. }
  719. page.cleanup(/* resetStats = */ true);
  720. this._snapshot(task, error);
  721. };
  722. initPromise
  723. .then(data => {
  724. const renderTask = page.render(renderContext);
  725. if (task.renderTaskOnContinue) {
  726. renderTask.onContinue = function (cont) {
  727. // Slightly delay the continued rendering.
  728. setTimeout(cont, RENDER_TASK_ON_CONTINUE_DELAY);
  729. };
  730. }
  731. return renderTask.promise.then(() => {
  732. if (annotationCanvasMap) {
  733. Rasterize.annotationLayer(
  734. annotationLayerContext,
  735. viewport,
  736. outputScale,
  737. data,
  738. annotationCanvasMap,
  739. page,
  740. IMAGE_RESOURCES_PATH,
  741. renderForms,
  742. this._l10n
  743. ).then(() => {
  744. completeRender(false);
  745. });
  746. } else {
  747. completeRender(false);
  748. }
  749. });
  750. })
  751. .catch(function (error) {
  752. completeRender("render : " + error);
  753. });
  754. },
  755. error => {
  756. this._snapshot(task, "render : " + error);
  757. }
  758. );
  759. } catch (e) {
  760. failure = "page setup : " + this._exceptionToString(e);
  761. this._snapshot(task, failure);
  762. }
  763. }
  764. }
  765. _clearCanvas() {
  766. const ctx = this.canvas.getContext("2d", { alpha: false });
  767. ctx.beginPath();
  768. ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
  769. }
  770. _snapshot(task, failure) {
  771. this._log("Snapshotting... ");
  772. const dataUrl = this.canvas.toDataURL("image/png");
  773. this._sendResult(dataUrl, task, failure).then(() => {
  774. this._log(
  775. "done" + (failure ? " (failed !: " + failure + ")" : "") + "\n"
  776. );
  777. task.pageNum++;
  778. this._nextPage(task);
  779. });
  780. }
  781. _quit() {
  782. this._log("Done !");
  783. this.end.textContent = "Tests finished. Close this window!";
  784. // Send the quit request
  785. fetch(`/tellMeToQuit?browser=${escape(this.browser)}`, {
  786. method: "POST",
  787. });
  788. }
  789. _info(message) {
  790. this._send(
  791. "/info",
  792. JSON.stringify({
  793. browser: this.browser,
  794. message,
  795. })
  796. );
  797. }
  798. _log(message) {
  799. // Using insertAdjacentHTML yields a large performance gain and
  800. // reduces runtime significantly.
  801. if (this.output.insertAdjacentHTML) {
  802. // eslint-disable-next-line no-unsanitized/method
  803. this.output.insertAdjacentHTML("BeforeEnd", message);
  804. } else {
  805. this.output.textContent += message;
  806. }
  807. if (message.lastIndexOf("\n") >= 0 && !this.disableScrolling.checked) {
  808. // Scroll to the bottom of the page
  809. this.output.scrollTop = this.output.scrollHeight;
  810. }
  811. }
  812. _done() {
  813. if (this.inFlightRequests > 0) {
  814. this.inflight.textContent = this.inFlightRequests;
  815. setTimeout(this._done.bind(this), WAITING_TIME);
  816. } else {
  817. setTimeout(this._quit.bind(this), WAITING_TIME);
  818. }
  819. }
  820. _sendResult(snapshot, task, failure) {
  821. const result = JSON.stringify({
  822. browser: this.browser,
  823. id: task.id,
  824. numPages: task.pdfDoc ? task.lastPage || task.pdfDoc.numPages : 0,
  825. lastPageNum: this._getLastPageNumber(task),
  826. failure,
  827. file: task.file,
  828. round: task.round,
  829. page: task.pageNum,
  830. snapshot,
  831. stats: task.stats.times,
  832. viewportWidth: task.viewportWidth,
  833. viewportHeight: task.viewportHeight,
  834. outputScale: task.outputScale,
  835. });
  836. return this._send("/submit_task_results", result);
  837. }
  838. _send(url, message) {
  839. const capability = createPromiseCapability();
  840. this.inflight.textContent = this.inFlightRequests++;
  841. fetch(url, {
  842. method: "POST",
  843. headers: {
  844. "Content-Type": "application/json",
  845. },
  846. body: message,
  847. })
  848. .then(response => {
  849. // Retry until successful.
  850. if (!response.ok || response.status !== 200) {
  851. throw new Error(response.statusText);
  852. }
  853. this.inFlightRequests--;
  854. capability.resolve();
  855. })
  856. .catch(reason => {
  857. console.warn(`Driver._send failed (${url}): ${reason}`);
  858. this.inFlightRequests--;
  859. capability.resolve();
  860. this._send(url, message);
  861. });
  862. return capability.promise;
  863. }
  864. }
  865. export { Driver };