UNPKG

50 kBJavaScriptView Raw
1/** @preserve
2The MIT License (MIT)
3
4Copyright (c) 2015 yWorks GmbH
5
6Permission is hereby granted, free of charge, to any person obtaining a copy
7of this software and associated documentation files (the "Software"), to deal
8in the Software without restriction, including without limitation the rights
9to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10copies of the Software, and to permit persons to whom the Software is
11furnished to do so, subject to the following conditions:
12
13The above copyright notice and this permission notice shall be included in all
14copies or substantial portions of the Software.
15
16THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22SOFTWARE.
23*/
24
25/**
26 * Renders an svg element to a jsPDF document.
27 * For accurate results a DOM document is required (mainly used for text size measurement and image format conversion)
28 * @param element {HTMLElement} The svg element, which will be cloned, so the original stays unchanged.
29 * @param pdf {jsPDF} The jsPDF object.
30 * @param options {object} An object that may contain render options. Currently supported are:
31 * scale: The global factor by which everything is scaled.
32 * xOffset, yOffset: Offsets that are added to every coordinate AFTER scaling (They are not
33 * influenced by the scale attribute).
34 */
35(function (global) {
36 var RGBColor;
37
38 var _pdf; // jsPDF pdf-document
39
40 var cToQ = 2 / 3; // ratio to convert quadratic bezier curves to cubic ones
41
42 // pathSegList is marked deprecated in chrome, so parse the d attribute manually if necessary
43 var getPathSegList = function (node) {
44 var pathSegList = node.pathSegList;
45 if (pathSegList) {
46 return pathSegList;
47 }
48
49 pathSegList = [];
50
51 var d = node.getAttribute("d");
52
53 var regex = /([a-df-zA-DF-Z])([^a-df-zA-DF-Z]*)/g,
54 match;
55 while (match = regex.exec(d)) {
56 var coords = parseFloats(match[2]);
57
58 var type = match[1];
59 var length = "zZ".indexOf(type) >= 0 ? 0 :
60 "hHvV".indexOf(type) >= 0 ? 1 :
61 "mMlLtT".indexOf(type) >= 0 ? 2 :
62 "sSqQ".indexOf(type) >= 0 ? 4 :
63 "cC".indexOf(type) >= 0 ? 6 : -1;
64
65 var i = 0;
66 do {
67 var pathSeg = {pathSegTypeAsLetter: type};
68 switch (type) {
69 case "h":
70 case "H":
71 pathSeg.x = coords[i];
72 break;
73
74 case "v":
75 case "V":
76 pathSeg.y = coords[i];
77 break;
78
79 case "c":
80 case "C":
81 pathSeg.x1 = coords[i + length - 6];
82 pathSeg.y1 = coords[i + length - 5];
83 case "s":
84 case "S":
85 pathSeg.x2 = coords[i + length - 4];
86 pathSeg.y2 = coords[i + length - 3];
87 case "t":
88 case "T":
89 case "l":
90 case "L":
91 case "m":
92 case "M":
93 pathSeg.x = coords[i + length - 2];
94 pathSeg.y = coords[i + length - 1];
95 break;
96
97 case "q":
98 case "Q":
99 pathSeg.x1 = coords[i];
100 pathSeg.y1 = coords[i + 1];
101 pathSeg.x = coords[i + 2];
102 pathSeg.y = coords[i + 3];
103 break;
104 // TODO: a,A
105 }
106
107 pathSegList.push(pathSeg);
108 i += length;
109 } while(i < coords.length);
110 }
111
112 pathSegList.getItem = function (i) {
113 return this[i]
114 };
115 pathSegList.numberOfItems = pathSegList.length;
116
117 return pathSegList;
118 };
119
120 // returns an attribute of a node, either from the node directly or from css
121 var getAttribute = function (node, propertyNode, propertyCss) {
122 propertyCss = propertyCss || propertyNode;
123 return node.getAttribute(propertyNode) || node.style[propertyCss];
124 };
125
126 var nodeIs = function (node, tagsString) {
127 return tagsString.split(",").indexOf(node.tagName.toLowerCase()) >= 0;
128 };
129
130 var forEachChild = function (node, fn) {
131 // copy list of children, as the original might be modified
132 var children = [];
133 for (var i = 0; i < node.childNodes.length; i++) {
134 var childNode = node.childNodes[i];
135 if (childNode.nodeName.charAt(0) !== "#")
136 children.push(childNode);
137 }
138 for (i = 0; i < children.length; i++) {
139 fn(i, children[i]);
140 }
141 };
142
143 var getAngle = function (from, to) {
144 return Math.atan2(to[1] - from[1], to[0] - from[0]);
145 };
146
147 // mirrors p1 at p2
148 var mirrorPoint = function (p1, p2) {
149 var dx = p2[0] - p1[0];
150 var dy = p2[1] - p1[1];
151
152 return [p1[0] + 2 * dx, p1[1] + 2 * dy];
153 };
154
155 // transforms a cubic bezier control point to a quadratic one: returns from + (2/3) * (to - from)
156 var toCubic = function (from, to) {
157 return [cToQ * (to[0] - from[0]) + from[0], cToQ * (to[1] - from[1]) + from[1]];
158 };
159
160 // extracts a control point from a previous path segment (for t,T,s,S segments)
161 var getControlPointFromPrevious = function (i, from, list, prevX, prevY) {
162 var prev = list.getItem(i - 1);
163 var p2;
164 if (i > 0 && (prev.pathSegTypeAsLetter === "C" || prev.pathSegTypeAsLetter === "S")) {
165 p2 = mirrorPoint([prev.x2, prev.y2], from);
166 } else if (i > 0 && (prev.pathSegTypeAsLetter === "c" || prev.pathSegTypeAsLetter === "s")) {
167 p2 = mirrorPoint([prev.x2 + prevX, prev.y2 + prevY], from);
168 } else {
169 p2 = [from[0], from[1]];
170 }
171 return p2;
172 };
173
174 // an id prefix to handle duplicate ids
175 var SvgPrefix = function (prefix) {
176 this.prefix = prefix;
177 this.id = 0;
178 this.nextChild = function () {
179 return new SvgPrefix("_" + this.id++ + "_" + this.get());
180 };
181 this.get = function () {
182 return this.prefix;
183 }
184 };
185
186 // returns the node for the specified id or incrementally removes prefixes to search "higher" levels
187 var getFromDefs = function (id, defs) {
188 var regExp = /_\d+_/;
189 while (!defs[id] && regExp.exec(id)) {
190 id = id.replace(regExp, "");
191 }
192 return defs[id];
193 };
194
195 // replace any newline characters by space and trim
196 var removeNewlinesAndTrim = function (str) {
197 return str.replace(/[\n\s\r]+/, " ").trim();
198 };
199
200 // clones the defs object (or basically any object)
201 var cloneDefs = function (defs) {
202 var clone = {};
203 for (var key in defs) {
204 if (defs.hasOwnProperty(key)) {
205 clone[key] = defs[key];
206 }
207 }
208 return clone;
209 };
210
211 // computes the transform directly applied at the node (such as viewbox scaling and the "transform" atrribute)
212 // x,y,cx,cy,r,... are omitted
213 var computeNodeTransform = function (node) {
214 var height, width, viewBoxHeight, viewBoxWidth, bounds, viewBox, y, x;
215 var nodeTransform = _pdf.unitMatrix;
216 if (nodeIs(node, "svg,g")) {
217 x = parseFloat(node.getAttribute("x")) || 0;
218 y = parseFloat(node.getAttribute("y")) || 0;
219
220 // jquery doesn't like camelCase notation...
221 viewBox = node.getAttribute("viewBox");
222 if (viewBox) {
223 bounds = parseFloats(viewBox);
224 viewBoxWidth = bounds[2] - bounds[0];
225 viewBoxHeight = bounds[3] - bounds[1];
226 width = parseFloat(node.getAttribute("width")) || viewBoxWidth;
227 height = parseFloat(node.getAttribute("height")) || viewBoxHeight;
228 nodeTransform = new _pdf.Matrix(width / viewBoxWidth, 0, 0, height / viewBoxHeight, x - bounds[0], y - bounds[1]);
229 } else {
230 nodeTransform = new _pdf.Matrix(1, 0, 0, 1, x, y);
231 }
232 } else if (nodeIs(node, "marker")) {
233 x = -parseFloat(node.getAttribute("refX")) || 0;
234 y = -parseFloat(node.getAttribute("refY")) || 0;
235
236 viewBox = node.getAttribute("viewBox");
237 if (viewBox) {
238 bounds = parseFloats(viewBox);
239 viewBoxWidth = bounds[2] - bounds[0];
240 viewBoxHeight = bounds[3] - bounds[1];
241 width = parseFloat(node.getAttribute("markerWidth")) || viewBoxWidth;
242 height = parseFloat(node.getAttribute("markerHeight")) || viewBoxHeight;
243
244 var s = new _pdf.Matrix(width / viewBoxWidth, 0, 0, height / viewBoxHeight, 0, 0);
245 var t = new _pdf.Matrix(1, 0, 0, 1, x, y);
246 nodeTransform = _pdf.matrixMult(t, s);
247 } else {
248 nodeTransform = new _pdf.Matrix(1, 0, 0, 1, x, y);
249 }
250 }
251
252 var transformString = node.getAttribute("transform");
253 if (!transformString)
254 return nodeTransform;
255 else
256 return _pdf.matrixMult(nodeTransform, parseTransform(transformString));
257 };
258
259 // parses the "points" string used by polygons and returns an array of points
260 var parsePointsString = function (string) {
261 var floats = parseFloats(string);
262 var result = [];
263 for (var i = 0; i < floats.length - 1; i += 2) {
264 var x = floats[i];
265 var y = floats[i + 1];
266 result.push([x, y]);
267 }
268 return result;
269 };
270
271 // parses the "transform" string
272 var parseTransform = function (transformString) {
273 if (!transformString)
274 return _pdf.unitMatrix;
275
276 var mRegex = /^\s*matrix\(([^\)]+)\)\s*/,
277 tRegex = /^\s*translate\(([^\)]+)\)\s*/,
278 rRegex = /^\s*rotate\(([^\)]+)\)\s*/,
279 sRegex = /^\s*scale\(([^\)]+)\)\s*/,
280 sXRegex = /^\s*skewX\(([^\)]+)\)\s*/,
281 sYRegex = /^\s*skewY\(([^\)]+)\)\s*/;
282
283 var resultMatrix = _pdf.unitMatrix, m;
284
285 while (transformString.length > 0) {
286 var match = mRegex.exec(transformString);
287 if (match) {
288 m = parseFloats(match[1]);
289 resultMatrix = _pdf.matrixMult(new _pdf.Matrix(m[0], m[1], m[2], m[3], m[4], m[5]), resultMatrix);
290 transformString = transformString.substr(match[0].length);
291 }
292 match = rRegex.exec(transformString);
293 if (match) {
294 m = parseFloats(match[1]);
295 var a = Math.PI * m[0] / 180;
296 resultMatrix = _pdf.matrixMult(new _pdf.Matrix(Math.cos(a), Math.sin(a), -Math.sin(a), Math.cos(a), 0, 0), resultMatrix);
297 if (m[1] && m[2]) {
298 var t1 = new _pdf.Matrix(1, 0, 0, 1, m[1], m[2]);
299 var t2 = new _pdf.Matrix(1, 0, 0, 1, -m[1], -m[2]);
300 resultMatrix = _pdf.matrixMult(t2, _pdf.matrixMult(resultMatrix, t1));
301 }
302 transformString = transformString.substr(match[0].length);
303 }
304 match = tRegex.exec(transformString);
305 if (match) {
306 m = parseFloats(match[1]);
307 resultMatrix = _pdf.matrixMult(new _pdf.Matrix(1, 0, 0, 1, m[0], m[1] || 0), resultMatrix);
308 transformString = transformString.substr(match[0].length);
309 }
310 match = sRegex.exec(transformString);
311 if (match) {
312 m = parseFloats(match[1]);
313 if (!m[1])
314 m[1] = m[0];
315 resultMatrix = _pdf.matrixMult(new _pdf.Matrix(m[0], 0, 0, m[1], 0, 0), resultMatrix);
316 transformString = transformString.substr(match[0].length);
317 }
318 match = sXRegex.exec(transformString);
319 if (match) {
320 m = parseFloat(match[1]);
321 resultMatrix = _pdf.matrixMult(new _pdf.Matrix(1, 0, Math.tan(m), 1, 0, 0), resultMatrix);
322 transformString = transformString.substr(match[0].length);
323 }
324 match = sYRegex.exec(transformString);
325 if (match) {
326 m = parseFloat(match[1]);
327 resultMatrix = _pdf.matrixMult(new _pdf.Matrix(1, Math.tan(m), 0, 1, 0, 0), resultMatrix);
328 transformString = transformString.substr(match[0].length);
329 }
330 }
331 return resultMatrix;
332 };
333
334 // parses a comma, sign and/or whitespace separated string of floats and returns the single floats in an array
335 var parseFloats = function (str) {
336 var floats = [], match,
337 regex = /[+-]?(?:(?:\d+\.?\d*)|(?:\d*\.?\d+))(?:[eE][+-]?\d+)?/g;
338 while(match = regex.exec(str)) {
339 floats.push(parseFloat(match[0]));
340 }
341 return floats;
342 };
343
344 // extends RGBColor by rgba colors as RGBColor is not capable of it
345 var parseColor = function (colorString) {
346 var match = /\s*rgba\(((?:[^,\)]*,){3}[^,\)]*)\)\s*/.exec(colorString);
347 if (match) {
348 var floats = parseFloats(match[1]);
349 var color = new RGBColor("rgb(" + floats.slice(0,3).join(",") + ")");
350 color.a = floats[3];
351 return color;
352 } else {
353 return new RGBColor(colorString);
354 }
355 };
356
357 // multiplies a vector with a matrix: vec' = vec * matrix
358 var multVecMatrix = function (vec, matrix) {
359 var x = vec[0];
360 var y = vec[1];
361 return [
362 matrix.a * x + matrix.c * y + matrix.e,
363 matrix.b * x + matrix.d * y + matrix.f
364 ];
365 };
366
367 // returns the untransformed bounding box [x, y, width, height] of an svg element (quite expensive for path and polygon objects, as
368 // the whole points/d-string has to be processed)
369 var getUntransformedBBox = function (node) {
370 var i, minX, minY, maxX, maxY, viewBox, vb, boundingBox;
371 var pf = parseFloat;
372
373 if (nodeIs(node, "polygon")) {
374 var points = parsePointsString(node.getAttribute("points"));
375 minX = Number.POSITIVE_INFINITY;
376 minY = Number.POSITIVE_INFINITY;
377 maxX = Number.NEGATIVE_INFINITY;
378 maxY = Number.NEGATIVE_INFINITY;
379 for (i = 0; i < points.length; i++) {
380 var point = points[i];
381 minX = Math.min(minX, point[0]);
382 maxX = Math.max(maxX, point[0]);
383 minY = Math.min(minY, point[1]);
384 maxY = Math.max(maxY, point[1]);
385 }
386 boundingBox = [
387 minX,
388 minY,
389 maxX - minX,
390 maxY - minY
391 ];
392 } else if (nodeIs(node, "path")) {
393 var list = getPathSegList(node);
394 minX = Number.POSITIVE_INFINITY;
395 minY = Number.POSITIVE_INFINITY;
396 maxX = Number.NEGATIVE_INFINITY;
397 maxY = Number.NEGATIVE_INFINITY;
398 var x = 0, y = 0;
399 var prevX, prevY, newX, newY;
400 var p2, p3, to;
401 for (i = 0; i < list.numberOfItems; i++) {
402 var seg = list.getItem(i);
403 var cmd = seg.pathSegTypeAsLetter;
404 switch (cmd) {
405 case "H":
406 newX = seg.x;
407 newY = y;
408 break;
409 case "h":
410 newX = seg.x + x;
411 newY = y;
412 break;
413 case "V":
414 newX = x;
415 newY = seg.y;
416 break;
417 case "v":
418 newX = x;
419 newY = seg.y + y;
420 break;
421 case "C":
422 p2 = [seg.x1, seg.y1];
423 p3 = [seg.x2, seg.y2];
424 to = [seg.x, seg.y];
425 break;
426 case "c":
427 p2 = [seg.x1 + x, seg.y1 + y];
428 p3 = [seg.x2 + x, seg.y2 + y];
429 to = [seg.x + x, seg.y + y];
430 break;
431 case "S":
432 p2 = getControlPointFromPrevious(i, [x, y], list, prevX, prevY);
433 p3 = [seg.x2, seg.y2];
434 to = [seg.x, seg.y];
435 break;
436 case "s":
437 p2 = getControlPointFromPrevious(i, [x, y], list, prevX, prevY);
438 p3 = [seg.x2 + x, seg.y2 + y];
439 to = [seg.x + x, seg.y + y];
440 break;
441 case "Q":
442 pf = [seg.x1, seg.y1];
443 p2 = toCubic([x, y], pf);
444 p3 = toCubic([seg.x, seg.y], pf);
445 to = [seg.x, seg.y];
446 break;
447 case "q":
448 pf = [seg.x1 + x, seg.y1 + y];
449 p2 = toCubic([x, y], pf);
450 p3 = toCubic([x + seg.x, y + seg.y], pf);
451 to = [seg.x + x, seg.y + y];
452 break;
453 case "T":
454 p2 = getControlPointFromPrevious(i, [x, y], list, prevX, prevY);
455 p2 = toCubic([x, y], pf);
456 p3 = toCubic([seg.x, seg.y], pf);
457 to = [seg.x, seg.y];
458 break;
459 case "t":
460 pf = getControlPointFromPrevious(i, [x, y], list, prevX, prevY);
461 p2 = toCubic([x, y], pf);
462 p3 = toCubic([x + seg.x, y + seg.y], pf);
463 to = [seg.x + x, seg.y + y];
464 break;
465 // TODO: A,a
466 }
467 if ("sScCqQtT".indexOf(cmd) >= 0) {
468 prevX = x;
469 prevY = y;
470 }
471 if ("MLCSQT".indexOf(cmd) >= 0) {
472 x = seg.x;
473 y = seg.y;
474 } else if ("mlcsqt".indexOf(cmd) >= 0) {
475 x = seg.x + x;
476 y = seg.y + y;
477 } else if ("zZ".indexOf(cmd) < 0) {
478 x = newX;
479 y = newY;
480 }
481 if ("CSQTcsqt".indexOf(cmd) >= 0) {
482 minX = Math.min(minX, x, p2[0], p3[0], to[0]);
483 maxX = Math.max(maxX, x, p2[0], p3[0], to[0]);
484 minY = Math.min(minY, y, p2[1], p3[1], to[1]);
485 maxY = Math.max(maxY, y, p2[1], p3[1], to[1]);
486 } else {
487 minX = Math.min(minX, x);
488 maxX = Math.max(maxX, x);
489 minY = Math.min(minY, y);
490 maxY = Math.max(maxY, y);
491 }
492 }
493 boundingBox = [
494 minX,
495 minY,
496 maxX - minX,
497 maxY - minY
498 ];
499 } else if (nodeIs(node, "svg")) {
500 viewBox = node.getAttribute("viewBox");
501 if (viewBox) {
502 vb = parseFloats(viewBox);
503 }
504 return [
505 pf(node.getAttribute("x")) || (vb && vb[0]) || 0,
506 pf(node.getAttribute("y")) || (vb && vb[1]) || 0,
507 pf(node.getAttribute("width")) || (vb && vb[2]) || 0,
508 pf(node.getAttribute("height")) || (vb && vb[3]) || 0
509 ];
510 } else if (nodeIs(node, "g")) {
511 boundingBox = [0, 0, 0, 0];
512 forEachChild(node, function (i, node) {
513 var nodeBox = getUntransformedBBox(node);
514 boundingBox = [
515 Math.min(boundingBox[0], nodeBox[0]),
516 Math.min(boundingBox[1], nodeBox[1]),
517 Math.max(boundingBox[0] + boundingBox[2], nodeBox[0] + nodeBox[2]) - Math.min(boundingBox[0], nodeBox[0]),
518 Math.max(boundingBox[1] + boundingBox[3], nodeBox[1] + nodeBox[3]) - Math.min(boundingBox[1], nodeBox[1])
519 ];
520 });
521 } else if (nodeIs(node, "marker")) {
522 viewBox = node.getAttribute("viewBox");
523 if (viewBox) {
524 vb = parseFloats(viewBox);
525 }
526 return [
527 (vb && vb[0]) || 0,
528 (vb && vb[1]) || 0,
529 (vb && vb[2]) || pf(node.getAttribute("marker-width")) || 0,
530 (vb && vb[3]) || pf(node.getAttribute("marker-height")) || 0
531 ];
532 } else if (nodeIs(node, "pattern")) {
533 return [
534 pf(node.getAttribute("x")) || 0,
535 pf(node.getAttribute("y")) || 0,
536 pf(node.getAttribute("width")) || 0,
537 pf(node.getAttribute("height")) || 0
538 ]
539 } else {
540 // TODO: check if there are other possible coordinate attributes
541 var x1 = pf(node.getAttribute("x1")) || pf(node.getAttribute("x")) || pf((node.getAttribute("cx")) - pf(node.getAttribute("r"))) || 0;
542 var x2 = pf(node.getAttribute("x2")) || (x1 + pf(node.getAttribute("width"))) || (pf(node.getAttribute("cx")) + pf(node.getAttribute("r"))) || 0;
543 var y1 = pf(node.getAttribute("y1")) || pf(node.getAttribute("y")) || (pf(node.getAttribute("cy")) - pf(node.getAttribute("r"))) || 0;
544 var y2 = pf(node.getAttribute("y2")) || (y1 + pf(node.getAttribute("height"))) || (pf(node.getAttribute("cy")) + pf(node.getAttribute("r"))) || 0;
545 boundingBox = [
546 Math.min(x1, x2),
547 Math.min(y1, y2),
548 Math.max(x1, x2) - Math.min(x1, x2),
549 Math.max(y1, y2) - Math.min(y1, y2)
550 ];
551 }
552
553 if (!nodeIs(node, "marker,svg,g")) {
554 // add line-width
555 var lineWidth = getAttribute(node, "stroke-width") || 1;
556 var miterLimit = getAttribute(node, "stroke-miterlimit");
557 // miterLength / lineWidth = 1 / sin(phi / 2)
558 miterLimit && (lineWidth *= 0.5 / (Math.sin(Math.PI / 12)));
559 return [
560 boundingBox[0] - lineWidth,
561 boundingBox[1] - lineWidth,
562 boundingBox[2] + 2 * lineWidth,
563 boundingBox[3] + 2 * lineWidth
564 ];
565 }
566
567 return boundingBox;
568 };
569
570 // transforms a bounding box and returns a new rect that contains it
571 var transformBBox = function (box, matrix) {
572 var bl = multVecMatrix([box[0], box[1]], matrix);
573 var br = multVecMatrix([box[0] + box[2], box[1]], matrix);
574 var tl = multVecMatrix([box[0], box[1] + box[3]], matrix);
575 var tr = multVecMatrix([box[0] + box[2], box[1] + box[3]], matrix);
576
577 var bottom = Math.min(bl[1], br[1], tl[1], tr[1]);
578 var left = Math.min(bl[0], br[0], tl[0], tr[0]);
579 var top = Math.max(bl[1], br[1], tl[1], tr[1]);
580 var right = Math.max(bl[0], br[0], tl[0], tr[0]);
581
582 return [
583 left,
584 bottom,
585 right - left,
586 top - bottom
587 ]
588 };
589
590 // draws a polygon
591 var polygon = function (node, tfMatrix, colorMode, gradient, gradientMatrix) {
592 var points = parsePointsString(node.getAttribute("points"));
593 var lines = [{op: "m", c: multVecMatrix(points[0], tfMatrix)}];
594 for (var i = 1; i < points.length; i++) {
595 var p = points[i];
596 var to = multVecMatrix(p, tfMatrix);
597 lines.push({op: "l", c: to});
598 }
599 lines.push({op: "h"});
600 _pdf.path(lines, colorMode, gradient, gradientMatrix);
601 };
602
603 // draws an image (converts it to jpeg first, as jsPDF doesn't support png or other formats)
604 var image = function (node) {
605 // convert image to jpeg
606 var imageUrl = node.getAttribute("xlink:href") || node.getAttribute("href");
607 var image = new Image();
608 image.src = imageUrl;
609
610 var canvas = document.createElement("canvas");
611 var width = parseFloat(node.getAttribute("width")),
612 height = parseFloat(node.getAttribute("height")),
613 x = parseFloat(node.getAttribute("x") || 0),
614 y = parseFloat(node.getAttribute("y") || 0);
615 canvas.width = width;
616 canvas.height = height;
617 var context = canvas.getContext("2d");
618 context.fillStyle = "#fff";
619 context.fillRect(0, 0, width, height);
620 context.drawImage(image, 0, 0, width, height);
621 var jpegUrl = canvas.toDataURL("image/jpeg");
622
623 _pdf.addImage(jpegUrl,
624 "jpeg",
625 x,
626 y,
627 width,
628 height
629 );
630 };
631
632 // draws a path
633 var path = function (node, tfMatrix, svgIdPrefix, colorMode, gradient, gradientMatrix) {
634 var list = getPathSegList(node);
635 var markerEnd = node.getAttribute("marker-end"),
636 markerStart = node.getAttribute("marker-start"),
637 markerMid = node.getAttribute("marker-mid");
638
639 var getLinesFromPath = function (pathSegList, tfMatrix) {
640 var x = 0, y = 0;
641 var x0 = x, y0 = y;
642 var prevX, prevY, newX, newY;
643 var to, p, p2, p3;
644 var lines = [];
645 var markers = [];
646 var op;
647 var prevAngle = 0, curAngle;
648
649 var addMarker = function (angle, anchor, type) {
650 var cos = Math.cos(angle);
651 var sin = Math.sin(angle);
652 var tf;
653 tf = new _pdf.Matrix(cos, sin, -sin, cos, anchor[0], anchor[1]);
654 markers.push({type: type, tf: _pdf.matrixMult(tf, tfMatrix)});
655 };
656
657 for (var i = 0; i < list.numberOfItems; i++) {
658 var seg = list.getItem(i);
659 var cmd = seg.pathSegTypeAsLetter;
660 switch (cmd) {
661 case "M":
662 x0 = x;
663 y0 = y;
664 to = [seg.x, seg.y];
665 op = "m";
666 break;
667 case "m":
668 x0 = x;
669 y0 = y;
670 to = [seg.x + x, seg.y + y];
671 op = "m";
672 break;
673 case "L":
674 to = [seg.x, seg.y];
675 op = "l";
676 break;
677 case "l":
678 to = [seg.x + x, seg.y + y];
679 op = "l";
680 break;
681 case "H":
682 to = [seg.x, y];
683 op = "l";
684 newX = seg.x;
685 newY = y;
686 break;
687 case "h":
688 to = [seg.x + x, y];
689 op = "l";
690 newX = seg.x + x;
691 newY = y;
692 break;
693 case "V":
694 to = [x, seg.y];
695 op = "l";
696 newX = x;
697 newY = seg.y;
698 break;
699 case "v":
700 to = [x, seg.y + y];
701 op = "l";
702 newX = x;
703 newY = seg.y + y;
704 break;
705 case "C":
706 p2 = [seg.x1, seg.y1];
707 p3 = [seg.x2, seg.y2];
708 to = [seg.x, seg.y];
709 break;
710 case "c":
711 p2 = [seg.x1 + x, seg.y1 + y];
712 p3 = [seg.x2 + x, seg.y2 + y];
713 to = [seg.x + x, seg.y + y];
714 break;
715 case "S":
716 p2 = getControlPointFromPrevious(i, [x, y], list, prevX, prevY);
717 p3 = [seg.x2, seg.y2];
718 to = [seg.x, seg.y];
719 break;
720 case "s":
721 p2 = getControlPointFromPrevious(i, [x, y], list, prevX, prevY);
722 p3 = [seg.x2 + x, seg.y2 + y];
723 to = [seg.x + x, seg.y + y];
724 break;
725 case "Q":
726 p = [seg.x1, seg.y1];
727 p2 = toCubic([x, y], p);
728 p3 = toCubic([seg.x, seg.y], p);
729 to = [seg.x, seg.y];
730 break;
731 case "q":
732 p = [seg.x1 + x, seg.y1 + y];
733 p2 = toCubic([x, y], p);
734 p3 = toCubic([x + seg.x, y + seg.y], p);
735 to = [seg.x + x, seg.y + y];
736 break;
737 case "T":
738 p2 = getControlPointFromPrevious(i, [x, y], list, prevX, prevY);
739 p2 = toCubic([x, y], p);
740 p3 = toCubic([seg.x, seg.y], p);
741 to = [seg.x, seg.y];
742 break;
743 case "t":
744 p = getControlPointFromPrevious(i, [x, y], list, prevX, prevY);
745 p2 = toCubic([x, y], p);
746 p3 = toCubic([x + seg.x, y + seg.y], p);
747 to = [seg.x + x, seg.y + y];
748 break;
749 // TODO: A,a
750 case "Z":
751 case "z":
752 x = x0;
753 y = y0;
754 lines.push({op: "h"});
755 break;
756 }
757
758 var hasStartMarker = markerStart
759 && (i === 1
760 || ("mM".indexOf(cmd) < 0 && "mM".indexOf(list.getItem(i - 1).pathSegTypeAsLetter) >= 0));
761 var hasEndMarker = markerEnd
762 && (i === list.numberOfItems - 1
763 || ("mM".indexOf(cmd) < 0 && "mM".indexOf(list.getItem(i + 1).pathSegTypeAsLetter) >= 0));
764 var hasMidMarker = markerMid
765 && i > 0
766 && !(i === 1 && "mM".indexOf(list.getItem(i - 1).pathSegTypeAsLetter) >= 0);
767
768 if ("sScCqQtT".indexOf(cmd) >= 0) {
769 hasStartMarker && addMarker(getAngle([x, y], p2), [x, y], "start");
770 hasEndMarker && addMarker(getAngle(p3, to), to, "end");
771 if (hasMidMarker) {
772 curAngle = getAngle([x, y], p2);
773 curAngle = "mM".indexOf(list.getItem(i - 1).pathSegTypeAsLetter) >= 0 ?
774 curAngle : .5 * (prevAngle + curAngle);
775 addMarker(curAngle, [x, y], "mid");
776 }
777
778 prevAngle = getAngle(p3, to);
779
780 prevX = x;
781 prevY = y;
782 p2 = multVecMatrix(p2, tfMatrix);
783 p3 = multVecMatrix(p3, tfMatrix);
784 p = multVecMatrix(to, tfMatrix);
785 lines.push({
786 op: "c", c: [
787 p2[0], p2[1],
788 p3[0], p3[1],
789 p[0], p[1]
790 ]
791 });
792 } else if ("lLhHvVmM".indexOf(cmd) >= 0) {
793 curAngle = getAngle([x, y], to);
794 hasStartMarker && addMarker(curAngle, [x, y], "start");
795 hasEndMarker && addMarker(curAngle, to, "end");
796 if (hasMidMarker) {
797 var angle = "mM".indexOf(cmd) >= 0 ?
798 prevAngle : "mM".indexOf(list.getItem(i - 1).pathSegTypeAsLetter) >= 0 ?
799 curAngle : .5 * (prevAngle + curAngle);
800 addMarker(angle, [x, y], "mid");
801 }
802 prevAngle = curAngle;
803
804 p = multVecMatrix(to, tfMatrix);
805 lines.push({op: op, c: p});
806 }
807
808 if ("MLCSQT".indexOf(cmd) >= 0) {
809 x = seg.x;
810 y = seg.y;
811 } else if ("mlcsqt".indexOf(cmd) >= 0) {
812 x = seg.x + x;
813 y = seg.y + y;
814 } else if ("zZ".indexOf(cmd) < 0) {
815 x = newX;
816 y = newY;
817 }
818 }
819
820 return {lines: lines, markers: markers};
821 };
822 var lines = getLinesFromPath(list, tfMatrix);
823
824 if (markerEnd || markerStart || markerMid) {
825 for (var i = 0; i < lines.markers.length; i++) {
826 var marker = lines.markers[i];
827 var markerElement;
828 switch (marker.type) {
829 case "start":
830 markerElement = svgIdPrefix.get() + /url\(#(\w+)\)/.exec(markerStart)[1];
831 break;
832 case "end":
833 markerElement = svgIdPrefix.get() + /url\(#(\w+)\)/.exec(markerEnd)[1];
834 break;
835 case "mid":
836 markerElement = svgIdPrefix.get() + /url\(#(\w+)\)/.exec(markerMid)[1];
837 break;
838 }
839 _pdf.doFormObject(markerElement, marker.tf);
840 }
841 }
842
843 if (lines.lines.length > 0) {
844 _pdf.path(lines.lines, colorMode, gradient, gradientMatrix);
845 }
846 };
847
848 // draws the element referenced by a use node, makes use of pdf's XObjects/FormObjects so nodes are only written once
849 // to the pdf document. This highly reduces the file size and computation time.
850 var use = function (node, tfMatrix, svgIdPrefix) {
851 var url = (node.getAttribute("href") || node.getAttribute("xlink:href"));
852 // just in case someone has the idea to use empty use-tags, wtf???
853 if (!url)
854 return;
855
856 // get the size of the referenced form object (to apply the correct scaling)
857 var formObject = _pdf.getFormObject(svgIdPrefix.get() + url.substring(1));
858
859 // scale and position it right
860 var x = node.getAttribute("x") || 0;
861 var y = node.getAttribute("y") || 0;
862 var width = node.getAttribute("width") || formObject.width;
863 var height = node.getAttribute("height") || formObject.height;
864 var t = new _pdf.Matrix(width / formObject.width || 0, 0, 0, height / formObject.height || 0, x, y);
865 t = _pdf.matrixMult(t, tfMatrix);
866 _pdf.doFormObject(svgIdPrefix.get() + url.substring(1), t);
867 };
868
869 // draws a line
870 var line = function (node, tfMatrix) {
871 var p1 = multVecMatrix([parseFloat(node.getAttribute('x1')), parseFloat(node.getAttribute('y1'))], tfMatrix);
872 var p2 = multVecMatrix([parseFloat(node.getAttribute('x2')), parseFloat(node.getAttribute('y2'))], tfMatrix);
873 _pdf.line(p1[0], p1[1], p2[0], p2[1]);
874 };
875
876 // draws a rect
877 var rect = function (node, colorMode, gradient, gradientMatrix) {
878 _pdf.roundedRect(
879 parseFloat(node.getAttribute('x')) || 0,
880 parseFloat(node.getAttribute('y')) || 0,
881 parseFloat(node.getAttribute('width')),
882 parseFloat(node.getAttribute('height')),
883 parseFloat(node.getAttribute('rx')) || 0,
884 parseFloat(node.getAttribute('ry')) || 0,
885 colorMode,
886 gradient,
887 gradientMatrix
888 );
889 };
890
891 // draws an ellipse
892 var ellipse = function (node, colorMode, gradient, gradientMatrix) {
893 _pdf.ellipse(
894 parseFloat(node.getAttribute('cx')) || 0,
895 parseFloat(node.getAttribute('cy')) || 0,
896 parseFloat(node.getAttribute('rx')),
897 parseFloat(node.getAttribute('ry')),
898 colorMode,
899 gradient,
900 gradientMatrix
901 );
902 };
903
904 // draws a circle
905 var circle = function (node, colorMode, gradient, gradientMatrix) {
906 var radius = parseFloat(node.getAttribute('r')) || 0;
907 _pdf.ellipse(
908 parseFloat(node.getAttribute('cx')) || 0,
909 parseFloat(node.getAttribute('cy')) || 0,
910 radius,
911 radius,
912 colorMode,
913 gradient,
914 gradientMatrix
915 );
916 };
917
918 // applies text transformations to a text node
919 var transformText = function (node, text) {
920 var textTransform = getAttribute(node, "text-transform");
921 switch (textTransform) {
922 case "uppercase": return text.toUpperCase();
923 case "lowercase": return text.toLowerCase();
924 default: return text;
925 // TODO: capitalize, full-width
926 }
927 };
928
929 // draws a text element and its tspan children
930 var text = function (node, tfMatrix, hasFillColor, fillRGB) {
931 _pdf.saveGraphicsState();
932 setTextProperties(node, fillRGB);
933
934 var getTextOffset = function (textAnchor, width) {
935 var xOffset = 0;
936 switch (textAnchor) {
937 case 'end':
938 xOffset = width;
939 break;
940 case 'middle':
941 xOffset = width / 2;
942 break;
943 case 'start':
944 break;
945 }
946 return xOffset;
947 };
948
949 /**
950 * Convert em, px and bare number attributes to pixel values
951 */
952 var toPixels = function (value, pdfFontSize) {
953 var match;
954
955 // em
956 match = value && value.toString().match(/^([\-0-9.]+)em$/);
957 if (match) {
958 return parseFloat(match[1]) * pdfFontSize;
959 }
960
961 // pixels
962 match = value && value.toString().match(/^([\-0-9.]+)(px|)$/);
963 if (match) {
964 return parseFloat(match[1]);
965 }
966 return 0;
967 };
968
969 // creates an svg element and append the text node to properly measure the text size
970 var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
971 svg.appendChild(node);
972 svg.setAttribute("visibility", "hidden");
973 document.body.appendChild(svg);
974
975 var box = node.getBBox();
976 var x, y, xOffset = 0;
977 var textAnchor = getAttribute(node, "text-anchor");
978 if (textAnchor) {
979 xOffset = getTextOffset(textAnchor, box.width);
980 }
981
982 var pdfFontSize = _pdf.getFontSize();
983 var textX = toPixels(node.getAttribute('x'), pdfFontSize);
984 var textY = toPixels(node.getAttribute('y'), pdfFontSize);
985 var m = _pdf.matrixMult(new _pdf.Matrix(1, 0, 0, 1, textX, textY), tfMatrix);
986
987 x = toPixels(node.getAttribute("dx"), pdfFontSize);
988 y = toPixels(node.getAttribute("dy"), pdfFontSize);
989
990 // when there are no tspans draw the text directly
991 if (node.childElementCount === 0) {
992 _pdf.text(
993 (x - xOffset),
994 y,
995 transformText(node, removeNewlinesAndTrim(node.textContent)),
996 void 0,
997 m
998 );
999 } else {
1000 // otherwise loop over tspans and position each relative to the previous one
1001 forEachChild(node, function (i, tSpan) {
1002 _pdf.saveGraphicsState();
1003 var tSpanColor = getAttribute(tSpan, "fill");
1004 setTextProperties(tSpan, tSpanColor && new RGBColor(tSpanColor));
1005 var extent = tSpan.getExtentOfChar(0);
1006 _pdf.text(
1007 extent.x - textX,//x - xOffset,
1008 extent.y + extent.height * 0.7 - textY, // 0.7 roughly mimicks the text baseline
1009 transformText(node, removeNewlinesAndTrim(tSpan.textContent)),
1010 void 0,
1011 m
1012 );
1013
1014 _pdf.restoreGraphicsState();
1015 });
1016
1017 }
1018
1019 document.body.removeChild(svg);
1020 _pdf.restoreGraphicsState();
1021 };
1022
1023 // As defs elements are allowed to appear after they are referenced, we search for them first
1024 var findAndRenderDefs = function (node, tfMatrix, defs, svgIdPrefix, withinDefs) {
1025 forEachChild(node, function (i, child) {
1026 if (child.tagName.toLowerCase() === "defs") {
1027 renderNode(child, tfMatrix, defs, svgIdPrefix, withinDefs);
1028 // prevent defs from being evaluated twice // TODO: make this better
1029 child.parentNode.removeChild(child);
1030 }
1031 });
1032 };
1033
1034 // processes a svg node
1035 var svg = function (node, tfMatrix, defs, svgIdPrefix, withinDefs) {
1036 // create a new prefix and clone the defs, as defs within the svg should not be visible outside
1037 var newSvgIdPrefix = svgIdPrefix.nextChild();
1038 var newDefs = cloneDefs(defs);
1039 findAndRenderDefs(node, tfMatrix, newDefs, newSvgIdPrefix, withinDefs);
1040 renderChildren(node, tfMatrix, newDefs, newSvgIdPrefix, withinDefs);
1041 };
1042
1043 // renders all children of a node
1044 var renderChildren = function (node, tfMatrix, defs, svgIdPrefix, withinDefs) {
1045 forEachChild(node, function (i, node) {
1046 renderNode(node, tfMatrix, defs, svgIdPrefix, withinDefs);
1047 });
1048 };
1049
1050 // adds a gradient to defs and the pdf document for later use, type is either "axial" or "radial"
1051 // opacity is only supported rudimentary by averaging over all stops
1052 // transforms are applied on use
1053 var putGradient = function (node, type, coords, defs, svgIdPrefix) {
1054 var colors = [];
1055 var opacitySum = 0;
1056 var hasOpacity = false;
1057 var gState;
1058 forEachChild(node, function (i, element) {
1059 // since opacity gradients are hard to realize, average the opacity over the control points
1060 if (element.tagName.toLowerCase() === "stop") {
1061 var color = new RGBColor(getAttribute(element, "stop-color"));
1062 colors.push({
1063 offset: parseFloat(element.getAttribute("offset")),
1064 color: [color.r, color.g, color.b]
1065 });
1066 var opacity = getAttribute(element, "stop-opacity");
1067 if (opacity && opacity != 1) {
1068 opacitySum += parseFloat(opacity);
1069 hasOpacity = true;
1070 }
1071 }
1072 });
1073
1074 if (hasOpacity) {
1075 gState = new _pdf.GState({opacity: opacitySum / coords.length});
1076 }
1077
1078 var pattern = new _pdf.ShadingPattern(type, coords, colors, gState);
1079 var id = svgIdPrefix.get() + node.getAttribute("id");
1080 _pdf.addShadingPattern(id, pattern);
1081 defs[id] = node;
1082 };
1083
1084 var pattern = function (node, defs, svgIdPrefix) {
1085 var id = svgIdPrefix.get() + node.getAttribute("id");
1086 defs[id] = node;
1087
1088 // the transformations directly at the node are written to the pattern transformation matrix
1089 var bBox = getUntransformedBBox(node);
1090 var pattern = new _pdf.TilingPattern([bBox[0], bBox[1], bBox[0] + bBox[2], bBox[1] + bBox[3]], bBox[2], bBox[3],
1091 null, computeNodeTransform(node));
1092
1093 _pdf.beginTilingPattern(pattern);
1094 // continue without transformation
1095 renderChildren(node, _pdf.unitMatrix, defs, svgIdPrefix, false);
1096 _pdf.endTilingPattern(id, pattern);
1097 };
1098
1099 function setTextProperties(node, fillRGB) {
1100 var fontFamily = getAttribute(node, "font-family");
1101 if (fontFamily) {
1102 _pdf.setFont(fontFamily);
1103 }
1104
1105 if (fillRGB && fillRGB.ok) {
1106 _pdf.setTextColor(fillRGB.r, fillRGB.g, fillRGB.b);
1107 }
1108
1109 var fontType;
1110 var fontWeight = getAttribute(node, "font-weight");
1111 if (fontWeight) {
1112 if (fontWeight === "bold") {
1113 fontType = "bold";
1114 }
1115 }
1116
1117 var fontStyle = getAttribute(node, "font-style");
1118 if (fontStyle) {
1119 if (fontStyle === "italic") {
1120 fontType += "italic";
1121 }
1122 }
1123 _pdf.setFontType(fontType);
1124
1125 var pdfFontSize = 16;
1126 var fontSize = getAttribute(node, "font-size");
1127 if (fontSize) {
1128 pdfFontSize = parseFloat(fontSize);
1129 _pdf.setFontSize(pdfFontSize);
1130 }
1131 }
1132
1133
1134 /**
1135 * Renders a svg node.
1136 * @param node The svg element
1137 * @param contextTransform The current transformation matrix
1138 * @param defs The defs map holding all svg nodes that can be referenced
1139 * @param svgIdPrefix The current id prefix
1140 * @param withinDefs True iff we are top-level within a defs node, so the target can be switched to an pdf form object
1141 */
1142 var renderNode = function (node, contextTransform, defs, svgIdPrefix, withinDefs) {
1143 var tfMatrix,
1144 hasFillColor = false,
1145 fillRGB = null,
1146 colorMode = null,
1147 fillUrl = null,
1148 fillData = null,
1149 bBox;
1150
1151 //
1152 // Decide about the render target and set the correct transformation
1153 //
1154
1155 // if we are within a defs node, start a new pdf form object and draw this node and all children on that instead
1156 // of the top-level page
1157 var targetIsFormObject = withinDefs && !nodeIs(node, "lineargradient,radialgradient,pattern");
1158 if (targetIsFormObject) {
1159
1160 // the transformations directly at the node are written to the pdf form object transformation matrix
1161 tfMatrix = computeNodeTransform(node);
1162 bBox = getUntransformedBBox(node);
1163
1164 _pdf.beginFormObject(bBox[0], bBox[1], bBox[2], bBox[3], tfMatrix);
1165
1166 // continue without transformation and set withinDefs to false to prevent child nodes from starting new form objects
1167 tfMatrix = _pdf.unitMatrix;
1168 withinDefs = false;
1169
1170 } else {
1171 tfMatrix = _pdf.matrixMult(computeNodeTransform(node), contextTransform);
1172 _pdf.saveGraphicsState();
1173 }
1174
1175 //
1176 // extract fill and stroke mode
1177 //
1178
1179 // fill mode
1180 if (nodeIs(node, "g,path,rect,text,ellipse,line,circle,polygon")) {
1181 function setDefaultColor() {
1182 fillRGB = new RGBColor("rgb(0, 0, 0)");
1183 hasFillColor = true;
1184 colorMode = "F";
1185 }
1186
1187 var fillColor = getAttribute(node, "fill");
1188 if (fillColor) {
1189 var url = /url\(#(\w+)\)/.exec(fillColor);
1190 if (url) {
1191 // probably a gradient (or something unsupported)
1192 fillUrl = svgIdPrefix.get() + url[1];
1193 var fill = getFromDefs(fillUrl, defs);
1194 if (fill && nodeIs(fill, "lineargradient,radialgradient")) {
1195
1196 // matrix to convert between gradient space and user space
1197 // for "userSpaceOnUse" this is the current transformation: tfMatrix
1198 // for "objectBoundingBox" or default, the gradient gets scaled and transformed to the bounding box
1199 var gradientUnitsMatrix = tfMatrix;
1200 if (!fill.hasAttribute("gradientUnits")
1201 || fill.getAttribute("gradientUnits").toLowerCase() === "objectboundingbox") {
1202 bBox || (bBox = getUntransformedBBox(node));
1203 gradientUnitsMatrix = new _pdf.Matrix(bBox[2], 0, 0, bBox[3], bBox[0], bBox[1]);
1204
1205 var nodeTransform = computeNodeTransform(node);
1206 gradientUnitsMatrix = _pdf.matrixMult(gradientUnitsMatrix, nodeTransform);
1207 }
1208
1209 // matrix that is applied to the gradient before any other transformations
1210 var gradientTransform = parseTransform(fill.getAttribute("gradientTransform"));
1211
1212 fillData = _pdf.matrixMult(gradientTransform, gradientUnitsMatrix);
1213 } else if (fill && nodeIs(fill, "pattern")) {
1214 var fillBBox, y, width, height, x;
1215 fillData = {};
1216
1217 var patternUnitsMatrix = _pdf.unitMatrix;
1218 if (!fill.hasAttribute("patternUnits")
1219 || fill.getAttribute("patternUnits").toLowerCase() === "objectboundingbox") {
1220 bBox || (bBox = getUntransformedBBox(node));
1221 patternUnitsMatrix = new _pdf.Matrix(1, 0, 0, 1, bBox[0], bBox[1]);
1222
1223 // TODO: slightly inaccurate (rounding errors? line width bBoxes?)
1224 fillBBox = getUntransformedBBox(fill);
1225 x = fillBBox[0] * bBox[0];
1226 y = fillBBox[1] * bBox[1];
1227 width = fillBBox[2] * bBox[2];
1228 height = fillBBox[3] * bBox[3];
1229 fillData.boundingBox = [x, y, x + width, y + height];
1230 fillData.xStep = width;
1231 fillData.yStep = height;
1232 }
1233
1234 var patternContentUnitsMatrix = _pdf.unitMatrix;
1235 if (fill.hasAttribute("patternContentUnits")
1236 && fill.getAttribute("patternContentUnits").toLowerCase() === "objectboundingbox") {
1237 bBox || (bBox = getUntransformedBBox(node));
1238 patternContentUnitsMatrix = new _pdf.Matrix(bBox[2], 0, 0, bBox[3], 0, 0);
1239
1240 fillBBox = fillData.boundingBox || getUntransformedBBox(fill);
1241 x = fillBBox[0] / bBox[0];
1242 y = fillBBox[1] / bBox[1];
1243 width = fillBBox[2] / bBox[2];
1244 height = fillBBox[3] / bBox[3];
1245 fillData.boundingBox = [x, y, x + width, y + height];
1246 fillData.xStep = width;
1247 fillData.yStep = height;
1248 }
1249
1250 fillData.matrix = _pdf.matrixMult(
1251 _pdf.matrixMult(patternContentUnitsMatrix, patternUnitsMatrix), tfMatrix);
1252
1253 colorMode = "F";
1254 } else {
1255 // unsupported fill argument (e.g. patterns) -> fill black
1256 fillUrl = fill = null;
1257 setDefaultColor();
1258 }
1259 } else {
1260 // plain color
1261 fillRGB = parseColor(fillColor);
1262 if (fillRGB.ok) {
1263 hasFillColor = true;
1264 colorMode = 'F';
1265 } else {
1266 colorMode = null;
1267 }
1268 }
1269 } else {
1270 // if no fill attribute is provided the default fill color is black
1271 setDefaultColor();
1272 }
1273
1274 // opacity is realized via a pdf graphics state
1275 var opacity = 1.0;
1276 var nodeOpacity = node.getAttribute("opacity") || node.getAttribute("fill-opacity");
1277 if (nodeOpacity) {
1278 opacity *= parseFloat(nodeOpacity);
1279 }
1280 if (fillRGB && typeof fillRGB.a === "number") {
1281 opacity *= fillRGB.a;
1282 }
1283 _pdf.setGState(new _pdf.GState({opacity: opacity}));
1284 }
1285
1286 if (nodeIs(node, "g,path,rect,ellipse,line,circle,polygon")) {
1287 // text has no fill color, so don't apply it until here
1288 if (hasFillColor) {
1289 _pdf.setFillColor(fillRGB.r, fillRGB.g, fillRGB.b);
1290 }
1291
1292 // stroke mode
1293 var strokeColor = node.getAttribute('stroke');
1294 if (strokeColor) {
1295 var strokeWidth;
1296 if (node.hasAttribute("stroke-width")) {
1297 strokeWidth = Math.abs(parseFloat(node.getAttribute('stroke-width')));
1298 _pdf.setLineWidth(strokeWidth);
1299 }
1300 var strokeRGB = new RGBColor(strokeColor);
1301 if (strokeRGB.ok) {
1302 _pdf.setDrawColor(strokeRGB.r, strokeRGB.g, strokeRGB.b);
1303 if (strokeWidth !== 0) {
1304 // pdf spec states: "A line width of 0 denotes the thinnest line that can be rendered at device resolution:
1305 // 1 device pixel wide". SVG, however, does not draw zero width lines.
1306 colorMode = (colorMode || "") + "D";
1307 }
1308 }
1309 if (node.hasAttribute("stroke-linecap")) {
1310 _pdf.setLineCap(node.getAttribute("stroke-linecap"));
1311 }
1312 if (node.hasAttribute("stroke-linejoin")) {
1313 _pdf.setLineJoin(node.getAttribute("stroke-linejoin"));
1314 }
1315 if (node.hasAttribute("stroke-dasharray")) {
1316 _pdf.setLineDashPattern(
1317 parseFloats(node.getAttribute("stroke-dasharray")),
1318 parseInt(node.getAttribute("stroke-dashoffset")) || 0
1319 );
1320 }
1321 if (node.hasAttribute("stroke-miterlimit")) {
1322 _pdf.setLineMiterLimit(parseFloat(node.getAttribute("stroke-miterlimit")));
1323 }
1324 }
1325 }
1326
1327 setTextProperties(node, fillRGB);
1328
1329 // do the actual drawing
1330 switch (node.tagName.toLowerCase()) {
1331 case 'svg':
1332 svg(node, tfMatrix, defs, svgIdPrefix, withinDefs);
1333 break;
1334 case 'g':
1335 findAndRenderDefs(node, tfMatrix, defs, svgIdPrefix, withinDefs);
1336 case 'a':
1337 case "marker":
1338 renderChildren(node, tfMatrix, defs, svgIdPrefix, withinDefs);
1339 break;
1340
1341 case 'defs':
1342 renderChildren(node, tfMatrix, defs, svgIdPrefix, true);
1343 break;
1344
1345 case 'use':
1346 use(node, tfMatrix, svgIdPrefix);
1347 break;
1348
1349 case 'line':
1350 line(node, tfMatrix);
1351 break;
1352
1353 case 'rect':
1354 _pdf.setCurrentTransformationMatrix(tfMatrix);
1355 rect(node, colorMode, fillUrl, fillData);
1356 break;
1357
1358 case 'ellipse':
1359 _pdf.setCurrentTransformationMatrix(tfMatrix);
1360 ellipse(node, colorMode, fillUrl, fillData);
1361 break;
1362
1363 case 'circle':
1364 _pdf.setCurrentTransformationMatrix(tfMatrix);
1365 circle(node, colorMode, fillUrl, fillData);
1366 break;
1367 case 'text':
1368 text(node, tfMatrix, hasFillColor, fillRGB);
1369 break;
1370
1371 case 'path':
1372 path(node, tfMatrix, svgIdPrefix, colorMode, fillUrl, fillData);
1373 break;
1374
1375 case 'polygon':
1376 polygon(node, tfMatrix, colorMode, fillUrl, fillData);
1377 break;
1378
1379 case 'image':
1380 _pdf.setCurrentTransformationMatrix(tfMatrix);
1381 image(node);
1382 break;
1383
1384 case "lineargradient":
1385 putGradient(node, "axial", [
1386 node.getAttribute("x1"),
1387 node.getAttribute("y1"),
1388 node.getAttribute("x2"),
1389 node.getAttribute("y2")
1390 ], defs, svgIdPrefix);
1391 break;
1392
1393 case "radialgradient":
1394 putGradient(node, "radial", [
1395 node.getAttribute("fx") || node.getAttribute("cx"),
1396 node.getAttribute("fy") || node.getAttribute("cy"),
1397 0,
1398 node.getAttribute("cx") || 0,
1399 node.getAttribute("cy") || 0,
1400 node.getAttribute("r") || 0
1401 ], defs, svgIdPrefix);
1402 break;
1403
1404 case "pattern":
1405 pattern(node, defs, svgIdPrefix);
1406 break;
1407 }
1408
1409 // close either the formObject or the graphics context
1410 if (targetIsFormObject) {
1411 _pdf.endFormObject(svgIdPrefix.get() + node.getAttribute("id"));
1412 } else {
1413 _pdf.restoreGraphicsState();
1414 }
1415 };
1416
1417 // the actual svgToPdf function (see above)
1418 var svg2pdf = function (element, pdf, options) {
1419 _pdf = pdf;
1420
1421 var k = options.scale || 1.0,
1422 xOffset = options.xOffset || 0.0,
1423 yOffset = options.yOffset || 0.0;
1424
1425 // set offsets and scale everything by k
1426 _pdf.saveGraphicsState();
1427 _pdf.setCurrentTransformationMatrix(new _pdf.Matrix(k, 0, 0, k, xOffset, yOffset));
1428
1429 renderNode(element.cloneNode(true), _pdf.unitMatrix, {}, new SvgPrefix(""), false);
1430
1431 _pdf.restoreGraphicsState();
1432
1433 return _pdf;
1434 };
1435
1436 if (typeof define === "function" && define.amd) {
1437 define(["rgbcolor"], function (rgbcolor) {
1438 RGBColor = rgbcolor;
1439 return svg2pdf;
1440 });
1441 } else if (typeof module !== "undefined" && module.exports) {
1442 RGBColor = require("./rgbcolor.js");
1443 module.exports = svg2pdf;
1444 } else {
1445 RGBColor = global.RGBColor;
1446 global.svg2pdf = svg2pdf;
1447 // for compatibility reasons
1448 global.svgElementToPdf = svg2pdf;
1449 }
1450 return svg2pdf;
1451}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this));