modules/split_text_to_size.js

  1. /** @license
  2. * MIT license.
  3. * Copyright (c) 2012 Willow Systems Corporation, https://github.com/willowsystems
  4. * 2014 Diego Casorran, https://github.com/diegocr
  5. *
  6. * Permission is hereby granted, free of charge, to any person obtaining
  7. * a copy of this software and associated documentation files (the
  8. * "Software"), to deal in the Software without restriction, including
  9. * without limitation the rights to use, copy, modify, merge, publish,
  10. * distribute, sublicense, and/or sell copies of the Software, and to
  11. * permit persons to whom the Software is furnished to do so, subject to
  12. * the following conditions:
  13. *
  14. * The above copyright notice and this permission notice shall be
  15. * included in all copies or substantial portions of the Software.
  16. *
  17. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  18. * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  19. * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  20. * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  21. * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  22. * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  23. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  24. * ====================================================================
  25. */
  26. import { jsPDF } from "../jspdf.js";
  27. /**
  28. * jsPDF split_text_to_size plugin
  29. *
  30. * @name split_text_to_size
  31. * @module
  32. */
  33. (function(API) {
  34. "use strict";
  35. /**
  36. * Returns an array of length matching length of the 'word' string, with each
  37. * cell occupied by the width of the char in that position.
  38. *
  39. * @name getCharWidthsArray
  40. * @function
  41. * @param {string} text
  42. * @param {Object} options
  43. * @returns {Array}
  44. */
  45. var getCharWidthsArray = (API.getCharWidthsArray = function(text, options) {
  46. options = options || {};
  47. var activeFont = options.font || this.internal.getFont();
  48. var fontSize = options.fontSize || this.internal.getFontSize();
  49. var charSpace = options.charSpace || this.internal.getCharSpace();
  50. var widths = options.widths
  51. ? options.widths
  52. : activeFont.metadata.Unicode.widths;
  53. var widthsFractionOf = widths.fof ? widths.fof : 1;
  54. var kerning = options.kerning
  55. ? options.kerning
  56. : activeFont.metadata.Unicode.kerning;
  57. var kerningFractionOf = kerning.fof ? kerning.fof : 1;
  58. var doKerning = options.doKerning === false ? false : true;
  59. var kerningValue = 0;
  60. var i;
  61. var length = text.length;
  62. var char_code;
  63. var prior_char_code = 0; //for kerning
  64. var default_char_width = widths[0] || widthsFractionOf;
  65. var output = [];
  66. for (i = 0; i < length; i++) {
  67. char_code = text.charCodeAt(i);
  68. if (typeof activeFont.metadata.widthOfString === "function") {
  69. output.push(
  70. (activeFont.metadata.widthOfGlyph(
  71. activeFont.metadata.characterToGlyph(char_code)
  72. ) +
  73. charSpace * (1000 / fontSize) || 0) / 1000
  74. );
  75. } else {
  76. if (
  77. doKerning &&
  78. typeof kerning[char_code] === "object" &&
  79. !isNaN(parseInt(kerning[char_code][prior_char_code], 10))
  80. ) {
  81. kerningValue =
  82. kerning[char_code][prior_char_code] / kerningFractionOf;
  83. } else {
  84. kerningValue = 0;
  85. }
  86. output.push(
  87. (widths[char_code] || default_char_width) / widthsFractionOf +
  88. kerningValue
  89. );
  90. }
  91. prior_char_code = char_code;
  92. }
  93. return output;
  94. });
  95. /**
  96. * Returns a widths of string in a given font, if the font size is set as 1 point.
  97. *
  98. * In other words, this is "proportional" value. For 1 unit of font size, the length
  99. * of the string will be that much.
  100. *
  101. * Multiply by font size to get actual width in *points*
  102. * Then divide by 72 to get inches or divide by (72/25.6) to get 'mm' etc.
  103. *
  104. * @name getStringUnitWidth
  105. * @public
  106. * @function
  107. * @param {string} text
  108. * @param {string} options
  109. * @returns {number} result
  110. */
  111. var getStringUnitWidth = (API.getStringUnitWidth = function(text, options) {
  112. options = options || {};
  113. var fontSize = options.fontSize || this.internal.getFontSize();
  114. var font = options.font || this.internal.getFont();
  115. var charSpace = options.charSpace || this.internal.getCharSpace();
  116. var result = 0;
  117. if (API.processArabic) {
  118. text = API.processArabic(text);
  119. }
  120. if (typeof font.metadata.widthOfString === "function") {
  121. result =
  122. font.metadata.widthOfString(text, fontSize, charSpace) / fontSize;
  123. } else {
  124. result = getCharWidthsArray
  125. .apply(this, arguments)
  126. .reduce(function(pv, cv) {
  127. return pv + cv;
  128. }, 0);
  129. }
  130. return result;
  131. });
  132. /**
  133. returns array of lines
  134. */
  135. var splitLongWord = function(word, widths_array, firstLineMaxLen, maxLen) {
  136. var answer = [];
  137. // 1st, chop off the piece that can fit on the hanging line.
  138. var i = 0,
  139. l = word.length,
  140. workingLen = 0;
  141. while (i !== l && workingLen + widths_array[i] < firstLineMaxLen) {
  142. workingLen += widths_array[i];
  143. i++;
  144. }
  145. // this is first line.
  146. answer.push(word.slice(0, i));
  147. // 2nd. Split the rest into maxLen pieces.
  148. var startOfLine = i;
  149. workingLen = 0;
  150. while (i !== l) {
  151. if (workingLen + widths_array[i] > maxLen) {
  152. answer.push(word.slice(startOfLine, i));
  153. workingLen = 0;
  154. startOfLine = i;
  155. }
  156. workingLen += widths_array[i];
  157. i++;
  158. }
  159. if (startOfLine !== i) {
  160. answer.push(word.slice(startOfLine, i));
  161. }
  162. return answer;
  163. };
  164. // Note, all sizing inputs for this function must be in "font measurement units"
  165. // By default, for PDF, it's "point".
  166. var splitParagraphIntoLines = function(text, maxlen, options) {
  167. // at this time works only on Western scripts, ones with space char
  168. // separating the words. Feel free to expand.
  169. if (!options) {
  170. options = {};
  171. }
  172. var line = [],
  173. lines = [line],
  174. line_length = options.textIndent || 0,
  175. separator_length = 0,
  176. current_word_length = 0,
  177. word,
  178. widths_array,
  179. words = text.split(" "),
  180. spaceCharWidth = getCharWidthsArray.apply(this, [" ", options])[0],
  181. i,
  182. l,
  183. tmp,
  184. lineIndent;
  185. if (options.lineIndent === -1) {
  186. lineIndent = words[0].length + 2;
  187. } else {
  188. lineIndent = options.lineIndent || 0;
  189. }
  190. if (lineIndent) {
  191. var pad = Array(lineIndent).join(" "),
  192. wrds = [];
  193. words.map(function(wrd) {
  194. wrd = wrd.split(/\s*\n/);
  195. if (wrd.length > 1) {
  196. wrds = wrds.concat(
  197. wrd.map(function(wrd, idx) {
  198. return (idx && wrd.length ? "\n" : "") + wrd;
  199. })
  200. );
  201. } else {
  202. wrds.push(wrd[0]);
  203. }
  204. });
  205. words = wrds;
  206. lineIndent = getStringUnitWidth.apply(this, [pad, options]);
  207. }
  208. for (i = 0, l = words.length; i < l; i++) {
  209. var force = 0;
  210. word = words[i];
  211. if (lineIndent && word[0] == "\n") {
  212. word = word.substr(1);
  213. force = 1;
  214. }
  215. widths_array = getCharWidthsArray.apply(this, [word, options]);
  216. current_word_length = widths_array.reduce(function(pv, cv) {
  217. return pv + cv;
  218. }, 0);
  219. if (
  220. line_length + separator_length + current_word_length > maxlen ||
  221. force
  222. ) {
  223. if (current_word_length > maxlen) {
  224. // this happens when you have space-less long URLs for example.
  225. // we just chop these to size. We do NOT insert hiphens
  226. tmp = splitLongWord.apply(this, [
  227. word,
  228. widths_array,
  229. maxlen - (line_length + separator_length),
  230. maxlen
  231. ]);
  232. // first line we add to existing line object
  233. line.push(tmp.shift()); // it's ok to have extra space indicator there
  234. // last line we make into new line object
  235. line = [tmp.pop()];
  236. // lines in the middle we apped to lines object as whole lines
  237. while (tmp.length) {
  238. lines.push([tmp.shift()]); // single fragment occupies whole line
  239. }
  240. current_word_length = widths_array
  241. .slice(word.length - (line[0] ? line[0].length : 0))
  242. .reduce(function(pv, cv) {
  243. return pv + cv;
  244. }, 0);
  245. } else {
  246. // just put it on a new line
  247. line = [word];
  248. }
  249. // now we attach new line to lines
  250. lines.push(line);
  251. line_length = current_word_length + lineIndent;
  252. separator_length = spaceCharWidth;
  253. } else {
  254. line.push(word);
  255. line_length += separator_length + current_word_length;
  256. separator_length = spaceCharWidth;
  257. }
  258. }
  259. var postProcess;
  260. if (lineIndent) {
  261. postProcess = function(ln, idx) {
  262. return (idx ? pad : "") + ln.join(" ");
  263. };
  264. } else {
  265. postProcess = function(ln) {
  266. return ln.join(" ");
  267. };
  268. }
  269. return lines.map(postProcess);
  270. };
  271. /**
  272. * Splits a given string into an array of strings. Uses 'size' value
  273. * (in measurement units declared as default for the jsPDF instance)
  274. * and the font's "widths" and "Kerning" tables, where available, to
  275. * determine display length of a given string for a given font.
  276. *
  277. * We use character's 100% of unit size (height) as width when Width
  278. * table or other default width is not available.
  279. *
  280. * @name splitTextToSize
  281. * @public
  282. * @function
  283. * @param {string} text Unencoded, regular JavaScript (Unicode, UTF-16 / UCS-2) string.
  284. * @param {number} size Nominal number, measured in units default to this instance of jsPDF.
  285. * @param {Object} options Optional flags needed for chopper to do the right thing.
  286. * @returns {Array} array Array with strings chopped to size.
  287. */
  288. API.splitTextToSize = function(text, maxlen, options) {
  289. "use strict";
  290. options = options || {};
  291. var fsize = options.fontSize || this.internal.getFontSize(),
  292. newOptions = function(options) {
  293. var widths = {
  294. 0: 1
  295. },
  296. kerning = {};
  297. if (!options.widths || !options.kerning) {
  298. var f = this.internal.getFont(options.fontName, options.fontStyle),
  299. encoding = "Unicode";
  300. // NOT UTF8, NOT UTF16BE/LE, NOT UCS2BE/LE
  301. // Actual JavaScript-native String's 16bit char codes used.
  302. // no multi-byte logic here
  303. if (f.metadata[encoding]) {
  304. return {
  305. widths: f.metadata[encoding].widths || widths,
  306. kerning: f.metadata[encoding].kerning || kerning
  307. };
  308. } else {
  309. return {
  310. font: f.metadata,
  311. fontSize: this.internal.getFontSize(),
  312. charSpace: this.internal.getCharSpace()
  313. };
  314. }
  315. } else {
  316. return {
  317. widths: options.widths,
  318. kerning: options.kerning
  319. };
  320. }
  321. }.call(this, options);
  322. // first we split on end-of-line chars
  323. var paragraphs;
  324. if (Array.isArray(text)) {
  325. paragraphs = text;
  326. } else {
  327. paragraphs = String(text).split(/\r?\n/);
  328. }
  329. // now we convert size (max length of line) into "font size units"
  330. // at present time, the "font size unit" is always 'point'
  331. // 'proportional' means, "in proportion to font size"
  332. var fontUnit_maxLen = (1.0 * this.internal.scaleFactor * maxlen) / fsize;
  333. // at this time, fsize is always in "points" regardless of the default measurement unit of the doc.
  334. // this may change in the future?
  335. // until then, proportional_maxlen is likely to be in 'points'
  336. // If first line is to be indented (shorter or longer) than maxLen
  337. // we indicate that by using CSS-style "text-indent" option.
  338. // here it's in font units too (which is likely 'points')
  339. // it can be negative (which makes the first line longer than maxLen)
  340. newOptions.textIndent = options.textIndent
  341. ? (options.textIndent * 1.0 * this.internal.scaleFactor) / fsize
  342. : 0;
  343. newOptions.lineIndent = options.lineIndent;
  344. var i,
  345. l,
  346. output = [];
  347. for (i = 0, l = paragraphs.length; i < l; i++) {
  348. output = output.concat(
  349. splitParagraphIntoLines.apply(this, [
  350. paragraphs[i],
  351. fontUnit_maxLen,
  352. newOptions
  353. ])
  354. );
  355. }
  356. return output;
  357. };
  358. })(jsPDF.API);