plugins/context2d.js

/**
 * jsPDF Context2D PlugIn Copyright (c) 2014 Steven Spungin (TwelveTone LLC) [email protected]
 *
 * Licensed under the MIT License. http://opensource.org/licenses/mit-license
 */

/**
 * This plugin mimics the HTML5 Canvas's context2d.
 *
 * The goal is to provide a way for current canvas implementations to print directly to a PDF.
 */

/**
 * TODO implement stroke opacity (refactor from fill() method )
 * TODO transform angle and radii parameters
 */

/**
 * require('jspdf.js'); require('lib/css_colors.js');
 */

(function(jsPDFAPI) {
  "use strict";

  jsPDFAPI.events.push([
    "initialized",
    function() {
      this.context2d.pdf = this;
      this.context2d.internal.pdf = this;
      this.context2d.ctx = new context();
      this.context2d.ctxStack = [];
      this.context2d.path = [];
    }
  ]);

  jsPDFAPI.context2d = {
    pageWrapXEnabled: false,
    pageWrapYEnabled: false,
    pageWrapX: 9999999,
    pageWrapY: 9999999,
    ctx: new context(),
    f2: function(number) {
      return number.toFixed(2);
    },

    fillRect: function(x, y, w, h) {
      if (this._isFillTransparent()) {
        return;
      }
      x = this._wrapX(x);
      y = this._wrapY(y);

      var xRect = this._matrix_map_rect(this.ctx._transform, {
        x: x,
        y: y,
        w: w,
        h: h
      });
      this.pdf.rect(xRect.x, xRect.y, xRect.w, xRect.h, "f");
    },

    strokeRect: function(x, y, w, h) {
      if (this._isStrokeTransparent()) {
        return;
      }
      x = this._wrapX(x);
      y = this._wrapY(y);

      var xRect = this._matrix_map_rect(this.ctx._transform, {
        x: x,
        y: y,
        w: w,
        h: h
      });
      this.pdf.rect(xRect.x, xRect.y, xRect.w, xRect.h, "s");
    },

    /**
     * We cannot clear PDF commands that were already written to PDF, so we use white instead. <br />
     * As a special case, read a special flag (ignoreClearRect) and do nothing if it is set.
     * This results in all calls to clearRect() to do nothing, and keep the canvas transparent.
     * This flag is stored in the save/restore context and is managed the same way as other drawing states.
     * @param x
     * @param y
     * @param w
     * @param h
     */
    clearRect: function(x, y, w, h) {
      if (this.ctx.ignoreClearRect) {
        return;
      }

      x = this._wrapX(x);
      y = this._wrapY(y);

      var xRect = this._matrix_map_rect(this.ctx._transform, {
        x: x,
        y: y,
        w: w,
        h: h
      });
      this.save();
      this.setFillStyle("#ffffff");
      //TODO This is hack to fill with white.
      this.pdf.rect(xRect.x, xRect.y, xRect.w, xRect.h, "f");
      this.restore();
    },

    save: function() {
      this.ctx._fontSize = this.pdf.internal.getFontSize();
      var ctx = new context();
      ctx.copy(this.ctx);
      this.ctxStack.push(this.ctx);
      this.ctx = ctx;
    },

    restore: function() {
      this.ctx = this.ctxStack.pop();
      this.setFillStyle(this.ctx.fillStyle);
      this.setStrokeStyle(this.ctx.strokeStyle);
      this.setFont(this.ctx.font);
      this.pdf.setFontSize(this.ctx._fontSize);
      this.setLineCap(this.ctx.lineCap);
      this.setLineWidth(this.ctx.lineWidth);
      this.setLineJoin(this.ctx.lineJoin);
    },

    rect: function(x, y, w, h) {
      this.moveTo(x, y);
      this.lineTo(x + w, y);
      this.lineTo(x + w, y + h);
      this.lineTo(x, y + h);
      this.lineTo(x, y); //TODO not needed
      this.closePath();
    },

    beginPath: function() {
      this.path = [];
    },

    closePath: function() {
      this.path.push({
        type: "close"
      });
    },

    _getRGBA: function(style) {
      // get the decimal values of r, g, and b;
      var r, g, b, a;
      var rgbColor = new RGBColor(style);

      if (!style) {
        return { r: 0, g: 0, b: 0, a: 0, style: style };
      }

      if (this.internal.rxTransparent.test(style)) {
        r = 0;
        g = 0;
        b = 0;
        a = 0;
      } else {
        var m = this.internal.rxRgb.exec(style);
        if (m != null) {
          r = parseInt(m[1]);
          g = parseInt(m[2]);
          b = parseInt(m[3]);
          a = 1;
        } else {
          m = this.internal.rxRgba.exec(style);
          if (m != null) {
            r = parseInt(m[1]);
            g = parseInt(m[2]);
            b = parseInt(m[3]);
            a = parseFloat(m[4]);
          } else {
            a = 1;
            if (style.charAt(0) != "#") {
              if (rgbColor.ok) {
                style = rgbColor.toHex();
              } else {
                style = "#000000";
              }
            }

            if (style.length === 4) {
              r = style.substring(1, 2);
              r += r;
              g = style.substring(2, 3);
              g += g;
              b = style.substring(3, 4);
              b += b;
            } else {
              r = style.substring(1, 3);
              g = style.substring(3, 5);
              b = style.substring(5, 7);
            }
            r = parseInt(r, 16);
            g = parseInt(g, 16);
            b = parseInt(b, 16);
          }
        }
      }
      return { r: r, g: g, b: b, a: a, style: style };
    },

    setFillStyle: function(style) {
      var rgba = this._getRGBA(style);

      this.ctx.fillStyle = style;
      this.ctx._isFillTransparent = rgba.a === 0;
      this.ctx._fillOpacity = rgba.a;

      this.pdf.setFillColor(rgba.r, rgba.g, rgba.b, {
        a: rgba.a
      });
      this.pdf.setTextColor(rgba.r, rgba.g, rgba.b, {
        a: rgba.a
      });
    },

    setStrokeStyle: function(style) {
      var rgba = this._getRGBA(style);

      this.ctx.strokeStyle = rgba.style;
      this.ctx._isStrokeTransparent = rgba.a === 0;
      this.ctx._strokeOpacity = rgba.a;

      //TODO jsPDF to handle rgba
      if (rgba.a === 0) {
        this.pdf.setDrawColor(255, 255, 255);
      } else if (rgba.a === 1) {
        this.pdf.setDrawColor(rgba.r, rgba.g, rgba.b);
      } else {
        //this.pdf.setDrawColor(rgba.r, rgba.g, rgba.b, {a: rgba.a});
        this.pdf.setDrawColor(rgba.r, rgba.g, rgba.b);
      }
    },

    fillText: function(text, x, y, maxWidth) {
      if (this._isFillTransparent()) {
        return;
      }
      x = this._wrapX(x);
      y = this._wrapY(y);

      var xpt = this._matrix_map_point(this.ctx._transform, [x, y]);
      x = xpt[0];
      y = xpt[1];
      var rads = this._matrix_rotation(this.ctx._transform);
      var degs = rads * 57.2958;

      //TODO only push the clip if it has not been applied to the current PDF context
      if (this.ctx._clip_path.length > 0) {
        var lines;
        if (window.outIntercept) {
          lines = window.outIntercept.type === "group" ? window.outIntercept.stream : window.outIntercept;
        } else {
          lines = this.internal.getCurrentPage();
        }
        lines.push("q");
        var origPath = this.path;
        this.path = this.ctx._clip_path;
        this.ctx._clip_path = [];
        this._fill(null, true);
        this.ctx._clip_path = this.path;
        this.path = origPath;
      }

      // We only use X axis as scale hint
      var scale = 1;
      try {
        scale = this._matrix_decompose(this._getTransform()).scale[0];
      } catch (e) {
        console.warn(e);
      }

      // In some cases the transform was very small (5.715760606202283e-17).  Most likely a canvg rounding error.
      if (scale < 0.01) {
        this.pdf.text(text, x, this._getBaseline(y), null, degs);
      } else {
        var oldSize = this.pdf.internal.getFontSize();
        this.pdf.setFontSize(oldSize * scale);
        this.pdf.text(text, x, this._getBaseline(y), null, degs);
        this.pdf.setFontSize(oldSize);
      }

      if (this.ctx._clip_path.length > 0) {
        lines.push("Q");
      }
    },

    strokeText: function(text, x, y, maxWidth) {
      if (this._isStrokeTransparent()) {
        return;
      }
      x = this._wrapX(x);
      y = this._wrapY(y);

      var xpt = this._matrix_map_point(this.ctx._transform, [x, y]);
      x = xpt[0];
      y = xpt[1];
      var rads = this._matrix_rotation(this.ctx._transform);
      var degs = rads * 57.2958;

      //TODO only push the clip if it has not been applied to the current PDF context
      if (this.ctx._clip_path.length > 0) {
        var lines;
        if (window.outIntercept) {
          lines = window.outIntercept.type === "group" ? window.outIntercept.stream : window.outIntercept;
        } else {
          lines = this.internal.getCurrentPage();
        }
        lines.push("q");
        var origPath = this.path;
        this.path = this.ctx._clip_path;
        this.ctx._clip_path = [];
        this._fill(null, true);
        this.ctx._clip_path = this.path;
        this.path = origPath;
      }

      var scale = 1;
      // We only use the X axis as scale hint
      try {
        scale = this._matrix_decompose(this._getTransform()).scale[0];
      } catch (e) {
        console.warn(e);
      }

      if (scale === 1) {
        this.pdf.text(
          text,
          x,
          this._getBaseline(y),
          {
            stroke: true
          },
          degs
        );
      } else {
        var oldSize = this.pdf.internal.getFontSize();
        this.pdf.setFontSize(oldSize * scale);
        this.pdf.text(
          text,
          x,
          this._getBaseline(y),
          {
            stroke: true
          },
          degs
        );
        this.pdf.setFontSize(oldSize);
      }

      if (this.ctx._clip_path.length > 0) {
        lines.push("Q");
      }
    },

    setFont: function(font) {
      this.ctx.font = font;

      //var rx = /\s*(\w+)\s+(\w+)\s+(\w+)\s+([\d\.]+)(px|pt|em)\s+["']?(\w+)['"]?/;
      var rx = /\s*(\w+)\s+(\w+)\s+(\w+)\s+([\d\.]+)(px|pt|em)\s+(.*)?/;
      m = rx.exec(font);
      if (m != null) {
        var fontStyle = m[1];
        var fontVariant = m[2];
        var fontWeight = m[3];
        var fontSize = m[4];
        var fontSizeUnit = m[5];
        var fontFamily = m[6];

        if ("px" === fontSizeUnit) {
          fontSize = Math.floor(parseFloat(fontSize));
          // fontSize = fontSize * 1.25;
        } else if ("em" === fontSizeUnit) {
          fontSize = Math.floor(parseFloat(fontSize) * this.pdf.getFontSize());
        } else {
          fontSize = Math.floor(parseFloat(fontSize));
        }

        this.pdf.setFontSize(fontSize);

        if (fontWeight === "bold" || fontWeight === "700") {
          this.pdf.setFontStyle("bold");
        } else {
          if (fontStyle === "italic") {
            this.pdf.setFontStyle("italic");
          } else {
            this.pdf.setFontStyle("normal");
          }
        }
        var style;
        if ("bold" === fontWeight || fontWeight === "700") {
          style = fontStyle === "italic" ? "bolditalic" : "bold";
        } else if (fontStyle === "italic") {
          style = "italic";
        } else {
          style = "normal";
        }

        var parts = fontFamily.toLowerCase().split(/\s*,\s*/);
        var jsPdfFontName = "Times";

        var fallbackFonts = {
          arial: "Helvetica",
          verdana: "Helvetica",
          helvetica: "Helvetica",
          "sans-serif": "Helvetica",
          fixed: "Courier",
          monospace: "Courier",
          terminal: "Courier",
          courier: "Courier",
          times: "Times",
          cursive: "Times",
          fantasy: "Times",
          serif: "Times"
        };

        for (var i = 0; i < parts.length; i++) {
          if (
            this.pdf.internal.getFont(parts[i], style, {
              noFallback: true,
              disableWarning: true
            }) !== undefined
          ) {
            jsPdfFontName = parts[i];
            break;
          } else if (
            style === "bolditalic" &&
            this.pdf.internal.getFont(parts[i], "bold", {
              noFallback: true,
              disableWarning: true
            }) !== undefined
          ) {
            jsPdfFontName = parts[i];
            style = "bold";
          } else if (
            this.pdf.internal.getFont(parts[i], "normal", {
              noFallback: true,
              disableWarning: true
            }) !== undefined
          ) {
            jsPdfFontName = parts[i];
            style = "normal";
            break;
          }
        }

        this.pdf.setFont(jsPdfFontName, style);
      } else {
        var rx = /\s*(\d+)(pt|px|em)\s+([\w "]+)\s*([\w "]+)?/;
        var m = rx.exec(font);
        if (m != null) {
          var size = m[1];
          var unit = m[2];
          var name = m[3];
          var style = m[4];
          if (!style) {
            style = "normal";
          }
          if ("em" === fontSizeUnit) {
            size = Math.floor(parseFloat(fontSize) * this.pdf.getFontSize());
          } else {
            size = Math.floor(parseFloat(size));
          }
          this.pdf.setFontSize(size);
          this.pdf.setFont(name, style);
        }
      }
    },

    setTextBaseline: function(baseline) {
      this.ctx.textBaseline = baseline;
    },

    getTextBaseline: function() {
      return this.ctx.textBaseline;
    },

    //TODO implement textAlign
    setTextAlign: function(align) {
      this.ctx.textAlign = align;
    },

    getTextAlign: function() {
      return this.ctx.textAlign;
    },

    setLineWidth: function(width) {
      this.ctx.lineWidth = width;
      this.pdf.setLineWidth(width);
    },

    setLineCap: function(style) {
      this.ctx.lineCap = style;
      this.pdf.setLineCap(style);
    },

    setLineJoin: function(style) {
      this.ctx.lineJoin = style;
      this.pdf.setLineJoin(style);
    },

    moveTo: function(x, y) {
      x = this._wrapX(x);
      y = this._wrapY(y);

      var xpt = this._matrix_map_point(this.ctx._transform, [x, y]);
      x = xpt[0];
      y = xpt[1];

      var obj = {
        type: "mt",
        x: x,
        y: y
      };
      this.path.push(obj);
    },

    _wrapX: function(x) {
      if (this.pageWrapXEnabled) {
        return x % this.pageWrapX;
      } else {
        return x;
      }
    },

    _wrapY: function(y) {
      if (this.pageWrapYEnabled) {
        this._gotoPage(this._page(y));
        return (y - this.lastBreak) % this.pageWrapY;
      } else {
        return y;
      }
    },

    transform: function(a, b, c, d, e, f) {
      this.ctx._transform = this._matrix_multiply(this.ctx._transform, [a, b, c, d, e, f]);
    },

    setTransform: function(a, b, c, d, e, f) {
      this.ctx._transform = [a, b, c, d, e, f];
    },

    _getTransform: function() {
      return this.ctx._transform;
    },

    lastBreak: 0,
    // Y Position of page breaks.
    pageBreaks: [],
    // returns: One-based Page Number
    // Should only be used if pageWrapYEnabled is true
    _page: function(y) {
      if (this.pageWrapYEnabled) {
        this.lastBreak = 0;
        var manualBreaks = 0;
        var autoBreaks = 0;
        for (var i = 0; i < this.pageBreaks.length; i++) {
          if (y >= this.pageBreaks[i]) {
            manualBreaks++;
            if (this.lastBreak === 0) {
              autoBreaks++;
            }
            var spaceBetweenLastBreak = this.pageBreaks[i] - this.lastBreak;
            this.lastBreak = this.pageBreaks[i];
            var pagesSinceLastBreak = Math.floor(spaceBetweenLastBreak / this.pageWrapY);
            autoBreaks += pagesSinceLastBreak;
          }
        }
        if (this.lastBreak === 0) {
          var pagesSinceLastBreak = Math.floor(y / this.pageWrapY) + 1;
          autoBreaks += pagesSinceLastBreak;
        }
        return autoBreaks + manualBreaks;
      } else {
        return this.pdf.internal.getCurrentPageInfo().pageNumber;
      }
    },

    _gotoPage: function(pageOneBased) {
      // This is a stub to be overriden if needed
    },

    lineTo: function(x, y) {
      x = this._wrapX(x);
      y = this._wrapY(y);

      var xpt = this._matrix_map_point(this.ctx._transform, [x, y]);
      x = xpt[0];
      y = xpt[1];

      var obj = {
        type: "lt",
        x: x,
        y: y
      };
      this.path.push(obj);
    },

    bezierCurveTo: function(x1, y1, x2, y2, x, y) {
      x1 = this._wrapX(x1);
      y1 = this._wrapY(y1);
      x2 = this._wrapX(x2);
      y2 = this._wrapY(y2);
      x = this._wrapX(x);
      y = this._wrapY(y);

      var xpt;
      xpt = this._matrix_map_point(this.ctx._transform, [x, y]);
      x = xpt[0];
      y = xpt[1];
      xpt = this._matrix_map_point(this.ctx._transform, [x1, y1]);
      x1 = xpt[0];
      y1 = xpt[1];
      xpt = this._matrix_map_point(this.ctx._transform, [x2, y2]);
      x2 = xpt[0];
      y2 = xpt[1];

      var obj = {
        type: "bct",
        x1: x1,
        y1: y1,
        x2: x2,
        y2: y2,
        x: x,
        y: y
      };
      this.path.push(obj);
    },

    quadraticCurveTo: function(x1, y1, x, y) {
      x1 = this._wrapX(x1);
      y1 = this._wrapY(y1);
      x = this._wrapX(x);
      y = this._wrapY(y);

      var xpt;
      xpt = this._matrix_map_point(this.ctx._transform, [x, y]);
      x = xpt[0];
      y = xpt[1];
      xpt = this._matrix_map_point(this.ctx._transform, [x1, y1]);
      x1 = xpt[0];
      y1 = xpt[1];

      var obj = {
        type: "qct",
        x1: x1,
        y1: y1,
        x: x,
        y: y
      };
      this.path.push(obj);
    },

    arc: function(x, y, radius, startAngle, endAngle, anticlockwise) {
      x = this._wrapX(x);
      y = this._wrapY(y);

      if (!this._matrix_is_identity(this.ctx._transform)) {
        var xpt = this._matrix_map_point(this.ctx._transform, [x, y]);
        x = xpt[0];
        y = xpt[1];

        var x_radPt0 = this._matrix_map_point(this.ctx._transform, [0, 0]);
        var x_radPt = this._matrix_map_point(this.ctx._transform, [0, radius]);
        radius = Math.sqrt(Math.pow(x_radPt[0] - x_radPt0[0], 2) + Math.pow(x_radPt[1] - x_radPt0[1], 2));

        //TODO angles need to be transformed
      }

      var obj = {
        type: "arc",
        x: x,
        y: y,
        radius: radius,
        startAngle: startAngle,
        endAngle: endAngle,
        anticlockwise: anticlockwise
      };
      this.path.push(obj);
    },

    drawImage: function(img, x, y, w, h, x2, y2, w2, h2) {
      if (x2 !== undefined) {
        x = x2;
        y = y2;
        w = w2;
        h = h2;
      }
      x = this._wrapX(x);
      y = this._wrapY(y);

      var xRect = this._matrix_map_rect(this.ctx._transform, {
        x: x,
        y: y,
        w: w,
        h: h
      });
      var xRect2 = this._matrix_map_rect(this.ctx._transform, {
        x: x2,
        y: y2,
        w: w2,
        h: h2
      });

      // TODO implement source clipping and image scaling
      var format;
      var rx = /data:image\/(\w+).*/i;
      var m = rx.exec(img);
      if (m != null) {
        format = m[1];
      } else {
        // format = "jpeg";
        format = "png";
      }

      this.pdf.addImage(img, format, xRect.x, xRect.y, xRect.w, xRect.h);
    },

    /**
     * Multiply the first matrix by the second
     * @param m1
     * @param m2
     * @returns {*[]}
     * @private
     */
    _matrix_multiply: function(m2, m1) {
      var sx = m1[0];
      var shy = m1[1];
      var shx = m1[2];
      var sy = m1[3];
      var tx = m1[4];
      var ty = m1[5];

      var t0 = sx * m2[0] + shy * m2[2];
      var t2 = shx * m2[0] + sy * m2[2];
      var t4 = tx * m2[0] + ty * m2[2] + m2[4];
      shy = sx * m2[1] + shy * m2[3];
      sy = shx * m2[1] + sy * m2[3];
      ty = tx * m2[1] + ty * m2[3] + m2[5];
      sx = t0;
      shx = t2;
      tx = t4;

      return [sx, shy, shx, sy, tx, ty];
    },

    _matrix_rotation: function(m) {
      return Math.atan2(m[2], m[0]);
    },

    _matrix_decompose: function(matrix) {
      var a = matrix[0];
      var b = matrix[1];
      var c = matrix[2];
      var d = matrix[3];

      var scaleX = Math.sqrt(a * a + b * b);
      a /= scaleX;
      b /= scaleX;

      var shear = a * c + b * d;
      c -= a * shear;
      d -= b * shear;

      var scaleY = Math.sqrt(c * c + d * d);
      c /= scaleY;
      d /= scaleY;
      shear /= scaleY;

      if (a * d < b * c) {
        a = -a;
        b = -b;
        shear = -shear;
        scaleX = -scaleX;
      }

      return {
        scale: [scaleX, 0, 0, scaleY, 0, 0],
        translate: [1, 0, 0, 1, matrix[4], matrix[5]],
        rotate: [a, b, -b, a, 0, 0],
        skew: [1, 0, shear, 1, 0, 0]
      };
    },

    _matrix_map_point: function(m1, pt) {
      var sx = m1[0];
      var shy = m1[1];
      var shx = m1[2];
      var sy = m1[3];
      var tx = m1[4];
      var ty = m1[5];

      var px = pt[0];
      var py = pt[1];

      var x = px * sx + py * shx + tx;
      var y = px * shy + py * sy + ty;
      return [x, y];
    },

    _matrix_map_point_obj: function(m1, pt) {
      var xpt = this._matrix_map_point(m1, [pt.x, pt.y]);
      return { x: xpt[0], y: xpt[1] };
    },

    _matrix_map_rect: function(m1, rect) {
      var p1 = this._matrix_map_point(m1, [rect.x, rect.y]);
      var p2 = this._matrix_map_point(m1, [rect.x + rect.w, rect.y + rect.h]);
      return { x: p1[0], y: p1[1], w: p2[0] - p1[0], h: p2[1] - p1[1] };
    },

    _matrix_is_identity: function(m1) {
      if (m1[0] != 1) {
        return false;
      }
      if (m1[1] != 0) {
        return false;
      }
      if (m1[2] != 0) {
        return false;
      }
      if (m1[3] != 1) {
        return false;
      }
      if (m1[4] != 0) {
        return false;
      }
      if (m1[5] != 0) {
        return false;
      }
      return true;
    },

    rotate: function(angle) {
      var matrix = [Math.cos(angle), Math.sin(angle), -Math.sin(angle), Math.cos(angle), 0.0, 0.0];
      this.ctx._transform = this._matrix_multiply(this.ctx._transform, matrix);
    },

    scale: function(sx, sy) {
      var matrix = [sx, 0.0, 0.0, sy, 0.0, 0.0];
      this.ctx._transform = this._matrix_multiply(this.ctx._transform, matrix);
    },

    translate: function(x, y) {
      var matrix = [1.0, 0.0, 0.0, 1.0, x, y];
      this.ctx._transform = this._matrix_multiply(this.ctx._transform, matrix);
    },

    stroke: function() {
      if (this.ctx._clip_path.length > 0) {
        var lines;
        if (window.outIntercept) {
          lines = window.outIntercept.type === "group" ? window.outIntercept.stream : window.outIntercept;
        } else {
          lines = this.internal.getCurrentPage();
        }
        lines.push("q");

        var origPath = this.path;
        this.path = this.ctx._clip_path;
        this.ctx._clip_path = [];
        this._stroke(true);

        this.ctx._clip_path = this.path;
        this.path = origPath;
        this._stroke(false);

        lines.push("Q");
      } else {
        this._stroke(false);
      }
    },

    _stroke: function(isClip) {
      if (!isClip && this._isStrokeTransparent()) {
        return;
      }

      //TODO opacity

      var moves = [];
      var closed = false;

      var xPath = this.path;

      for (var i = 0; i < xPath.length; i++) {
        var pt = xPath[i];
        switch (pt.type) {
          case "mt":
            moves.push({ start: pt, deltas: [], abs: [] });
            break;
          case "lt":
            var delta = [pt.x - xPath[i - 1].x, pt.y - xPath[i - 1].y];
            moves[moves.length - 1].deltas.push(delta);
            moves[moves.length - 1].abs.push(pt);
            break;
          case "bct":
            var delta = [
              pt.x1 - xPath[i - 1].x,
              pt.y1 - xPath[i - 1].y,
              pt.x2 - xPath[i - 1].x,
              pt.y2 - xPath[i - 1].y,
              pt.x - xPath[i - 1].x,
              pt.y - xPath[i - 1].y
            ];
            moves[moves.length - 1].deltas.push(delta);
            break;
          case "qct":
            // convert to bezier
            var x1 = xPath[i - 1].x + (2.0 / 3.0) * (pt.x1 - xPath[i - 1].x);
            var y1 = xPath[i - 1].y + (2.0 / 3.0) * (pt.y1 - xPath[i - 1].y);
            var x2 = pt.x + (2.0 / 3.0) * (pt.x1 - pt.x);
            var y2 = pt.y + (2.0 / 3.0) * (pt.y1 - pt.y);
            var x3 = pt.x;
            var y3 = pt.y;
            var delta = [
              x1 - xPath[i - 1].x,
              y1 - xPath[i - 1].y,
              x2 - xPath[i - 1].x,
              y2 - xPath[i - 1].y,
              x3 - xPath[i - 1].x,
              y3 - xPath[i - 1].y
            ];
            moves[moves.length - 1].deltas.push(delta);
            break;
          case "arc":
            //TODO this was hack to avoid out-of-bounds issue
            // No move-to before drawing the arc
            if (moves.length == 0) {
              moves.push({ start: { x: 0, y: 0 }, deltas: [], abs: [] });
            }
            moves[moves.length - 1].arc = true;
            if (Array.isArray(moves[moves.length - 1].abs)) {
              moves[moves.length - 1].abs.push(pt);
            }
            break;
          case "close":
            closed = true;
            break;
        }
      }

      for (var i = 0; i < moves.length; i++) {
        var style;
        if (i == moves.length - 1) {
          style = "s";
        } else {
          style = null;
        }
        if (moves[i].arc) {
          var arcs = moves[i].abs;
          for (var ii = 0; ii < arcs.length; ii++) {
            var arc = arcs[ii];
            var start = (arc.startAngle * 360) / (2 * Math.PI);
            var end = (arc.endAngle * 360) / (2 * Math.PI);
            var x = arc.x;
            var y = arc.y;
            this.internal.arc2(this, x, y, arc.radius, start, end, arc.anticlockwise, style, isClip);
          }
        } else {
          var x = moves[i].start.x;
          var y = moves[i].start.y;
          if (!isClip) {
            this.pdf.lines(moves[i].deltas, x, y, null, style);
          } else {
            this.pdf.lines(moves[i].deltas, x, y, null, null);
            this.pdf.clip_fixed();
          }
        }
      }
    },

    _isFillTransparent: function() {
      return this.ctx._isFillTransparent || this.globalAlpha == 0;
    },

    _isStrokeTransparent: function() {
      return this.ctx._isStrokeTransparent || this.globalAlpha == 0;
    },

    fill: function(fillRule) {
      //evenodd or nonzero (default)
      if (this.ctx._clip_path.length > 0) {
        var lines;
        if (window.outIntercept) {
          lines = window.outIntercept.type === "group" ? window.outIntercept.stream : window.outIntercept;
        } else {
          lines = this.internal.getCurrentPage();
        }
        lines.push("q");

        var origPath = this.path;
        this.path = this.ctx._clip_path;
        this.ctx._clip_path = [];
        this._fill(fillRule, true);

        this.ctx._clip_path = this.path;
        this.path = origPath;
        this._fill(fillRule, false);

        lines.push("Q");
      } else {
        this._fill(fillRule, false);
      }
    },

    _fill: function(fillRule, isClip) {
      if (this._isFillTransparent()) {
        return;
      }
      var v2Support = typeof this.pdf.internal.newObject2 === "function";

      var lines;
      if (window.outIntercept) {
        lines = window.outIntercept.type === "group" ? window.outIntercept.stream : window.outIntercept;
      } else {
        lines = this.internal.getCurrentPage();
      }

      // if (this.ctx._clip_path.length > 0) {
      //     lines.push('q');
      //     var oldPath = this.path;
      //     this.path = this.ctx._clip_path;
      //     this.ctx._clip_path = [];
      //     this._fill(fillRule, true);
      //     this.ctx._clip_path = this.path;
      //     this.path = oldPath;
      //     lines.push('Q');
      // }

      var moves = [];
      var outInterceptOld = window.outIntercept;

      if (v2Support) {
        // Blend and Mask
        switch (this.ctx.globalCompositeOperation) {
          case "normal":
          case "source-over":
            break;
          case "destination-in":
          case "destination-out":
            //TODO this need to be added to the current group or page
            // define a mask stream
            var obj = this.pdf.internal.newStreamObject();

            // define a mask state
            var obj2 = this.pdf.internal.newObject2();
            obj2.push("<</Type /ExtGState");
            obj2.push("/SMask <</S /Alpha /G " + obj.objId + " 0 R>>"); // /S /Luminosity will need to define color space
            obj2.push(">>");

            // add mask to page resources
            var gsName = "MASK" + obj2.objId;
            this.pdf.internal.addGraphicsState(gsName, obj2.objId);

            var instruction = "/" + gsName + " gs";
            // add mask to page, group, or stream
            lines.splice(0, 0, "q");
            lines.splice(1, 0, instruction);
            lines.push("Q");

            window.outIntercept = obj;
            break;
          default:
            var dictionaryEntry = "/" + this.pdf.internal.blendModeMap[this.ctx.globalCompositeOperation.toUpperCase()];
            if (dictionaryEntry) {
              this.pdf.internal.out(dictionaryEntry + " gs");
            }
            break;
        }
      }

      var alpha = this.ctx.globalAlpha;
      if (this.ctx._fillOpacity < 1) {
        // TODO combine this with global opacity
        alpha = this.ctx._fillOpacity;
      }

      //TODO check for an opacity graphics state that was already created
      //TODO do not set opacity if current value is already active
      if (v2Support) {
        var objOpac = this.pdf.internal.newObject2();
        objOpac.push("<</Type /ExtGState");
        //objOpac.push(this.ctx.globalAlpha + " CA"); // Stroke
        //objOpac.push(this.ctx.globalAlpha + " ca"); // Not Stroke
        objOpac.push("/CA " + alpha); // Stroke
        objOpac.push("/ca " + alpha); // Not Stroke
        objOpac.push(">>");
        var gsName = "GS_O_" + objOpac.objId;
        this.pdf.internal.addGraphicsState(gsName, objOpac.objId);
        this.pdf.internal.out("/" + gsName + " gs");
      }

      var xPath = this.path;

      for (var i = 0; i < xPath.length; i++) {
        var pt = xPath[i];
        switch (pt.type) {
          case "mt":
            moves.push({ start: pt, deltas: [], abs: [] });
            break;
          case "lt":
            var delta = [pt.x - xPath[i - 1].x, pt.y - xPath[i - 1].y];
            moves[moves.length - 1].deltas.push(delta);
            moves[moves.length - 1].abs.push(pt);
            break;
          case "bct":
            var delta = [
              pt.x1 - xPath[i - 1].x,
              pt.y1 - xPath[i - 1].y,
              pt.x2 - xPath[i - 1].x,
              pt.y2 - xPath[i - 1].y,
              pt.x - xPath[i - 1].x,
              pt.y - xPath[i - 1].y
            ];
            moves[moves.length - 1].deltas.push(delta);
            break;
          case "qct":
            // convert to bezier
            var x1 = xPath[i - 1].x + (2.0 / 3.0) * (pt.x1 - xPath[i - 1].x);
            var y1 = xPath[i - 1].y + (2.0 / 3.0) * (pt.y1 - xPath[i - 1].y);
            var x2 = pt.x + (2.0 / 3.0) * (pt.x1 - pt.x);
            var y2 = pt.y + (2.0 / 3.0) * (pt.y1 - pt.y);
            var x3 = pt.x;
            var y3 = pt.y;
            var delta = [
              x1 - xPath[i - 1].x,
              y1 - xPath[i - 1].y,
              x2 - xPath[i - 1].x,
              y2 - xPath[i - 1].y,
              x3 - xPath[i - 1].x,
              y3 - xPath[i - 1].y
            ];
            moves[moves.length - 1].deltas.push(delta);
            break;
          case "arc":
            //TODO this was hack to avoid out-of-bounds issue when drawing circle
            // No move-to before drawing the arc
            if (moves.length === 0) {
              moves.push({ deltas: [], abs: [] });
            }
            moves[moves.length - 1].arc = true;
            if (Array.isArray(moves[moves.length - 1].abs)) {
              moves[moves.length - 1].abs.push(pt);
            }
            break;
          case "close":
            moves.push({ close: true });
            break;
        }
      }

      for (var i = 0; i < moves.length; i++) {
        var style;
        if (i == moves.length - 1) {
          style = "f";
          if (fillRule === "evenodd") {
            style += "*";
          }
        } else {
          style = null;
        }

        if (moves[i].close) {
          this.pdf.internal.out("h");
          if (style) {
            // only fill at final path move
            this.pdf.internal.out(style);
          }
        } else if (moves[i].arc) {
          if (moves[i].start) {
            this.internal.move2(this, moves[i].start.x, moves[i].start.y);
          }
          var arcs = moves[i].abs;
          for (var ii = 0; ii < arcs.length; ii++) {
            var arc = arcs[ii];
            //TODO lines deltas were getting in here
            if (typeof arc.startAngle !== "undefined") {
              var start = (arc.startAngle * 360) / (2 * Math.PI);
              var end = (arc.endAngle * 360) / (2 * Math.PI);
              var x = arc.x;
              var y = arc.y;
              if (ii === 0) {
                this.internal.move2(this, x, y);
              }
              this.internal.arc2(this, x, y, arc.radius, start, end, arc.anticlockwise, null, isClip);
              if (ii === arcs.length - 1) {
                // The original arc move did not occur because of the algorithm
                if (moves[i].start) {
                  var x = moves[i].start.x;
                  var y = moves[i].start.y;
                  this.internal.line2(c2d, x, y);
                }
              }
            } else {
              this.internal.line2(c2d, arc.x, arc.y);
            }
          }
        } else {
          var x = moves[i].start.x;
          var y = moves[i].start.y;
          if (!isClip) {
            this.pdf.lines(moves[i].deltas, x, y, null, style);
          } else {
            this.pdf.lines(moves[i].deltas, x, y, null, null);
            this.pdf.clip_fixed();
          }
        }
      }

      window.outIntercept = outInterceptOld;

      // if (this.ctx._clip_path.length > 0) {
      //     lines.push('Q');
      // }
    },

    pushMask: function() {
      var v2Support = typeof this.pdf.internal.newObject2 === "function";

      if (!v2Support) {
        console.log("jsPDF v2 not enabled");
        return;
      }

      // define a mask stream
      var obj = this.pdf.internal.newStreamObject();

      // define a mask state
      var obj2 = this.pdf.internal.newObject2();
      obj2.push("<</Type /ExtGState");
      obj2.push("/SMask <</S /Alpha /G " + obj.objId + " 0 R>>"); // /S /Luminosity will need to define color space
      obj2.push(">>");

      // add mask to page resources
      var gsName = "MASK" + obj2.objId;
      this.pdf.internal.addGraphicsState(gsName, obj2.objId);

      var instruction = "/" + gsName + " gs";
      this.pdf.internal.out(instruction);
    },

    clip: function() {
      //TODO do we reset the path, or just copy it?
      if (this.ctx._clip_path.length > 0) {
        for (var i = 0; i < this.path.length; i++) {
          this.ctx._clip_path.push(this.path[i]);
        }
      } else {
        this.ctx._clip_path = this.path;
      }
      this.path = [];
    },

    measureText: function(text) {
      var pdf = this.pdf;
      return {
        getWidth: function() {
          var fontSize = pdf.internal.getFontSize();
          var txtWidth = (pdf.getStringUnitWidth(text) * fontSize) / pdf.internal.scaleFactor;
          // Convert points to pixels
          txtWidth *= 1.3333;
          return txtWidth;
        },

        get width() {
          return this.getWidth(text);
        }
      };
    },
    _getBaseline: function(y) {
      var height = parseInt(this.pdf.internal.getFontSize());
      // TODO Get descent from font descriptor
      var descent = height * 0.25;
      switch (this.ctx.textBaseline) {
        case "bottom":
          return y - descent;
        case "top":
          return y + height;
        case "hanging":
          return y + height - descent;
        case "middle":
          return y + height / 2 - descent;
        case "ideographic":
          // TODO not implemented
          return y;
        case "alphabetic":
        default:
          return y;
      }
    }
  };

  var c2d = jsPDFAPI.context2d;

  // accessor methods
  Object.defineProperty(c2d, "fillStyle", {
    set: function(value) {
      this.setFillStyle(value);
    },
    get: function() {
      return this.ctx.fillStyle;
    }
  });
  Object.defineProperty(c2d, "strokeStyle", {
    set: function(value) {
      this.setStrokeStyle(value);
    },
    get: function() {
      return this.ctx.strokeStyle;
    }
  });
  Object.defineProperty(c2d, "lineWidth", {
    set: function(value) {
      this.setLineWidth(value);
    },
    get: function() {
      return this.ctx.lineWidth;
    }
  });
  Object.defineProperty(c2d, "lineCap", {
    set: function(val) {
      this.setLineCap(val);
    },
    get: function() {
      return this.ctx.lineCap;
    }
  });
  Object.defineProperty(c2d, "lineJoin", {
    set: function(val) {
      this.setLineJoin(val);
    },
    get: function() {
      return this.ctx.lineJoin;
    }
  });
  Object.defineProperty(c2d, "miterLimit", {
    set: function(val) {
      this.ctx.miterLimit = val;
    },
    get: function() {
      return this.ctx.miterLimit;
    }
  });
  Object.defineProperty(c2d, "textBaseline", {
    set: function(value) {
      this.setTextBaseline(value);
    },
    get: function() {
      return this.getTextBaseline();
    }
  });
  Object.defineProperty(c2d, "textAlign", {
    set: function(value) {
      this.setTextAlign(value);
    },
    get: function() {
      return this.getTextAlign();
    }
  });
  Object.defineProperty(c2d, "font", {
    set: function(value) {
      this.setFont(value);
    },
    get: function() {
      return this.ctx.font;
    }
  });
  Object.defineProperty(c2d, "globalCompositeOperation", {
    set: function(value) {
      this.ctx.globalCompositeOperation = value;
    },
    get: function() {
      return this.ctx.globalCompositeOperation;
    }
  });
  Object.defineProperty(c2d, "globalAlpha", {
    set: function(value) {
      this.ctx.globalAlpha = value;
    },
    get: function() {
      return this.ctx.globalAlpha;
    }
  });
  Object.defineProperty(c2d, "canvas", {
    get: function() {
      return { parentNode: false, style: false };
    }
  });
  // Not HTML API
  Object.defineProperty(c2d, "ignoreClearRect", {
    set: function(value) {
      this.ctx.ignoreClearRect = value;
    },
    get: function() {
      return this.ctx.ignoreClearRect;
    }
  });
  // End Not HTML API

  c2d.internal = {};

  c2d.internal.rxRgb = /rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/;
  c2d.internal.rxRgba = /rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d\.]+)\s*\)/;
  c2d.internal.rxTransparent = /transparent|rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*0+\s*\)/;

  // http://hansmuller-flex.blogspot.com/2011/10/more-about-approximating-circular-arcs.html
  c2d.internal.arc = function(c2d, xc, yc, r, a1, a2, anticlockwise, style) {
    var includeMove = true;

    var k = this.pdf.internal.scaleFactor;
    var pageHeight = this.pdf.internal.pageSize.getHeight();
    var f2 = this.pdf.internal.f2;

    var a1r = a1 * (Math.PI / 180);
    var a2r = a2 * (Math.PI / 180);
    var curves = this.createArc(r, a1r, a2r, anticlockwise);
    var pathData = null;

    for (var i = 0; i < curves.length; i++) {
      var curve = curves[i];
      if (includeMove && i === 0) {
        this.pdf.internal.out(
          [
            f2((curve.x1 + xc) * k),
            f2((pageHeight - (curve.y1 + yc)) * k),
            "m",
            f2((curve.x2 + xc) * k),
            f2((pageHeight - (curve.y2 + yc)) * k),
            f2((curve.x3 + xc) * k),
            f2((pageHeight - (curve.y3 + yc)) * k),
            f2((curve.x4 + xc) * k),
            f2((pageHeight - (curve.y4 + yc)) * k),
            "c"
          ].join(" ")
        );
      } else {
        this.pdf.internal.out(
          [
            f2((curve.x2 + xc) * k),
            f2((pageHeight - (curve.y2 + yc)) * k),
            f2((curve.x3 + xc) * k),
            f2((pageHeight - (curve.y3 + yc)) * k),
            f2((curve.x4 + xc) * k),
            f2((pageHeight - (curve.y4 + yc)) * k),
            "c"
          ].join(" ")
        );
      }

      //c2d._lastPoint = {x: curve.x1 + xc, y: curve.y1 + yc};
      c2d._lastPoint = { x: xc, y: yc };
      // f2((curve.x1 + xc) * k), f2((pageHeight - (curve.y1 + yc)) * k), 'm', f2((curve.x2 + xc) * k), f2((pageHeight - (curve.y2 + yc)) * k), f2((curve.x3 + xc) * k), f2((pageHeight - (curve.y3 + yc)) * k), f2((curve.x4 + xc) * k), f2((pageHeight - (curve.y4 + yc)) * k), 'c'
    }

    if (style !== null) {
      this.pdf.internal.out(this.pdf.internal.getStyle(style));
    }
  };

  /**
   *
   * @param x Edge point X
   * @param y Edge point Y
   * @param r Radius
   * @param a1 start angle
   * @param a2 end angle
   * @param anticlockwise
   * @param style
   * @param isClip
   */
  c2d.internal.arc2 = function(c2d, x, y, r, a1, a2, anticlockwise, style, isClip) {
    // we need to convert from cartesian to polar here methinks.
    var centerX = x; // + r;
    var centerY = y;

    if (false) {
      var phi = a2 - a1;
      var start = {
        x: r,
        y: 0
      };

      var pt1 = {
        x: r,
        y: ((r * 4) / 3) * Math.tan(phi / 4)
      };

      var pt2 = {
        x: r * (Math.cos(phi) + (4 / 3) * Math.tan(phi / 4) * Math.sin(phi)),
        y: r * (Math.sin(phi) - (4 / 3) * Math.tan(phi / 4) * Math.cos(phi))
      };

      var end = {
        x: r * Math.cos(phi),
        y: r * Math.sin(phi)
      };

      var matrix = [Math.cos(a1), Math.sin(a1), -Math.sin(a1), Math.cos(a1), x, y];
      start = c2d._matrix_map_point_obj(matrix, start);
      pt1 = c2d._matrix_map_point_obj(matrix, pt1);
      pt2 = c2d._matrix_map_point_obj(matrix, pt2);
      end = c2d._matrix_map_point_obj(matrix, end);

      var k = this.pdf.internal.scaleFactor;
      var pageHeight = this.pdf.internal.pageSize.getHeight();
      var f2 = this.pdf.internal.f2;
      this.pdf.internal.out(
        [
          f2(start.x * k),
          f2((pageHeight - start.y) * k),
          "m",
          f2(pt1.x * k),
          f2((pageHeight - pt1.y) * k),
          f2(pt2.x * k),
          f2((pageHeight - pt2.y) * k),
          f2(end.x * k),
          f2((pageHeight - end.y) * k),
          "c"
        ].join(" ")
      );
      //this.pdf.internal.out('f');
      c2d._lastPoint = end;
      return;
    }

    if (!isClip) {
      this.arc(c2d, centerX, centerY, r, a1, a2, anticlockwise, style);
    } else {
      this.arc(c2d, centerX, centerY, r, a1, a2, anticlockwise, null);
      this.pdf.clip_fixed();
    }
  };

  c2d.internal.move2 = function(c2d, x, y) {
    var k = this.pdf.internal.scaleFactor;
    var pageHeight = this.pdf.internal.pageSize.getHeight();
    var f2 = this.pdf.internal.f2;

    this.pdf.internal.out([f2(x * k), f2((pageHeight - y) * k), "m"].join(" "));
    c2d._lastPoint = { x: x, y: y };
  };

  c2d.internal.line2 = function(c2d, dx, dy) {
    var k = this.pdf.internal.scaleFactor;
    var pageHeight = this.pdf.internal.pageSize.getHeight();
    var f2 = this.pdf.internal.f2;

    //var pt = {x: c2d._lastPoint.x + dx, y: c2d._lastPoint.y + dy};
    var pt = { x: dx, y: dy };

    this.pdf.internal.out([f2(pt.x * k), f2((pageHeight - pt.y) * k), "l"].join(" "));
    //this.pdf.internal.out('f');
    c2d._lastPoint = pt;
  };

  /**
   * Return a array of objects that represent bezier curves which approximate the circular arc centered at the origin, from startAngle to endAngle (radians) with the specified radius.
   *
   * Each bezier curve is an object with four points, where x1,y1 and x4,y4 are the arc's end points and x2,y2 and x3,y3 are the cubic bezier's control points.
   */

  c2d.internal.createArc = function(radius, startAngle, endAngle, anticlockwise) {
    var EPSILON = 0.00001; // Roughly 1/1000th of a degree, see below
    var twoPI = Math.PI * 2;
    var piOverTwo = Math.PI / 2.0;

    // normalize startAngle, endAngle to [0, 2PI]
    var startAngleN = startAngle;
    if (startAngleN < twoPI || startAngleN > twoPI) {
      startAngleN = startAngleN % twoPI;
    }
    if (startAngleN < 0) {
      startAngleN = twoPI + startAngleN;
    }

    while (startAngle > endAngle) {
      startAngle = startAngle - twoPI;
    }
    var totalAngle = Math.abs(endAngle - startAngle);
    if (totalAngle < twoPI) {
      if (anticlockwise) {
        totalAngle = twoPI - totalAngle;
      }
    }

    // Compute the sequence of arc curves, up to PI/2 at a time.
    var curves = [];
    var sgn = anticlockwise ? -1 : +1;

    var a1 = startAngleN;
    for (; totalAngle > EPSILON; ) {
      var remain = sgn * Math.min(totalAngle, piOverTwo);
      var a2 = a1 + remain;
      curves.push(this.createSmallArc(radius, a1, a2));
      totalAngle -= Math.abs(a2 - a1);
      a1 = a2;
    }

    return curves;
  };

  c2d.internal.getCurrentPage = function() {
    return this.pdf.internal.pages[this.pdf.internal.getCurrentPageInfo().pageNumber];
  };

  /**
   * Cubic bezier approximation of a circular arc centered at the origin, from (radians) a1 to a2, where a2-a1 < pi/2. The arc's radius is r.
   *
   * Returns an object with four points, where x1,y1 and x4,y4 are the arc's end points and x2,y2 and x3,y3 are the cubic bezier's control points.
   *
   * This algorithm is based on the approach described in: A. Riškus, "Approximation of a Cubic Bezier Curve by Circular Arcs and Vice Versa," Information Technology and Control, 35(4), 2006 pp. 371-378.
   */

  c2d.internal.createSmallArc = function(r, a1, a2) {
    // Compute all four points for an arc that subtends the same total angle
    // but is centered on the X-axis

    var a = (a2 - a1) / 2.0;

    var x4 = r * Math.cos(a);
    var y4 = r * Math.sin(a);
    var x1 = x4;
    var y1 = -y4;

    var q1 = x1 * x1 + y1 * y1;
    var q2 = q1 + x1 * x4 + y1 * y4;
    var k2 = ((4 / 3) * (Math.sqrt(2 * q1 * q2) - q2)) / (x1 * y4 - y1 * x4);

    var x2 = x1 - k2 * y1;
    var y2 = y1 + k2 * x1;
    var x3 = x2;
    var y3 = -y2;

    // Find the arc points' actual locations by computing x1,y1 and x4,y4
    // and rotating the control points by a + a1

    var ar = a + a1;
    var cos_ar = Math.cos(ar);
    var sin_ar = Math.sin(ar);

    return {
      x1: r * Math.cos(a1),
      y1: r * Math.sin(a1),
      x2: x2 * cos_ar - y2 * sin_ar,
      y2: x2 * sin_ar + y2 * cos_ar,
      x3: x3 * cos_ar - y3 * sin_ar,
      y3: x3 * sin_ar + y3 * cos_ar,
      x4: r * Math.cos(a2),
      y4: r * Math.sin(a2)
    };
  };

  function context() {
    this._isStrokeTransparent = false;
    this._strokeOpacity = 1;
    this.strokeStyle = "#000000";
    this.fillStyle = "#000000";
    this._isFillTransparent = false;
    this._fillOpacity = 1;
    this.font = "12pt times";
    this.textBaseline = "alphabetic"; // top,bottom,middle,ideographic,alphabetic,hanging
    this.textAlign = "start";
    this.lineWidth = 1;
    this.lineJoin = "miter"; // round, bevel, miter
    this.lineCap = "butt"; // butt, round, square
    this._transform = [1, 0, 0, 1, 0, 0]; // sx, shy, shx, sy, tx, ty
    this.globalCompositeOperation = "normal";
    this.globalAlpha = 1.0;
    this._clip_path = [];

    // TODO miter limit //default 10

    // Not HTML API
    this.ignoreClearRect = false;

    this.copy = function(ctx) {
      this._isStrokeTransparent = ctx._isStrokeTransparent;
      this._strokeOpacity = ctx._strokeOpacity;
      this.strokeStyle = ctx.strokeStyle;
      this._isFillTransparent = ctx._isFillTransparent;
      this._fillOpacity = ctx._fillOpacity;
      this.fillStyle = ctx.fillStyle;
      this.font = ctx.font;
      this.lineWidth = ctx.lineWidth;
      this.lineJoin = ctx.lineJoin;
      this.lineCap = ctx.lineCap;
      this.textBaseline = ctx.textBaseline;
      this.textAlign = ctx.textAlign;
      this._fontSize = ctx._fontSize;
      this._transform = ctx._transform.slice(0);
      this.globalCompositeOperation = ctx.globalCompositeOperation;
      this.globalAlpha = ctx.globalAlpha;
      this._clip_path = ctx._clip_path.slice(0); //TODO deep copy?

      // Not HTML API
      this.ignoreClearRect = ctx.ignoreClearRect;
    };
  }

  return this;
})(
  jsPDF.API,
  (typeof self !== "undefined" && self) ||
    (typeof window !== "undefined" && window) ||
    (typeof global !== "undefined" && global) ||
    Function('return typeof this === "object" && this.content')() ||
    Function("return this")()
);