plugins/annotations.js

/**
 * jsPDF Annotations PlugIn
 * Copyright (c) 2014 Steven Spungin (TwelveTone LLC)  [email protected]
 *
 * Licensed under the MIT License.
 * http://opensource.org/licenses/mit-license
 */

/**
 * 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>
 */

/*
    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
 */

(function(jsPDFAPI) {
  "use strict";

  var annotationPlugin = {
    /**
     * An array of arrays, indexed by <em>pageNumber</em>.
     */
    annotations: [],

    f2: function(number) {
      return number.toFixed(2);
    },

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

  jsPDF.API.annotationPlugin = annotationPlugin;

  jsPDF.API.events.push([
    "addPage",
    function(info) {
      this.annotationPlugin.annotations[info.pageNumber] = [];
    }
  ]);

  jsPDFAPI.events.push([
    "putPage",
    function(info) {
      //TODO store annotations in pageContext so reorder/remove will not affect them.
      var pageAnnos = this.annotationPlugin.annotations[info.pageNumber];

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

      this.internal.write("/Annots [");
      var f2 = this.annotationPlugin.f2;
      var k = this.internal.scaleFactor;
      var pageHeight = this.internal.pageSize.getHeight();
      var pageInfo = this.internal.getPageInfo(info.pageNumber);
      for (var a = 0; a < pageAnnos.length; a++) {
        var anno = pageAnnos[a];

        switch (anno.type) {
          case "reference":
            // References to Widget Anotations (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 title = anno.title || "Note";
            var rect =
              "/Rect [" +
              f2(anno.bounds.x * k) +
              " " +
              f2(pageHeight - (anno.bounds.y + anno.bounds.h) * k) +
              " " +
              f2((anno.bounds.x + anno.bounds.w) * k) +
              " " +
              f2((pageHeight - anno.bounds.y) * k) +
              "] ";
            line = "<</Type /Annot /Subtype /" + "Text" + " " + rect + "/Contents (" + anno.contents + ")";
            line += " /Popup " + objPopup.objId + " 0 R";
            line += " /P " + pageInfo.objId + " 0 R";
            line += " /T (" + title + ") >>";
            objText.content = line;

            var parent = objText.objId + " 0 R";
            var popoff = 30;
            var rect =
              "/Rect [" +
              f2((anno.bounds.x + popoff) * k) +
              " " +
              f2(pageHeight - (anno.bounds.y + anno.bounds.h) * k) +
              " " +
              f2((anno.bounds.x + anno.bounds.w + popoff) * k) +
              " " +
              f2((pageHeight - anno.bounds.y) * k) +
              "] ";
            //var rect2 = "/Rect [" + f2(anno.bounds.x * k) + " " + f2((pageHeight - anno.bounds.y) * k) + " " + f2(anno.bounds.x + anno.bounds.w * k) + " " + f2(pageHeight - (anno.bounds.y + anno.bounds.h) * k) + "] ";
            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":
            var rect =
              "/Rect [" +
              f2(anno.bounds.x * k) +
              " " +
              f2((pageHeight - anno.bounds.y) * k) +
              " " +
              f2(anno.bounds.x + anno.bounds.w * k) +
              " " +
              f2(pageHeight - (anno.bounds.y + anno.bounds.h) * k) +
              "] ";
            var color = anno.color || "#000000";
            line = "<</Type /Annot /Subtype /" + "FreeText" + " " + rect + "/Contents (" + 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;
              }
            }

            var rect =
              "/Rect [" +
              f2(anno.x * k) +
              " " +
              f2((pageHeight - anno.y) * k) +
              " " +
              f2((anno.x + anno.w) * k) +
              " " +
              f2((pageHeight - (anno.y + anno.h)) * k) +
              "] ";

            var line = "";
            if (anno.options.url) {
              line =
                "<</Type /Annot /Subtype /Link " +
                rect +
                "/Border [0 0 0] /A <</S /URI /URI (" +
                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":
                  //anno.options.top = anno.options.top || f2(pageHeight * k);
                  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 = f2((pageHeight - anno.options.top) * k); // || f2(pageHeight * k);
                  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;
              }
            } else {
              // TODO error - should not be here
            }
            if (line != "") {
              line += " >>";
              this.internal.write(line);
            }
            break;
        }
      }
      this.internal.write("]");
    }
  ]);

  jsPDFAPI.createAnnotation = function(options) {
    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":
        this.annotationPlugin.annotations[this.internal.getCurrentPageInfo().pageNumber].push(options);
        break;
    }
  };

  /**
   * valid options
   * <li> pageNumber or url [required]
   * <p>If pageNumber is specified, top and zoom may also be specified</p>
   */
  jsPDFAPI.link = function(x, y, w, h, options) {
    "use strict";
    this.annotationPlugin.annotations[this.internal.getCurrentPageInfo().pageNumber].push({
      x: x,
      y: y,
      w: w,
      h: h,
      options: options,
      type: "link"
    });
  };

  /**
   * Currently only supports single line text.
   * Returns the width of the text/link
   */
  jsPDFAPI.textWithLink = function(text, x, y, options) {
    "use strict";
    var width = this.getTextWidth(text);
    var height = this.internal.getLineHeight() / this.internal.scaleFactor;
    this.text(text, x, y);
    //TODO We really need the text baseline height to do this correctly.
    // Or ability to draw text on top, bottom, center, or baseline.
    y += height * 0.2;
    this.link(x, y - height, width, height, options);
    return width;
  };

  //TODO move into external library
  jsPDFAPI.getTextWidth = function(text) {
    "use strict";
    var fontSize = this.internal.getFontSize();
    var txtWidth = (this.getStringUnitWidth(text) * fontSize) / this.internal.scaleFactor;
    return txtWidth;
  };

  //TODO move into external library
  jsPDFAPI.getLineHeight = function() {
    return this.internal.getLineHeight();
  };

  return this;
})(jsPDF.API);