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