diff --git a/jspdf.js b/jspdf.js
index 29d391232..a1a9cab29 100644
--- a/jspdf.js
+++ b/jspdf.js
@@ -17,6 +17,7 @@
* 2014 Juan Pablo Gaviria, https://github.com/juanpgaviria
* 2014 James Makes, https://github.com/dollaruw
* 2014 Diego Casorran, https://github.com/diegocr
+ * 2014 Steven Spungin, https://github.com/Flamenco
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
@@ -39,7 +40,7 @@
*
* Contributor(s):
* siefkenj, ahwolf, rickygu, Midnith, saintclair, eaparango,
- * kim3er, mfo, alnorth,
+ * kim3er, mfo, alnorth, Flamenco
*/
/**
@@ -228,6 +229,17 @@ var jsPDF = (function(global) {
out(objectNumber + ' 0 obj');
return objectNumber;
},
+ // Does not output the object. The caller must call newObjectDeferredBegin(oid) before outputing any data
+ newObjectDeferred = function() {
+ objectNumber++;
+ offsets[objectNumber] = function(){
+ return content_length;
+ };
+ return objectNumber;
+ },
+ newObjectDeferredBegin = function(oid) {
+ offsets[oid] = content_length;
+ },
putStream = function(str) {
out('stream');
out(str);
@@ -251,7 +263,10 @@ var jsPDF = (function(global) {
out('/Parent 1 0 R');
out('/Resources 2 0 R');
out('/MediaBox [0 0 ' + f2(wPt) + ' ' + f2(hPt) + ']');
- out('/Contents ' + (objectNumber + 1) + ' 0 R>>');
+ out('/Contents ' + (objectNumber + 1) + ' 0 R');
+ // Added for annotation plugin
+ events.publish('putPage', {pageNumber:n,page:pages[n]});
+ out('>>');
out('endobj');
// Page content
@@ -766,7 +781,12 @@ var jsPDF = (function(global) {
out('0 ' + (objectNumber + 1));
out(p+' 65535 f ');
for (i = 1; i <= objectNumber; i++) {
- out((p + offsets[i]).slice(-10) + ' 00000 n ');
+ var offset = offsets[i];
+ if (typeof offset === 'function'){
+ out((p + offsets[i]()).slice(-10) + ' 00000 n ');
+ }else{
+ out((p + offsets[i]).slice(-10) + ' 00000 n ');
+ }
}
// Trailer
out('trailer');
@@ -918,6 +938,8 @@ var jsPDF = (function(global) {
},
'collections' : {},
'newObject' : newObject,
+ 'newObjectDeferred' : newObjectDeferred,
+ 'newObjectDeferredBegin' : newObjectDeferredBegin,
'putStream' : putStream,
'events' : events,
// ratio that you use in multiplication of a given "size" number to arrive to 'point'
diff --git a/jspdf.plugin.annotations.js b/jspdf.plugin.annotations.js
new file mode 100644
index 000000000..603c84f04
--- /dev/null
+++ b/jspdf.plugin.annotations.js
@@ -0,0 +1,143 @@
+/**
+ * jsPDF Annotations PlugIn
+ * Copyright (c) 2014 Steven Spungin (TwelveTone LLC) steven@twelvetone.tv
+ *
+ * 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.
+ *
+ * This plugin current supports
+ *
Goto Page (set pageNumber in options)
+ * Goto URL (set url in options)
+ *
+ *
+ * Options In PDF spec Not Implemented Yet
+ *
link border
+ * named target
+ * page coordinates
+ * destination page scaling and layout
+ * actions other than URL and GotoPage
+ * background / hover actions
+ *
+ */
+
+function notEmpty(obj) {
+ if (typeof obj != 'undefined') {
+ if (obj != '') {
+ return true;
+ }
+ }
+}
+
+(function(jsPDFAPI) {
+ 'use strict';
+
+ var annotationPlugin = {
+
+ /**
+ * An array of arrays, indexed by pageNumber.
+ */
+ annotations : [],
+
+ f2 : function(number) {
+ return number.toFixed(2);
+ }
+ };
+
+ jsPDF.API.annotationPlugin = annotationPlugin;
+
+ jsPDF.API.events.push([
+ 'addPage', function(info) {
+ this.annotationPlugin.annotations[info.pageNumber] = [];
+ }
+ ]);
+
+ jsPDFAPI.events.push([
+ 'putPage', function(info) {
+ var pageAnnos = this.annotationPlugin.annotations[info.pageNumber];
+
+ var found = false;
+ for (var a = 0; a < pageAnnos.length; a++) {
+ var anno = pageAnnos[a];
+ if (anno.type === 'link') {
+ if (notEmpty(anno.options.url) || notEmpty(anno.options.pageNumber)) {
+ found = true;
+ break;
+ }
+ }
+ }
+ if (found == false) {
+ return;
+ }
+
+ this.internal.write("/Annots [");
+ var f2 = this.annotationPlugin.f2;
+ for (var a = 0; a < pageAnnos.length; a++) {
+ var anno = pageAnnos[a];
+ var k = this.internal.scaleFactor;
+ var pageHeight = this.internal.pageSize.height;
+ 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) + "] ";
+ if (anno.options.url) {
+ this.internal.write('<> >>')
+ } else if (anno.options.pageNumber) {
+ // first page is 0
+ this.internal.write('<>')
+ } else {
+ // TODO error - should not be here
+ }
+ }
+ this.internal.write("]");
+ }
+ ]);
+
+ /**
+ * valid options
+ * pageNumber or url [required]
+ */
+ jsPDFAPI.link = function(x,y,w,h,options) {
+ 'use strict';
+ this.annotationPlugin.annotations[this.internal.getNumberOfPages()].push({
+ x : x,
+ y : y,
+ w : w,
+ h : h,
+ options : options,
+ type : 'link'
+ });
+ };
+
+ /**
+ * Currently only supports single line text.
+ */
+ jsPDFAPI.textWithLink = function(text,x,y,options) {
+ 'use strict';
+ var width = this.getTextWidth(text);
+ var height = this.internal.getLineHeight();
+ 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 * .2;
+ this.link(x, y - height, width, height, options);
+ return this;
+ };
+
+ //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);
diff --git a/jspdf.plugin.outline.js b/jspdf.plugin.outline.js
new file mode 100644
index 000000000..079b87e8b
--- /dev/null
+++ b/jspdf.plugin.outline.js
@@ -0,0 +1,240 @@
+/**
+ * jsPDF Outline PlugIn
+ * Copyright (c) 2014 Steven Spungin (TwelveTone LLC) steven@twelvetone.tv
+ *
+ * Licensed under the MIT License.
+ * http://opensource.org/licenses/mit-license
+ */
+
+/**
+ * Generates a PDF Outline
+ */
+;
+(function(jsPDFAPI) {
+ 'use strict';
+
+ 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("<> 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);
+ }
+ 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);
+ pdf.internal.write("<< /D[" + (i * 2 + 3) + " 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 : []
+ }
+ };
+
+ var namesOid;
+ var destsGoto = [];
+
+ /**
+ * 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) {
+ 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.
+ this.line('/Dest ' + '[' + ((item.options.pageNumber - 1) * 2 + 3) + ' 0 R /XYZ 0 ' + this.ctx.pdf.internal.pageSize.height + ' 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.height + ' 0]');
+
+ // Named Destination
+ // this.line('/Dest (page_' + (item.options.pageNumber) + ')');
+
+ // Action Destination
+ // var id = pdf.internal.newObject();
+ // pdf.internal.write('<> endobj');
+ // this.line('/A ' + id + ' 0 R' );
+ }
+ }
+ this.objEnd();
+ }
+ for (var i = 0; i < node.children.length; i++) {
+ var item = node.children[i];
+ this.renderItems(item);
+ }
+ };
+
+ 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(node) {
+ 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);
diff --git a/test/test_annotation.html b/test/test_annotation.html
new file mode 100644
index 000000000..cf2d3c876
--- /dev/null
+++ b/test/test_annotation.html
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+Annotaion Test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/test_outline.html b/test/test_outline.html
new file mode 100644
index 000000000..2d5f09412
--- /dev/null
+++ b/test/test_outline.html
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+Outline Test
+
+
+
+
+
+
+
+
+
+
+
+
+
+