| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761 | /* Copyright 2015 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * *     http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *//** @typedef {import("./event_utils").EventBus} EventBus *//** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */import { parseQueryString, removeNullCharacters } from "./ui_utils.js";const DEFAULT_LINK_REL = "noopener noreferrer nofollow";const LinkTarget = {  NONE: 0, // Default value.  SELF: 1,  BLANK: 2,  PARENT: 3,  TOP: 4,};/** * @typedef {Object} ExternalLinkParameters * @property {string} url - An absolute URL. * @property {LinkTarget} [target] - The link target. The default value is *   `LinkTarget.NONE`. * @property {string} [rel] - The link relationship. The default value is *   `DEFAULT_LINK_REL`. * @property {boolean} [enabled] - Whether the link should be enabled. The *   default value is true. *//** * Adds various attributes (href, title, target, rel) to hyperlinks. * @param {HTMLAnchorElement} link - The link element. * @param {ExternalLinkParameters} params */function addLinkAttributes(link, { url, target, rel, enabled = true } = {}) {  if (!url || typeof url !== "string") {    throw new Error('A valid "url" parameter must provided.');  }  const urlNullRemoved = removeNullCharacters(url);  if (enabled) {    link.href = link.title = urlNullRemoved;  } else {    link.href = "";    link.title = `Disabled: ${urlNullRemoved}`;    link.onclick = () => {      return false;    };  }  let targetStr = ""; // LinkTarget.NONE  switch (target) {    case LinkTarget.NONE:      break;    case LinkTarget.SELF:      targetStr = "_self";      break;    case LinkTarget.BLANK:      targetStr = "_blank";      break;    case LinkTarget.PARENT:      targetStr = "_parent";      break;    case LinkTarget.TOP:      targetStr = "_top";      break;  }  link.target = targetStr;  link.rel = typeof rel === "string" ? rel : DEFAULT_LINK_REL;}/** * @typedef {Object} PDFLinkServiceOptions * @property {EventBus} eventBus - The application event bus. * @property {number} [externalLinkTarget] - Specifies the `target` attribute *   for external links. Must use one of the values from {LinkTarget}. *   Defaults to using no target. * @property {string} [externalLinkRel] - Specifies the `rel` attribute for *   external links. Defaults to stripping the referrer. * @property {boolean} [ignoreDestinationZoom] - Ignores the zoom argument, *   thus preserving the current zoom level in the viewer, when navigating *   to internal destinations. The default value is `false`. *//** * Performs navigation functions inside PDF, such as opening specified page, * or destination. * @implements {IPDFLinkService} */class PDFLinkService {  #pagesRefCache = new Map();  /**   * @param {PDFLinkServiceOptions} options   */  constructor({    eventBus,    externalLinkTarget = null,    externalLinkRel = null,    ignoreDestinationZoom = false,  } = {}) {    this.eventBus = eventBus;    this.externalLinkTarget = externalLinkTarget;    this.externalLinkRel = externalLinkRel;    this.externalLinkEnabled = true;    this._ignoreDestinationZoom = ignoreDestinationZoom;    this.baseUrl = null;    this.pdfDocument = null;    this.pdfViewer = null;    this.pdfHistory = null;  }  setDocument(pdfDocument, baseUrl = null) {    this.baseUrl = baseUrl;    this.pdfDocument = pdfDocument;    this.#pagesRefCache.clear();  }  setViewer(pdfViewer) {    this.pdfViewer = pdfViewer;  }  setHistory(pdfHistory) {    this.pdfHistory = pdfHistory;  }  /**   * @type {number}   */  get pagesCount() {    return this.pdfDocument ? this.pdfDocument.numPages : 0;  }  /**   * @type {number}   */  get page() {    return this.pdfViewer.currentPageNumber;  }  /**   * @param {number} value   */  set page(value) {    this.pdfViewer.currentPageNumber = value;  }  /**   * @type {number}   */  get rotation() {    return this.pdfViewer.pagesRotation;  }  /**   * @param {number} value   */  set rotation(value) {    this.pdfViewer.pagesRotation = value;  }  /**   * @type {boolean}   */  get isInPresentationMode() {    return this.pdfViewer.isInPresentationMode;  }  #goToDestinationHelper(rawDest, namedDest = null, explicitDest) {    // Dest array looks like that: <page-ref> </XYZ|/FitXXX> <args..>    const destRef = explicitDest[0];    let pageNumber;    if (typeof destRef === "object" && destRef !== null) {      pageNumber = this._cachedPageNumber(destRef);      if (!pageNumber) {        // Fetch the page reference if it's not yet available. This could        // only occur during loading, before all pages have been resolved.        this.pdfDocument          .getPageIndex(destRef)          .then(pageIndex => {            this.cachePageRef(pageIndex + 1, destRef);            this.#goToDestinationHelper(rawDest, namedDest, explicitDest);          })          .catch(() => {            console.error(              `PDFLinkService.#goToDestinationHelper: "${destRef}" is not ` +                `a valid page reference, for dest="${rawDest}".`            );          });        return;      }    } else if (Number.isInteger(destRef)) {      pageNumber = destRef + 1;    } else {      console.error(        `PDFLinkService.#goToDestinationHelper: "${destRef}" is not ` +          `a valid destination reference, for dest="${rawDest}".`      );      return;    }    if (!pageNumber || pageNumber < 1 || pageNumber > this.pagesCount) {      console.error(        `PDFLinkService.#goToDestinationHelper: "${pageNumber}" is not ` +          `a valid page number, for dest="${rawDest}".`      );      return;    }    if (this.pdfHistory) {      // Update the browser history before scrolling the new destination into      // view, to be able to accurately capture the current document position.      this.pdfHistory.pushCurrentPosition();      this.pdfHistory.push({ namedDest, explicitDest, pageNumber });    }    this.pdfViewer.scrollPageIntoView({      pageNumber,      destArray: explicitDest,      ignoreDestinationZoom: this._ignoreDestinationZoom,    });  }  /**   * This method will, when available, also update the browser history.   *   * @param {string|Array} dest - The named, or explicit, PDF destination.   */  async goToDestination(dest) {    if (!this.pdfDocument) {      return;    }    let namedDest, explicitDest;    if (typeof dest === "string") {      namedDest = dest;      explicitDest = await this.pdfDocument.getDestination(dest);    } else {      namedDest = null;      explicitDest = await dest;    }    if (!Array.isArray(explicitDest)) {      console.error(        `PDFLinkService.goToDestination: "${explicitDest}" is not ` +          `a valid destination array, for dest="${dest}".`      );      return;    }    this.#goToDestinationHelper(dest, namedDest, explicitDest);  }  /**   * This method will, when available, also update the browser history.   *   * @param {number|string} val - The page number, or page label.   */  goToPage(val) {    if (!this.pdfDocument) {      return;    }    const pageNumber =      (typeof val === "string" && this.pdfViewer.pageLabelToPageNumber(val)) ||      val | 0;    if (      !(        Number.isInteger(pageNumber) &&        pageNumber > 0 &&        pageNumber <= this.pagesCount      )    ) {      console.error(`PDFLinkService.goToPage: "${val}" is not a valid page.`);      return;    }    if (this.pdfHistory) {      // Update the browser history before scrolling the new page into view,      // to be able to accurately capture the current document position.      this.pdfHistory.pushCurrentPosition();      this.pdfHistory.pushPage(pageNumber);    }    this.pdfViewer.scrollPageIntoView({ pageNumber });  }  /**   * Wrapper around the `addLinkAttributes` helper function.   * @param {HTMLAnchorElement} link   * @param {string} url   * @param {boolean} [newWindow]   */  addLinkAttributes(link, url, newWindow = false) {    addLinkAttributes(link, {      url,      target: newWindow ? LinkTarget.BLANK : this.externalLinkTarget,      rel: this.externalLinkRel,      enabled: this.externalLinkEnabled,    });  }  /**   * @param {string|Array} dest - The PDF destination object.   * @returns {string} The hyperlink to the PDF object.   */  getDestinationHash(dest) {    if (typeof dest === "string") {      if (dest.length > 0) {        return this.getAnchorUrl("#" + escape(dest));      }    } else if (Array.isArray(dest)) {      const str = JSON.stringify(dest);      if (str.length > 0) {        return this.getAnchorUrl("#" + escape(str));      }    }    return this.getAnchorUrl("");  }  /**   * Prefix the full url on anchor links to make sure that links are resolved   * relative to the current URL instead of the one defined in <base href>.   * @param {string} anchor - The anchor hash, including the #.   * @returns {string} The hyperlink to the PDF object.   */  getAnchorUrl(anchor) {    return (this.baseUrl || "") + anchor;  }  /**   * @param {string} hash   */  setHash(hash) {    if (!this.pdfDocument) {      return;    }    let pageNumber, dest;    if (hash.includes("=")) {      const params = parseQueryString(hash);      if (params.has("search")) {        this.eventBus.dispatch("findfromurlhash", {          source: this,          query: params.get("search").replace(/"/g, ""),          phraseSearch: params.get("phrase") === "true",        });      }      // borrowing syntax from "Parameters for Opening PDF Files"      if (params.has("page")) {        pageNumber = params.get("page") | 0 || 1;      }      if (params.has("zoom")) {        // Build the destination array.        const zoomArgs = params.get("zoom").split(","); // scale,left,top        const zoomArg = zoomArgs[0];        const zoomArgNumber = parseFloat(zoomArg);        if (!zoomArg.includes("Fit")) {          // If the zoomArg is a number, it has to get divided by 100. If it's          // a string, it should stay as it is.          dest = [            null,            { name: "XYZ" },            zoomArgs.length > 1 ? zoomArgs[1] | 0 : null,            zoomArgs.length > 2 ? zoomArgs[2] | 0 : null,            zoomArgNumber ? zoomArgNumber / 100 : zoomArg,          ];        } else {          if (zoomArg === "Fit" || zoomArg === "FitB") {            dest = [null, { name: zoomArg }];          } else if (            zoomArg === "FitH" ||            zoomArg === "FitBH" ||            zoomArg === "FitV" ||            zoomArg === "FitBV"          ) {            dest = [              null,              { name: zoomArg },              zoomArgs.length > 1 ? zoomArgs[1] | 0 : null,            ];          } else if (zoomArg === "FitR") {            if (zoomArgs.length !== 5) {              console.error(                'PDFLinkService.setHash: Not enough parameters for "FitR".'              );            } else {              dest = [                null,                { name: zoomArg },                zoomArgs[1] | 0,                zoomArgs[2] | 0,                zoomArgs[3] | 0,                zoomArgs[4] | 0,              ];            }          } else {            console.error(              `PDFLinkService.setHash: "${zoomArg}" is not a valid zoom value.`            );          }        }      }      if (dest) {        this.pdfViewer.scrollPageIntoView({          pageNumber: pageNumber || this.page,          destArray: dest,          allowNegativeOffset: true,        });      } else if (pageNumber) {        this.page = pageNumber; // simple page      }      if (params.has("pagemode")) {        this.eventBus.dispatch("pagemode", {          source: this,          mode: params.get("pagemode"),        });      }      // Ensure that this parameter is *always* handled last, in order to      // guarantee that it won't be overridden (e.g. by the "page" parameter).      if (params.has("nameddest")) {        this.goToDestination(params.get("nameddest"));      }    } else {      // Named (or explicit) destination.      dest = unescape(hash);      try {        dest = JSON.parse(dest);        if (!Array.isArray(dest)) {          // Avoid incorrectly rejecting a valid named destination, such as          // e.g. "4.3" or "true", because `JSON.parse` converted its type.          dest = dest.toString();        }      } catch (ex) {}      if (        typeof dest === "string" ||        PDFLinkService.#isValidExplicitDestination(dest)      ) {        this.goToDestination(dest);        return;      }      console.error(        `PDFLinkService.setHash: "${unescape(          hash        )}" is not a valid destination.`      );    }  }  /**   * @param {string} action   */  executeNamedAction(action) {    // See PDF reference, table 8.45 - Named action    switch (action) {      case "GoBack":        this.pdfHistory?.back();        break;      case "GoForward":        this.pdfHistory?.forward();        break;      case "NextPage":        this.pdfViewer.nextPage();        break;      case "PrevPage":        this.pdfViewer.previousPage();        break;      case "LastPage":        this.page = this.pagesCount;        break;      case "FirstPage":        this.page = 1;        break;      default:        break; // No action according to spec    }    this.eventBus.dispatch("namedaction", {      source: this,      action,    });  }  /**   * @param {Object} action   */  async executeSetOCGState(action) {    const pdfDocument = this.pdfDocument;    const optionalContentConfig = await this.pdfViewer      .optionalContentConfigPromise;    if (pdfDocument !== this.pdfDocument) {      return; // The document was closed while the optional content resolved.    }    let operator;    for (const elem of action.state) {      switch (elem) {        case "ON":        case "OFF":        case "Toggle":          operator = elem;          continue;      }      switch (operator) {        case "ON":          optionalContentConfig.setVisibility(elem, true);          break;        case "OFF":          optionalContentConfig.setVisibility(elem, false);          break;        case "Toggle":          const group = optionalContentConfig.getGroup(elem);          if (group) {            optionalContentConfig.setVisibility(elem, !group.visible);          }          break;      }    }    this.pdfViewer.optionalContentConfigPromise = Promise.resolve(      optionalContentConfig    );  }  /**   * @param {number} pageNum - page number.   * @param {Object} pageRef - reference to the page.   */  cachePageRef(pageNum, pageRef) {    if (!pageRef) {      return;    }    const refStr =      pageRef.gen === 0 ? `${pageRef.num}R` : `${pageRef.num}R${pageRef.gen}`;    this.#pagesRefCache.set(refStr, pageNum);  }  /**   * @ignore   */  _cachedPageNumber(pageRef) {    if (!pageRef) {      return null;    }    const refStr =      pageRef.gen === 0 ? `${pageRef.num}R` : `${pageRef.num}R${pageRef.gen}`;    return this.#pagesRefCache.get(refStr) || null;  }  /**   * @param {number} pageNumber   */  isPageVisible(pageNumber) {    return this.pdfViewer.isPageVisible(pageNumber);  }  /**   * @param {number} pageNumber   */  isPageCached(pageNumber) {    return this.pdfViewer.isPageCached(pageNumber);  }  static #isValidExplicitDestination(dest) {    if (!Array.isArray(dest)) {      return false;    }    const destLength = dest.length;    if (destLength < 2) {      return false;    }    const page = dest[0];    if (      !(        typeof page === "object" &&        Number.isInteger(page.num) &&        Number.isInteger(page.gen)      ) &&      !(Number.isInteger(page) && page >= 0)    ) {      return false;    }    const zoom = dest[1];    if (!(typeof zoom === "object" && typeof zoom.name === "string")) {      return false;    }    let allowNull = true;    switch (zoom.name) {      case "XYZ":        if (destLength !== 5) {          return false;        }        break;      case "Fit":      case "FitB":        return destLength === 2;      case "FitH":      case "FitBH":      case "FitV":      case "FitBV":        if (destLength !== 3) {          return false;        }        break;      case "FitR":        if (destLength !== 6) {          return false;        }        allowNull = false;        break;      default:        return false;    }    for (let i = 2; i < destLength; i++) {      const param = dest[i];      if (!(typeof param === "number" || (allowNull && param === null))) {        return false;      }    }    return true;  }}/** * @implements {IPDFLinkService} */class SimpleLinkService {  constructor() {    this.externalLinkEnabled = true;  }  /**   * @type {number}   */  get pagesCount() {    return 0;  }  /**   * @type {number}   */  get page() {    return 0;  }  /**   * @param {number} value   */  set page(value) {}  /**   * @type {number}   */  get rotation() {    return 0;  }  /**   * @param {number} value   */  set rotation(value) {}  /**   * @type {boolean}   */  get isInPresentationMode() {    return false;  }  /**   * @param {string|Array} dest - The named, or explicit, PDF destination.   */  async goToDestination(dest) {}  /**   * @param {number|string} val - The page number, or page label.   */  goToPage(val) {}  /**   * @param {HTMLAnchorElement} link   * @param {string} url   * @param {boolean} [newWindow]   */  addLinkAttributes(link, url, newWindow = false) {    addLinkAttributes(link, { url, enabled: this.externalLinkEnabled });  }  /**   * @param dest - The PDF destination object.   * @returns {string} The hyperlink to the PDF object.   */  getDestinationHash(dest) {    return "#";  }  /**   * @param hash - The PDF parameters/hash.   * @returns {string} The hyperlink to the PDF object.   */  getAnchorUrl(hash) {    return "#";  }  /**   * @param {string} hash   */  setHash(hash) {}  /**   * @param {string} action   */  executeNamedAction(action) {}  /**   * @param {Object} action   */  executeSetOCGState(action) {}  /**   * @param {number} pageNum - page number.   * @param {Object} pageRef - reference to the page.   */  cachePageRef(pageNum, pageRef) {}  /**   * @param {number} pageNumber   */  isPageVisible(pageNumber) {    return true;  }  /**   * @param {number} pageNumber   */  isPageCached(pageNumber) {    return true;  }}export { LinkTarget, PDFLinkService, SimpleLinkService };
 |