modules/split_text_to_size.js

/** @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.
 * ====================================================================
 */

import { jsPDF } from "../jspdf.js";

/**
 * 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;
        } 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 = [];
      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 = 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;
  };
})(jsPDF.API);