modules/annotations.js

  1. /**
  2. * @license
  3. * Copyright (c) 2014 Steven Spungin (TwelveTone LLC) steven@twelvetone.tv
  4. *
  5. * Licensed under the MIT License.
  6. * http://opensource.org/licenses/mit-license
  7. */
  8. /**
  9. * jsPDF Annotations PlugIn
  10. *
  11. * There are many types of annotations in a PDF document. Annotations are placed
  12. * on a page at a particular location. They are not 'attached' to an object.
  13. * <br />
  14. * This plugin current supports <br />
  15. * <li> Goto Page (set pageNumber and top in options)
  16. * <li> Goto Name (set name and top in options)
  17. * <li> Goto URL (set url in options)
  18. * <p>
  19. * The destination magnification factor can also be specified when goto is a page number or a named destination. (see documentation below)
  20. * (set magFactor in options). XYZ is the default.
  21. * </p>
  22. * <p>
  23. * Links, Text, Popup, and FreeText are supported.
  24. * </p>
  25. * <p>
  26. * Options In PDF spec Not Implemented Yet
  27. * <li> link border
  28. * <li> named target
  29. * <li> page coordinates
  30. * <li> destination page scaling and layout
  31. * <li> actions other than URL and GotoPage
  32. * <li> background / hover actions
  33. * </p>
  34. * @name annotations
  35. * @module
  36. */
  37. /*
  38. Destination Magnification Factors
  39. See PDF 1.3 Page 386 for meanings and options
  40. [supported]
  41. XYZ (options; left top zoom)
  42. Fit (no options)
  43. FitH (options: top)
  44. FitV (options: left)
  45. [not supported]
  46. FitR
  47. FitB
  48. FitBH
  49. FitBV
  50. */
  51. import { jsPDF } from "../jspdf.js";
  52. (function(jsPDFAPI) {
  53. "use strict";
  54. var notEmpty = function(obj) {
  55. if (typeof obj != "undefined") {
  56. if (obj != "") {
  57. return true;
  58. }
  59. }
  60. };
  61. jsPDF.API.events.push([
  62. "addPage",
  63. function(addPageData) {
  64. var pageInfo = this.internal.getPageInfo(addPageData.pageNumber);
  65. pageInfo.pageContext.annotations = [];
  66. }
  67. ]);
  68. jsPDFAPI.events.push([
  69. "putPage",
  70. function(putPageData) {
  71. var getHorizontalCoordinateString = this.internal.getCoordinateString;
  72. var getVerticalCoordinateString = this.internal
  73. .getVerticalCoordinateString;
  74. var pageInfo = this.internal.getPageInfoByObjId(putPageData.objId);
  75. var pageAnnos = putPageData.pageContext.annotations;
  76. var anno, rect, line;
  77. var found = false;
  78. for (var a = 0; a < pageAnnos.length && !found; a++) {
  79. anno = pageAnnos[a];
  80. switch (anno.type) {
  81. case "link":
  82. if (
  83. notEmpty(anno.options.url) ||
  84. notEmpty(anno.options.pageNumber)
  85. ) {
  86. found = true;
  87. }
  88. break;
  89. case "reference":
  90. case "text":
  91. case "freetext":
  92. found = true;
  93. break;
  94. }
  95. }
  96. if (found == false) {
  97. return;
  98. }
  99. this.internal.write("/Annots [");
  100. for (var i = 0; i < pageAnnos.length; i++) {
  101. anno = pageAnnos[i];
  102. var escape = this.internal.pdfEscape;
  103. var encryptor = this.internal.getEncryptor(putPageData.objId);
  104. switch (anno.type) {
  105. case "reference":
  106. // References to Widget Annotations (for AcroForm Fields)
  107. this.internal.write(" " + anno.object.objId + " 0 R ");
  108. break;
  109. case "text":
  110. // Create a an object for both the text and the popup
  111. var objText = this.internal.newAdditionalObject();
  112. var objPopup = this.internal.newAdditionalObject();
  113. var encryptorText = this.internal.getEncryptor(objText.objId);
  114. var title = anno.title || "Note";
  115. rect =
  116. "/Rect [" +
  117. getHorizontalCoordinateString(anno.bounds.x) +
  118. " " +
  119. getVerticalCoordinateString(anno.bounds.y + anno.bounds.h) +
  120. " " +
  121. getHorizontalCoordinateString(anno.bounds.x + anno.bounds.w) +
  122. " " +
  123. getVerticalCoordinateString(anno.bounds.y) +
  124. "] ";
  125. line =
  126. "<</Type /Annot /Subtype /" +
  127. "Text" +
  128. " " +
  129. rect +
  130. "/Contents (" +
  131. escape(encryptorText(anno.contents)) +
  132. ")";
  133. line += " /Popup " + objPopup.objId + " 0 R";
  134. line += " /P " + pageInfo.objId + " 0 R";
  135. line += " /T (" + escape(encryptorText(title)) + ") >>";
  136. objText.content = line;
  137. var parent = objText.objId + " 0 R";
  138. var popoff = 30;
  139. rect =
  140. "/Rect [" +
  141. getHorizontalCoordinateString(anno.bounds.x + popoff) +
  142. " " +
  143. getVerticalCoordinateString(anno.bounds.y + anno.bounds.h) +
  144. " " +
  145. getHorizontalCoordinateString(
  146. anno.bounds.x + anno.bounds.w + popoff
  147. ) +
  148. " " +
  149. getVerticalCoordinateString(anno.bounds.y) +
  150. "] ";
  151. line =
  152. "<</Type /Annot /Subtype /" +
  153. "Popup" +
  154. " " +
  155. rect +
  156. " /Parent " +
  157. parent;
  158. if (anno.open) {
  159. line += " /Open true";
  160. }
  161. line += " >>";
  162. objPopup.content = line;
  163. this.internal.write(objText.objId, "0 R", objPopup.objId, "0 R");
  164. break;
  165. case "freetext":
  166. rect =
  167. "/Rect [" +
  168. getHorizontalCoordinateString(anno.bounds.x) +
  169. " " +
  170. getVerticalCoordinateString(anno.bounds.y) +
  171. " " +
  172. getHorizontalCoordinateString(anno.bounds.x + anno.bounds.w) +
  173. " " +
  174. getVerticalCoordinateString(anno.bounds.y + anno.bounds.h) +
  175. "] ";
  176. var color = anno.color || "#000000";
  177. line =
  178. "<</Type /Annot /Subtype /" +
  179. "FreeText" +
  180. " " +
  181. rect +
  182. "/Contents (" +
  183. escape(encryptor(anno.contents)) +
  184. ")";
  185. line +=
  186. " /DS(font: Helvetica,sans-serif 12.0pt; text-align:left; color:#" +
  187. color +
  188. ")";
  189. line += " /Border [0 0 0]";
  190. line += " >>";
  191. this.internal.write(line);
  192. break;
  193. case "link":
  194. if (anno.options.name) {
  195. var loc = this.annotations._nameMap[anno.options.name];
  196. anno.options.pageNumber = loc.page;
  197. anno.options.top = loc.y;
  198. } else {
  199. if (!anno.options.top) {
  200. anno.options.top = 0;
  201. }
  202. }
  203. rect =
  204. "/Rect [" +
  205. anno.finalBounds.x +
  206. " " +
  207. anno.finalBounds.y +
  208. " " +
  209. anno.finalBounds.w +
  210. " " +
  211. anno.finalBounds.h +
  212. "] ";
  213. line = "";
  214. if (anno.options.url) {
  215. line =
  216. "<</Type /Annot /Subtype /Link " +
  217. rect +
  218. "/Border [0 0 0] /A <</S /URI /URI (" +
  219. escape(encryptor(anno.options.url)) +
  220. ") >>";
  221. } else if (anno.options.pageNumber) {
  222. // first page is 0
  223. var info = this.internal.getPageInfo(anno.options.pageNumber);
  224. line =
  225. "<</Type /Annot /Subtype /Link " +
  226. rect +
  227. "/Border [0 0 0] /Dest [" +
  228. info.objId +
  229. " 0 R";
  230. anno.options.magFactor = anno.options.magFactor || "XYZ";
  231. switch (anno.options.magFactor) {
  232. case "Fit":
  233. line += " /Fit]";
  234. break;
  235. case "FitH":
  236. line += " /FitH " + anno.options.top + "]";
  237. break;
  238. case "FitV":
  239. anno.options.left = anno.options.left || 0;
  240. line += " /FitV " + anno.options.left + "]";
  241. break;
  242. case "XYZ":
  243. default:
  244. var top = getVerticalCoordinateString(anno.options.top);
  245. anno.options.left = anno.options.left || 0;
  246. // 0 or null zoom will not change zoom factor
  247. if (typeof anno.options.zoom === "undefined") {
  248. anno.options.zoom = 0;
  249. }
  250. line +=
  251. " /XYZ " +
  252. anno.options.left +
  253. " " +
  254. top +
  255. " " +
  256. anno.options.zoom +
  257. "]";
  258. break;
  259. }
  260. }
  261. if (line != "") {
  262. line += " >>";
  263. this.internal.write(line);
  264. }
  265. break;
  266. }
  267. }
  268. this.internal.write("]");
  269. }
  270. ]);
  271. /**
  272. * @name createAnnotation
  273. * @function
  274. * @param {Object} options
  275. */
  276. jsPDFAPI.createAnnotation = function(options) {
  277. var pageInfo = this.internal.getCurrentPageInfo();
  278. switch (options.type) {
  279. case "link":
  280. this.link(
  281. options.bounds.x,
  282. options.bounds.y,
  283. options.bounds.w,
  284. options.bounds.h,
  285. options
  286. );
  287. break;
  288. case "text":
  289. case "freetext":
  290. pageInfo.pageContext.annotations.push(options);
  291. break;
  292. }
  293. };
  294. /**
  295. * Create a link
  296. *
  297. * valid options
  298. * <li> pageNumber or url [required]
  299. * <p>If pageNumber is specified, top and zoom may also be specified</p>
  300. * @name link
  301. * @function
  302. * @param {number} x
  303. * @param {number} y
  304. * @param {number} w
  305. * @param {number} h
  306. * @param {Object} options
  307. */
  308. jsPDFAPI.link = function(x, y, w, h, options) {
  309. var pageInfo = this.internal.getCurrentPageInfo();
  310. var getHorizontalCoordinateString = this.internal.getCoordinateString;
  311. var getVerticalCoordinateString = this.internal.getVerticalCoordinateString;
  312. pageInfo.pageContext.annotations.push({
  313. finalBounds: {
  314. x: getHorizontalCoordinateString(x),
  315. y: getVerticalCoordinateString(y),
  316. w: getHorizontalCoordinateString(x + w),
  317. h: getVerticalCoordinateString(y + h)
  318. },
  319. options: options,
  320. type: "link"
  321. });
  322. };
  323. /**
  324. * Currently only supports single line text.
  325. * Returns the width of the text/link
  326. *
  327. * @name textWithLink
  328. * @function
  329. * @param {string} text
  330. * @param {number} x
  331. * @param {number} y
  332. * @param {Object} options
  333. * @returns {number} width the width of the text/link
  334. */
  335. jsPDFAPI.textWithLink = function(text, x, y, options) {
  336. var totalLineWidth = this.getTextWidth(text);
  337. var lineHeight = this.internal.getLineHeight() / this.internal.scaleFactor;
  338. var linkHeight, linkWidth;
  339. // Checking if maxWidth option is passed to determine lineWidth and number of lines for each line
  340. if (options.maxWidth !== undefined) {
  341. var { maxWidth } = options;
  342. linkWidth = maxWidth;
  343. var numOfLines = this.splitTextToSize(text, linkWidth).length;
  344. linkHeight = Math.ceil(lineHeight * numOfLines);
  345. } else {
  346. linkWidth = totalLineWidth;
  347. linkHeight = lineHeight;
  348. }
  349. this.text(text, x, y, options);
  350. //TODO We really need the text baseline height to do this correctly.
  351. // Or ability to draw text on top, bottom, center, or baseline.
  352. y += lineHeight * 0.2;
  353. //handle x position based on the align option
  354. if (options.align === "center") {
  355. x = x - totalLineWidth / 2; //since starting from center move the x position by half of text width
  356. }
  357. if (options.align === "right") {
  358. x = x - totalLineWidth;
  359. }
  360. this.link(x, y - lineHeight, linkWidth, linkHeight, options);
  361. return totalLineWidth;
  362. };
  363. //TODO move into external library
  364. /**
  365. * @name getTextWidth
  366. * @function
  367. * @param {string} text
  368. * @returns {number} txtWidth
  369. */
  370. jsPDFAPI.getTextWidth = function(text) {
  371. var fontSize = this.internal.getFontSize();
  372. var txtWidth =
  373. (this.getStringUnitWidth(text) * fontSize) / this.internal.scaleFactor;
  374. return txtWidth;
  375. };
  376. return this;
  377. })(jsPDF.API);