modules/annotations.js

/**
 * @license
 * Copyright (c) 2014 Steven Spungin (TwelveTone LLC)  steven@twelvetone.tv
 *
 * Licensed under the MIT License.
 * http://opensource.org/licenses/mit-license
 */

/**
 * jsPDF Annotations PlugIn
 *
 * There are many types of annotations in a PDF document. Annotations are placed
 * on a page at a particular location. They are not 'attached' to an object.
 * <br />
 * This plugin current supports <br />
 * <li> Goto Page (set pageNumber and top in options)
 * <li> Goto Name (set name and top in options)
 * <li> Goto URL (set url in options)
 * <p>
 * 	The destination magnification factor can also be specified when goto is a page number or a named destination. (see documentation below)
 *  (set magFactor in options).  XYZ is the default.
 * </p>
 * <p>
 *  Links, Text, Popup, and FreeText are supported.
 * </p>
 * <p>
 * Options In PDF spec Not Implemented Yet
 * <li> link border
 * <li> named target
 * <li> page coordinates
 * <li> destination page scaling and layout
 * <li> actions other than URL and GotoPage
 * <li> background / hover actions
 * </p>
 * @name annotations
 * @module
 */

/*
    Destination Magnification Factors
    See PDF 1.3 Page 386 for meanings and options

    [supported]
	XYZ (options; left top zoom)
	Fit (no options)
	FitH (options: top)
	FitV (options: left)

	[not supported]
	FitR
	FitB
	FitBH
	FitBV
 */

import { jsPDF } from "../jspdf.js";

(function(jsPDFAPI) {
  "use strict";

  var notEmpty = function(obj) {
    if (typeof obj != "undefined") {
      if (obj != "") {
        return true;
      }
    }
  };

  jsPDF.API.events.push([
    "addPage",
    function(addPageData) {
      var pageInfo = this.internal.getPageInfo(addPageData.pageNumber);
      pageInfo.pageContext.annotations = [];
    }
  ]);

  jsPDFAPI.events.push([
    "putPage",
    function(putPageData) {
      var getHorizontalCoordinateString = this.internal.getCoordinateString;
      var getVerticalCoordinateString = this.internal
        .getVerticalCoordinateString;
      var pageInfo = this.internal.getPageInfoByObjId(putPageData.objId);
      var pageAnnos = putPageData.pageContext.annotations;

      var anno, rect, line;
      var found = false;
      for (var a = 0; a < pageAnnos.length && !found; a++) {
        anno = pageAnnos[a];
        switch (anno.type) {
          case "link":
            if (
              notEmpty(anno.options.url) ||
              notEmpty(anno.options.pageNumber)
            ) {
              found = true;
            }
            break;
          case "reference":
          case "text":
          case "freetext":
            found = true;
            break;
        }
      }
      if (found == false) {
        return;
      }

      this.internal.write("/Annots [");
      for (var i = 0; i < pageAnnos.length; i++) {
        anno = pageAnnos[i];
        var escape = this.internal.pdfEscape;
        var encryptor = this.internal.getEncryptor(putPageData.objId);

        switch (anno.type) {
          case "reference":
            // References to Widget Annotations (for AcroForm Fields)
            this.internal.write(" " + anno.object.objId + " 0 R ");
            break;
          case "text":
            // Create a an object for both the text and the popup
            var objText = this.internal.newAdditionalObject();
            var objPopup = this.internal.newAdditionalObject();
            var encryptorText = this.internal.getEncryptor(objText.objId);

            var title = anno.title || "Note";
            rect =
              "/Rect [" +
              getHorizontalCoordinateString(anno.bounds.x) +
              " " +
              getVerticalCoordinateString(anno.bounds.y + anno.bounds.h) +
              " " +
              getHorizontalCoordinateString(anno.bounds.x + anno.bounds.w) +
              " " +
              getVerticalCoordinateString(anno.bounds.y) +
              "] ";

            line =
              "<</Type /Annot /Subtype /" +
              "Text" +
              " " +
              rect +
              "/Contents (" +
              escape(encryptorText(anno.contents)) +
              ")";
            line += " /Popup " + objPopup.objId + " 0 R";
            line += " /P " + pageInfo.objId + " 0 R";
            line += " /T (" + escape(encryptorText(title)) + ") >>";
            objText.content = line;

            var parent = objText.objId + " 0 R";
            var popoff = 30;
            rect =
              "/Rect [" +
              getHorizontalCoordinateString(anno.bounds.x + popoff) +
              " " +
              getVerticalCoordinateString(anno.bounds.y + anno.bounds.h) +
              " " +
              getHorizontalCoordinateString(
                anno.bounds.x + anno.bounds.w + popoff
              ) +
              " " +
              getVerticalCoordinateString(anno.bounds.y) +
              "] ";
            line =
              "<</Type /Annot /Subtype /" +
              "Popup" +
              " " +
              rect +
              " /Parent " +
              parent;
            if (anno.open) {
              line += " /Open true";
            }
            line += " >>";
            objPopup.content = line;

            this.internal.write(objText.objId, "0 R", objPopup.objId, "0 R");

            break;
          case "freetext":
            rect =
              "/Rect [" +
              getHorizontalCoordinateString(anno.bounds.x) +
              " " +
              getVerticalCoordinateString(anno.bounds.y) +
              " " +
              getHorizontalCoordinateString(anno.bounds.x + anno.bounds.w) +
              " " +
              getVerticalCoordinateString(anno.bounds.y + anno.bounds.h) +
              "] ";
            var color = anno.color || "#000000";
            line =
              "<</Type /Annot /Subtype /" +
              "FreeText" +
              " " +
              rect +
              "/Contents (" +
              escape(encryptor(anno.contents)) +
              ")";
            line +=
              " /DS(font: Helvetica,sans-serif 12.0pt; text-align:left; color:#" +
              color +
              ")";
            line += " /Border [0 0 0]";
            line += " >>";
            this.internal.write(line);
            break;
          case "link":
            if (anno.options.name) {
              var loc = this.annotations._nameMap[anno.options.name];
              anno.options.pageNumber = loc.page;
              anno.options.top = loc.y;
            } else {
              if (!anno.options.top) {
                anno.options.top = 0;
              }
            }

            rect =
              "/Rect [" +
              anno.finalBounds.x +
              " " +
              anno.finalBounds.y +
              " " +
              anno.finalBounds.w +
              " " +
              anno.finalBounds.h +
              "] ";

            line = "";
            if (anno.options.url) {
              line =
                "<</Type /Annot /Subtype /Link " +
                rect +
                "/Border [0 0 0] /A <</S /URI /URI (" +
                escape(encryptor(anno.options.url)) +
                ") >>";
            } else if (anno.options.pageNumber) {
              // first page is 0
              var info = this.internal.getPageInfo(anno.options.pageNumber);
              line =
                "<</Type /Annot /Subtype /Link " +
                rect +
                "/Border [0 0 0] /Dest [" +
                info.objId +
                " 0 R";
              anno.options.magFactor = anno.options.magFactor || "XYZ";
              switch (anno.options.magFactor) {
                case "Fit":
                  line += " /Fit]";
                  break;
                case "FitH":
                  line += " /FitH " + anno.options.top + "]";
                  break;
                case "FitV":
                  anno.options.left = anno.options.left || 0;
                  line += " /FitV " + anno.options.left + "]";
                  break;
                case "XYZ":
                default:
                  var top = getVerticalCoordinateString(anno.options.top);
                  anno.options.left = anno.options.left || 0;
                  // 0 or null zoom will not change zoom factor
                  if (typeof anno.options.zoom === "undefined") {
                    anno.options.zoom = 0;
                  }
                  line +=
                    " /XYZ " +
                    anno.options.left +
                    " " +
                    top +
                    " " +
                    anno.options.zoom +
                    "]";
                  break;
              }
            }

            if (line != "") {
              line += " >>";
              this.internal.write(line);
            }
            break;
        }
      }
      this.internal.write("]");
    }
  ]);

  /**
   * @name createAnnotation
   * @function
   * @param {Object} options
   */
  jsPDFAPI.createAnnotation = function(options) {
    var pageInfo = this.internal.getCurrentPageInfo();
    switch (options.type) {
      case "link":
        this.link(
          options.bounds.x,
          options.bounds.y,
          options.bounds.w,
          options.bounds.h,
          options
        );
        break;
      case "text":
      case "freetext":
        pageInfo.pageContext.annotations.push(options);
        break;
    }
  };

  /**
   * Create a link
   *
   * valid options
   * <li> pageNumber or url [required]
   * <p>If pageNumber is specified, top and zoom may also be specified</p>
   * @name link
   * @function
   * @param {number} x
   * @param {number} y
   * @param {number} w
   * @param {number} h
   * @param {Object} options
   */
  jsPDFAPI.link = function(x, y, w, h, options) {
    var pageInfo = this.internal.getCurrentPageInfo();
    var getHorizontalCoordinateString = this.internal.getCoordinateString;
    var getVerticalCoordinateString = this.internal.getVerticalCoordinateString;

    pageInfo.pageContext.annotations.push({
      finalBounds: {
        x: getHorizontalCoordinateString(x),
        y: getVerticalCoordinateString(y),
        w: getHorizontalCoordinateString(x + w),
        h: getVerticalCoordinateString(y + h)
      },
      options: options,
      type: "link"
    });
  };

  /**
   * Currently only supports single line text.
   * Returns the width of the text/link
   *
   * @name textWithLink
   * @function
   * @param {string} text
   * @param {number} x
   * @param {number} y
   * @param {Object} options
   * @returns {number} width the width of the text/link
   */
  jsPDFAPI.textWithLink = function(text, x, y, options) {
    var totalLineWidth = this.getTextWidth(text);
    var lineHeight = this.internal.getLineHeight() / this.internal.scaleFactor;
    var linkHeight, linkWidth;

    // Checking if maxWidth option is passed to determine lineWidth and number of lines for each line
    if (options.maxWidth !== undefined) {
      var { maxWidth } = options;
      linkWidth = maxWidth;
      var numOfLines = this.splitTextToSize(text, linkWidth).length;
      linkHeight = Math.ceil(lineHeight * numOfLines);
    } else {
      linkWidth = totalLineWidth;
      linkHeight = lineHeight;
    }

    this.text(text, x, y, options);

    //TODO We really need the text baseline height to do this correctly.
    // Or ability to draw text on top, bottom, center, or baseline.
    y += lineHeight * 0.2;
    //handle x position based on the align option
    if (options.align === "center") {
      x = x - totalLineWidth / 2; //since starting from center move the x position by half of text width
    }
    if (options.align === "right") {
      x = x - totalLineWidth;
    }
    this.link(x, y - lineHeight, linkWidth, linkHeight, options);
    return totalLineWidth;
  };

  //TODO move into external library
  /**
   * @name getTextWidth
   * @function
   * @param {string} text
   * @returns {number} txtWidth
   */
  jsPDFAPI.getTextWidth = function(text) {
    var fontSize = this.internal.getFontSize();
    var txtWidth =
      (this.getStringUnitWidth(text) * fontSize) / this.internal.scaleFactor;
    return txtWidth;
  };

  return this;
})(jsPDF.API);