diff --git a/cartridges/jsPDF/main.js b/cartridges/jsPDF/main.js index 7ddf741..772d862 100644 --- a/cartridges/jsPDF/main.js +++ b/cartridges/jsPDF/main.js @@ -7,6 +7,7 @@ require('./libs/ttffont')(jsPDF.API); require('./plugins/addImage')(jsPDF.API); require('./plugins/total_pages')(jsPDF.API); require('./plugins/javascript')(jsPDF.API); +require('./plugins/split_text_to_size')(jsPDF.API); require('./plugins/autoprint')(jsPDF.API); require('./plugins/viewerpreferences')(jsPDF.API); require('./plugins/standard_fonts_metrics')(jsPDF.API); diff --git a/cartridges/jsPDF/plugins/split_text_to_size.js b/cartridges/jsPDF/plugins/split_text_to_size.js new file mode 100644 index 0000000..a7d1309 --- /dev/null +++ b/cartridges/jsPDF/plugins/split_text_to_size.js @@ -0,0 +1,404 @@ +/** @license + * MIT license. + * Copyright (c) 2012 Willow Systems Corporation, https://github.com/willowsystems + * 2014 Diego Casorran, https://github.com/diegocr + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * ==================================================================== + */ +/* eslint-disable valid-jsdoc */ +/* eslint-disable camelcase */ +/* eslint-disable no-else-return */ +/* eslint-disable one-var */ +/* eslint-disable no-param-reassign */ +/* eslint-disable no-mixed-operators */ +/* eslint-disable no-shadow */ + +/** + * jsPDF split_text_to_size plugin + * + * @name split_text_to_size + * @module + */ +module.exports = function (API) { + 'use strict'; + /** + * Returns an array of length matching length of the 'word' string, with each + * cell occupied by the width of the char in that position. + * + * @name getCharWidthsArray + * @function + * @param {string} text + * @param {Object} options + * @returns {Array} + */ + var getCharWidthsArray = (API.getCharWidthsArray = function (text, options) { + options = options || {}; + + var activeFont = options.font || this.internal.getFont(); + var fontSize = options.fontSize || this.internal.getFontSize(); + var charSpace = options.charSpace || this.internal.getCharSpace(); + + var widths = options.widths + ? options.widths + : activeFont.metadata.Unicode.widths; + var widthsFractionOf = widths.fof ? widths.fof : 1; + var kerning = options.kerning + ? options.kerning + : activeFont.metadata.Unicode.kerning; + var kerningFractionOf = kerning.fof ? kerning.fof : 1; + // eslint-disable-next-line no-unneeded-ternary + var doKerning = options.doKerning === false ? false : true; + var kerningValue = 0; + + var i; + var length = text.length; + var char_code; + var prior_char_code = 0; // for kerning + var default_char_width = widths[0] || widthsFractionOf; + var output = []; + + for (i = 0; i < length; i++) { + char_code = text.charCodeAt(i); + + if (typeof activeFont.metadata.widthOfString === 'function') { + output.push( + (activeFont.metadata.widthOfGlyph( + activeFont.metadata.characterToGlyph(char_code) + ) + + charSpace * (1000 / fontSize) || 0) / 1000 + ); + } else { + if ( + doKerning && + typeof kerning[char_code] === 'object' && + !isNaN(parseInt(kerning[char_code][prior_char_code], 10)) + ) { + kerningValue = + kerning[char_code][prior_char_code] / kerningFractionOf; + } else { + kerningValue = 0; + } + output.push( + (widths[char_code] || default_char_width) / widthsFractionOf + + kerningValue + ); + } + prior_char_code = char_code; + } + + return output; + }); + + /** + * Returns a widths of string in a given font, if the font size is set as 1 point. + * + * In other words, this is 'proportional' value. For 1 unit of font size, the length + * of the string will be that much. + * + * Multiply by font size to get actual width in *points* + * Then divide by 72 to get inches or divide by (72/25.6) to get 'mm' etc. + * + * @name getStringUnitWidth + * @public + * @function + * @param {string} text + * @param {string} options + * @returns {number} result + */ + var getStringUnitWidth = (API.getStringUnitWidth = function (text, options) { + options = options || {}; + + var fontSize = options.fontSize || this.internal.getFontSize(); + var font = options.font || this.internal.getFont(); + var charSpace = options.charSpace || this.internal.getCharSpace(); + var result = 0; + + if (API.processArabic) { + text = API.processArabic(text); + } + + if (typeof font.metadata.widthOfString === 'function') { + result = + font.metadata.widthOfString(text, fontSize, charSpace) / fontSize; + } else { + result = getCharWidthsArray + .apply(this, arguments) + .reduce(function (pv, cv) { + return pv + cv; + }, 0); + } + return result; + }); + + /** + returns array of lines + */ + var splitLongWord = function (word, widths_array, firstLineMaxLen, maxLen) { + var answer = []; + + // 1st, chop off the piece that can fit on the hanging line. + var i = 0, + l = word.length, + workingLen = 0; + while (i !== l && workingLen + widths_array[i] < firstLineMaxLen) { + workingLen += widths_array[i]; + i++; + } + // this is first line. + answer.push(word.slice(0, i)); + + // 2nd. Split the rest into maxLen pieces. + var startOfLine = i; + workingLen = 0; + while (i !== l) { + if (workingLen + widths_array[i] > maxLen) { + answer.push(word.slice(startOfLine, i)); + workingLen = 0; + startOfLine = i; + } + workingLen += widths_array[i]; + i++; + } + if (startOfLine !== i) { + answer.push(word.slice(startOfLine, i)); + } + + return answer; + }; + + // Note, all sizing inputs for this function must be in 'font measurement units' + // By default, for PDF, it's 'point'. + var splitParagraphIntoLines = function (text, maxlen, options) { + // at this time works only on Western scripts, ones with space char + // separating the words. Feel free to expand. + + if (!options) { + options = {}; + } + + var line = [], + lines = [line], + line_length = options.textIndent || 0, + separator_length = 0, + current_word_length = 0, + word, + widths_array, + words = text.split(' '), + spaceCharWidth = getCharWidthsArray.apply(this, [' ', options])[0], + i, + l, + tmp, + lineIndent; + + if (options.lineIndent === -1) { + lineIndent = words[0].length + 2; + } else { + lineIndent = options.lineIndent || 0; + } + if (lineIndent) { + var pad = Array(lineIndent).join(' '), + wrds = []; + // eslint-disable-next-line array-callback-return + words.map(function (wrd) { + wrd = wrd.split(/\s*\n/); + if (wrd.length > 1) { + wrds = wrds.concat( + wrd.map(function (wrd, idx) { + return (idx && wrd.length ? '\n' : '') + wrd; + }) + ); + } else { + wrds.push(wrd[0]); + } + }); + words = wrds; + lineIndent = getStringUnitWidth.apply(this, [pad, options]); + } + + for (i = 0, l = words.length; i < l; i++) { + var force = 0; + + word = words[i]; + // eslint-disable-next-line eqeqeq + if (lineIndent && word[0] == '\n') { + word = word.substr(1); + force = 1; + } + widths_array = getCharWidthsArray.apply(this, [word, options]); + current_word_length = widths_array.reduce(function (pv, cv) { + return pv + cv; + }, 0); + + if ( + line_length + separator_length + current_word_length > maxlen || + force + ) { + if (current_word_length > maxlen) { + // this happens when you have space-less long URLs for example. + // we just chop these to size. We do NOT insert hiphens + tmp = splitLongWord.apply(this, [ + word, + widths_array, + maxlen - (line_length + separator_length), + maxlen + ]); + // first line we add to existing line object + line.push(tmp.shift()); // it's ok to have extra space indicator there + // last line we make into new line object + line = [tmp.pop()]; + // lines in the middle we apped to lines object as whole lines + while (tmp.length) { + lines.push([tmp.shift()]); // single fragment occupies whole line + } + current_word_length = widths_array + .slice(word.length - (line[0] ? line[0].length : 0)) + .reduce(function (pv, cv) { + return pv + cv; + }, 0); + } else { + // just put it on a new line + line = [word]; + } + + // now we attach new line to lines + lines.push(line); + line_length = current_word_length + lineIndent; + separator_length = spaceCharWidth; + } else { + line.push(word); + + line_length += separator_length + current_word_length; + separator_length = spaceCharWidth; + } + } + + var postProcess; + if (lineIndent) { + postProcess = function (ln, idx) { + // eslint-disable-next-line block-scoped-var + return (idx ? pad : '') + ln.join(' '); + }; + } else { + postProcess = function (ln) { + return ln.join(' '); + }; + } + + return lines.map(postProcess); + }; + + /** + * Splits a given string into an array of strings. Uses 'size' value + * (in measurement units declared as default for the jsPDF instance) + * and the font's 'widths' and 'Kerning' tables, where available, to + * determine display length of a given string for a given font. + * + * We use character's 100% of unit size (height) as width when Width + * table or other default width is not available. + * + * @name splitTextToSize + * @public + * @function + * @param {string} text Unencoded, regular JavaScript (Unicode, UTF-16 / UCS-2) string. + * @param {number} size Nominal number, measured in units default to this instance of jsPDF. + * @param {Object} options Optional flags needed for chopper to do the right thing. + * @returns {Array} array Array with strings chopped to size. + */ + API.splitTextToSize = function (text, maxlen, options) { + 'use strict'; + + options = options || {}; + + var fsize = options.fontSize || this.internal.getFontSize(), + newOptions = function (options) { + var widths = { + 0: 1 + }, + kerning = {}; + + if (!options.widths || !options.kerning) { + var f = this.internal.getFont(options.fontName, options.fontStyle), + encoding = 'Unicode'; + // NOT UTF8, NOT UTF16BE/LE, NOT UCS2BE/LE + // Actual JavaScript-native String's 16bit char codes used. + // no multi-byte logic here + + if (f.metadata[encoding]) { + return { + widths: f.metadata[encoding].widths || widths, + kerning: f.metadata[encoding].kerning || kerning + }; + } else { + return { + font: f.metadata, + fontSize: this.internal.getFontSize(), + charSpace: this.internal.getCharSpace() + }; + } + } else { + return { + widths: options.widths, + kerning: options.kerning + }; + } + }.call(this, options); + + // first we split on end-of-line chars + var paragraphs; + if (Array.isArray(text)) { + paragraphs = text; + } else { + paragraphs = String(text).split(/\r?\n/); + } + + // now we convert size (max length of line) into 'font size units' + // at present time, the 'font size unit' is always 'point' + // 'proportional' means, 'in proportion to font size' + var fontUnit_maxLen = (1.0 * this.internal.scaleFactor * maxlen) / fsize; + // at this time, fsize is always in 'points' regardless of the default measurement unit of the doc. + // this may change in the future? + // until then, proportional_maxlen is likely to be in 'points' + + // If first line is to be indented (shorter or longer) than maxLen + // we indicate that by using CSS-style 'text-indent' option. + // here it's in font units too (which is likely 'points') + // it can be negative (which makes the first line longer than maxLen) + newOptions.textIndent = options.textIndent + ? (options.textIndent * 1.0 * this.internal.scaleFactor) / fsize + : 0; + newOptions.lineIndent = options.lineIndent; + + var i, + l, + output = []; + for (i = 0, l = paragraphs.length; i < l; i++) { + output = output.concat( + splitParagraphIntoLines.apply(this, [ + paragraphs[i], + fontUnit_maxLen, + newOptions + ]) + ); + } + + return output; + }; +}; diff --git a/cartridges/plugin_testlibraries/cartridge/controllers/jsPDF.js b/cartridges/plugin_testlibraries/cartridge/controllers/jsPDF.js index 037488b..ad6e26e 100644 --- a/cartridges/plugin_testlibraries/cartridge/controllers/jsPDF.js +++ b/cartridges/plugin_testlibraries/cartridge/controllers/jsPDF.js @@ -83,6 +83,11 @@ function generatePDF() { doc.setTextColor(0, 0, 255); doc.text(20, 160, 'This is blue.'); + doc.setFont('Open Sans', 'normal'); + + doc.setTextColor('#fffff'); + doc.textWithLink('This is link.', 20, 175, { url: 'http://www.google.com' }); + doc.addImage(jpg, 'jpg', 100, 200, 280, 210, undefined, 'none'); doc.addJS('app.alert("This is injected JS inside of the PDF.", 3);');