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
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +	
    
    +	
    +
    +