modules/split_text_to_size.js

/* global jsPDF */
/** @license
 * MIT license.
 * Copyright (c) 2012 Willow Systems Corporation, willow-systems.com
 *               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.
 * ====================================================================
 */
 
/**
* jsPDF split_text_to_size plugin 
* 
* @name split_text_to_size
* @module
*/
(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;
    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;
            }
          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 = [];
      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]
      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) {
        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 = 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
  }

})(jsPDF.API);