modules/context2d.js

  1. /* eslint-disable no-fallthrough */
  2. /* eslint-disable no-console */
  3. /**
  4. * @license
  5. * jsPDF Context2D PlugIn Copyright (c) 2014 Steven Spungin (TwelveTone LLC) steven@twelvetone.tv
  6. *
  7. * Licensed under the MIT License. http://opensource.org/licenses/mit-license
  8. */
  9. import { jsPDF } from "../jspdf.js";
  10. import { RGBColor } from "../libs/rgbcolor.js";
  11. import { console } from "../libs/console.js";
  12. import {
  13. buildFontFaceMap,
  14. parseFontFamily,
  15. resolveFontFace
  16. } from "../libs/fontFace.js";
  17. /**
  18. * This plugin mimics the HTML5 CanvasRenderingContext2D.
  19. *
  20. * The goal is to provide a way for current canvas implementations to print directly to a PDF.
  21. *
  22. * @name context2d
  23. * @module
  24. */
  25. (function(jsPDFAPI) {
  26. "use strict";
  27. var ContextLayer = function(ctx) {
  28. ctx = ctx || {};
  29. this.isStrokeTransparent = ctx.isStrokeTransparent || false;
  30. this.strokeOpacity = ctx.strokeOpacity || 1;
  31. this.strokeStyle = ctx.strokeStyle || "#000000";
  32. this.fillStyle = ctx.fillStyle || "#000000";
  33. this.isFillTransparent = ctx.isFillTransparent || false;
  34. this.fillOpacity = ctx.fillOpacity || 1;
  35. this.font = ctx.font || "10px sans-serif";
  36. this.textBaseline = ctx.textBaseline || "alphabetic";
  37. this.textAlign = ctx.textAlign || "left";
  38. this.lineWidth = ctx.lineWidth || 1;
  39. this.lineJoin = ctx.lineJoin || "miter";
  40. this.lineCap = ctx.lineCap || "butt";
  41. this.path = ctx.path || [];
  42. this.transform =
  43. typeof ctx.transform !== "undefined"
  44. ? ctx.transform.clone()
  45. : new Matrix();
  46. this.globalCompositeOperation = ctx.globalCompositeOperation || "normal";
  47. this.globalAlpha = ctx.globalAlpha || 1.0;
  48. this.clip_path = ctx.clip_path || [];
  49. this.currentPoint = ctx.currentPoint || new Point();
  50. this.miterLimit = ctx.miterLimit || 10.0;
  51. this.lastPoint = ctx.lastPoint || new Point();
  52. this.lineDashOffset = ctx.lineDashOffset || 0.0;
  53. this.lineDash = ctx.lineDash || [];
  54. this.margin = ctx.margin || [0, 0, 0, 0];
  55. this.prevPageLastElemOffset = ctx.prevPageLastElemOffset || 0;
  56. this.ignoreClearRect =
  57. typeof ctx.ignoreClearRect === "boolean" ? ctx.ignoreClearRect : true;
  58. return this;
  59. };
  60. //stub
  61. var f2,
  62. getHorizontalCoordinateString,
  63. getVerticalCoordinateString,
  64. getHorizontalCoordinate,
  65. getVerticalCoordinate,
  66. Point,
  67. Rectangle,
  68. Matrix,
  69. _ctx;
  70. jsPDFAPI.events.push([
  71. "initialized",
  72. function() {
  73. this.context2d = new Context2D(this);
  74. f2 = this.internal.f2;
  75. getHorizontalCoordinateString = this.internal.getCoordinateString;
  76. getVerticalCoordinateString = this.internal.getVerticalCoordinateString;
  77. getHorizontalCoordinate = this.internal.getHorizontalCoordinate;
  78. getVerticalCoordinate = this.internal.getVerticalCoordinate;
  79. Point = this.internal.Point;
  80. Rectangle = this.internal.Rectangle;
  81. Matrix = this.internal.Matrix;
  82. _ctx = new ContextLayer();
  83. }
  84. ]);
  85. var Context2D = function(pdf) {
  86. Object.defineProperty(this, "canvas", {
  87. get: function() {
  88. return { parentNode: false, style: false };
  89. }
  90. });
  91. var _pdf = pdf;
  92. Object.defineProperty(this, "pdf", {
  93. get: function() {
  94. return _pdf;
  95. }
  96. });
  97. var _pageWrapXEnabled = false;
  98. /**
  99. * @name pageWrapXEnabled
  100. * @type {boolean}
  101. * @default false
  102. */
  103. Object.defineProperty(this, "pageWrapXEnabled", {
  104. get: function() {
  105. return _pageWrapXEnabled;
  106. },
  107. set: function(value) {
  108. _pageWrapXEnabled = Boolean(value);
  109. }
  110. });
  111. var _pageWrapYEnabled = false;
  112. /**
  113. * @name pageWrapYEnabled
  114. * @type {boolean}
  115. * @default true
  116. */
  117. Object.defineProperty(this, "pageWrapYEnabled", {
  118. get: function() {
  119. return _pageWrapYEnabled;
  120. },
  121. set: function(value) {
  122. _pageWrapYEnabled = Boolean(value);
  123. }
  124. });
  125. var _posX = 0;
  126. /**
  127. * @name posX
  128. * @type {number}
  129. * @default 0
  130. */
  131. Object.defineProperty(this, "posX", {
  132. get: function() {
  133. return _posX;
  134. },
  135. set: function(value) {
  136. if (!isNaN(value)) {
  137. _posX = value;
  138. }
  139. }
  140. });
  141. var _posY = 0;
  142. /**
  143. * @name posY
  144. * @type {number}
  145. * @default 0
  146. */
  147. Object.defineProperty(this, "posY", {
  148. get: function() {
  149. return _posY;
  150. },
  151. set: function(value) {
  152. if (!isNaN(value)) {
  153. _posY = value;
  154. }
  155. }
  156. });
  157. /**
  158. * Gets or sets the page margin when using auto paging. Has no effect when {@link autoPaging} is off.
  159. * @name margin
  160. * @type {number|number[]}
  161. * @default [0, 0, 0, 0]
  162. */
  163. Object.defineProperty(this, "margin", {
  164. get: function() {
  165. return _ctx.margin;
  166. },
  167. set: function(value) {
  168. var margin;
  169. if (typeof value === "number") {
  170. margin = [value, value, value, value];
  171. } else {
  172. margin = new Array(4);
  173. margin[0] = value[0];
  174. margin[1] = value.length >= 2 ? value[1] : margin[0];
  175. margin[2] = value.length >= 3 ? value[2] : margin[0];
  176. margin[3] = value.length >= 4 ? value[3] : margin[1];
  177. }
  178. _ctx.margin = margin;
  179. }
  180. });
  181. var _autoPaging = false;
  182. /**
  183. * Gets or sets the auto paging mode. When auto paging is enabled, the context2d will automatically draw on the
  184. * next page if a shape or text chunk doesn't fit entirely on the current page. The context2d will create new
  185. * pages if required.
  186. *
  187. * Context2d supports different modes:
  188. * <ul>
  189. * <li>
  190. * <code>false</code>: Auto paging is disabled.
  191. * </li>
  192. * <li>
  193. * <code>true</code> or <code>'slice'</code>: Will cut shapes or text chunks across page breaks. Will possibly
  194. * slice text in half, making it difficult to read.
  195. * </li>
  196. * <li>
  197. * <code>'text'</code>: Trys not to cut text in half across page breaks. Works best for documents consisting
  198. * mostly of a single column of text.
  199. * </li>
  200. * </ul>
  201. * @name Context2D#autoPaging
  202. * @type {boolean|"slice"|"text"}
  203. * @default false
  204. */
  205. Object.defineProperty(this, "autoPaging", {
  206. get: function() {
  207. return _autoPaging;
  208. },
  209. set: function(value) {
  210. _autoPaging = value;
  211. }
  212. });
  213. var lastBreak = 0;
  214. /**
  215. * @name lastBreak
  216. * @type {number}
  217. * @default 0
  218. */
  219. Object.defineProperty(this, "lastBreak", {
  220. get: function() {
  221. return lastBreak;
  222. },
  223. set: function(value) {
  224. lastBreak = value;
  225. }
  226. });
  227. var pageBreaks = [];
  228. /**
  229. * Y Position of page breaks.
  230. * @name pageBreaks
  231. * @type {number}
  232. * @default 0
  233. */
  234. Object.defineProperty(this, "pageBreaks", {
  235. get: function() {
  236. return pageBreaks;
  237. },
  238. set: function(value) {
  239. pageBreaks = value;
  240. }
  241. });
  242. /**
  243. * @name ctx
  244. * @type {object}
  245. * @default {}
  246. */
  247. Object.defineProperty(this, "ctx", {
  248. get: function() {
  249. return _ctx;
  250. },
  251. set: function(value) {
  252. if (value instanceof ContextLayer) {
  253. _ctx = value;
  254. }
  255. }
  256. });
  257. /**
  258. * @name path
  259. * @type {array}
  260. * @default []
  261. */
  262. Object.defineProperty(this, "path", {
  263. get: function() {
  264. return _ctx.path;
  265. },
  266. set: function(value) {
  267. _ctx.path = value;
  268. }
  269. });
  270. /**
  271. * @name ctxStack
  272. * @type {array}
  273. * @default []
  274. */
  275. var _ctxStack = [];
  276. Object.defineProperty(this, "ctxStack", {
  277. get: function() {
  278. return _ctxStack;
  279. },
  280. set: function(value) {
  281. _ctxStack = value;
  282. }
  283. });
  284. /**
  285. * Sets or returns the color, gradient, or pattern used to fill the drawing
  286. *
  287. * @name fillStyle
  288. * @default #000000
  289. * @property {(color|gradient|pattern)} value The color of the drawing. Default value is #000000<br />
  290. * A gradient object (linear or radial) used to fill the drawing (not supported by context2d)<br />
  291. * A pattern object to use to fill the drawing (not supported by context2d)
  292. */
  293. Object.defineProperty(this, "fillStyle", {
  294. get: function() {
  295. return this.ctx.fillStyle;
  296. },
  297. set: function(value) {
  298. var rgba;
  299. rgba = getRGBA(value);
  300. this.ctx.fillStyle = rgba.style;
  301. this.ctx.isFillTransparent = rgba.a === 0;
  302. this.ctx.fillOpacity = rgba.a;
  303. this.pdf.setFillColor(rgba.r, rgba.g, rgba.b, { a: rgba.a });
  304. this.pdf.setTextColor(rgba.r, rgba.g, rgba.b, { a: rgba.a });
  305. }
  306. });
  307. /**
  308. * Sets or returns the color, gradient, or pattern used for strokes
  309. *
  310. * @name strokeStyle
  311. * @default #000000
  312. * @property {color} color A CSS color value that indicates the stroke color of the drawing. Default value is #000000 (not supported by context2d)
  313. * @property {gradient} gradient A gradient object (linear or radial) used to create a gradient stroke (not supported by context2d)
  314. * @property {pattern} pattern A pattern object used to create a pattern stroke (not supported by context2d)
  315. */
  316. Object.defineProperty(this, "strokeStyle", {
  317. get: function() {
  318. return this.ctx.strokeStyle;
  319. },
  320. set: function(value) {
  321. var rgba = getRGBA(value);
  322. this.ctx.strokeStyle = rgba.style;
  323. this.ctx.isStrokeTransparent = rgba.a === 0;
  324. this.ctx.strokeOpacity = rgba.a;
  325. if (rgba.a === 0) {
  326. this.pdf.setDrawColor(255, 255, 255);
  327. } else if (rgba.a === 1) {
  328. this.pdf.setDrawColor(rgba.r, rgba.g, rgba.b);
  329. } else {
  330. this.pdf.setDrawColor(rgba.r, rgba.g, rgba.b);
  331. }
  332. }
  333. });
  334. /**
  335. * Sets or returns the style of the end caps for a line
  336. *
  337. * @name lineCap
  338. * @default butt
  339. * @property {(butt|round|square)} lineCap butt A flat edge is added to each end of the line <br/>
  340. * round A rounded end cap is added to each end of the line<br/>
  341. * square A square end cap is added to each end of the line<br/>
  342. */
  343. Object.defineProperty(this, "lineCap", {
  344. get: function() {
  345. return this.ctx.lineCap;
  346. },
  347. set: function(value) {
  348. if (["butt", "round", "square"].indexOf(value) !== -1) {
  349. this.ctx.lineCap = value;
  350. this.pdf.setLineCap(value);
  351. }
  352. }
  353. });
  354. /**
  355. * Sets or returns the current line width
  356. *
  357. * @name lineWidth
  358. * @default 1
  359. * @property {number} lineWidth The current line width, in pixels
  360. */
  361. Object.defineProperty(this, "lineWidth", {
  362. get: function() {
  363. return this.ctx.lineWidth;
  364. },
  365. set: function(value) {
  366. if (!isNaN(value)) {
  367. this.ctx.lineWidth = value;
  368. this.pdf.setLineWidth(value);
  369. }
  370. }
  371. });
  372. /**
  373. * Sets or returns the type of corner created, when two lines meet
  374. */
  375. Object.defineProperty(this, "lineJoin", {
  376. get: function() {
  377. return this.ctx.lineJoin;
  378. },
  379. set: function(value) {
  380. if (["bevel", "round", "miter"].indexOf(value) !== -1) {
  381. this.ctx.lineJoin = value;
  382. this.pdf.setLineJoin(value);
  383. }
  384. }
  385. });
  386. /**
  387. * A number specifying the miter limit ratio in coordinate space units. Zero, negative, Infinity, and NaN values are ignored. The default value is 10.0.
  388. *
  389. * @name miterLimit
  390. * @default 10
  391. */
  392. Object.defineProperty(this, "miterLimit", {
  393. get: function() {
  394. return this.ctx.miterLimit;
  395. },
  396. set: function(value) {
  397. if (!isNaN(value)) {
  398. this.ctx.miterLimit = value;
  399. this.pdf.setMiterLimit(value);
  400. }
  401. }
  402. });
  403. Object.defineProperty(this, "textBaseline", {
  404. get: function() {
  405. return this.ctx.textBaseline;
  406. },
  407. set: function(value) {
  408. this.ctx.textBaseline = value;
  409. }
  410. });
  411. Object.defineProperty(this, "textAlign", {
  412. get: function() {
  413. return this.ctx.textAlign;
  414. },
  415. set: function(value) {
  416. if (["right", "end", "center", "left", "start"].indexOf(value) !== -1) {
  417. this.ctx.textAlign = value;
  418. }
  419. }
  420. });
  421. var _fontFaceMap = null;
  422. function getFontFaceMap(pdf, fontFaces) {
  423. if (_fontFaceMap === null) {
  424. var fontMap = pdf.getFontList();
  425. var convertedFontFaces = convertToFontFaces(fontMap);
  426. _fontFaceMap = buildFontFaceMap(convertedFontFaces.concat(fontFaces));
  427. }
  428. return _fontFaceMap;
  429. }
  430. function convertToFontFaces(fontMap) {
  431. var fontFaces = [];
  432. Object.keys(fontMap).forEach(function(family) {
  433. var styles = fontMap[family];
  434. styles.forEach(function(style) {
  435. var fontFace = null;
  436. switch (style) {
  437. case "bold":
  438. fontFace = {
  439. family: family,
  440. weight: "bold"
  441. };
  442. break;
  443. case "italic":
  444. fontFace = {
  445. family: family,
  446. style: "italic"
  447. };
  448. break;
  449. case "bolditalic":
  450. fontFace = {
  451. family: family,
  452. weight: "bold",
  453. style: "italic"
  454. };
  455. break;
  456. case "":
  457. case "normal":
  458. fontFace = {
  459. family: family
  460. };
  461. break;
  462. }
  463. // If font-face is still null here, it is a font with some styling we don't recognize and
  464. // cannot map or it is a font added via the fontFaces option of .html().
  465. if (fontFace !== null) {
  466. fontFace.ref = {
  467. name: family,
  468. style: style
  469. };
  470. fontFaces.push(fontFace);
  471. }
  472. });
  473. });
  474. return fontFaces;
  475. }
  476. var _fontFaces = null;
  477. /**
  478. * A map of available font-faces, as passed in the options of
  479. * .html(). If set a limited implementation of the font style matching
  480. * algorithm defined by https://www.w3.org/TR/css-fonts-3/#font-matching-algorithm
  481. * will be used. If not set it will fallback to previous behavior.
  482. */
  483. Object.defineProperty(this, "fontFaces", {
  484. get: function() {
  485. return _fontFaces;
  486. },
  487. set: function(value) {
  488. _fontFaceMap = null;
  489. _fontFaces = value;
  490. }
  491. });
  492. Object.defineProperty(this, "font", {
  493. get: function() {
  494. return this.ctx.font;
  495. },
  496. set: function(value) {
  497. this.ctx.font = value;
  498. var rx, matches;
  499. //source: https://stackoverflow.com/a/10136041
  500. // eslint-disable-next-line no-useless-escape
  501. rx = /^\s*(?=(?:(?:[-a-z]+\s*){0,2}(italic|oblique))?)(?=(?:(?:[-a-z]+\s*){0,2}(small-caps))?)(?=(?:(?:[-a-z]+\s*){0,2}(bold(?:er)?|lighter|[1-9]00))?)(?:(?:normal|\1|\2|\3)\s*){0,3}((?:xx?-)?(?:small|large)|medium|smaller|larger|[.\d]+(?:\%|in|[cem]m|ex|p[ctx]))(?:\s*\/\s*(normal|[.\d]+(?:\%|in|[cem]m|ex|p[ctx])))?\s*([-_,\"\'\sa-z]+?)\s*$/i;
  502. matches = rx.exec(value);
  503. if (matches !== null) {
  504. var fontStyle = matches[1];
  505. var fontVariant = matches[2];
  506. var fontWeight = matches[3];
  507. var fontSize = matches[4];
  508. var lineHeight = matches[5];
  509. var fontFamily = matches[6];
  510. } else {
  511. return;
  512. }
  513. var rxFontSize = /^([.\d]+)((?:%|in|[cem]m|ex|p[ctx]))$/i;
  514. var fontSizeUnit = rxFontSize.exec(fontSize)[2];
  515. if ("px" === fontSizeUnit) {
  516. fontSize = Math.floor(
  517. parseFloat(fontSize) * this.pdf.internal.scaleFactor
  518. );
  519. } else if ("em" === fontSizeUnit) {
  520. fontSize = Math.floor(parseFloat(fontSize) * this.pdf.getFontSize());
  521. } else {
  522. fontSize = Math.floor(
  523. parseFloat(fontSize) * this.pdf.internal.scaleFactor
  524. );
  525. }
  526. this.pdf.setFontSize(fontSize);
  527. var parts = parseFontFamily(fontFamily);
  528. if (this.fontFaces) {
  529. var fontFaceMap = getFontFaceMap(this.pdf, this.fontFaces);
  530. var rules = parts.map(function(ff) {
  531. return {
  532. family: ff,
  533. stretch: "normal", // TODO: Extract font-stretch from font rule (perhaps write proper parser for it?)
  534. weight: fontWeight,
  535. style: fontStyle
  536. };
  537. });
  538. var font = resolveFontFace(fontFaceMap, rules);
  539. this.pdf.setFont(font.ref.name, font.ref.style);
  540. return;
  541. }
  542. var style = "";
  543. if (
  544. fontWeight === "bold" ||
  545. parseInt(fontWeight, 10) >= 700 ||
  546. fontStyle === "bold"
  547. ) {
  548. style = "bold";
  549. }
  550. if (fontStyle === "italic") {
  551. style += "italic";
  552. }
  553. if (style.length === 0) {
  554. style = "normal";
  555. }
  556. var jsPdfFontName = "";
  557. var fallbackFonts = {
  558. arial: "Helvetica",
  559. Arial: "Helvetica",
  560. verdana: "Helvetica",
  561. Verdana: "Helvetica",
  562. helvetica: "Helvetica",
  563. Helvetica: "Helvetica",
  564. "sans-serif": "Helvetica",
  565. fixed: "Courier",
  566. monospace: "Courier",
  567. terminal: "Courier",
  568. cursive: "Times",
  569. fantasy: "Times",
  570. serif: "Times"
  571. };
  572. for (var i = 0; i < parts.length; i++) {
  573. if (
  574. this.pdf.internal.getFont(parts[i], style, {
  575. noFallback: true,
  576. disableWarning: true
  577. }) !== undefined
  578. ) {
  579. jsPdfFontName = parts[i];
  580. break;
  581. } else if (
  582. style === "bolditalic" &&
  583. this.pdf.internal.getFont(parts[i], "bold", {
  584. noFallback: true,
  585. disableWarning: true
  586. }) !== undefined
  587. ) {
  588. jsPdfFontName = parts[i];
  589. style = "bold";
  590. } else if (
  591. this.pdf.internal.getFont(parts[i], "normal", {
  592. noFallback: true,
  593. disableWarning: true
  594. }) !== undefined
  595. ) {
  596. jsPdfFontName = parts[i];
  597. style = "normal";
  598. break;
  599. }
  600. }
  601. if (jsPdfFontName === "") {
  602. for (var j = 0; j < parts.length; j++) {
  603. if (fallbackFonts[parts[j]]) {
  604. jsPdfFontName = fallbackFonts[parts[j]];
  605. break;
  606. }
  607. }
  608. }
  609. jsPdfFontName = jsPdfFontName === "" ? "Times" : jsPdfFontName;
  610. this.pdf.setFont(jsPdfFontName, style);
  611. }
  612. });
  613. Object.defineProperty(this, "globalCompositeOperation", {
  614. get: function() {
  615. return this.ctx.globalCompositeOperation;
  616. },
  617. set: function(value) {
  618. this.ctx.globalCompositeOperation = value;
  619. }
  620. });
  621. Object.defineProperty(this, "globalAlpha", {
  622. get: function() {
  623. return this.ctx.globalAlpha;
  624. },
  625. set: function(value) {
  626. this.ctx.globalAlpha = value;
  627. }
  628. });
  629. /**
  630. * A float specifying the amount of the line dash offset. The default value is 0.0.
  631. *
  632. * @name lineDashOffset
  633. * @default 0.0
  634. */
  635. Object.defineProperty(this, "lineDashOffset", {
  636. get: function() {
  637. return this.ctx.lineDashOffset;
  638. },
  639. set: function(value) {
  640. this.ctx.lineDashOffset = value;
  641. setLineDash.call(this);
  642. }
  643. });
  644. // Not HTML API
  645. Object.defineProperty(this, "lineDash", {
  646. get: function() {
  647. return this.ctx.lineDash;
  648. },
  649. set: function(value) {
  650. this.ctx.lineDash = value;
  651. setLineDash.call(this);
  652. }
  653. });
  654. // Not HTML API
  655. Object.defineProperty(this, "ignoreClearRect", {
  656. get: function() {
  657. return this.ctx.ignoreClearRect;
  658. },
  659. set: function(value) {
  660. this.ctx.ignoreClearRect = Boolean(value);
  661. }
  662. });
  663. };
  664. /**
  665. * Sets the line dash pattern used when stroking lines.
  666. * @name setLineDash
  667. * @function
  668. * @description It uses an array of values that specify alternating lengths of lines and gaps which describe the pattern.
  669. */
  670. Context2D.prototype.setLineDash = function(dashArray) {
  671. this.lineDash = dashArray;
  672. };
  673. /**
  674. * gets the current line dash pattern.
  675. * @name getLineDash
  676. * @function
  677. * @returns {Array} An Array of numbers that specify distances to alternately draw a line and a gap (in coordinate space units). If the number, when setting the elements, is odd, the elements of the array get copied and concatenated. For example, setting the line dash to [5, 15, 25] will result in getting back [5, 15, 25, 5, 15, 25].
  678. */
  679. Context2D.prototype.getLineDash = function() {
  680. if (this.lineDash.length % 2) {
  681. // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getLineDash#return_value
  682. return this.lineDash.concat(this.lineDash);
  683. } else {
  684. // The copied value is returned to prevent contamination from outside.
  685. return this.lineDash.slice();
  686. }
  687. };
  688. Context2D.prototype.fill = function() {
  689. pathPreProcess.call(this, "fill", false);
  690. };
  691. /**
  692. * Actually draws the path you have defined
  693. *
  694. * @name stroke
  695. * @function
  696. * @description The stroke() method actually draws the path you have defined with all those moveTo() and lineTo() methods. The default color is black.
  697. */
  698. Context2D.prototype.stroke = function() {
  699. pathPreProcess.call(this, "stroke", false);
  700. };
  701. /**
  702. * Begins a path, or resets the current
  703. *
  704. * @name beginPath
  705. * @function
  706. * @description The beginPath() method begins a path, or resets the current path.
  707. */
  708. Context2D.prototype.beginPath = function() {
  709. this.path = [
  710. {
  711. type: "begin"
  712. }
  713. ];
  714. };
  715. /**
  716. * Moves the path to the specified point in the canvas, without creating a line
  717. *
  718. * @name moveTo
  719. * @function
  720. * @param x {Number} The x-coordinate of where to move the path to
  721. * @param y {Number} The y-coordinate of where to move the path to
  722. */
  723. Context2D.prototype.moveTo = function(x, y) {
  724. if (isNaN(x) || isNaN(y)) {
  725. console.error("jsPDF.context2d.moveTo: Invalid arguments", arguments);
  726. throw new Error("Invalid arguments passed to jsPDF.context2d.moveTo");
  727. }
  728. var pt = this.ctx.transform.applyToPoint(new Point(x, y));
  729. this.path.push({
  730. type: "mt",
  731. x: pt.x,
  732. y: pt.y
  733. });
  734. this.ctx.lastPoint = new Point(x, y);
  735. };
  736. /**
  737. * Creates a path from the current point back to the starting point
  738. *
  739. * @name closePath
  740. * @function
  741. * @description The closePath() method creates a path from the current point back to the starting point.
  742. */
  743. Context2D.prototype.closePath = function() {
  744. var pathBegin = new Point(0, 0);
  745. var i = 0;
  746. for (i = this.path.length - 1; i !== -1; i--) {
  747. if (this.path[i].type === "begin") {
  748. if (
  749. typeof this.path[i + 1] === "object" &&
  750. typeof this.path[i + 1].x === "number"
  751. ) {
  752. pathBegin = new Point(this.path[i + 1].x, this.path[i + 1].y);
  753. break;
  754. }
  755. }
  756. }
  757. this.path.push({
  758. type: "close"
  759. });
  760. this.ctx.lastPoint = new Point(pathBegin.x, pathBegin.y);
  761. };
  762. /**
  763. * Adds a new point and creates a line to that point from the last specified point in the canvas
  764. *
  765. * @name lineTo
  766. * @function
  767. * @param x The x-coordinate of where to create the line to
  768. * @param y The y-coordinate of where to create the line to
  769. * @description The lineTo() method adds a new point and creates a line TO that point FROM the last specified point in the canvas (this method does not draw the line).
  770. */
  771. Context2D.prototype.lineTo = function(x, y) {
  772. if (isNaN(x) || isNaN(y)) {
  773. console.error("jsPDF.context2d.lineTo: Invalid arguments", arguments);
  774. throw new Error("Invalid arguments passed to jsPDF.context2d.lineTo");
  775. }
  776. var pt = this.ctx.transform.applyToPoint(new Point(x, y));
  777. this.path.push({
  778. type: "lt",
  779. x: pt.x,
  780. y: pt.y
  781. });
  782. this.ctx.lastPoint = new Point(pt.x, pt.y);
  783. };
  784. /**
  785. * Clips a region of any shape and size from the original canvas
  786. *
  787. * @name clip
  788. * @function
  789. * @description The clip() method clips a region of any shape and size from the original canvas.
  790. */
  791. Context2D.prototype.clip = function() {
  792. this.ctx.clip_path = JSON.parse(JSON.stringify(this.path));
  793. pathPreProcess.call(this, null, true);
  794. };
  795. /**
  796. * Creates a cubic Bézier curve
  797. *
  798. * @name quadraticCurveTo
  799. * @function
  800. * @param cpx {Number} The x-coordinate of the Bézier control point
  801. * @param cpy {Number} The y-coordinate of the Bézier control point
  802. * @param x {Number} The x-coordinate of the ending point
  803. * @param y {Number} The y-coordinate of the ending point
  804. * @description The quadraticCurveTo() method adds a point to the current path by using the specified control points that represent a quadratic Bézier curve.<br /><br /> A quadratic Bézier curve requires two points. The first point is a control point that is used in the quadratic Bézier calculation and the second point is the ending point for the curve. The starting point for the curve is the last point in the current path. If a path does not exist, use the beginPath() and moveTo() methods to define a starting point.
  805. */
  806. Context2D.prototype.quadraticCurveTo = function(cpx, cpy, x, y) {
  807. if (isNaN(x) || isNaN(y) || isNaN(cpx) || isNaN(cpy)) {
  808. console.error(
  809. "jsPDF.context2d.quadraticCurveTo: Invalid arguments",
  810. arguments
  811. );
  812. throw new Error(
  813. "Invalid arguments passed to jsPDF.context2d.quadraticCurveTo"
  814. );
  815. }
  816. var pt0 = this.ctx.transform.applyToPoint(new Point(x, y));
  817. var pt1 = this.ctx.transform.applyToPoint(new Point(cpx, cpy));
  818. this.path.push({
  819. type: "qct",
  820. x1: pt1.x,
  821. y1: pt1.y,
  822. x: pt0.x,
  823. y: pt0.y
  824. });
  825. this.ctx.lastPoint = new Point(pt0.x, pt0.y);
  826. };
  827. /**
  828. * Creates a cubic Bézier curve
  829. *
  830. * @name bezierCurveTo
  831. * @function
  832. * @param cp1x {Number} The x-coordinate of the first Bézier control point
  833. * @param cp1y {Number} The y-coordinate of the first Bézier control point
  834. * @param cp2x {Number} The x-coordinate of the second Bézier control point
  835. * @param cp2y {Number} The y-coordinate of the second Bézier control point
  836. * @param x {Number} The x-coordinate of the ending point
  837. * @param y {Number} The y-coordinate of the ending point
  838. * @description The bezierCurveTo() method adds a point to the current path by using the specified control points that represent a cubic Bézier curve. <br /><br />A cubic bezier curve requires three points. The first two points are control points that are used in the cubic Bézier calculation and the last point is the ending point for the curve. The starting point for the curve is the last point in the current path. If a path does not exist, use the beginPath() and moveTo() methods to define a starting point.
  839. */
  840. Context2D.prototype.bezierCurveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) {
  841. if (
  842. isNaN(x) ||
  843. isNaN(y) ||
  844. isNaN(cp1x) ||
  845. isNaN(cp1y) ||
  846. isNaN(cp2x) ||
  847. isNaN(cp2y)
  848. ) {
  849. console.error(
  850. "jsPDF.context2d.bezierCurveTo: Invalid arguments",
  851. arguments
  852. );
  853. throw new Error(
  854. "Invalid arguments passed to jsPDF.context2d.bezierCurveTo"
  855. );
  856. }
  857. var pt0 = this.ctx.transform.applyToPoint(new Point(x, y));
  858. var pt1 = this.ctx.transform.applyToPoint(new Point(cp1x, cp1y));
  859. var pt2 = this.ctx.transform.applyToPoint(new Point(cp2x, cp2y));
  860. this.path.push({
  861. type: "bct",
  862. x1: pt1.x,
  863. y1: pt1.y,
  864. x2: pt2.x,
  865. y2: pt2.y,
  866. x: pt0.x,
  867. y: pt0.y
  868. });
  869. this.ctx.lastPoint = new Point(pt0.x, pt0.y);
  870. };
  871. /**
  872. * Creates an arc/curve (used to create circles, or parts of circles)
  873. *
  874. * @name arc
  875. * @function
  876. * @param x {Number} The x-coordinate of the center of the circle
  877. * @param y {Number} The y-coordinate of the center of the circle
  878. * @param radius {Number} The radius of the circle
  879. * @param startAngle {Number} The starting angle, in radians (0 is at the 3 o'clock position of the arc's circle)
  880. * @param endAngle {Number} The ending angle, in radians
  881. * @param counterclockwise {Boolean} Optional. Specifies whether the drawing should be counterclockwise or clockwise. False is default, and indicates clockwise, while true indicates counter-clockwise.
  882. * @description The arc() method creates an arc/curve (used to create circles, or parts of circles).
  883. */
  884. Context2D.prototype.arc = function(
  885. x,
  886. y,
  887. radius,
  888. startAngle,
  889. endAngle,
  890. counterclockwise
  891. ) {
  892. if (
  893. isNaN(x) ||
  894. isNaN(y) ||
  895. isNaN(radius) ||
  896. isNaN(startAngle) ||
  897. isNaN(endAngle)
  898. ) {
  899. console.error("jsPDF.context2d.arc: Invalid arguments", arguments);
  900. throw new Error("Invalid arguments passed to jsPDF.context2d.arc");
  901. }
  902. counterclockwise = Boolean(counterclockwise);
  903. if (!this.ctx.transform.isIdentity) {
  904. var xpt = this.ctx.transform.applyToPoint(new Point(x, y));
  905. x = xpt.x;
  906. y = xpt.y;
  907. var x_radPt = this.ctx.transform.applyToPoint(new Point(0, radius));
  908. var x_radPt0 = this.ctx.transform.applyToPoint(new Point(0, 0));
  909. radius = Math.sqrt(
  910. Math.pow(x_radPt.x - x_radPt0.x, 2) +
  911. Math.pow(x_radPt.y - x_radPt0.y, 2)
  912. );
  913. }
  914. if (Math.abs(endAngle - startAngle) >= 2 * Math.PI) {
  915. startAngle = 0;
  916. endAngle = 2 * Math.PI;
  917. }
  918. this.path.push({
  919. type: "arc",
  920. x: x,
  921. y: y,
  922. radius: radius,
  923. startAngle: startAngle,
  924. endAngle: endAngle,
  925. counterclockwise: counterclockwise
  926. });
  927. // this.ctx.lastPoint(new Point(pt.x,pt.y));
  928. };
  929. /**
  930. * Creates an arc/curve between two tangents
  931. *
  932. * @name arcTo
  933. * @function
  934. * @param x1 {Number} The x-coordinate of the first tangent
  935. * @param y1 {Number} The y-coordinate of the first tangent
  936. * @param x2 {Number} The x-coordinate of the second tangent
  937. * @param y2 {Number} The y-coordinate of the second tangent
  938. * @param radius The radius of the arc
  939. * @description The arcTo() method creates an arc/curve between two tangents on the canvas.
  940. */
  941. // eslint-disable-next-line no-unused-vars
  942. Context2D.prototype.arcTo = function(x1, y1, x2, y2, radius) {
  943. throw new Error("arcTo not implemented.");
  944. };
  945. /**
  946. * Creates a rectangle
  947. *
  948. * @name rect
  949. * @function
  950. * @param x {Number} The x-coordinate of the upper-left corner of the rectangle
  951. * @param y {Number} The y-coordinate of the upper-left corner of the rectangle
  952. * @param w {Number} The width of the rectangle, in pixels
  953. * @param h {Number} The height of the rectangle, in pixels
  954. * @description The rect() method creates a rectangle.
  955. */
  956. Context2D.prototype.rect = function(x, y, w, h) {
  957. if (isNaN(x) || isNaN(y) || isNaN(w) || isNaN(h)) {
  958. console.error("jsPDF.context2d.rect: Invalid arguments", arguments);
  959. throw new Error("Invalid arguments passed to jsPDF.context2d.rect");
  960. }
  961. this.moveTo(x, y);
  962. this.lineTo(x + w, y);
  963. this.lineTo(x + w, y + h);
  964. this.lineTo(x, y + h);
  965. this.lineTo(x, y);
  966. this.lineTo(x + w, y);
  967. this.lineTo(x, y);
  968. };
  969. /**
  970. * Draws a "filled" rectangle
  971. *
  972. * @name fillRect
  973. * @function
  974. * @param x {Number} The x-coordinate of the upper-left corner of the rectangle
  975. * @param y {Number} The y-coordinate of the upper-left corner of the rectangle
  976. * @param w {Number} The width of the rectangle, in pixels
  977. * @param h {Number} The height of the rectangle, in pixels
  978. * @description The fillRect() method draws a "filled" rectangle. The default color of the fill is black.
  979. */
  980. Context2D.prototype.fillRect = function(x, y, w, h) {
  981. if (isNaN(x) || isNaN(y) || isNaN(w) || isNaN(h)) {
  982. console.error("jsPDF.context2d.fillRect: Invalid arguments", arguments);
  983. throw new Error("Invalid arguments passed to jsPDF.context2d.fillRect");
  984. }
  985. if (isFillTransparent.call(this)) {
  986. return;
  987. }
  988. var tmp = {};
  989. if (this.lineCap !== "butt") {
  990. tmp.lineCap = this.lineCap;
  991. this.lineCap = "butt";
  992. }
  993. if (this.lineJoin !== "miter") {
  994. tmp.lineJoin = this.lineJoin;
  995. this.lineJoin = "miter";
  996. }
  997. this.beginPath();
  998. this.rect(x, y, w, h);
  999. this.fill();
  1000. if (tmp.hasOwnProperty("lineCap")) {
  1001. this.lineCap = tmp.lineCap;
  1002. }
  1003. if (tmp.hasOwnProperty("lineJoin")) {
  1004. this.lineJoin = tmp.lineJoin;
  1005. }
  1006. };
  1007. /**
  1008. * Draws a rectangle (no fill)
  1009. *
  1010. * @name strokeRect
  1011. * @function
  1012. * @param x {Number} The x-coordinate of the upper-left corner of the rectangle
  1013. * @param y {Number} The y-coordinate of the upper-left corner of the rectangle
  1014. * @param w {Number} The width of the rectangle, in pixels
  1015. * @param h {Number} The height of the rectangle, in pixels
  1016. * @description The strokeRect() method draws a rectangle (no fill). The default color of the stroke is black.
  1017. */
  1018. Context2D.prototype.strokeRect = function strokeRect(x, y, w, h) {
  1019. if (isNaN(x) || isNaN(y) || isNaN(w) || isNaN(h)) {
  1020. console.error("jsPDF.context2d.strokeRect: Invalid arguments", arguments);
  1021. throw new Error("Invalid arguments passed to jsPDF.context2d.strokeRect");
  1022. }
  1023. if (isStrokeTransparent.call(this)) {
  1024. return;
  1025. }
  1026. this.beginPath();
  1027. this.rect(x, y, w, h);
  1028. this.stroke();
  1029. };
  1030. /**
  1031. * Clears the specified pixels within a given rectangle
  1032. *
  1033. * @name clearRect
  1034. * @function
  1035. * @param x {Number} The x-coordinate of the upper-left corner of the rectangle
  1036. * @param y {Number} The y-coordinate of the upper-left corner of the rectangle
  1037. * @param w {Number} The width of the rectangle to clear, in pixels
  1038. * @param h {Number} The height of the rectangle to clear, in pixels
  1039. * @description We cannot clear PDF commands that were already written to PDF, so we use white instead. <br />
  1040. * As a special case, read a special flag (ignoreClearRect) and do nothing if it is set.
  1041. * This results in all calls to clearRect() to do nothing, and keep the canvas transparent.
  1042. * This flag is stored in the save/restore context and is managed the same way as other drawing states.
  1043. *
  1044. */
  1045. Context2D.prototype.clearRect = function(x, y, w, h) {
  1046. if (isNaN(x) || isNaN(y) || isNaN(w) || isNaN(h)) {
  1047. console.error("jsPDF.context2d.clearRect: Invalid arguments", arguments);
  1048. throw new Error("Invalid arguments passed to jsPDF.context2d.clearRect");
  1049. }
  1050. if (this.ignoreClearRect) {
  1051. return;
  1052. }
  1053. this.fillStyle = "#ffffff";
  1054. this.fillRect(x, y, w, h);
  1055. };
  1056. /**
  1057. * Saves the state of the current context
  1058. *
  1059. * @name save
  1060. * @function
  1061. */
  1062. Context2D.prototype.save = function(doStackPush) {
  1063. doStackPush = typeof doStackPush === "boolean" ? doStackPush : true;
  1064. var tmpPageNumber = this.pdf.internal.getCurrentPageInfo().pageNumber;
  1065. for (var i = 0; i < this.pdf.internal.getNumberOfPages(); i++) {
  1066. this.pdf.setPage(i + 1);
  1067. this.pdf.internal.out("q");
  1068. }
  1069. this.pdf.setPage(tmpPageNumber);
  1070. if (doStackPush) {
  1071. this.ctx.fontSize = this.pdf.internal.getFontSize();
  1072. var ctx = new ContextLayer(this.ctx);
  1073. this.ctxStack.push(this.ctx);
  1074. this.ctx = ctx;
  1075. }
  1076. };
  1077. /**
  1078. * Returns previously saved path state and attributes
  1079. *
  1080. * @name restore
  1081. * @function
  1082. */
  1083. Context2D.prototype.restore = function(doStackPop) {
  1084. doStackPop = typeof doStackPop === "boolean" ? doStackPop : true;
  1085. var tmpPageNumber = this.pdf.internal.getCurrentPageInfo().pageNumber;
  1086. for (var i = 0; i < this.pdf.internal.getNumberOfPages(); i++) {
  1087. this.pdf.setPage(i + 1);
  1088. this.pdf.internal.out("Q");
  1089. }
  1090. this.pdf.setPage(tmpPageNumber);
  1091. if (doStackPop && this.ctxStack.length !== 0) {
  1092. this.ctx = this.ctxStack.pop();
  1093. this.fillStyle = this.ctx.fillStyle;
  1094. this.strokeStyle = this.ctx.strokeStyle;
  1095. this.font = this.ctx.font;
  1096. this.lineCap = this.ctx.lineCap;
  1097. this.lineWidth = this.ctx.lineWidth;
  1098. this.lineJoin = this.ctx.lineJoin;
  1099. this.lineDash = this.ctx.lineDash;
  1100. this.lineDashOffset = this.ctx.lineDashOffset;
  1101. }
  1102. };
  1103. /**
  1104. * @name toDataURL
  1105. * @function
  1106. */
  1107. Context2D.prototype.toDataURL = function() {
  1108. throw new Error("toDataUrl not implemented.");
  1109. };
  1110. //helper functions
  1111. /**
  1112. * Get the decimal values of r, g, b and a
  1113. *
  1114. * @name getRGBA
  1115. * @function
  1116. * @private
  1117. * @ignore
  1118. */
  1119. var getRGBA = function(style) {
  1120. var rxRgb = /rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/;
  1121. var rxRgba = /rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)/;
  1122. var rxTransparent = /transparent|rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*0+\s*\)/;
  1123. var r, g, b, a;
  1124. if (style.isCanvasGradient === true) {
  1125. style = style.getColor();
  1126. }
  1127. if (!style) {
  1128. return { r: 0, g: 0, b: 0, a: 0, style: style };
  1129. }
  1130. if (rxTransparent.test(style)) {
  1131. r = 0;
  1132. g = 0;
  1133. b = 0;
  1134. a = 0;
  1135. } else {
  1136. var matches = rxRgb.exec(style);
  1137. if (matches !== null) {
  1138. r = parseInt(matches[1]);
  1139. g = parseInt(matches[2]);
  1140. b = parseInt(matches[3]);
  1141. a = 1;
  1142. } else {
  1143. matches = rxRgba.exec(style);
  1144. if (matches !== null) {
  1145. r = parseInt(matches[1]);
  1146. g = parseInt(matches[2]);
  1147. b = parseInt(matches[3]);
  1148. a = parseFloat(matches[4]);
  1149. } else {
  1150. a = 1;
  1151. if (typeof style === "string" && style.charAt(0) !== "#") {
  1152. var rgbColor = new RGBColor(style);
  1153. if (rgbColor.ok) {
  1154. style = rgbColor.toHex();
  1155. } else {
  1156. style = "#000000";
  1157. }
  1158. }
  1159. if (style.length === 4) {
  1160. r = style.substring(1, 2);
  1161. r += r;
  1162. g = style.substring(2, 3);
  1163. g += g;
  1164. b = style.substring(3, 4);
  1165. b += b;
  1166. } else {
  1167. r = style.substring(1, 3);
  1168. g = style.substring(3, 5);
  1169. b = style.substring(5, 7);
  1170. }
  1171. r = parseInt(r, 16);
  1172. g = parseInt(g, 16);
  1173. b = parseInt(b, 16);
  1174. }
  1175. }
  1176. }
  1177. return { r: r, g: g, b: b, a: a, style: style };
  1178. };
  1179. /**
  1180. * @name isFillTransparent
  1181. * @function
  1182. * @private
  1183. * @ignore
  1184. * @returns {Boolean}
  1185. */
  1186. var isFillTransparent = function() {
  1187. return this.ctx.isFillTransparent || this.globalAlpha == 0;
  1188. };
  1189. /**
  1190. * @name isStrokeTransparent
  1191. * @function
  1192. * @private
  1193. * @ignore
  1194. * @returns {Boolean}
  1195. */
  1196. var isStrokeTransparent = function() {
  1197. return Boolean(this.ctx.isStrokeTransparent || this.globalAlpha == 0);
  1198. };
  1199. /**
  1200. * Draws "filled" text on the canvas
  1201. *
  1202. * @name fillText
  1203. * @function
  1204. * @param text {String} Specifies the text that will be written on the canvas
  1205. * @param x {Number} The x coordinate where to start painting the text (relative to the canvas)
  1206. * @param y {Number} The y coordinate where to start painting the text (relative to the canvas)
  1207. * @param maxWidth {Number} Optional. The maximum allowed width of the text, in pixels
  1208. * @description The fillText() method draws filled text on the canvas. The default color of the text is black.
  1209. */
  1210. Context2D.prototype.fillText = function(text, x, y, maxWidth) {
  1211. if (isNaN(x) || isNaN(y) || typeof text !== "string") {
  1212. console.error("jsPDF.context2d.fillText: Invalid arguments", arguments);
  1213. throw new Error("Invalid arguments passed to jsPDF.context2d.fillText");
  1214. }
  1215. maxWidth = isNaN(maxWidth) ? undefined : maxWidth;
  1216. if (isFillTransparent.call(this)) {
  1217. return;
  1218. }
  1219. var degs = rad2deg(this.ctx.transform.rotation);
  1220. // We only use X axis as scale hint
  1221. var scale = this.ctx.transform.scaleX;
  1222. putText.call(this, {
  1223. text: text,
  1224. x: x,
  1225. y: y,
  1226. scale: scale,
  1227. angle: degs,
  1228. align: this.textAlign,
  1229. maxWidth: maxWidth
  1230. });
  1231. };
  1232. /**
  1233. * Draws text on the canvas (no fill)
  1234. *
  1235. * @name strokeText
  1236. * @function
  1237. * @param text {String} Specifies the text that will be written on the canvas
  1238. * @param x {Number} The x coordinate where to start painting the text (relative to the canvas)
  1239. * @param y {Number} The y coordinate where to start painting the text (relative to the canvas)
  1240. * @param maxWidth {Number} Optional. The maximum allowed width of the text, in pixels
  1241. * @description The strokeText() method draws text (with no fill) on the canvas. The default color of the text is black.
  1242. */
  1243. Context2D.prototype.strokeText = function(text, x, y, maxWidth) {
  1244. if (isNaN(x) || isNaN(y) || typeof text !== "string") {
  1245. console.error("jsPDF.context2d.strokeText: Invalid arguments", arguments);
  1246. throw new Error("Invalid arguments passed to jsPDF.context2d.strokeText");
  1247. }
  1248. if (isStrokeTransparent.call(this)) {
  1249. return;
  1250. }
  1251. maxWidth = isNaN(maxWidth) ? undefined : maxWidth;
  1252. var degs = rad2deg(this.ctx.transform.rotation);
  1253. var scale = this.ctx.transform.scaleX;
  1254. putText.call(this, {
  1255. text: text,
  1256. x: x,
  1257. y: y,
  1258. scale: scale,
  1259. renderingMode: "stroke",
  1260. angle: degs,
  1261. align: this.textAlign,
  1262. maxWidth: maxWidth
  1263. });
  1264. };
  1265. /**
  1266. * Returns an object that contains the width of the specified text
  1267. *
  1268. * @name measureText
  1269. * @function
  1270. * @param text {String} The text to be measured
  1271. * @description The measureText() method returns an object that contains the width of the specified text, in pixels.
  1272. * @returns {Number}
  1273. */
  1274. Context2D.prototype.measureText = function(text) {
  1275. if (typeof text !== "string") {
  1276. console.error(
  1277. "jsPDF.context2d.measureText: Invalid arguments",
  1278. arguments
  1279. );
  1280. throw new Error(
  1281. "Invalid arguments passed to jsPDF.context2d.measureText"
  1282. );
  1283. }
  1284. var pdf = this.pdf;
  1285. var k = this.pdf.internal.scaleFactor;
  1286. var fontSize = pdf.internal.getFontSize();
  1287. var txtWidth =
  1288. (pdf.getStringUnitWidth(text) * fontSize) / pdf.internal.scaleFactor;
  1289. txtWidth *= Math.round(((k * 96) / 72) * 10000) / 10000;
  1290. var TextMetrics = function(options) {
  1291. options = options || {};
  1292. var _width = options.width || 0;
  1293. Object.defineProperty(this, "width", {
  1294. get: function() {
  1295. return _width;
  1296. }
  1297. });
  1298. return this;
  1299. };
  1300. return new TextMetrics({ width: txtWidth });
  1301. };
  1302. //Transformations
  1303. /**
  1304. * Scales the current drawing bigger or smaller
  1305. *
  1306. * @name scale
  1307. * @function
  1308. * @param scalewidth {Number} Scales the width of the current drawing (1=100%, 0.5=50%, 2=200%, etc.)
  1309. * @param scaleheight {Number} Scales the height of the current drawing (1=100%, 0.5=50%, 2=200%, etc.)
  1310. * @description The scale() method scales the current drawing, bigger or smaller.
  1311. */
  1312. Context2D.prototype.scale = function(scalewidth, scaleheight) {
  1313. if (isNaN(scalewidth) || isNaN(scaleheight)) {
  1314. console.error("jsPDF.context2d.scale: Invalid arguments", arguments);
  1315. throw new Error("Invalid arguments passed to jsPDF.context2d.scale");
  1316. }
  1317. var matrix = new Matrix(scalewidth, 0.0, 0.0, scaleheight, 0.0, 0.0);
  1318. this.ctx.transform = this.ctx.transform.multiply(matrix);
  1319. };
  1320. /**
  1321. * Rotates the current drawing
  1322. *
  1323. * @name rotate
  1324. * @function
  1325. * @param angle {Number} The rotation angle, in radians.
  1326. * @description To calculate from degrees to radians: degrees*Math.PI/180. <br />
  1327. * Example: to rotate 5 degrees, specify the following: 5*Math.PI/180
  1328. */
  1329. Context2D.prototype.rotate = function(angle) {
  1330. if (isNaN(angle)) {
  1331. console.error("jsPDF.context2d.rotate: Invalid arguments", arguments);
  1332. throw new Error("Invalid arguments passed to jsPDF.context2d.rotate");
  1333. }
  1334. var matrix = new Matrix(
  1335. Math.cos(angle),
  1336. Math.sin(angle),
  1337. -Math.sin(angle),
  1338. Math.cos(angle),
  1339. 0.0,
  1340. 0.0
  1341. );
  1342. this.ctx.transform = this.ctx.transform.multiply(matrix);
  1343. };
  1344. /**
  1345. * Remaps the (0,0) position on the canvas
  1346. *
  1347. * @name translate
  1348. * @function
  1349. * @param x {Number} The value to add to horizontal (x) coordinates
  1350. * @param y {Number} The value to add to vertical (y) coordinates
  1351. * @description The translate() method remaps the (0,0) position on the canvas.
  1352. */
  1353. Context2D.prototype.translate = function(x, y) {
  1354. if (isNaN(x) || isNaN(y)) {
  1355. console.error("jsPDF.context2d.translate: Invalid arguments", arguments);
  1356. throw new Error("Invalid arguments passed to jsPDF.context2d.translate");
  1357. }
  1358. var matrix = new Matrix(1.0, 0.0, 0.0, 1.0, x, y);
  1359. this.ctx.transform = this.ctx.transform.multiply(matrix);
  1360. };
  1361. /**
  1362. * Replaces the current transformation matrix for the drawing
  1363. *
  1364. * @name transform
  1365. * @function
  1366. * @param a {Number} Horizontal scaling
  1367. * @param b {Number} Horizontal skewing
  1368. * @param c {Number} Vertical skewing
  1369. * @param d {Number} Vertical scaling
  1370. * @param e {Number} Horizontal moving
  1371. * @param f {Number} Vertical moving
  1372. * @description Each object on the canvas has a current transformation matrix.<br /><br />The transform() method replaces the current transformation matrix. It multiplies the current transformation matrix with the matrix described by:<br /><br /><br /><br />a c e<br /><br />b d f<br /><br />0 0 1<br /><br />In other words, the transform() method lets you scale, rotate, move, and skew the current context.
  1373. */
  1374. Context2D.prototype.transform = function(a, b, c, d, e, f) {
  1375. if (isNaN(a) || isNaN(b) || isNaN(c) || isNaN(d) || isNaN(e) || isNaN(f)) {
  1376. console.error("jsPDF.context2d.transform: Invalid arguments", arguments);
  1377. throw new Error("Invalid arguments passed to jsPDF.context2d.transform");
  1378. }
  1379. var matrix = new Matrix(a, b, c, d, e, f);
  1380. this.ctx.transform = this.ctx.transform.multiply(matrix);
  1381. };
  1382. /**
  1383. * Resets the current transform to the identity matrix. Then runs transform()
  1384. *
  1385. * @name setTransform
  1386. * @function
  1387. * @param a {Number} Horizontal scaling
  1388. * @param b {Number} Horizontal skewing
  1389. * @param c {Number} Vertical skewing
  1390. * @param d {Number} Vertical scaling
  1391. * @param e {Number} Horizontal moving
  1392. * @param f {Number} Vertical moving
  1393. * @description Each object on the canvas has a current transformation matrix. <br /><br />The setTransform() method resets the current transform to the identity matrix, and then runs transform() with the same arguments.<br /><br />In other words, the setTransform() method lets you scale, rotate, move, and skew the current context.
  1394. */
  1395. Context2D.prototype.setTransform = function(a, b, c, d, e, f) {
  1396. a = isNaN(a) ? 1 : a;
  1397. b = isNaN(b) ? 0 : b;
  1398. c = isNaN(c) ? 0 : c;
  1399. d = isNaN(d) ? 1 : d;
  1400. e = isNaN(e) ? 0 : e;
  1401. f = isNaN(f) ? 0 : f;
  1402. this.ctx.transform = new Matrix(a, b, c, d, e, f);
  1403. };
  1404. /**
  1405. * Should only be used if pageWrapYEnabled is true
  1406. *
  1407. * @name setPageByYPosition
  1408. * @function
  1409. * @private
  1410. * @ignore
  1411. * @returns One-based Page Number
  1412. */
  1413. var setPageByYPosition = function(y) {
  1414. if (this.pageWrapYEnabled) {
  1415. this.lastBreak = 0;
  1416. var manualBreaks = 0;
  1417. var autoBreaks = 0;
  1418. for (var i = 0; i < this.pageBreaks.length; i++) {
  1419. if (y >= this.pageBreaks[i]) {
  1420. manualBreaks++;
  1421. if (this.lastBreak === 0) {
  1422. autoBreaks++;
  1423. }
  1424. var spaceBetweenLastBreak = this.pageBreaks[i] - this.lastBreak;
  1425. this.lastBreak = this.pageBreaks[i];
  1426. var pagesSinceLastBreak = Math.floor(
  1427. spaceBetweenLastBreak / this.pageWrapY
  1428. );
  1429. autoBreaks += pagesSinceLastBreak;
  1430. }
  1431. }
  1432. if (this.lastBreak === 0) {
  1433. var pagesSinceLastBreak = Math.floor(y / this.pageWrapY) + 1;
  1434. autoBreaks += pagesSinceLastBreak;
  1435. }
  1436. return autoBreaks + manualBreaks;
  1437. } else {
  1438. return this.pdf.internal.getCurrentPageInfo().pageNumber;
  1439. }
  1440. };
  1441. var hasMargins = function() {
  1442. return (
  1443. this.margin[0] > 0 ||
  1444. this.margin[1] > 0 ||
  1445. this.margin[2] > 0 ||
  1446. this.margin[3] > 0
  1447. );
  1448. };
  1449. /**
  1450. * Draws an image, canvas, or video onto the canvas
  1451. *
  1452. * @function
  1453. * @param img {} Specifies the image, canvas, or video element to use
  1454. * @param sx {Number} Optional. The x coordinate where to start clipping
  1455. * @param sy {Number} Optional. The y coordinate where to start clipping
  1456. * @param swidth {Number} Optional. The width of the clipped image
  1457. * @param sheight {Number} Optional. The height of the clipped image
  1458. * @param x {Number} The x coordinate where to place the image on the canvas
  1459. * @param y {Number} The y coordinate where to place the image on the canvas
  1460. * @param width {Number} Optional. The width of the image to use (stretch or reduce the image)
  1461. * @param height {Number} Optional. The height of the image to use (stretch or reduce the image)
  1462. */
  1463. Context2D.prototype.drawImage = function(
  1464. img,
  1465. sx,
  1466. sy,
  1467. swidth,
  1468. sheight,
  1469. x,
  1470. y,
  1471. width,
  1472. height
  1473. ) {
  1474. var imageProperties = this.pdf.getImageProperties(img);
  1475. var factorX = 1;
  1476. var factorY = 1;
  1477. var isClip;
  1478. var clipFactorX = 1;
  1479. var clipFactorY = 1;
  1480. if (typeof swidth !== "undefined" && typeof width !== "undefined") {
  1481. isClip = true;
  1482. clipFactorX = width / swidth;
  1483. clipFactorY = height / sheight;
  1484. factorX = ((imageProperties.width / swidth) * width) / swidth;
  1485. factorY = ((imageProperties.height / sheight) * height) / sheight;
  1486. }
  1487. //is sx and sy are set and x and y not, set x and y with values of sx and sy
  1488. if (typeof x === "undefined") {
  1489. x = sx;
  1490. y = sy;
  1491. sx = 0;
  1492. sy = 0;
  1493. }
  1494. if (typeof swidth !== "undefined" && typeof width === "undefined") {
  1495. width = swidth;
  1496. height = sheight;
  1497. }
  1498. if (typeof swidth === "undefined" && typeof width === "undefined") {
  1499. width = imageProperties.width;
  1500. height = imageProperties.height;
  1501. }
  1502. var decomposedTransformationMatrix = this.ctx.transform.decompose();
  1503. var angle = rad2deg(decomposedTransformationMatrix.rotate.shx);
  1504. var matrix = new Matrix();
  1505. matrix = matrix.multiply(decomposedTransformationMatrix.translate);
  1506. matrix = matrix.multiply(decomposedTransformationMatrix.skew);
  1507. matrix = matrix.multiply(decomposedTransformationMatrix.scale);
  1508. var xRect = matrix.applyToRectangle(
  1509. new Rectangle(
  1510. x - sx * clipFactorX,
  1511. y - sy * clipFactorY,
  1512. swidth * factorX,
  1513. sheight * factorY
  1514. )
  1515. );
  1516. var pageArray = getPagesByPath.call(this, xRect);
  1517. var pages = [];
  1518. for (var ii = 0; ii < pageArray.length; ii += 1) {
  1519. if (pages.indexOf(pageArray[ii]) === -1) {
  1520. pages.push(pageArray[ii]);
  1521. }
  1522. }
  1523. sortPages(pages);
  1524. var clipPath;
  1525. if (this.autoPaging) {
  1526. var min = pages[0];
  1527. var max = pages[pages.length - 1];
  1528. for (var i = min; i < max + 1; i++) {
  1529. this.pdf.setPage(i);
  1530. var pageWidthMinusMargins =
  1531. this.pdf.internal.pageSize.width - this.margin[3] - this.margin[1];
  1532. var topMargin = i === 1 ? this.posY + this.margin[0] : this.margin[0];
  1533. var firstPageHeight =
  1534. this.pdf.internal.pageSize.height -
  1535. this.posY -
  1536. this.margin[0] -
  1537. this.margin[2];
  1538. var pageHeightMinusMargins =
  1539. this.pdf.internal.pageSize.height - this.margin[0] - this.margin[2];
  1540. var previousPageHeightSum =
  1541. i === 1 ? 0 : firstPageHeight + (i - 2) * pageHeightMinusMargins;
  1542. if (this.ctx.clip_path.length !== 0) {
  1543. var tmpPaths = this.path;
  1544. clipPath = JSON.parse(JSON.stringify(this.ctx.clip_path));
  1545. this.path = pathPositionRedo(
  1546. clipPath,
  1547. this.posX + this.margin[3],
  1548. -previousPageHeightSum + topMargin + this.ctx.prevPageLastElemOffset
  1549. );
  1550. drawPaths.call(this, "fill", true);
  1551. this.path = tmpPaths;
  1552. }
  1553. var tmpRect = JSON.parse(JSON.stringify(xRect));
  1554. tmpRect = pathPositionRedo(
  1555. [tmpRect],
  1556. this.posX + this.margin[3],
  1557. -previousPageHeightSum + topMargin + this.ctx.prevPageLastElemOffset
  1558. )[0];
  1559. const needsClipping = (i > min || i < max) && hasMargins.call(this);
  1560. if (needsClipping) {
  1561. this.pdf.saveGraphicsState();
  1562. this.pdf
  1563. .rect(
  1564. this.margin[3],
  1565. this.margin[0],
  1566. pageWidthMinusMargins,
  1567. pageHeightMinusMargins,
  1568. null
  1569. )
  1570. .clip()
  1571. .discardPath();
  1572. }
  1573. this.pdf.addImage(
  1574. img,
  1575. "JPEG",
  1576. tmpRect.x,
  1577. tmpRect.y,
  1578. tmpRect.w,
  1579. tmpRect.h,
  1580. null,
  1581. null,
  1582. angle
  1583. );
  1584. if (needsClipping) {
  1585. this.pdf.restoreGraphicsState();
  1586. }
  1587. }
  1588. } else {
  1589. this.pdf.addImage(
  1590. img,
  1591. "JPEG",
  1592. xRect.x,
  1593. xRect.y,
  1594. xRect.w,
  1595. xRect.h,
  1596. null,
  1597. null,
  1598. angle
  1599. );
  1600. }
  1601. };
  1602. var getPagesByPath = function(path, pageWrapX, pageWrapY) {
  1603. var result = [];
  1604. pageWrapX = pageWrapX || this.pdf.internal.pageSize.width;
  1605. pageWrapY =
  1606. pageWrapY ||
  1607. this.pdf.internal.pageSize.height - this.margin[0] - this.margin[2];
  1608. var yOffset = this.posY + this.ctx.prevPageLastElemOffset;
  1609. switch (path.type) {
  1610. default:
  1611. case "mt":
  1612. case "lt":
  1613. result.push(Math.floor((path.y + yOffset) / pageWrapY) + 1);
  1614. break;
  1615. case "arc":
  1616. result.push(
  1617. Math.floor((path.y + yOffset - path.radius) / pageWrapY) + 1
  1618. );
  1619. result.push(
  1620. Math.floor((path.y + yOffset + path.radius) / pageWrapY) + 1
  1621. );
  1622. break;
  1623. case "qct":
  1624. var rectOfQuadraticCurve = getQuadraticCurveBoundary(
  1625. this.ctx.lastPoint.x,
  1626. this.ctx.lastPoint.y,
  1627. path.x1,
  1628. path.y1,
  1629. path.x,
  1630. path.y
  1631. );
  1632. result.push(
  1633. Math.floor((rectOfQuadraticCurve.y + yOffset) / pageWrapY) + 1
  1634. );
  1635. result.push(
  1636. Math.floor(
  1637. (rectOfQuadraticCurve.y + rectOfQuadraticCurve.h + yOffset) /
  1638. pageWrapY
  1639. ) + 1
  1640. );
  1641. break;
  1642. case "bct":
  1643. var rectOfBezierCurve = getBezierCurveBoundary(
  1644. this.ctx.lastPoint.x,
  1645. this.ctx.lastPoint.y,
  1646. path.x1,
  1647. path.y1,
  1648. path.x2,
  1649. path.y2,
  1650. path.x,
  1651. path.y
  1652. );
  1653. result.push(
  1654. Math.floor((rectOfBezierCurve.y + yOffset) / pageWrapY) + 1
  1655. );
  1656. result.push(
  1657. Math.floor(
  1658. (rectOfBezierCurve.y + rectOfBezierCurve.h + yOffset) / pageWrapY
  1659. ) + 1
  1660. );
  1661. break;
  1662. case "rect":
  1663. result.push(Math.floor((path.y + yOffset) / pageWrapY) + 1);
  1664. result.push(Math.floor((path.y + path.h + yOffset) / pageWrapY) + 1);
  1665. }
  1666. for (var i = 0; i < result.length; i += 1) {
  1667. while (this.pdf.internal.getNumberOfPages() < result[i]) {
  1668. addPage.call(this);
  1669. }
  1670. }
  1671. return result;
  1672. };
  1673. var addPage = function() {
  1674. var fillStyle = this.fillStyle;
  1675. var strokeStyle = this.strokeStyle;
  1676. var font = this.font;
  1677. var lineCap = this.lineCap;
  1678. var lineWidth = this.lineWidth;
  1679. var lineJoin = this.lineJoin;
  1680. this.pdf.addPage();
  1681. this.fillStyle = fillStyle;
  1682. this.strokeStyle = strokeStyle;
  1683. this.font = font;
  1684. this.lineCap = lineCap;
  1685. this.lineWidth = lineWidth;
  1686. this.lineJoin = lineJoin;
  1687. };
  1688. var pathPositionRedo = function(paths, x, y) {
  1689. for (var i = 0; i < paths.length; i++) {
  1690. switch (paths[i].type) {
  1691. case "bct":
  1692. paths[i].x2 += x;
  1693. paths[i].y2 += y;
  1694. case "qct":
  1695. paths[i].x1 += x;
  1696. paths[i].y1 += y;
  1697. case "mt":
  1698. case "lt":
  1699. case "arc":
  1700. default:
  1701. paths[i].x += x;
  1702. paths[i].y += y;
  1703. }
  1704. }
  1705. return paths;
  1706. };
  1707. var sortPages = function(pages) {
  1708. return pages.sort(function(a, b) {
  1709. return a - b;
  1710. });
  1711. };
  1712. var pathPreProcess = function(rule, isClip) {
  1713. var fillStyle = this.fillStyle;
  1714. var strokeStyle = this.strokeStyle;
  1715. var lineCap = this.lineCap;
  1716. var oldLineWidth = this.lineWidth;
  1717. var lineWidth = Math.abs(oldLineWidth * this.ctx.transform.scaleX);
  1718. var lineJoin = this.lineJoin;
  1719. var origPath = JSON.parse(JSON.stringify(this.path));
  1720. var xPath = JSON.parse(JSON.stringify(this.path));
  1721. var clipPath;
  1722. var tmpPath;
  1723. var pages = [];
  1724. for (var i = 0; i < xPath.length; i++) {
  1725. if (typeof xPath[i].x !== "undefined") {
  1726. var page = getPagesByPath.call(this, xPath[i]);
  1727. for (var ii = 0; ii < page.length; ii += 1) {
  1728. if (pages.indexOf(page[ii]) === -1) {
  1729. pages.push(page[ii]);
  1730. }
  1731. }
  1732. }
  1733. }
  1734. for (var j = 0; j < pages.length; j++) {
  1735. while (this.pdf.internal.getNumberOfPages() < pages[j]) {
  1736. addPage.call(this);
  1737. }
  1738. }
  1739. sortPages(pages);
  1740. if (this.autoPaging) {
  1741. var min = pages[0];
  1742. var max = pages[pages.length - 1];
  1743. for (var k = min; k < max + 1; k++) {
  1744. this.pdf.setPage(k);
  1745. this.fillStyle = fillStyle;
  1746. this.strokeStyle = strokeStyle;
  1747. this.lineCap = lineCap;
  1748. this.lineWidth = lineWidth;
  1749. this.lineJoin = lineJoin;
  1750. var pageWidthMinusMargins =
  1751. this.pdf.internal.pageSize.width - this.margin[3] - this.margin[1];
  1752. var topMargin = k === 1 ? this.posY + this.margin[0] : this.margin[0];
  1753. var firstPageHeight =
  1754. this.pdf.internal.pageSize.height -
  1755. this.posY -
  1756. this.margin[0] -
  1757. this.margin[2];
  1758. var pageHeightMinusMargins =
  1759. this.pdf.internal.pageSize.height - this.margin[0] - this.margin[2];
  1760. var previousPageHeightSum =
  1761. k === 1 ? 0 : firstPageHeight + (k - 2) * pageHeightMinusMargins;
  1762. if (this.ctx.clip_path.length !== 0) {
  1763. var tmpPaths = this.path;
  1764. clipPath = JSON.parse(JSON.stringify(this.ctx.clip_path));
  1765. this.path = pathPositionRedo(
  1766. clipPath,
  1767. this.posX + this.margin[3],
  1768. -previousPageHeightSum + topMargin + this.ctx.prevPageLastElemOffset
  1769. );
  1770. drawPaths.call(this, rule, true);
  1771. this.path = tmpPaths;
  1772. }
  1773. tmpPath = JSON.parse(JSON.stringify(origPath));
  1774. this.path = pathPositionRedo(
  1775. tmpPath,
  1776. this.posX + this.margin[3],
  1777. -previousPageHeightSum + topMargin + this.ctx.prevPageLastElemOffset
  1778. );
  1779. if (isClip === false || k === 0) {
  1780. const needsClipping = (k > min || k < max) && hasMargins.call(this);
  1781. if (needsClipping) {
  1782. this.pdf.saveGraphicsState();
  1783. this.pdf
  1784. .rect(
  1785. this.margin[3],
  1786. this.margin[0],
  1787. pageWidthMinusMargins,
  1788. pageHeightMinusMargins,
  1789. null
  1790. )
  1791. .clip()
  1792. .discardPath();
  1793. }
  1794. drawPaths.call(this, rule, isClip);
  1795. if (needsClipping) {
  1796. this.pdf.restoreGraphicsState();
  1797. }
  1798. }
  1799. this.lineWidth = oldLineWidth;
  1800. }
  1801. } else {
  1802. this.lineWidth = lineWidth;
  1803. drawPaths.call(this, rule, isClip);
  1804. this.lineWidth = oldLineWidth;
  1805. }
  1806. this.path = origPath;
  1807. };
  1808. /**
  1809. * Processes the paths
  1810. *
  1811. * @function
  1812. * @param rule {String}
  1813. * @param isClip {Boolean}
  1814. * @private
  1815. * @ignore
  1816. */
  1817. var drawPaths = function(rule, isClip) {
  1818. if (rule === "stroke" && !isClip && isStrokeTransparent.call(this)) {
  1819. return;
  1820. }
  1821. if (rule !== "stroke" && !isClip && isFillTransparent.call(this)) {
  1822. return;
  1823. }
  1824. var moves = [];
  1825. //var alpha = (this.ctx.fillOpacity < 1) ? this.ctx.fillOpacity : this.ctx.globalAlpha;
  1826. var delta;
  1827. var xPath = this.path;
  1828. for (var i = 0; i < xPath.length; i++) {
  1829. var pt = xPath[i];
  1830. switch (pt.type) {
  1831. case "begin":
  1832. moves.push({
  1833. begin: true
  1834. });
  1835. break;
  1836. case "close":
  1837. moves.push({
  1838. close: true
  1839. });
  1840. break;
  1841. case "mt":
  1842. moves.push({
  1843. start: pt,
  1844. deltas: [],
  1845. abs: []
  1846. });
  1847. break;
  1848. case "lt":
  1849. var iii = moves.length;
  1850. if (xPath[i - 1] && !isNaN(xPath[i - 1].x)) {
  1851. delta = [pt.x - xPath[i - 1].x, pt.y - xPath[i - 1].y];
  1852. if (iii > 0) {
  1853. for (iii; iii >= 0; iii--) {
  1854. if (
  1855. moves[iii - 1].close !== true &&
  1856. moves[iii - 1].begin !== true
  1857. ) {
  1858. moves[iii - 1].deltas.push(delta);
  1859. moves[iii - 1].abs.push(pt);
  1860. break;
  1861. }
  1862. }
  1863. }
  1864. }
  1865. break;
  1866. case "bct":
  1867. delta = [
  1868. pt.x1 - xPath[i - 1].x,
  1869. pt.y1 - xPath[i - 1].y,
  1870. pt.x2 - xPath[i - 1].x,
  1871. pt.y2 - xPath[i - 1].y,
  1872. pt.x - xPath[i - 1].x,
  1873. pt.y - xPath[i - 1].y
  1874. ];
  1875. moves[moves.length - 1].deltas.push(delta);
  1876. break;
  1877. case "qct":
  1878. var x1 = xPath[i - 1].x + (2.0 / 3.0) * (pt.x1 - xPath[i - 1].x);
  1879. var y1 = xPath[i - 1].y + (2.0 / 3.0) * (pt.y1 - xPath[i - 1].y);
  1880. var x2 = pt.x + (2.0 / 3.0) * (pt.x1 - pt.x);
  1881. var y2 = pt.y + (2.0 / 3.0) * (pt.y1 - pt.y);
  1882. var x3 = pt.x;
  1883. var y3 = pt.y;
  1884. delta = [
  1885. x1 - xPath[i - 1].x,
  1886. y1 - xPath[i - 1].y,
  1887. x2 - xPath[i - 1].x,
  1888. y2 - xPath[i - 1].y,
  1889. x3 - xPath[i - 1].x,
  1890. y3 - xPath[i - 1].y
  1891. ];
  1892. moves[moves.length - 1].deltas.push(delta);
  1893. break;
  1894. case "arc":
  1895. moves.push({
  1896. deltas: [],
  1897. abs: [],
  1898. arc: true
  1899. });
  1900. if (Array.isArray(moves[moves.length - 1].abs)) {
  1901. moves[moves.length - 1].abs.push(pt);
  1902. }
  1903. break;
  1904. }
  1905. }
  1906. var style;
  1907. if (!isClip) {
  1908. if (rule === "stroke") {
  1909. style = "stroke";
  1910. } else {
  1911. style = "fill";
  1912. }
  1913. } else {
  1914. style = null;
  1915. }
  1916. var began = false;
  1917. for (var k = 0; k < moves.length; k++) {
  1918. if (moves[k].arc) {
  1919. var arcs = moves[k].abs;
  1920. for (var ii = 0; ii < arcs.length; ii++) {
  1921. var arc = arcs[ii];
  1922. if (arc.type === "arc") {
  1923. drawArc.call(
  1924. this,
  1925. arc.x,
  1926. arc.y,
  1927. arc.radius,
  1928. arc.startAngle,
  1929. arc.endAngle,
  1930. arc.counterclockwise,
  1931. undefined,
  1932. isClip,
  1933. !began
  1934. );
  1935. } else {
  1936. drawLine.call(this, arc.x, arc.y);
  1937. }
  1938. began = true;
  1939. }
  1940. } else if (moves[k].close === true) {
  1941. this.pdf.internal.out("h");
  1942. began = false;
  1943. } else if (moves[k].begin !== true) {
  1944. var x = moves[k].start.x;
  1945. var y = moves[k].start.y;
  1946. drawLines.call(this, moves[k].deltas, x, y);
  1947. began = true;
  1948. }
  1949. }
  1950. if (style) {
  1951. putStyle.call(this, style);
  1952. }
  1953. if (isClip) {
  1954. doClip.call(this);
  1955. }
  1956. };
  1957. var getBaseline = function(y) {
  1958. var height =
  1959. this.pdf.internal.getFontSize() / this.pdf.internal.scaleFactor;
  1960. var descent = height * (this.pdf.internal.getLineHeightFactor() - 1);
  1961. switch (this.ctx.textBaseline) {
  1962. case "bottom":
  1963. return y - descent;
  1964. case "top":
  1965. return y + height - descent;
  1966. case "hanging":
  1967. return y + height - 2 * descent;
  1968. case "middle":
  1969. return y + height / 2 - descent;
  1970. case "ideographic":
  1971. // TODO not implemented
  1972. return y;
  1973. case "alphabetic":
  1974. default:
  1975. return y;
  1976. }
  1977. };
  1978. var getTextBottom = function(yBaseLine) {
  1979. var height =
  1980. this.pdf.internal.getFontSize() / this.pdf.internal.scaleFactor;
  1981. var descent = height * (this.pdf.internal.getLineHeightFactor() - 1);
  1982. return yBaseLine + descent;
  1983. };
  1984. Context2D.prototype.createLinearGradient = function createLinearGradient() {
  1985. var canvasGradient = function canvasGradient() {};
  1986. canvasGradient.colorStops = [];
  1987. canvasGradient.addColorStop = function(offset, color) {
  1988. this.colorStops.push([offset, color]);
  1989. };
  1990. canvasGradient.getColor = function() {
  1991. if (this.colorStops.length === 0) {
  1992. return "#000000";
  1993. }
  1994. return this.colorStops[0][1];
  1995. };
  1996. canvasGradient.isCanvasGradient = true;
  1997. return canvasGradient;
  1998. };
  1999. Context2D.prototype.createPattern = function createPattern() {
  2000. return this.createLinearGradient();
  2001. };
  2002. Context2D.prototype.createRadialGradient = function createRadialGradient() {
  2003. return this.createLinearGradient();
  2004. };
  2005. /**
  2006. *
  2007. * @param x Edge point X
  2008. * @param y Edge point Y
  2009. * @param r Radius
  2010. * @param a1 start angle
  2011. * @param a2 end angle
  2012. * @param counterclockwise
  2013. * @param style
  2014. * @param isClip
  2015. */
  2016. var drawArc = function(
  2017. x,
  2018. y,
  2019. r,
  2020. a1,
  2021. a2,
  2022. counterclockwise,
  2023. style,
  2024. isClip,
  2025. includeMove
  2026. ) {
  2027. // http://hansmuller-flex.blogspot.com/2011/10/more-about-approximating-circular-arcs.html
  2028. var curves = createArc.call(this, r, a1, a2, counterclockwise);
  2029. for (var i = 0; i < curves.length; i++) {
  2030. var curve = curves[i];
  2031. if (i === 0) {
  2032. if (includeMove) {
  2033. doMove.call(this, curve.x1 + x, curve.y1 + y);
  2034. } else {
  2035. drawLine.call(this, curve.x1 + x, curve.y1 + y);
  2036. }
  2037. }
  2038. drawCurve.call(
  2039. this,
  2040. x,
  2041. y,
  2042. curve.x2,
  2043. curve.y2,
  2044. curve.x3,
  2045. curve.y3,
  2046. curve.x4,
  2047. curve.y4
  2048. );
  2049. }
  2050. if (!isClip) {
  2051. putStyle.call(this, style);
  2052. } else {
  2053. doClip.call(this);
  2054. }
  2055. };
  2056. var putStyle = function(style) {
  2057. switch (style) {
  2058. case "stroke":
  2059. this.pdf.internal.out("S");
  2060. break;
  2061. case "fill":
  2062. this.pdf.internal.out("f");
  2063. break;
  2064. }
  2065. };
  2066. var doClip = function() {
  2067. this.pdf.clip();
  2068. this.pdf.discardPath();
  2069. };
  2070. var doMove = function(x, y) {
  2071. this.pdf.internal.out(
  2072. getHorizontalCoordinateString(x) +
  2073. " " +
  2074. getVerticalCoordinateString(y) +
  2075. " m"
  2076. );
  2077. };
  2078. var putText = function(options) {
  2079. var textAlign;
  2080. switch (options.align) {
  2081. case "right":
  2082. case "end":
  2083. textAlign = "right";
  2084. break;
  2085. case "center":
  2086. textAlign = "center";
  2087. break;
  2088. case "left":
  2089. case "start":
  2090. default:
  2091. textAlign = "left";
  2092. break;
  2093. }
  2094. var textDimensions = this.pdf.getTextDimensions(options.text);
  2095. var yBaseLine = getBaseline.call(this, options.y);
  2096. var yBottom = getTextBottom.call(this, yBaseLine);
  2097. var yTop = yBottom - textDimensions.h;
  2098. var pt = this.ctx.transform.applyToPoint(new Point(options.x, yBaseLine));
  2099. var decomposedTransformationMatrix = this.ctx.transform.decompose();
  2100. var matrix = new Matrix();
  2101. matrix = matrix.multiply(decomposedTransformationMatrix.translate);
  2102. matrix = matrix.multiply(decomposedTransformationMatrix.skew);
  2103. matrix = matrix.multiply(decomposedTransformationMatrix.scale);
  2104. var baselineRect = this.ctx.transform.applyToRectangle(
  2105. new Rectangle(options.x, yBaseLine, textDimensions.w, textDimensions.h)
  2106. );
  2107. var textBounds = matrix.applyToRectangle(
  2108. new Rectangle(options.x, yTop, textDimensions.w, textDimensions.h)
  2109. );
  2110. var pageArray = getPagesByPath.call(this, textBounds);
  2111. var pages = [];
  2112. for (var ii = 0; ii < pageArray.length; ii += 1) {
  2113. if (pages.indexOf(pageArray[ii]) === -1) {
  2114. pages.push(pageArray[ii]);
  2115. }
  2116. }
  2117. sortPages(pages);
  2118. var clipPath, oldSize, oldLineWidth;
  2119. if (this.autoPaging) {
  2120. var min = pages[0];
  2121. var max = pages[pages.length - 1];
  2122. for (var i = min; i < max + 1; i++) {
  2123. this.pdf.setPage(i);
  2124. var topMargin = i === 1 ? this.posY + this.margin[0] : this.margin[0];
  2125. var firstPageHeight =
  2126. this.pdf.internal.pageSize.height -
  2127. this.posY -
  2128. this.margin[0] -
  2129. this.margin[2];
  2130. var pageHeightMinusBottomMargin =
  2131. this.pdf.internal.pageSize.height - this.margin[2];
  2132. var pageHeightMinusMargins =
  2133. pageHeightMinusBottomMargin - this.margin[0];
  2134. var pageWidthMinusRightMargin =
  2135. this.pdf.internal.pageSize.width - this.margin[1];
  2136. var pageWidthMinusMargins = pageWidthMinusRightMargin - this.margin[3];
  2137. var previousPageHeightSum =
  2138. i === 1 ? 0 : firstPageHeight + (i - 2) * pageHeightMinusMargins;
  2139. if (this.ctx.clip_path.length !== 0) {
  2140. var tmpPaths = this.path;
  2141. clipPath = JSON.parse(JSON.stringify(this.ctx.clip_path));
  2142. this.path = pathPositionRedo(
  2143. clipPath,
  2144. this.posX + this.margin[3],
  2145. -1 * previousPageHeightSum + topMargin
  2146. );
  2147. drawPaths.call(this, "fill", true);
  2148. this.path = tmpPaths;
  2149. }
  2150. var textBoundsOnPage = pathPositionRedo(
  2151. [JSON.parse(JSON.stringify(textBounds))],
  2152. this.posX + this.margin[3],
  2153. -previousPageHeightSum + topMargin + this.ctx.prevPageLastElemOffset
  2154. )[0];
  2155. if (options.scale >= 0.01) {
  2156. oldSize = this.pdf.internal.getFontSize();
  2157. this.pdf.setFontSize(oldSize * options.scale);
  2158. oldLineWidth = this.lineWidth;
  2159. this.lineWidth = oldLineWidth * options.scale;
  2160. }
  2161. var doSlice = this.autoPaging !== "text";
  2162. if (
  2163. doSlice ||
  2164. textBoundsOnPage.y + textBoundsOnPage.h <= pageHeightMinusBottomMargin
  2165. ) {
  2166. if (
  2167. doSlice ||
  2168. (textBoundsOnPage.y >= topMargin &&
  2169. textBoundsOnPage.x <= pageWidthMinusRightMargin)
  2170. ) {
  2171. var croppedText = doSlice
  2172. ? options.text
  2173. : this.pdf.splitTextToSize(
  2174. options.text,
  2175. options.maxWidth ||
  2176. pageWidthMinusRightMargin - textBoundsOnPage.x
  2177. )[0];
  2178. var baseLineRectOnPage = pathPositionRedo(
  2179. [JSON.parse(JSON.stringify(baselineRect))],
  2180. this.posX + this.margin[3],
  2181. -previousPageHeightSum +
  2182. topMargin +
  2183. this.ctx.prevPageLastElemOffset
  2184. )[0];
  2185. const needsClipping =
  2186. doSlice && (i > min || i < max) && hasMargins.call(this);
  2187. if (needsClipping) {
  2188. this.pdf.saveGraphicsState();
  2189. this.pdf
  2190. .rect(
  2191. this.margin[3],
  2192. this.margin[0],
  2193. pageWidthMinusMargins,
  2194. pageHeightMinusMargins,
  2195. null
  2196. )
  2197. .clip()
  2198. .discardPath();
  2199. }
  2200. this.pdf.text(
  2201. croppedText,
  2202. baseLineRectOnPage.x,
  2203. baseLineRectOnPage.y,
  2204. {
  2205. angle: options.angle,
  2206. align: textAlign,
  2207. renderingMode: options.renderingMode
  2208. }
  2209. );
  2210. if (needsClipping) {
  2211. this.pdf.restoreGraphicsState();
  2212. }
  2213. }
  2214. } else {
  2215. // This text is the last element of the page, but it got cut off due to the margin
  2216. // so we render it in the next page
  2217. if (textBoundsOnPage.y < pageHeightMinusBottomMargin) {
  2218. // As a result, all other elements have their y offset increased
  2219. this.ctx.prevPageLastElemOffset +=
  2220. pageHeightMinusBottomMargin - textBoundsOnPage.y;
  2221. }
  2222. }
  2223. if (options.scale >= 0.01) {
  2224. this.pdf.setFontSize(oldSize);
  2225. this.lineWidth = oldLineWidth;
  2226. }
  2227. }
  2228. } else {
  2229. if (options.scale >= 0.01) {
  2230. oldSize = this.pdf.internal.getFontSize();
  2231. this.pdf.setFontSize(oldSize * options.scale);
  2232. oldLineWidth = this.lineWidth;
  2233. this.lineWidth = oldLineWidth * options.scale;
  2234. }
  2235. this.pdf.text(options.text, pt.x + this.posX, pt.y + this.posY, {
  2236. angle: options.angle,
  2237. align: textAlign,
  2238. renderingMode: options.renderingMode,
  2239. maxWidth: options.maxWidth
  2240. });
  2241. if (options.scale >= 0.01) {
  2242. this.pdf.setFontSize(oldSize);
  2243. this.lineWidth = oldLineWidth;
  2244. }
  2245. }
  2246. };
  2247. var drawLine = function(x, y, prevX, prevY) {
  2248. prevX = prevX || 0;
  2249. prevY = prevY || 0;
  2250. this.pdf.internal.out(
  2251. getHorizontalCoordinateString(x + prevX) +
  2252. " " +
  2253. getVerticalCoordinateString(y + prevY) +
  2254. " l"
  2255. );
  2256. };
  2257. var drawLines = function(lines, x, y) {
  2258. return this.pdf.lines(lines, x, y, null, null);
  2259. };
  2260. var drawCurve = function(x, y, x1, y1, x2, y2, x3, y3) {
  2261. this.pdf.internal.out(
  2262. [
  2263. f2(getHorizontalCoordinate(x1 + x)),
  2264. f2(getVerticalCoordinate(y1 + y)),
  2265. f2(getHorizontalCoordinate(x2 + x)),
  2266. f2(getVerticalCoordinate(y2 + y)),
  2267. f2(getHorizontalCoordinate(x3 + x)),
  2268. f2(getVerticalCoordinate(y3 + y)),
  2269. "c"
  2270. ].join(" ")
  2271. );
  2272. };
  2273. /**
  2274. * 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.
  2275. *
  2276. * 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.
  2277. * @function createArc
  2278. */
  2279. var createArc = function(radius, startAngle, endAngle, anticlockwise) {
  2280. var EPSILON = 0.00001; // Roughly 1/1000th of a degree, see below
  2281. var twoPi = Math.PI * 2;
  2282. var halfPi = Math.PI / 2.0;
  2283. while (startAngle > endAngle) {
  2284. startAngle = startAngle - twoPi;
  2285. }
  2286. var totalAngle = Math.abs(endAngle - startAngle);
  2287. if (totalAngle < twoPi) {
  2288. if (anticlockwise) {
  2289. totalAngle = twoPi - totalAngle;
  2290. }
  2291. }
  2292. // Compute the sequence of arc curves, up to PI/2 at a time.
  2293. var curves = [];
  2294. // clockwise or counterclockwise
  2295. var sgn = anticlockwise ? -1 : +1;
  2296. var a1 = startAngle;
  2297. for (; totalAngle > EPSILON; ) {
  2298. var remain = sgn * Math.min(totalAngle, halfPi);
  2299. var a2 = a1 + remain;
  2300. curves.push(createSmallArc.call(this, radius, a1, a2));
  2301. totalAngle -= Math.abs(a2 - a1);
  2302. a1 = a2;
  2303. }
  2304. return curves;
  2305. };
  2306. /**
  2307. * 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.
  2308. *
  2309. * 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.
  2310. *
  2311. * 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.
  2312. */
  2313. var createSmallArc = function(r, a1, a2) {
  2314. var a = (a2 - a1) / 2.0;
  2315. var x4 = r * Math.cos(a);
  2316. var y4 = r * Math.sin(a);
  2317. var x1 = x4;
  2318. var y1 = -y4;
  2319. var q1 = x1 * x1 + y1 * y1;
  2320. var q2 = q1 + x1 * x4 + y1 * y4;
  2321. var k2 = ((4 / 3) * (Math.sqrt(2 * q1 * q2) - q2)) / (x1 * y4 - y1 * x4);
  2322. var x2 = x1 - k2 * y1;
  2323. var y2 = y1 + k2 * x1;
  2324. var x3 = x2;
  2325. var y3 = -y2;
  2326. var ar = a + a1;
  2327. var cos_ar = Math.cos(ar);
  2328. var sin_ar = Math.sin(ar);
  2329. return {
  2330. x1: r * Math.cos(a1),
  2331. y1: r * Math.sin(a1),
  2332. x2: x2 * cos_ar - y2 * sin_ar,
  2333. y2: x2 * sin_ar + y2 * cos_ar,
  2334. x3: x3 * cos_ar - y3 * sin_ar,
  2335. y3: x3 * sin_ar + y3 * cos_ar,
  2336. x4: r * Math.cos(a2),
  2337. y4: r * Math.sin(a2)
  2338. };
  2339. };
  2340. var rad2deg = function(value) {
  2341. return (value * 180) / Math.PI;
  2342. };
  2343. var getQuadraticCurveBoundary = function(sx, sy, cpx, cpy, ex, ey) {
  2344. var midX1 = sx + (cpx - sx) * 0.5;
  2345. var midY1 = sy + (cpy - sy) * 0.5;
  2346. var midX2 = ex + (cpx - ex) * 0.5;
  2347. var midY2 = ey + (cpy - ey) * 0.5;
  2348. var resultX1 = Math.min(sx, ex, midX1, midX2);
  2349. var resultX2 = Math.max(sx, ex, midX1, midX2);
  2350. var resultY1 = Math.min(sy, ey, midY1, midY2);
  2351. var resultY2 = Math.max(sy, ey, midY1, midY2);
  2352. return new Rectangle(
  2353. resultX1,
  2354. resultY1,
  2355. resultX2 - resultX1,
  2356. resultY2 - resultY1
  2357. );
  2358. };
  2359. //De Casteljau algorithm
  2360. var getBezierCurveBoundary = function(ax, ay, bx, by, cx, cy, dx, dy) {
  2361. var tobx = bx - ax;
  2362. var toby = by - ay;
  2363. var tocx = cx - bx;
  2364. var tocy = cy - by;
  2365. var todx = dx - cx;
  2366. var tody = dy - cy;
  2367. var precision = 40;
  2368. var d,
  2369. i,
  2370. px,
  2371. py,
  2372. qx,
  2373. qy,
  2374. rx,
  2375. ry,
  2376. tx,
  2377. ty,
  2378. sx,
  2379. sy,
  2380. x,
  2381. y,
  2382. minx,
  2383. miny,
  2384. maxx,
  2385. maxy,
  2386. toqx,
  2387. toqy,
  2388. torx,
  2389. tory,
  2390. totx,
  2391. toty;
  2392. for (i = 0; i < precision + 1; i++) {
  2393. d = i / precision;
  2394. px = ax + d * tobx;
  2395. py = ay + d * toby;
  2396. qx = bx + d * tocx;
  2397. qy = by + d * tocy;
  2398. rx = cx + d * todx;
  2399. ry = cy + d * tody;
  2400. toqx = qx - px;
  2401. toqy = qy - py;
  2402. torx = rx - qx;
  2403. tory = ry - qy;
  2404. sx = px + d * toqx;
  2405. sy = py + d * toqy;
  2406. tx = qx + d * torx;
  2407. ty = qy + d * tory;
  2408. totx = tx - sx;
  2409. toty = ty - sy;
  2410. x = sx + d * totx;
  2411. y = sy + d * toty;
  2412. if (i == 0) {
  2413. minx = x;
  2414. miny = y;
  2415. maxx = x;
  2416. maxy = y;
  2417. } else {
  2418. minx = Math.min(minx, x);
  2419. miny = Math.min(miny, y);
  2420. maxx = Math.max(maxx, x);
  2421. maxy = Math.max(maxy, y);
  2422. }
  2423. }
  2424. return new Rectangle(
  2425. Math.round(minx),
  2426. Math.round(miny),
  2427. Math.round(maxx - minx),
  2428. Math.round(maxy - miny)
  2429. );
  2430. };
  2431. var getPrevLineDashValue = function(lineDash, lineDashOffset) {
  2432. return JSON.stringify({
  2433. lineDash: lineDash,
  2434. lineDashOffset: lineDashOffset
  2435. });
  2436. };
  2437. var setLineDash = function() {
  2438. // Avoid unnecessary line dash declarations.
  2439. if (
  2440. !this.prevLineDash &&
  2441. !this.ctx.lineDash.length &&
  2442. !this.ctx.lineDashOffset
  2443. ) {
  2444. return;
  2445. }
  2446. // Avoid unnecessary line dash declarations.
  2447. const nextLineDash = getPrevLineDashValue(
  2448. this.ctx.lineDash,
  2449. this.ctx.lineDashOffset
  2450. );
  2451. if (this.prevLineDash !== nextLineDash) {
  2452. this.pdf.setLineDash(this.ctx.lineDash, this.ctx.lineDashOffset);
  2453. this.prevLineDash = nextLineDash;
  2454. }
  2455. };
  2456. })(jsPDF.API);