pdf_link_service.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761
  1. /* Copyright 2015 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. /** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
  17. import { parseQueryString, removeNullCharacters } from "./ui_utils.js";
  18. const DEFAULT_LINK_REL = "noopener noreferrer nofollow";
  19. const LinkTarget = {
  20. NONE: 0, // Default value.
  21. SELF: 1,
  22. BLANK: 2,
  23. PARENT: 3,
  24. TOP: 4,
  25. };
  26. /**
  27. * @typedef {Object} ExternalLinkParameters
  28. * @property {string} url - An absolute URL.
  29. * @property {LinkTarget} [target] - The link target. The default value is
  30. * `LinkTarget.NONE`.
  31. * @property {string} [rel] - The link relationship. The default value is
  32. * `DEFAULT_LINK_REL`.
  33. * @property {boolean} [enabled] - Whether the link should be enabled. The
  34. * default value is true.
  35. */
  36. /**
  37. * Adds various attributes (href, title, target, rel) to hyperlinks.
  38. * @param {HTMLAnchorElement} link - The link element.
  39. * @param {ExternalLinkParameters} params
  40. */
  41. function addLinkAttributes(link, { url, target, rel, enabled = true } = {}) {
  42. if (!url || typeof url !== "string") {
  43. throw new Error('A valid "url" parameter must provided.');
  44. }
  45. const urlNullRemoved = removeNullCharacters(url);
  46. if (enabled) {
  47. link.href = link.title = urlNullRemoved;
  48. } else {
  49. link.href = "";
  50. link.title = `Disabled: ${urlNullRemoved}`;
  51. link.onclick = () => {
  52. return false;
  53. };
  54. }
  55. let targetStr = ""; // LinkTarget.NONE
  56. switch (target) {
  57. case LinkTarget.NONE:
  58. break;
  59. case LinkTarget.SELF:
  60. targetStr = "_self";
  61. break;
  62. case LinkTarget.BLANK:
  63. targetStr = "_blank";
  64. break;
  65. case LinkTarget.PARENT:
  66. targetStr = "_parent";
  67. break;
  68. case LinkTarget.TOP:
  69. targetStr = "_top";
  70. break;
  71. }
  72. link.target = targetStr;
  73. link.rel = typeof rel === "string" ? rel : DEFAULT_LINK_REL;
  74. }
  75. /**
  76. * @typedef {Object} PDFLinkServiceOptions
  77. * @property {EventBus} eventBus - The application event bus.
  78. * @property {number} [externalLinkTarget] - Specifies the `target` attribute
  79. * for external links. Must use one of the values from {LinkTarget}.
  80. * Defaults to using no target.
  81. * @property {string} [externalLinkRel] - Specifies the `rel` attribute for
  82. * external links. Defaults to stripping the referrer.
  83. * @property {boolean} [ignoreDestinationZoom] - Ignores the zoom argument,
  84. * thus preserving the current zoom level in the viewer, when navigating
  85. * to internal destinations. The default value is `false`.
  86. */
  87. /**
  88. * Performs navigation functions inside PDF, such as opening specified page,
  89. * or destination.
  90. * @implements {IPDFLinkService}
  91. */
  92. class PDFLinkService {
  93. #pagesRefCache = new Map();
  94. /**
  95. * @param {PDFLinkServiceOptions} options
  96. */
  97. constructor({
  98. eventBus,
  99. externalLinkTarget = null,
  100. externalLinkRel = null,
  101. ignoreDestinationZoom = false,
  102. } = {}) {
  103. this.eventBus = eventBus;
  104. this.externalLinkTarget = externalLinkTarget;
  105. this.externalLinkRel = externalLinkRel;
  106. this.externalLinkEnabled = true;
  107. this._ignoreDestinationZoom = ignoreDestinationZoom;
  108. this.baseUrl = null;
  109. this.pdfDocument = null;
  110. this.pdfViewer = null;
  111. this.pdfHistory = null;
  112. }
  113. setDocument(pdfDocument, baseUrl = null) {
  114. this.baseUrl = baseUrl;
  115. this.pdfDocument = pdfDocument;
  116. this.#pagesRefCache.clear();
  117. }
  118. setViewer(pdfViewer) {
  119. this.pdfViewer = pdfViewer;
  120. }
  121. setHistory(pdfHistory) {
  122. this.pdfHistory = pdfHistory;
  123. }
  124. /**
  125. * @type {number}
  126. */
  127. get pagesCount() {
  128. return this.pdfDocument ? this.pdfDocument.numPages : 0;
  129. }
  130. /**
  131. * @type {number}
  132. */
  133. get page() {
  134. return this.pdfViewer.currentPageNumber;
  135. }
  136. /**
  137. * @param {number} value
  138. */
  139. set page(value) {
  140. this.pdfViewer.currentPageNumber = value;
  141. }
  142. /**
  143. * @type {number}
  144. */
  145. get rotation() {
  146. return this.pdfViewer.pagesRotation;
  147. }
  148. /**
  149. * @param {number} value
  150. */
  151. set rotation(value) {
  152. this.pdfViewer.pagesRotation = value;
  153. }
  154. /**
  155. * @type {boolean}
  156. */
  157. get isInPresentationMode() {
  158. return this.pdfViewer.isInPresentationMode;
  159. }
  160. #goToDestinationHelper(rawDest, namedDest = null, explicitDest) {
  161. // Dest array looks like that: <page-ref> </XYZ|/FitXXX> <args..>
  162. const destRef = explicitDest[0];
  163. let pageNumber;
  164. if (typeof destRef === "object" && destRef !== null) {
  165. pageNumber = this._cachedPageNumber(destRef);
  166. if (!pageNumber) {
  167. // Fetch the page reference if it's not yet available. This could
  168. // only occur during loading, before all pages have been resolved.
  169. this.pdfDocument
  170. .getPageIndex(destRef)
  171. .then(pageIndex => {
  172. this.cachePageRef(pageIndex + 1, destRef);
  173. this.#goToDestinationHelper(rawDest, namedDest, explicitDest);
  174. })
  175. .catch(() => {
  176. console.error(
  177. `PDFLinkService.#goToDestinationHelper: "${destRef}" is not ` +
  178. `a valid page reference, for dest="${rawDest}".`
  179. );
  180. });
  181. return;
  182. }
  183. } else if (Number.isInteger(destRef)) {
  184. pageNumber = destRef + 1;
  185. } else {
  186. console.error(
  187. `PDFLinkService.#goToDestinationHelper: "${destRef}" is not ` +
  188. `a valid destination reference, for dest="${rawDest}".`
  189. );
  190. return;
  191. }
  192. if (!pageNumber || pageNumber < 1 || pageNumber > this.pagesCount) {
  193. console.error(
  194. `PDFLinkService.#goToDestinationHelper: "${pageNumber}" is not ` +
  195. `a valid page number, for dest="${rawDest}".`
  196. );
  197. return;
  198. }
  199. if (this.pdfHistory) {
  200. // Update the browser history before scrolling the new destination into
  201. // view, to be able to accurately capture the current document position.
  202. this.pdfHistory.pushCurrentPosition();
  203. this.pdfHistory.push({ namedDest, explicitDest, pageNumber });
  204. }
  205. this.pdfViewer.scrollPageIntoView({
  206. pageNumber,
  207. destArray: explicitDest,
  208. ignoreDestinationZoom: this._ignoreDestinationZoom,
  209. });
  210. }
  211. /**
  212. * This method will, when available, also update the browser history.
  213. *
  214. * @param {string|Array} dest - The named, or explicit, PDF destination.
  215. */
  216. async goToDestination(dest) {
  217. if (!this.pdfDocument) {
  218. return;
  219. }
  220. let namedDest, explicitDest;
  221. if (typeof dest === "string") {
  222. namedDest = dest;
  223. explicitDest = await this.pdfDocument.getDestination(dest);
  224. } else {
  225. namedDest = null;
  226. explicitDest = await dest;
  227. }
  228. if (!Array.isArray(explicitDest)) {
  229. console.error(
  230. `PDFLinkService.goToDestination: "${explicitDest}" is not ` +
  231. `a valid destination array, for dest="${dest}".`
  232. );
  233. return;
  234. }
  235. this.#goToDestinationHelper(dest, namedDest, explicitDest);
  236. }
  237. /**
  238. * This method will, when available, also update the browser history.
  239. *
  240. * @param {number|string} val - The page number, or page label.
  241. */
  242. goToPage(val) {
  243. if (!this.pdfDocument) {
  244. return;
  245. }
  246. const pageNumber =
  247. (typeof val === "string" && this.pdfViewer.pageLabelToPageNumber(val)) ||
  248. val | 0;
  249. if (
  250. !(
  251. Number.isInteger(pageNumber) &&
  252. pageNumber > 0 &&
  253. pageNumber <= this.pagesCount
  254. )
  255. ) {
  256. console.error(`PDFLinkService.goToPage: "${val}" is not a valid page.`);
  257. return;
  258. }
  259. if (this.pdfHistory) {
  260. // Update the browser history before scrolling the new page into view,
  261. // to be able to accurately capture the current document position.
  262. this.pdfHistory.pushCurrentPosition();
  263. this.pdfHistory.pushPage(pageNumber);
  264. }
  265. this.pdfViewer.scrollPageIntoView({ pageNumber });
  266. }
  267. /**
  268. * Wrapper around the `addLinkAttributes` helper function.
  269. * @param {HTMLAnchorElement} link
  270. * @param {string} url
  271. * @param {boolean} [newWindow]
  272. */
  273. addLinkAttributes(link, url, newWindow = false) {
  274. addLinkAttributes(link, {
  275. url,
  276. target: newWindow ? LinkTarget.BLANK : this.externalLinkTarget,
  277. rel: this.externalLinkRel,
  278. enabled: this.externalLinkEnabled,
  279. });
  280. }
  281. /**
  282. * @param {string|Array} dest - The PDF destination object.
  283. * @returns {string} The hyperlink to the PDF object.
  284. */
  285. getDestinationHash(dest) {
  286. if (typeof dest === "string") {
  287. if (dest.length > 0) {
  288. return this.getAnchorUrl("#" + escape(dest));
  289. }
  290. } else if (Array.isArray(dest)) {
  291. const str = JSON.stringify(dest);
  292. if (str.length > 0) {
  293. return this.getAnchorUrl("#" + escape(str));
  294. }
  295. }
  296. return this.getAnchorUrl("");
  297. }
  298. /**
  299. * Prefix the full url on anchor links to make sure that links are resolved
  300. * relative to the current URL instead of the one defined in <base href>.
  301. * @param {string} anchor - The anchor hash, including the #.
  302. * @returns {string} The hyperlink to the PDF object.
  303. */
  304. getAnchorUrl(anchor) {
  305. return (this.baseUrl || "") + anchor;
  306. }
  307. /**
  308. * @param {string} hash
  309. */
  310. setHash(hash) {
  311. if (!this.pdfDocument) {
  312. return;
  313. }
  314. let pageNumber, dest;
  315. if (hash.includes("=")) {
  316. const params = parseQueryString(hash);
  317. if (params.has("search")) {
  318. this.eventBus.dispatch("findfromurlhash", {
  319. source: this,
  320. query: params.get("search").replace(/"/g, ""),
  321. phraseSearch: params.get("phrase") === "true",
  322. });
  323. }
  324. // borrowing syntax from "Parameters for Opening PDF Files"
  325. if (params.has("page")) {
  326. pageNumber = params.get("page") | 0 || 1;
  327. }
  328. if (params.has("zoom")) {
  329. // Build the destination array.
  330. const zoomArgs = params.get("zoom").split(","); // scale,left,top
  331. const zoomArg = zoomArgs[0];
  332. const zoomArgNumber = parseFloat(zoomArg);
  333. if (!zoomArg.includes("Fit")) {
  334. // If the zoomArg is a number, it has to get divided by 100. If it's
  335. // a string, it should stay as it is.
  336. dest = [
  337. null,
  338. { name: "XYZ" },
  339. zoomArgs.length > 1 ? zoomArgs[1] | 0 : null,
  340. zoomArgs.length > 2 ? zoomArgs[2] | 0 : null,
  341. zoomArgNumber ? zoomArgNumber / 100 : zoomArg,
  342. ];
  343. } else {
  344. if (zoomArg === "Fit" || zoomArg === "FitB") {
  345. dest = [null, { name: zoomArg }];
  346. } else if (
  347. zoomArg === "FitH" ||
  348. zoomArg === "FitBH" ||
  349. zoomArg === "FitV" ||
  350. zoomArg === "FitBV"
  351. ) {
  352. dest = [
  353. null,
  354. { name: zoomArg },
  355. zoomArgs.length > 1 ? zoomArgs[1] | 0 : null,
  356. ];
  357. } else if (zoomArg === "FitR") {
  358. if (zoomArgs.length !== 5) {
  359. console.error(
  360. 'PDFLinkService.setHash: Not enough parameters for "FitR".'
  361. );
  362. } else {
  363. dest = [
  364. null,
  365. { name: zoomArg },
  366. zoomArgs[1] | 0,
  367. zoomArgs[2] | 0,
  368. zoomArgs[3] | 0,
  369. zoomArgs[4] | 0,
  370. ];
  371. }
  372. } else {
  373. console.error(
  374. `PDFLinkService.setHash: "${zoomArg}" is not a valid zoom value.`
  375. );
  376. }
  377. }
  378. }
  379. if (dest) {
  380. this.pdfViewer.scrollPageIntoView({
  381. pageNumber: pageNumber || this.page,
  382. destArray: dest,
  383. allowNegativeOffset: true,
  384. });
  385. } else if (pageNumber) {
  386. this.page = pageNumber; // simple page
  387. }
  388. if (params.has("pagemode")) {
  389. this.eventBus.dispatch("pagemode", {
  390. source: this,
  391. mode: params.get("pagemode"),
  392. });
  393. }
  394. // Ensure that this parameter is *always* handled last, in order to
  395. // guarantee that it won't be overridden (e.g. by the "page" parameter).
  396. if (params.has("nameddest")) {
  397. this.goToDestination(params.get("nameddest"));
  398. }
  399. } else {
  400. // Named (or explicit) destination.
  401. dest = unescape(hash);
  402. try {
  403. dest = JSON.parse(dest);
  404. if (!Array.isArray(dest)) {
  405. // Avoid incorrectly rejecting a valid named destination, such as
  406. // e.g. "4.3" or "true", because `JSON.parse` converted its type.
  407. dest = dest.toString();
  408. }
  409. } catch (ex) {}
  410. if (
  411. typeof dest === "string" ||
  412. PDFLinkService.#isValidExplicitDestination(dest)
  413. ) {
  414. this.goToDestination(dest);
  415. return;
  416. }
  417. console.error(
  418. `PDFLinkService.setHash: "${unescape(
  419. hash
  420. )}" is not a valid destination.`
  421. );
  422. }
  423. }
  424. /**
  425. * @param {string} action
  426. */
  427. executeNamedAction(action) {
  428. // See PDF reference, table 8.45 - Named action
  429. switch (action) {
  430. case "GoBack":
  431. this.pdfHistory?.back();
  432. break;
  433. case "GoForward":
  434. this.pdfHistory?.forward();
  435. break;
  436. case "NextPage":
  437. this.pdfViewer.nextPage();
  438. break;
  439. case "PrevPage":
  440. this.pdfViewer.previousPage();
  441. break;
  442. case "LastPage":
  443. this.page = this.pagesCount;
  444. break;
  445. case "FirstPage":
  446. this.page = 1;
  447. break;
  448. default:
  449. break; // No action according to spec
  450. }
  451. this.eventBus.dispatch("namedaction", {
  452. source: this,
  453. action,
  454. });
  455. }
  456. /**
  457. * @param {Object} action
  458. */
  459. async executeSetOCGState(action) {
  460. const pdfDocument = this.pdfDocument;
  461. const optionalContentConfig = await this.pdfViewer
  462. .optionalContentConfigPromise;
  463. if (pdfDocument !== this.pdfDocument) {
  464. return; // The document was closed while the optional content resolved.
  465. }
  466. let operator;
  467. for (const elem of action.state) {
  468. switch (elem) {
  469. case "ON":
  470. case "OFF":
  471. case "Toggle":
  472. operator = elem;
  473. continue;
  474. }
  475. switch (operator) {
  476. case "ON":
  477. optionalContentConfig.setVisibility(elem, true);
  478. break;
  479. case "OFF":
  480. optionalContentConfig.setVisibility(elem, false);
  481. break;
  482. case "Toggle":
  483. const group = optionalContentConfig.getGroup(elem);
  484. if (group) {
  485. optionalContentConfig.setVisibility(elem, !group.visible);
  486. }
  487. break;
  488. }
  489. }
  490. this.pdfViewer.optionalContentConfigPromise = Promise.resolve(
  491. optionalContentConfig
  492. );
  493. }
  494. /**
  495. * @param {number} pageNum - page number.
  496. * @param {Object} pageRef - reference to the page.
  497. */
  498. cachePageRef(pageNum, pageRef) {
  499. if (!pageRef) {
  500. return;
  501. }
  502. const refStr =
  503. pageRef.gen === 0 ? `${pageRef.num}R` : `${pageRef.num}R${pageRef.gen}`;
  504. this.#pagesRefCache.set(refStr, pageNum);
  505. }
  506. /**
  507. * @ignore
  508. */
  509. _cachedPageNumber(pageRef) {
  510. if (!pageRef) {
  511. return null;
  512. }
  513. const refStr =
  514. pageRef.gen === 0 ? `${pageRef.num}R` : `${pageRef.num}R${pageRef.gen}`;
  515. return this.#pagesRefCache.get(refStr) || null;
  516. }
  517. /**
  518. * @param {number} pageNumber
  519. */
  520. isPageVisible(pageNumber) {
  521. return this.pdfViewer.isPageVisible(pageNumber);
  522. }
  523. /**
  524. * @param {number} pageNumber
  525. */
  526. isPageCached(pageNumber) {
  527. return this.pdfViewer.isPageCached(pageNumber);
  528. }
  529. static #isValidExplicitDestination(dest) {
  530. if (!Array.isArray(dest)) {
  531. return false;
  532. }
  533. const destLength = dest.length;
  534. if (destLength < 2) {
  535. return false;
  536. }
  537. const page = dest[0];
  538. if (
  539. !(
  540. typeof page === "object" &&
  541. Number.isInteger(page.num) &&
  542. Number.isInteger(page.gen)
  543. ) &&
  544. !(Number.isInteger(page) && page >= 0)
  545. ) {
  546. return false;
  547. }
  548. const zoom = dest[1];
  549. if (!(typeof zoom === "object" && typeof zoom.name === "string")) {
  550. return false;
  551. }
  552. let allowNull = true;
  553. switch (zoom.name) {
  554. case "XYZ":
  555. if (destLength !== 5) {
  556. return false;
  557. }
  558. break;
  559. case "Fit":
  560. case "FitB":
  561. return destLength === 2;
  562. case "FitH":
  563. case "FitBH":
  564. case "FitV":
  565. case "FitBV":
  566. if (destLength !== 3) {
  567. return false;
  568. }
  569. break;
  570. case "FitR":
  571. if (destLength !== 6) {
  572. return false;
  573. }
  574. allowNull = false;
  575. break;
  576. default:
  577. return false;
  578. }
  579. for (let i = 2; i < destLength; i++) {
  580. const param = dest[i];
  581. if (!(typeof param === "number" || (allowNull && param === null))) {
  582. return false;
  583. }
  584. }
  585. return true;
  586. }
  587. }
  588. /**
  589. * @implements {IPDFLinkService}
  590. */
  591. class SimpleLinkService {
  592. constructor() {
  593. this.externalLinkEnabled = true;
  594. }
  595. /**
  596. * @type {number}
  597. */
  598. get pagesCount() {
  599. return 0;
  600. }
  601. /**
  602. * @type {number}
  603. */
  604. get page() {
  605. return 0;
  606. }
  607. /**
  608. * @param {number} value
  609. */
  610. set page(value) {}
  611. /**
  612. * @type {number}
  613. */
  614. get rotation() {
  615. return 0;
  616. }
  617. /**
  618. * @param {number} value
  619. */
  620. set rotation(value) {}
  621. /**
  622. * @type {boolean}
  623. */
  624. get isInPresentationMode() {
  625. return false;
  626. }
  627. /**
  628. * @param {string|Array} dest - The named, or explicit, PDF destination.
  629. */
  630. async goToDestination(dest) {}
  631. /**
  632. * @param {number|string} val - The page number, or page label.
  633. */
  634. goToPage(val) {}
  635. /**
  636. * @param {HTMLAnchorElement} link
  637. * @param {string} url
  638. * @param {boolean} [newWindow]
  639. */
  640. addLinkAttributes(link, url, newWindow = false) {
  641. addLinkAttributes(link, { url, enabled: this.externalLinkEnabled });
  642. }
  643. /**
  644. * @param dest - The PDF destination object.
  645. * @returns {string} The hyperlink to the PDF object.
  646. */
  647. getDestinationHash(dest) {
  648. return "#";
  649. }
  650. /**
  651. * @param hash - The PDF parameters/hash.
  652. * @returns {string} The hyperlink to the PDF object.
  653. */
  654. getAnchorUrl(hash) {
  655. return "#";
  656. }
  657. /**
  658. * @param {string} hash
  659. */
  660. setHash(hash) {}
  661. /**
  662. * @param {string} action
  663. */
  664. executeNamedAction(action) {}
  665. /**
  666. * @param {Object} action
  667. */
  668. executeSetOCGState(action) {}
  669. /**
  670. * @param {number} pageNum - page number.
  671. * @param {Object} pageRef - reference to the page.
  672. */
  673. cachePageRef(pageNum, pageRef) {}
  674. /**
  675. * @param {number} pageNumber
  676. */
  677. isPageVisible(pageNumber) {
  678. return true;
  679. }
  680. /**
  681. * @param {number} pageNumber
  682. */
  683. isPageCached(pageNumber) {
  684. return true;
  685. }
  686. }
  687. export { LinkTarget, PDFLinkService, SimpleLinkService };