pdf_print_service.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. /* Copyright 2016 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. import { AnnotationMode, PixelsPerInch } from "pdfjs-lib";
  16. import { PDFPrintServiceFactory, PDFViewerApplication } from "./app.js";
  17. import { getXfaHtmlForPrinting } from "./print_utils.js";
  18. let activeService = null;
  19. let dialog = null;
  20. let overlayManager = null;
  21. // Renders the page to the canvas of the given print service, and returns
  22. // the suggested dimensions of the output page.
  23. function renderPage(
  24. activeServiceOnEntry,
  25. pdfDocument,
  26. pageNumber,
  27. size,
  28. printResolution,
  29. optionalContentConfigPromise,
  30. printAnnotationStoragePromise
  31. ) {
  32. const scratchCanvas = activeService.scratchCanvas;
  33. // The size of the canvas in pixels for printing.
  34. const PRINT_UNITS = printResolution / PixelsPerInch.PDF;
  35. scratchCanvas.width = Math.floor(size.width * PRINT_UNITS);
  36. scratchCanvas.height = Math.floor(size.height * PRINT_UNITS);
  37. const ctx = scratchCanvas.getContext("2d");
  38. ctx.save();
  39. ctx.fillStyle = "rgb(255, 255, 255)";
  40. ctx.fillRect(0, 0, scratchCanvas.width, scratchCanvas.height);
  41. ctx.restore();
  42. return Promise.all([
  43. pdfDocument.getPage(pageNumber),
  44. printAnnotationStoragePromise,
  45. ]).then(function ([pdfPage, printAnnotationStorage]) {
  46. const renderContext = {
  47. canvasContext: ctx,
  48. transform: [PRINT_UNITS, 0, 0, PRINT_UNITS, 0, 0],
  49. viewport: pdfPage.getViewport({ scale: 1, rotation: size.rotation }),
  50. intent: "print",
  51. annotationMode: AnnotationMode.ENABLE_STORAGE,
  52. optionalContentConfigPromise,
  53. printAnnotationStorage,
  54. };
  55. return pdfPage.render(renderContext).promise;
  56. });
  57. }
  58. function PDFPrintService(
  59. pdfDocument,
  60. pagesOverview,
  61. printContainer,
  62. printResolution,
  63. optionalContentConfigPromise = null,
  64. printAnnotationStoragePromise = null,
  65. l10n
  66. ) {
  67. this.pdfDocument = pdfDocument;
  68. this.pagesOverview = pagesOverview;
  69. this.printContainer = printContainer;
  70. this._printResolution = printResolution || 150;
  71. this._optionalContentConfigPromise =
  72. optionalContentConfigPromise || pdfDocument.getOptionalContentConfig();
  73. this._printAnnotationStoragePromise =
  74. printAnnotationStoragePromise || Promise.resolve();
  75. this.l10n = l10n;
  76. this.currentPage = -1;
  77. // The temporary canvas where renderPage paints one page at a time.
  78. this.scratchCanvas = document.createElement("canvas");
  79. }
  80. PDFPrintService.prototype = {
  81. layout() {
  82. this.throwIfInactive();
  83. const body = document.querySelector("body");
  84. body.setAttribute("data-pdfjsprinting", true);
  85. const hasEqualPageSizes = this.pagesOverview.every(function (size) {
  86. return (
  87. size.width === this.pagesOverview[0].width &&
  88. size.height === this.pagesOverview[0].height
  89. );
  90. }, this);
  91. if (!hasEqualPageSizes) {
  92. console.warn(
  93. "Not all pages have the same size. The printed " +
  94. "result may be incorrect!"
  95. );
  96. }
  97. // Insert a @page + size rule to make sure that the page size is correctly
  98. // set. Note that we assume that all pages have the same size, because
  99. // variable-size pages are not supported yet (e.g. in Chrome & Firefox).
  100. // TODO(robwu): Use named pages when size calculation bugs get resolved
  101. // (e.g. https://crbug.com/355116) AND when support for named pages is
  102. // added (http://www.w3.org/TR/css3-page/#using-named-pages).
  103. // In browsers where @page + size is not supported (such as Firefox,
  104. // https://bugzil.la/851441), the next stylesheet will be ignored and the
  105. // user has to select the correct paper size in the UI if wanted.
  106. this.pageStyleSheet = document.createElement("style");
  107. const pageSize = this.pagesOverview[0];
  108. this.pageStyleSheet.textContent =
  109. "@page { size: " + pageSize.width + "pt " + pageSize.height + "pt;}";
  110. body.append(this.pageStyleSheet);
  111. },
  112. destroy() {
  113. if (activeService !== this) {
  114. // |activeService| cannot be replaced without calling destroy() first,
  115. // so if it differs then an external consumer has a stale reference to us.
  116. return;
  117. }
  118. this.printContainer.textContent = "";
  119. const body = document.querySelector("body");
  120. body.removeAttribute("data-pdfjsprinting");
  121. if (this.pageStyleSheet) {
  122. this.pageStyleSheet.remove();
  123. this.pageStyleSheet = null;
  124. }
  125. this.scratchCanvas.width = this.scratchCanvas.height = 0;
  126. this.scratchCanvas = null;
  127. activeService = null;
  128. ensureOverlay().then(function () {
  129. if (overlayManager.active === dialog) {
  130. overlayManager.close(dialog);
  131. }
  132. });
  133. },
  134. renderPages() {
  135. if (this.pdfDocument.isPureXfa) {
  136. getXfaHtmlForPrinting(this.printContainer, this.pdfDocument);
  137. return Promise.resolve();
  138. }
  139. const pageCount = this.pagesOverview.length;
  140. const renderNextPage = (resolve, reject) => {
  141. this.throwIfInactive();
  142. if (++this.currentPage >= pageCount) {
  143. renderProgress(pageCount, pageCount, this.l10n);
  144. resolve();
  145. return;
  146. }
  147. const index = this.currentPage;
  148. renderProgress(index, pageCount, this.l10n);
  149. renderPage(
  150. this,
  151. this.pdfDocument,
  152. /* pageNumber = */ index + 1,
  153. this.pagesOverview[index],
  154. this._printResolution,
  155. this._optionalContentConfigPromise,
  156. this._printAnnotationStoragePromise
  157. )
  158. .then(this.useRenderedPage.bind(this))
  159. .then(function () {
  160. renderNextPage(resolve, reject);
  161. }, reject);
  162. };
  163. return new Promise(renderNextPage);
  164. },
  165. useRenderedPage() {
  166. this.throwIfInactive();
  167. const img = document.createElement("img");
  168. const scratchCanvas = this.scratchCanvas;
  169. if ("toBlob" in scratchCanvas) {
  170. scratchCanvas.toBlob(function (blob) {
  171. img.src = URL.createObjectURL(blob);
  172. });
  173. } else {
  174. img.src = scratchCanvas.toDataURL();
  175. }
  176. const wrapper = document.createElement("div");
  177. wrapper.className = "printedPage";
  178. wrapper.append(img);
  179. this.printContainer.append(wrapper);
  180. return new Promise(function (resolve, reject) {
  181. img.onload = resolve;
  182. img.onerror = reject;
  183. });
  184. },
  185. performPrint() {
  186. this.throwIfInactive();
  187. return new Promise(resolve => {
  188. // Push window.print in the macrotask queue to avoid being affected by
  189. // the deprecation of running print() code in a microtask, see
  190. // https://github.com/mozilla/pdf.js/issues/7547.
  191. setTimeout(() => {
  192. if (!this.active) {
  193. resolve();
  194. return;
  195. }
  196. print.call(window);
  197. // Delay promise resolution in case print() was not synchronous.
  198. setTimeout(resolve, 20); // Tidy-up.
  199. }, 0);
  200. });
  201. },
  202. get active() {
  203. return this === activeService;
  204. },
  205. throwIfInactive() {
  206. if (!this.active) {
  207. throw new Error("This print request was cancelled or completed.");
  208. }
  209. },
  210. };
  211. const print = window.print;
  212. window.print = function () {
  213. if (activeService) {
  214. console.warn("Ignored window.print() because of a pending print job.");
  215. return;
  216. }
  217. ensureOverlay().then(function () {
  218. if (activeService) {
  219. overlayManager.open(dialog);
  220. }
  221. });
  222. try {
  223. dispatchEvent("beforeprint");
  224. } finally {
  225. if (!activeService) {
  226. console.error("Expected print service to be initialized.");
  227. ensureOverlay().then(function () {
  228. if (overlayManager.active === dialog) {
  229. overlayManager.close(dialog);
  230. }
  231. });
  232. return; // eslint-disable-line no-unsafe-finally
  233. }
  234. const activeServiceOnEntry = activeService;
  235. activeService
  236. .renderPages()
  237. .then(function () {
  238. return activeServiceOnEntry.performPrint();
  239. })
  240. .catch(function () {
  241. // Ignore any error messages.
  242. })
  243. .then(function () {
  244. // aborts acts on the "active" print request, so we need to check
  245. // whether the print request (activeServiceOnEntry) is still active.
  246. // Without the check, an unrelated print request (created after aborting
  247. // this print request while the pages were being generated) would be
  248. // aborted.
  249. if (activeServiceOnEntry.active) {
  250. abort();
  251. }
  252. });
  253. }
  254. };
  255. function dispatchEvent(eventType) {
  256. const event = document.createEvent("CustomEvent");
  257. event.initCustomEvent(eventType, false, false, "custom");
  258. window.dispatchEvent(event);
  259. }
  260. function abort() {
  261. if (activeService) {
  262. activeService.destroy();
  263. dispatchEvent("afterprint");
  264. }
  265. }
  266. function renderProgress(index, total, l10n) {
  267. dialog ||= document.getElementById("printServiceDialog");
  268. const progress = Math.round((100 * index) / total);
  269. const progressBar = dialog.querySelector("progress");
  270. const progressPerc = dialog.querySelector(".relative-progress");
  271. progressBar.value = progress;
  272. l10n.get("print_progress_percent", { progress }).then(msg => {
  273. progressPerc.textContent = msg;
  274. });
  275. }
  276. window.addEventListener(
  277. "keydown",
  278. function (event) {
  279. // Intercept Cmd/Ctrl + P in all browsers.
  280. // Also intercept Cmd/Ctrl + Shift + P in Chrome and Opera
  281. if (
  282. event.keyCode === /* P= */ 80 &&
  283. (event.ctrlKey || event.metaKey) &&
  284. !event.altKey &&
  285. (!event.shiftKey || window.chrome || window.opera)
  286. ) {
  287. window.print();
  288. event.preventDefault();
  289. event.stopImmediatePropagation();
  290. }
  291. },
  292. true
  293. );
  294. if ("onbeforeprint" in window) {
  295. // Do not propagate before/afterprint events when they are not triggered
  296. // from within this polyfill. (FF / Chrome 63+).
  297. const stopPropagationIfNeeded = function (event) {
  298. if (event.detail !== "custom") {
  299. event.stopImmediatePropagation();
  300. }
  301. };
  302. window.addEventListener("beforeprint", stopPropagationIfNeeded);
  303. window.addEventListener("afterprint", stopPropagationIfNeeded);
  304. }
  305. let overlayPromise;
  306. function ensureOverlay() {
  307. if (!overlayPromise) {
  308. overlayManager = PDFViewerApplication.overlayManager;
  309. if (!overlayManager) {
  310. throw new Error("The overlay manager has not yet been initialized.");
  311. }
  312. dialog ||= document.getElementById("printServiceDialog");
  313. overlayPromise = overlayManager.register(
  314. dialog,
  315. /* canForceClose = */ true
  316. );
  317. document.getElementById("printCancel").onclick = abort;
  318. dialog.addEventListener("close", abort);
  319. }
  320. return overlayPromise;
  321. }
  322. PDFPrintServiceFactory.instance = {
  323. supportsPrinting: true,
  324. createPrintService(
  325. pdfDocument,
  326. pagesOverview,
  327. printContainer,
  328. printResolution,
  329. optionalContentConfigPromise,
  330. printAnnotationStoragePromise,
  331. l10n
  332. ) {
  333. if (activeService) {
  334. throw new Error("The print service is created and active.");
  335. }
  336. activeService = new PDFPrintService(
  337. pdfDocument,
  338. pagesOverview,
  339. printContainer,
  340. printResolution,
  341. optionalContentConfigPromise,
  342. printAnnotationStoragePromise,
  343. l10n
  344. );
  345. return activeService;
  346. },
  347. };
  348. export { PDFPrintService };