modules/outline.js

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

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

/**
 * jsPDF Outline PlugIn
 *
 * Generates a PDF Outline
 * @name outline
 * @module
 */
(function(jsPDFAPI) {
  "use strict";

  var namesOid;
  //var destsGoto = [];

  jsPDFAPI.events.push([
    "postPutResources",
    function() {
      var pdf = this;
      var rx = /^(\d+) 0 obj$/;

      // Write action goto objects for each page
      // this.outline.destsGoto = [];
      // for (var i = 0; i < totalPages; i++) {
      // var id = pdf.internal.newObject();
      // this.outline.destsGoto.push(id);
      // pdf.internal.write("<</D[" + (i * 2 + 3) + " 0 R /XYZ null
      // null null]/S/GoTo>> endobj");
      // }
      //
      // for (var i = 0; i < dests.length; i++) {
      // pdf.internal.write("(page_" + (i + 1) + ")" + dests[i] + " 0
      // R");
      // }
      //
      if (this.outline.root.children.length > 0) {
        var lines = pdf.outline.render().split(/\r\n/);
        for (var i = 0; i < lines.length; i++) {
          var line = lines[i];
          var m = rx.exec(line);
          if (m != null) {
            var oid = m[1];
            pdf.internal.newObjectDeferredBegin(oid, false);
          }
          pdf.internal.write(line);
        }
      }

      // This code will write named destination for each page reference
      // (page_1, etc)
      if (this.outline.createNamedDestinations) {
        var totalPages = this.internal.pages.length;
        // WARNING: this assumes jsPDF starts on page 3 and pageIDs
        // follow 5, 7, 9, etc
        // Write destination objects for each page
        var dests = [];
        for (var i = 0; i < totalPages; i++) {
          var id = pdf.internal.newObject();
          dests.push(id);
          var info = pdf.internal.getPageInfo(i + 1);
          pdf.internal.write(
            "<< /D[" + info.objId + " 0 R /XYZ null null null]>> endobj"
          );
        }

        // assign a name for each destination
        var names2Oid = pdf.internal.newObject();
        pdf.internal.write("<< /Names [ ");
        for (var i = 0; i < dests.length; i++) {
          pdf.internal.write("(page_" + (i + 1) + ")" + dests[i] + " 0 R");
        }
        pdf.internal.write(" ] >>", "endobj");

        // var kids = pdf.internal.newObject();
        // pdf.internal.write('<< /Kids [ ' + names2Oid + ' 0 R');
        // pdf.internal.write(' ] >>', 'endobj');

        namesOid = pdf.internal.newObject();
        pdf.internal.write("<< /Dests " + names2Oid + " 0 R");
        pdf.internal.write(">>", "endobj");
      }
    }
  ]);

  jsPDFAPI.events.push([
    "putCatalog",
    function() {
      var pdf = this;
      if (pdf.outline.root.children.length > 0) {
        pdf.internal.write(
          "/Outlines",
          this.outline.makeRef(this.outline.root)
        );
        if (this.outline.createNamedDestinations) {
          pdf.internal.write("/Names " + namesOid + " 0 R");
        }
        // Open with Bookmarks showing
        // pdf.internal.write("/PageMode /UseOutlines");
      }
    }
  ]);

  jsPDFAPI.events.push([
    "initialized",
    function() {
      var pdf = this;

      pdf.outline = {
        createNamedDestinations: false,
        root: {
          children: []
        }
      };

      /**
       * Options: pageNumber
       */
      pdf.outline.add = function(parent, title, options) {
        var item = {
          title: title,
          options: options,
          children: []
        };
        if (parent == null) {
          parent = this.root;
        }
        parent.children.push(item);
        return item;
      };

      pdf.outline.render = function() {
        this.ctx = {};
        this.ctx.val = "";
        this.ctx.pdf = pdf;

        this.genIds_r(this.root);
        this.renderRoot(this.root);
        this.renderItems(this.root);

        return this.ctx.val;
      };

      pdf.outline.genIds_r = function(node) {
        node.id = pdf.internal.newObjectDeferred();
        for (var i = 0; i < node.children.length; i++) {
          this.genIds_r(node.children[i]);
        }
      };

      pdf.outline.renderRoot = function(node) {
        this.objStart(node);
        this.line("/Type /Outlines");
        if (node.children.length > 0) {
          this.line("/First " + this.makeRef(node.children[0]));
          this.line(
            "/Last " + this.makeRef(node.children[node.children.length - 1])
          );
        }
        this.line(
          "/Count " +
            this.count_r(
              {
                count: 0
              },
              node
            )
        );
        this.objEnd();
      };

      pdf.outline.renderItems = function(node) {
        var getVerticalCoordinateString = this.ctx.pdf.internal
          .getVerticalCoordinateString;
        for (var i = 0; i < node.children.length; i++) {
          var item = node.children[i];
          this.objStart(item);

          this.line("/Title " + this.makeString(item.title));

          this.line("/Parent " + this.makeRef(node));
          if (i > 0) {
            this.line("/Prev " + this.makeRef(node.children[i - 1]));
          }
          if (i < node.children.length - 1) {
            this.line("/Next " + this.makeRef(node.children[i + 1]));
          }
          if (item.children.length > 0) {
            this.line("/First " + this.makeRef(item.children[0]));
            this.line(
              "/Last " + this.makeRef(item.children[item.children.length - 1])
            );
          }

          var count = (this.count = this.count_r(
            {
              count: 0
            },
            item
          ));
          if (count > 0) {
            this.line("/Count " + count);
          }

          if (item.options) {
            if (item.options.pageNumber) {
              // Explicit Destination
              //WARNING this assumes page ids are 3,5,7, etc.
              var info = pdf.internal.getPageInfo(item.options.pageNumber);
              this.line(
                "/Dest " +
                  "[" +
                  info.objId +
                  " 0 R /XYZ 0 " +
                  getVerticalCoordinateString(0) +
                  " 0]"
              );
              // this line does not work on all clients (pageNumber instead of page ref)
              //this.line('/Dest ' + '[' + (item.options.pageNumber - 1) + ' /XYZ 0 ' + this.ctx.pdf.internal.pageSize.getHeight() + ' 0]');

              // Named Destination
              // this.line('/Dest (page_' + (item.options.pageNumber) + ')');

              // Action Destination
              // var id = pdf.internal.newObject();
              // pdf.internal.write('<</D[' + (item.options.pageNumber - 1) + ' /XYZ null null null]/S/GoTo>> endobj');
              // this.line('/A ' + id + ' 0 R' );
            }
          }
          this.objEnd();
        }
        for (var z = 0; z < node.children.length; z++) {
          this.renderItems(node.children[z]);
        }
      };

      pdf.outline.line = function(text) {
        this.ctx.val += text + "\r\n";
      };

      pdf.outline.makeRef = function(node) {
        return node.id + " 0 R";
      };

      pdf.outline.makeString = function(val) {
        return "(" + pdf.internal.pdfEscape(val) + ")";
      };

      pdf.outline.objStart = function(node) {
        this.ctx.val += "\r\n" + node.id + " 0 obj" + "\r\n<<\r\n";
      };

      pdf.outline.objEnd = function() {
        this.ctx.val += ">> \r\n" + "endobj" + "\r\n";
      };

      pdf.outline.count_r = function(ctx, node) {
        for (var i = 0; i < node.children.length; i++) {
          ctx.count++;
          this.count_r(ctx, node.children[i]);
        }
        return ctx.count;
      };
    }
  ]);

  return this;
})(jsPDF.API);