pdf_scripting_manager.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. /* Copyright 2021 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. /** @typedef {import("./event_utils").EventBus} EventBus */
  16. import { apiPageLayoutToViewerModes, RenderingStates } from "./ui_utils.js";
  17. import { createPromiseCapability, shadow } from "pdfjs-lib";
  18. /**
  19. * @typedef {Object} PDFScriptingManagerOptions
  20. * @property {EventBus} eventBus - The application event bus.
  21. * @property {string} sandboxBundleSrc - The path and filename of the scripting
  22. * bundle.
  23. * @property {Object} [scriptingFactory] - The factory that is used when
  24. * initializing scripting; must contain a `createScripting` method.
  25. * PLEASE NOTE: Primarily intended for the default viewer use-case.
  26. * @property {function} [docPropertiesLookup] - The function that is used to
  27. * lookup the necessary document properties.
  28. */
  29. class PDFScriptingManager {
  30. /**
  31. * @param {PDFScriptingManagerOptions} options
  32. */
  33. constructor({
  34. eventBus,
  35. sandboxBundleSrc = null,
  36. scriptingFactory = null,
  37. docPropertiesLookup = null,
  38. }) {
  39. this._pdfDocument = null;
  40. this._pdfViewer = null;
  41. this._closeCapability = null;
  42. this._destroyCapability = null;
  43. this._scripting = null;
  44. this._ready = false;
  45. this._eventBus = eventBus;
  46. this._sandboxBundleSrc = sandboxBundleSrc;
  47. this._scriptingFactory = scriptingFactory;
  48. this._docPropertiesLookup = docPropertiesLookup;
  49. // The default viewer already handles adding/removing of DOM events,
  50. // hence limit this to only the viewer components.
  51. if (
  52. typeof PDFJSDev !== "undefined" &&
  53. PDFJSDev.test("COMPONENTS") &&
  54. !this._scriptingFactory
  55. ) {
  56. window.addEventListener("updatefromsandbox", event => {
  57. this._eventBus.dispatch("updatefromsandbox", {
  58. source: window,
  59. detail: event.detail,
  60. });
  61. });
  62. }
  63. }
  64. setViewer(pdfViewer) {
  65. this._pdfViewer = pdfViewer;
  66. }
  67. async setDocument(pdfDocument) {
  68. if (this._pdfDocument) {
  69. await this._destroyScripting();
  70. }
  71. this._pdfDocument = pdfDocument;
  72. if (!pdfDocument) {
  73. return;
  74. }
  75. const [objects, calculationOrder, docActions] = await Promise.all([
  76. pdfDocument.getFieldObjects(),
  77. pdfDocument.getCalculationOrderIds(),
  78. pdfDocument.getJSActions(),
  79. ]);
  80. if (!objects && !docActions) {
  81. // No FieldObjects or JavaScript actions were found in the document.
  82. await this._destroyScripting();
  83. return;
  84. }
  85. if (pdfDocument !== this._pdfDocument) {
  86. return; // The document was closed while the data resolved.
  87. }
  88. try {
  89. this._scripting = this._createScripting();
  90. } catch (error) {
  91. console.error(`PDFScriptingManager.setDocument: "${error?.message}".`);
  92. await this._destroyScripting();
  93. return;
  94. }
  95. this._internalEvents.set("updatefromsandbox", event => {
  96. if (event?.source !== window) {
  97. return;
  98. }
  99. this._updateFromSandbox(event.detail);
  100. });
  101. this._internalEvents.set("dispatcheventinsandbox", event => {
  102. this._scripting?.dispatchEventInSandbox(event.detail);
  103. });
  104. this._internalEvents.set("pagechanging", ({ pageNumber, previous }) => {
  105. if (pageNumber === previous) {
  106. return; // The current page didn't change.
  107. }
  108. this._dispatchPageClose(previous);
  109. this._dispatchPageOpen(pageNumber);
  110. });
  111. this._internalEvents.set("pagerendered", ({ pageNumber }) => {
  112. if (!this._pageOpenPending.has(pageNumber)) {
  113. return; // No pending "PageOpen" event for the newly rendered page.
  114. }
  115. if (pageNumber !== this._pdfViewer.currentPageNumber) {
  116. return; // The newly rendered page is no longer the current one.
  117. }
  118. this._dispatchPageOpen(pageNumber);
  119. });
  120. this._internalEvents.set("pagesdestroy", async event => {
  121. await this._dispatchPageClose(this._pdfViewer.currentPageNumber);
  122. await this._scripting?.dispatchEventInSandbox({
  123. id: "doc",
  124. name: "WillClose",
  125. });
  126. this._closeCapability?.resolve();
  127. });
  128. for (const [name, listener] of this._internalEvents) {
  129. this._eventBus._on(name, listener);
  130. }
  131. try {
  132. const docProperties = await this._getDocProperties();
  133. if (pdfDocument !== this._pdfDocument) {
  134. return; // The document was closed while the properties resolved.
  135. }
  136. await this._scripting.createSandbox({
  137. objects,
  138. calculationOrder,
  139. appInfo: {
  140. platform: navigator.platform,
  141. language: navigator.language,
  142. },
  143. docInfo: {
  144. ...docProperties,
  145. actions: docActions,
  146. },
  147. });
  148. this._eventBus.dispatch("sandboxcreated", { source: this });
  149. } catch (error) {
  150. console.error(`PDFScriptingManager.setDocument: "${error?.message}".`);
  151. await this._destroyScripting();
  152. return;
  153. }
  154. await this._scripting?.dispatchEventInSandbox({
  155. id: "doc",
  156. name: "Open",
  157. });
  158. await this._dispatchPageOpen(
  159. this._pdfViewer.currentPageNumber,
  160. /* initialize = */ true
  161. );
  162. // Defer this slightly, to ensure that scripting is *fully* initialized.
  163. Promise.resolve().then(() => {
  164. if (pdfDocument === this._pdfDocument) {
  165. this._ready = true;
  166. }
  167. });
  168. }
  169. async dispatchWillSave(detail) {
  170. return this._scripting?.dispatchEventInSandbox({
  171. id: "doc",
  172. name: "WillSave",
  173. });
  174. }
  175. async dispatchDidSave(detail) {
  176. return this._scripting?.dispatchEventInSandbox({
  177. id: "doc",
  178. name: "DidSave",
  179. });
  180. }
  181. async dispatchWillPrint(detail) {
  182. return this._scripting?.dispatchEventInSandbox({
  183. id: "doc",
  184. name: "WillPrint",
  185. });
  186. }
  187. async dispatchDidPrint(detail) {
  188. return this._scripting?.dispatchEventInSandbox({
  189. id: "doc",
  190. name: "DidPrint",
  191. });
  192. }
  193. get destroyPromise() {
  194. return this._destroyCapability?.promise || null;
  195. }
  196. get ready() {
  197. return this._ready;
  198. }
  199. /**
  200. * @private
  201. */
  202. get _internalEvents() {
  203. return shadow(this, "_internalEvents", new Map());
  204. }
  205. /**
  206. * @private
  207. */
  208. get _pageOpenPending() {
  209. return shadow(this, "_pageOpenPending", new Set());
  210. }
  211. /**
  212. * @private
  213. */
  214. get _visitedPages() {
  215. return shadow(this, "_visitedPages", new Map());
  216. }
  217. /**
  218. * @private
  219. */
  220. async _updateFromSandbox(detail) {
  221. // Ignore some events, see below, that don't make sense in PresentationMode.
  222. const isInPresentationMode =
  223. this._pdfViewer.isInPresentationMode ||
  224. this._pdfViewer.isChangingPresentationMode;
  225. const { id, siblings, command, value } = detail;
  226. if (!id) {
  227. switch (command) {
  228. case "clear":
  229. console.clear();
  230. break;
  231. case "error":
  232. console.error(value);
  233. break;
  234. case "layout": {
  235. // NOTE: Always ignore the pageLayout in GeckoView since there's
  236. // no UI available to change Scroll/Spread modes for the user.
  237. if (
  238. (typeof PDFJSDev !== "undefined" && PDFJSDev.test("GECKOVIEW")) ||
  239. isInPresentationMode
  240. ) {
  241. return;
  242. }
  243. const modes = apiPageLayoutToViewerModes(value);
  244. this._pdfViewer.spreadMode = modes.spreadMode;
  245. break;
  246. }
  247. case "page-num":
  248. this._pdfViewer.currentPageNumber = value + 1;
  249. break;
  250. case "print":
  251. await this._pdfViewer.pagesPromise;
  252. this._eventBus.dispatch("print", { source: this });
  253. break;
  254. case "println":
  255. console.log(value);
  256. break;
  257. case "zoom":
  258. if (isInPresentationMode) {
  259. return;
  260. }
  261. this._pdfViewer.currentScaleValue = value;
  262. break;
  263. case "SaveAs":
  264. this._eventBus.dispatch("download", { source: this });
  265. break;
  266. case "FirstPage":
  267. this._pdfViewer.currentPageNumber = 1;
  268. break;
  269. case "LastPage":
  270. this._pdfViewer.currentPageNumber = this._pdfViewer.pagesCount;
  271. break;
  272. case "NextPage":
  273. this._pdfViewer.nextPage();
  274. break;
  275. case "PrevPage":
  276. this._pdfViewer.previousPage();
  277. break;
  278. case "ZoomViewIn":
  279. if (isInPresentationMode) {
  280. return;
  281. }
  282. this._pdfViewer.increaseScale();
  283. break;
  284. case "ZoomViewOut":
  285. if (isInPresentationMode) {
  286. return;
  287. }
  288. this._pdfViewer.decreaseScale();
  289. break;
  290. }
  291. return;
  292. }
  293. if (isInPresentationMode) {
  294. if (detail.focus) {
  295. return;
  296. }
  297. }
  298. delete detail.id;
  299. delete detail.siblings;
  300. const ids = siblings ? [id, ...siblings] : [id];
  301. for (const elementId of ids) {
  302. const element = document.querySelector(
  303. `[data-element-id="${elementId}"]`
  304. );
  305. if (element) {
  306. element.dispatchEvent(new CustomEvent("updatefromsandbox", { detail }));
  307. } else {
  308. // The element hasn't been rendered yet, use the AnnotationStorage.
  309. this._pdfDocument?.annotationStorage.setValue(elementId, detail);
  310. }
  311. }
  312. }
  313. /**
  314. * @private
  315. */
  316. async _dispatchPageOpen(pageNumber, initialize = false) {
  317. const pdfDocument = this._pdfDocument,
  318. visitedPages = this._visitedPages;
  319. if (initialize) {
  320. this._closeCapability = createPromiseCapability();
  321. }
  322. if (!this._closeCapability) {
  323. return; // Scripting isn't fully initialized yet.
  324. }
  325. const pageView = this._pdfViewer.getPageView(/* index = */ pageNumber - 1);
  326. if (pageView?.renderingState !== RenderingStates.FINISHED) {
  327. this._pageOpenPending.add(pageNumber);
  328. return; // Wait for the page to finish rendering.
  329. }
  330. this._pageOpenPending.delete(pageNumber);
  331. const actionsPromise = (async () => {
  332. // Avoid sending, and thus serializing, the `actions` data more than once.
  333. const actions = await (!visitedPages.has(pageNumber)
  334. ? pageView.pdfPage?.getJSActions()
  335. : null);
  336. if (pdfDocument !== this._pdfDocument) {
  337. return; // The document was closed while the actions resolved.
  338. }
  339. await this._scripting?.dispatchEventInSandbox({
  340. id: "page",
  341. name: "PageOpen",
  342. pageNumber,
  343. actions,
  344. });
  345. })();
  346. visitedPages.set(pageNumber, actionsPromise);
  347. }
  348. /**
  349. * @private
  350. */
  351. async _dispatchPageClose(pageNumber) {
  352. const pdfDocument = this._pdfDocument,
  353. visitedPages = this._visitedPages;
  354. if (!this._closeCapability) {
  355. return; // Scripting isn't fully initialized yet.
  356. }
  357. if (this._pageOpenPending.has(pageNumber)) {
  358. return; // The page is still rendering; no "PageOpen" event dispatched.
  359. }
  360. const actionsPromise = visitedPages.get(pageNumber);
  361. if (!actionsPromise) {
  362. return; // The "PageClose" event must be preceded by a "PageOpen" event.
  363. }
  364. visitedPages.set(pageNumber, null);
  365. // Ensure that the "PageOpen" event is dispatched first.
  366. await actionsPromise;
  367. if (pdfDocument !== this._pdfDocument) {
  368. return; // The document was closed while the actions resolved.
  369. }
  370. await this._scripting?.dispatchEventInSandbox({
  371. id: "page",
  372. name: "PageClose",
  373. pageNumber,
  374. });
  375. }
  376. /**
  377. * @returns {Promise<Object>} A promise that is resolved with an {Object}
  378. * containing the necessary document properties; please find the expected
  379. * format in `PDFViewerApplication._scriptingDocProperties`.
  380. * @private
  381. */
  382. async _getDocProperties() {
  383. if (this._docPropertiesLookup) {
  384. return this._docPropertiesLookup(this._pdfDocument);
  385. }
  386. if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("COMPONENTS")) {
  387. const { docPropertiesLookup } = require("./generic_scripting.js");
  388. return docPropertiesLookup(this._pdfDocument);
  389. }
  390. throw new Error("_getDocProperties: Unable to lookup properties.");
  391. }
  392. /**
  393. * @private
  394. */
  395. _createScripting() {
  396. this._destroyCapability = createPromiseCapability();
  397. if (this._scripting) {
  398. throw new Error("_createScripting: Scripting already exists.");
  399. }
  400. if (this._scriptingFactory) {
  401. return this._scriptingFactory.createScripting({
  402. sandboxBundleSrc: this._sandboxBundleSrc,
  403. });
  404. }
  405. if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("COMPONENTS")) {
  406. const { GenericScripting } = require("./generic_scripting.js");
  407. return new GenericScripting(this._sandboxBundleSrc);
  408. }
  409. throw new Error("_createScripting: Cannot create scripting.");
  410. }
  411. /**
  412. * @private
  413. */
  414. async _destroyScripting() {
  415. if (!this._scripting) {
  416. this._pdfDocument = null;
  417. this._destroyCapability?.resolve();
  418. return;
  419. }
  420. if (this._closeCapability) {
  421. await Promise.race([
  422. this._closeCapability.promise,
  423. new Promise(resolve => {
  424. // Avoid the scripting/sandbox-destruction hanging indefinitely.
  425. setTimeout(resolve, 1000);
  426. }),
  427. ]).catch(reason => {
  428. // Ignore any errors, to ensure that the sandbox is always destroyed.
  429. });
  430. this._closeCapability = null;
  431. }
  432. this._pdfDocument = null;
  433. try {
  434. await this._scripting.destroySandbox();
  435. } catch (ex) {}
  436. for (const [name, listener] of this._internalEvents) {
  437. this._eventBus._off(name, listener);
  438. }
  439. this._internalEvents.clear();
  440. this._pageOpenPending.clear();
  441. this._visitedPages.clear();
  442. this._scripting = null;
  443. this._ready = false;
  444. this._destroyCapability?.resolve();
  445. }
  446. }
  447. export { PDFScriptingManager };