pdf_outline_viewer.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  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. import { BaseTreeViewer } from "./base_tree_viewer.js";
  16. import { createPromiseCapability } from "pdfjs-lib";
  17. import { SidebarView } from "./ui_utils.js";
  18. /**
  19. * @typedef {Object} PDFOutlineViewerOptions
  20. * @property {HTMLDivElement} container - The viewer element.
  21. * @property {EventBus} eventBus - The application event bus.
  22. * @property {IPDFLinkService} linkService - The navigation/linking service.
  23. * @property {DownloadManager} downloadManager - The download manager.
  24. */
  25. /**
  26. * @typedef {Object} PDFOutlineViewerRenderParameters
  27. * @property {Array|null} outline - An array of outline objects.
  28. * @property {PDFDocument} pdfDocument - A {PDFDocument} instance.
  29. */
  30. class PDFOutlineViewer extends BaseTreeViewer {
  31. /**
  32. * @param {PDFOutlineViewerOptions} options
  33. */
  34. constructor(options) {
  35. super(options);
  36. this.linkService = options.linkService;
  37. this.downloadManager = options.downloadManager;
  38. this.eventBus._on("toggleoutlinetree", this._toggleAllTreeItems.bind(this));
  39. this.eventBus._on(
  40. "currentoutlineitem",
  41. this._currentOutlineItem.bind(this)
  42. );
  43. this.eventBus._on("pagechanging", evt => {
  44. this._currentPageNumber = evt.pageNumber;
  45. });
  46. this.eventBus._on("pagesloaded", evt => {
  47. this._isPagesLoaded = !!evt.pagesCount;
  48. // If the capability is still pending, see the `_dispatchEvent`-method,
  49. // we know that the `currentOutlineItem`-button can be enabled here.
  50. if (
  51. this._currentOutlineItemCapability &&
  52. !this._currentOutlineItemCapability.settled
  53. ) {
  54. this._currentOutlineItemCapability.resolve(
  55. /* enabled = */ this._isPagesLoaded
  56. );
  57. }
  58. });
  59. this.eventBus._on("sidebarviewchanged", evt => {
  60. this._sidebarView = evt.view;
  61. });
  62. }
  63. reset() {
  64. super.reset();
  65. this._outline = null;
  66. this._pageNumberToDestHashCapability = null;
  67. this._currentPageNumber = 1;
  68. this._isPagesLoaded = null;
  69. if (
  70. this._currentOutlineItemCapability &&
  71. !this._currentOutlineItemCapability.settled
  72. ) {
  73. this._currentOutlineItemCapability.resolve(/* enabled = */ false);
  74. }
  75. this._currentOutlineItemCapability = null;
  76. }
  77. /**
  78. * @private
  79. */
  80. _dispatchEvent(outlineCount) {
  81. this._currentOutlineItemCapability = createPromiseCapability();
  82. if (
  83. outlineCount === 0 ||
  84. this._pdfDocument?.loadingParams.disableAutoFetch
  85. ) {
  86. this._currentOutlineItemCapability.resolve(/* enabled = */ false);
  87. } else if (this._isPagesLoaded !== null) {
  88. this._currentOutlineItemCapability.resolve(
  89. /* enabled = */ this._isPagesLoaded
  90. );
  91. }
  92. this.eventBus.dispatch("outlineloaded", {
  93. source: this,
  94. outlineCount,
  95. currentOutlineItemPromise: this._currentOutlineItemCapability.promise,
  96. });
  97. }
  98. /**
  99. * @private
  100. */
  101. _bindLink(
  102. element,
  103. { url, newWindow, action, attachment, dest, setOCGState }
  104. ) {
  105. const { linkService } = this;
  106. if (url) {
  107. linkService.addLinkAttributes(element, url, newWindow);
  108. return;
  109. }
  110. if (action) {
  111. element.href = linkService.getAnchorUrl("");
  112. element.onclick = () => {
  113. linkService.executeNamedAction(action);
  114. return false;
  115. };
  116. return;
  117. }
  118. if (attachment) {
  119. element.href = linkService.getAnchorUrl("");
  120. element.onclick = () => {
  121. this.downloadManager.openOrDownloadData(
  122. element,
  123. attachment.content,
  124. attachment.filename
  125. );
  126. return false;
  127. };
  128. return;
  129. }
  130. if (setOCGState) {
  131. element.href = linkService.getAnchorUrl("");
  132. element.onclick = () => {
  133. linkService.executeSetOCGState(setOCGState);
  134. return false;
  135. };
  136. return;
  137. }
  138. element.href = linkService.getDestinationHash(dest);
  139. element.onclick = evt => {
  140. this._updateCurrentTreeItem(evt.target.parentNode);
  141. if (dest) {
  142. linkService.goToDestination(dest);
  143. }
  144. return false;
  145. };
  146. }
  147. /**
  148. * @private
  149. */
  150. _setStyles(element, { bold, italic }) {
  151. if (bold) {
  152. element.style.fontWeight = "bold";
  153. }
  154. if (italic) {
  155. element.style.fontStyle = "italic";
  156. }
  157. }
  158. /**
  159. * @private
  160. */
  161. _addToggleButton(div, { count, items }) {
  162. let hidden = false;
  163. if (count < 0) {
  164. let totalCount = items.length;
  165. if (totalCount > 0) {
  166. const queue = [...items];
  167. while (queue.length > 0) {
  168. const { count: nestedCount, items: nestedItems } = queue.shift();
  169. if (nestedCount > 0 && nestedItems.length > 0) {
  170. totalCount += nestedItems.length;
  171. queue.push(...nestedItems);
  172. }
  173. }
  174. }
  175. if (Math.abs(count) === totalCount) {
  176. hidden = true;
  177. }
  178. }
  179. super._addToggleButton(div, hidden);
  180. }
  181. /**
  182. * @private
  183. */
  184. _toggleAllTreeItems() {
  185. if (!this._outline) {
  186. return;
  187. }
  188. super._toggleAllTreeItems();
  189. }
  190. /**
  191. * @param {PDFOutlineViewerRenderParameters} params
  192. */
  193. render({ outline, pdfDocument }) {
  194. if (this._outline) {
  195. this.reset();
  196. }
  197. this._outline = outline || null;
  198. this._pdfDocument = pdfDocument || null;
  199. if (!outline) {
  200. this._dispatchEvent(/* outlineCount = */ 0);
  201. return;
  202. }
  203. const fragment = document.createDocumentFragment();
  204. const queue = [{ parent: fragment, items: outline }];
  205. let outlineCount = 0,
  206. hasAnyNesting = false;
  207. while (queue.length > 0) {
  208. const levelData = queue.shift();
  209. for (const item of levelData.items) {
  210. const div = document.createElement("div");
  211. div.className = "treeItem";
  212. const element = document.createElement("a");
  213. this._bindLink(element, item);
  214. this._setStyles(element, item);
  215. element.textContent = this._normalizeTextContent(item.title);
  216. div.append(element);
  217. if (item.items.length > 0) {
  218. hasAnyNesting = true;
  219. this._addToggleButton(div, item);
  220. const itemsDiv = document.createElement("div");
  221. itemsDiv.className = "treeItems";
  222. div.append(itemsDiv);
  223. queue.push({ parent: itemsDiv, items: item.items });
  224. }
  225. levelData.parent.append(div);
  226. outlineCount++;
  227. }
  228. }
  229. this._finishRendering(fragment, outlineCount, hasAnyNesting);
  230. }
  231. /**
  232. * Find/highlight the current outline item, corresponding to the active page.
  233. * @private
  234. */
  235. async _currentOutlineItem() {
  236. if (!this._isPagesLoaded) {
  237. throw new Error("_currentOutlineItem: All pages have not been loaded.");
  238. }
  239. if (!this._outline || !this._pdfDocument) {
  240. return;
  241. }
  242. const pageNumberToDestHash = await this._getPageNumberToDestHash(
  243. this._pdfDocument
  244. );
  245. if (!pageNumberToDestHash) {
  246. return;
  247. }
  248. this._updateCurrentTreeItem(/* treeItem = */ null);
  249. if (this._sidebarView !== SidebarView.OUTLINE) {
  250. return; // The outline view is no longer visible, hence do nothing.
  251. }
  252. // When there is no destination on the current page, always check the
  253. // previous ones in (reverse) order.
  254. for (let i = this._currentPageNumber; i > 0; i--) {
  255. const destHash = pageNumberToDestHash.get(i);
  256. if (!destHash) {
  257. continue;
  258. }
  259. const linkElement = this.container.querySelector(`a[href="${destHash}"]`);
  260. if (!linkElement) {
  261. continue;
  262. }
  263. this._scrollToCurrentTreeItem(linkElement.parentNode);
  264. break;
  265. }
  266. }
  267. /**
  268. * To (significantly) simplify the overall implementation, we will only
  269. * consider *one* destination per page when finding/highlighting the current
  270. * outline item (similar to e.g. Adobe Reader); more specifically, we choose
  271. * the *first* outline item at the *lowest* level of the outline tree.
  272. * @private
  273. */
  274. async _getPageNumberToDestHash(pdfDocument) {
  275. if (this._pageNumberToDestHashCapability) {
  276. return this._pageNumberToDestHashCapability.promise;
  277. }
  278. this._pageNumberToDestHashCapability = createPromiseCapability();
  279. const pageNumberToDestHash = new Map(),
  280. pageNumberNesting = new Map();
  281. const queue = [{ nesting: 0, items: this._outline }];
  282. while (queue.length > 0) {
  283. const levelData = queue.shift(),
  284. currentNesting = levelData.nesting;
  285. for (const { dest, items } of levelData.items) {
  286. let explicitDest, pageNumber;
  287. if (typeof dest === "string") {
  288. explicitDest = await pdfDocument.getDestination(dest);
  289. if (pdfDocument !== this._pdfDocument) {
  290. return null; // The document was closed while the data resolved.
  291. }
  292. } else {
  293. explicitDest = dest;
  294. }
  295. if (Array.isArray(explicitDest)) {
  296. const [destRef] = explicitDest;
  297. if (typeof destRef === "object" && destRef !== null) {
  298. pageNumber = this.linkService._cachedPageNumber(destRef);
  299. if (!pageNumber) {
  300. try {
  301. pageNumber = (await pdfDocument.getPageIndex(destRef)) + 1;
  302. if (pdfDocument !== this._pdfDocument) {
  303. return null; // The document was closed while the data resolved.
  304. }
  305. this.linkService.cachePageRef(pageNumber, destRef);
  306. } catch (ex) {
  307. // Invalid page reference, ignore it and continue parsing.
  308. }
  309. }
  310. } else if (Number.isInteger(destRef)) {
  311. pageNumber = destRef + 1;
  312. }
  313. if (
  314. Number.isInteger(pageNumber) &&
  315. (!pageNumberToDestHash.has(pageNumber) ||
  316. currentNesting > pageNumberNesting.get(pageNumber))
  317. ) {
  318. const destHash = this.linkService.getDestinationHash(dest);
  319. pageNumberToDestHash.set(pageNumber, destHash);
  320. pageNumberNesting.set(pageNumber, currentNesting);
  321. }
  322. }
  323. if (items.length > 0) {
  324. queue.push({ nesting: currentNesting + 1, items });
  325. }
  326. }
  327. }
  328. this._pageNumberToDestHashCapability.resolve(
  329. pageNumberToDestHash.size > 0 ? pageNumberToDestHash : null
  330. );
  331. return this._pageNumberToDestHashCapability.promise;
  332. }
  333. }
  334. export { PDFOutlineViewer };