1 /** @license
  2  * Copyright (c) 2010 James Hall, https://github.com/MrRio/jsPDF
  3  * Copyright (c) 2012 Willow Systems Corporation, willow-systems.com
  4  * 
  5  * Permission is hereby granted, free of charge, to any person obtaining
  6  * a copy of this software and associated documentation files (the
  7  * "Software"), to deal in the Software without restriction, including
  8  * without limitation the rights to use, copy, modify, merge, publish,
  9  * distribute, sublicense, and/or sell copies of the Software, and to
 10  * permit persons to whom the Software is furnished to do so, subject to
 11  * the following conditions:
 12  * 
 13  * The above copyright notice and this permission notice shall be
 14  * included in all copies or substantial portions of the Software.
 15  * 
 16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 17  * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 18  * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 19  * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 20  * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 21  * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 22  * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 23  */
 24 
 25 /**
 26  * Creates new jsPDF document object instance
 27  * @constructor jsPDF
 28  * @param orientation One of "portrait" or "landscape" (or shortcuts "p" (Default), "l")
 29  * @param unit Measurement unit to be used when coordinates are specified. One of "pt" (points), "mm" (Default), "cm", "in"
 30  * @param format One of 'a3', 'a4' (Default),'a5' ,'letter' ,'legal'
 31  * @returns {jsPDF}
 32  */
 33 var jsPDF = function(/** String */ orientation, /** String */ unit, /** String */ format){
 34 
 35 	// Default parameter values
 36 	if (typeof orientation === 'undefined') orientation = 'p'
 37 	else orientation = orientation.toString().toLowerCase() 
 38 	if (typeof unit === 'undefined') unit = 'mm';
 39 	if (typeof format === 'undefined') format = 'a4';
 40 	
 41 	var format_as_string = format.toString().toLowerCase() 
 42 	, HELVETICA = "helvetica"
 43 	, TIMES = "times"
 44 	, COURIER = "courier"
 45 	, NORMAL = "normal"
 46 	, BOLD = "bold"
 47 	, ITALIC = "italic"
 48 	, BOLD_ITALIC = "bolditalic"
 49 
 50 	, version = '20120220'
 51 	, content = []
 52 	, content_length = 0
 53 	
 54 	, pdfVersion = '1.3' // PDF Version
 55 	, pageFormats = { // Size in pt of various paper formats
 56 		'a3': [841.89, 1190.55]
 57 		, 'a4': [595.28, 841.89]
 58 		, 'a5': [420.94, 595.28]
 59 		, 'letter': [612, 792]
 60 		, 'legal': [612, 1008]
 61 	}
 62 	, textColor = '0 g'
 63 	, drawColor = '0 G'
 64 	, page = 0
 65 	, objectNumber = 2 // 'n' Current object number
 66 	, outToPages = false // switches where out() prints. outToPages true = push to pages obj. outToPages false = doc builder content 
 67 	, pages = []
 68 	, offsets = [] // List of offsets. Activated and reset by buildDocument(). Pupulated by various calls buildDocument makes.
 69 	, fonts = [] // List of fonts
 70 	, lineWidth = 0.200025 // 2mm
 71 	, pageHeight
 72 	, pageWidth
 73 	, k // Scale factor
 74 	, fontNumber // @TODO: This is temp, replace with real font handling
 75 	, documentProperties = {}
 76 	, fontSize = 16 // Default font size
 77 	, fontName = HELVETICA // Default font
 78 	, fontType = NORMAL // Default type
 79 	, textColor = "0 g"
 80 
 81 	// Private functions
 82 	, out = function(string) {
 83 		if(outToPages /* set by beginPage */) {
 84 			pages[page].push(string);
 85 		} else {
 86 			content.push(string)
 87 			content_length += string.length + 1 // +1 is for '\n' that will be used to join contents of content 
 88 		}
 89 	}
 90 	, newObject = function() {
 91 		// Begin a new object
 92 		objectNumber ++;
 93 		offsets[objectNumber] = content_length;
 94 		out(objectNumber + ' 0 obj');		
 95 	}
 96 	, putPages = function() {
 97 		var wPt = pageWidth * k;
 98 		var hPt = pageHeight * k;
 99 
100 		// outToPages = false as set in endDocument(). out() writes to content.
101 		
102 		for(n=1; n <= page; n++) {
103 			newObject();
104 			out('<</Type /Page');
105 			out('/Parent 1 0 R');	
106 			out('/Resources 2 0 R');
107 			out('/Contents ' + (objectNumber + 1) + ' 0 R>>');
108 			out('endobj');
109 			
110 			// Page content
111 			p = pages[n].join('\n');
112 			newObject();
113 			out('<</Length ' + p.length  + '>>');
114 			putStream(p);
115 			out('endobj');
116 		}
117 		offsets[1] = content_length;
118 		out('1 0 obj');
119 		out('<</Type /Pages');
120 		var kids = '/Kids [';
121 		for (i = 0; i < page; i++) {
122 			kids += (3 + 2 * i) + ' 0 R ';
123 		}
124 		out(kids + ']');
125 		out('/Count ' + page);
126 		out(sprintf('/MediaBox [0 0 %.2f %.2f]', wPt, hPt));
127 		out('>>');
128 		out('endobj');		
129 	}
130 	, putStream = function(str) {
131 		out('stream');
132 		out(str);
133 		out('endstream');
134 	}
135 	, putResources = function() {
136 		putFonts();
137 		// Resource dictionary
138 		offsets[2] = content_length;
139 		out('2 0 obj');
140 		out('<<');
141 		putResourceDictionary();
142 		out('>>');
143 		out('endobj');
144 	}	
145 	, putFonts = function() {
146 		for (var i = 0; i < fonts.length; i++) {
147 			putFont(fonts[i]);
148 		}
149 	}
150 	, putFont = function(font) {
151 		newObject();
152 		font.number = objectNumber;
153 		out('<</BaseFont/' + font.name + '/Type/Font');
154 		out('/Subtype/Type1>>');
155 		out('endobj');
156 	}
157 	, addFont = function(name, fontName, fontType) {
158 		fonts.push({key: 'F' + (fonts.length + 1), number: objectNumber, name: name, fontName: fontName, type: fontType});
159 	}
160 	, addFonts = function() {
161 		addFont('Helvetica', HELVETICA, NORMAL);
162 		addFont('Helvetica-Bold', HELVETICA, BOLD);
163 		addFont('Helvetica-Oblique', HELVETICA, ITALIC);
164 		addFont('Helvetica-BoldOblique', HELVETICA, BOLD_ITALIC);
165 		addFont('Courier', COURIER, NORMAL);
166 		addFont('Courier-Bold', COURIER, BOLD);
167 		addFont('Courier-Oblique', COURIER, ITALIC);
168 		addFont('Courier-BoldOblique', COURIER, BOLD_ITALIC);
169 		addFont('Times-Roman', TIMES, NORMAL);
170 		addFont('Times-Bold', TIMES, BOLD);
171 		addFont('Times-Italic', TIMES, ITALIC);
172 		addFont('Times-BoldItalic', TIMES, BOLD_ITALIC);
173 	}
174 	, putResourceDictionary = function() {
175 		out('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]');
176 		out('/Font <<');
177 		// Do this for each font, the '1' bit is the index of the font
178 		// fontNumber is currently the object number related to 'putFonts'
179 		for (var i = 0; i < fonts.length; i++) {
180 			out('/' + fonts[i].key + ' ' + fonts[i].number + ' 0 R');
181 		}
182 		
183 		out('>>');
184 		out('/XObject <<');
185 		putXobjectDict();
186 		out('>>');
187 	}
188 	, putXobjectDict = function() {
189 		// @TODO: Loop through images, or other data objects
190 	}
191 	, putInfo = function() {
192 		out('/Producer (jsPDF ' + version + ')');
193 		if(documentProperties.title != undefined) {
194 			out('/Title (' + pdfEscape(documentProperties.title) + ')');
195 		}
196 		if(documentProperties.subject != undefined) {
197 			out('/Subject (' + pdfEscape(documentProperties.subject) + ')');
198 		}
199 		if(documentProperties.author != undefined) {
200 			out('/Author (' + pdfEscape(documentProperties.author) + ')');
201 		}
202 		if(documentProperties.keywords != undefined) {
203 			out('/Keywords (' + pdfEscape(documentProperties.keywords) + ')');
204 		}
205 		if(documentProperties.creator != undefined) {
206 			out('/Creator (' + pdfEscape(documentProperties.creator) + ')');
207 		}		
208 		var created = new Date();
209 		out('/CreationDate (D:' + sprintf(
210 			'%02d%02d%02d%02d%02d%02d'
211 			, created.getFullYear()
212 			, (created.getMonth() + 1)
213 			, created.getDate()
214 			, created.getHours()
215 			, created.getMinutes()
216 			, created.getSeconds()
217 		) + ')');
218 	}
219 	, putCatalog = function () {
220 		out('/Type /Catalog');
221 		out('/Pages 1 0 R');
222 		// @TODO: Add zoom and layout modes
223 		out('/OpenAction [3 0 R /FitH null]');
224 		out('/PageLayout /OneColumn');
225 	}	
226 	, putTrailer = function () {
227 		out('/Size ' + (objectNumber + 1));
228 		out('/Root ' + objectNumber + ' 0 R');
229 		out('/Info ' + (objectNumber - 1) + ' 0 R');
230 	}	
231 	, beginPage = function() {
232 		page ++;
233 		// Do dimension stuff
234 		outToPages = true
235 		pages[page] = [];
236 	}
237 	, _addPage = function() {
238 		beginPage();
239 		// Set line width
240 		out(sprintf('%.2f w', (lineWidth * k)));
241 		// Set draw color
242 		out(drawColor);
243 	}
244 	, getFont = function() {
245 		for (var i = 0; i < fonts.length; i++) {
246 			if (fonts[i].fontName == fontName && fonts[i].type == fontType) {
247 				return fonts[i].key;
248 			}
249 		}
250 		return 'F1'; // shouldn't happen
251 	}
252 	, buildDocument = function() {
253 		
254 		outToPages = false // switches out() to content
255 		content = []
256 		offsets = []
257 		
258 		// putHeader();
259 		out('%PDF-' + pdfVersion);
260 		
261 		putPages();
262 		
263 		putResources();
264 
265 		// Info
266 		newObject();
267 		out('<<');
268 		putInfo();
269 		out('>>');
270 		out('endobj');
271 		
272 		// Catalog
273 		newObject();
274 		out('<<');
275 		putCatalog();
276 		out('>>');
277 		out('endobj');
278 		
279 		// Cross-ref
280 		var o = content_length;
281 		out('xref');
282 		out('0 ' + (objectNumber + 1));
283 		out('0000000000 65535 f ');
284 		for (var i=1; i <= objectNumber; i++) {
285 			out(sprintf('%010d 00000 n ', offsets[i]));
286 		}
287 		// Trailer
288 		out('trailer');
289 		out('<<');
290 		putTrailer();
291 		out('>>');
292 		out('startxref');
293 		out(o);
294 		out('%%EOF');
295 		
296 		outToPages = true
297 		
298 		return content.join('\n')
299 	}
300 	, pdfEscape = function(text) {
301 		return text.replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)');
302 	}
303 	
304 	// Public API
305 	, _jsPDF = {
306 		/**
307 		 * Adds (and transfers the focus to) new page to the PDF document.
308 		 * @function
309 		 * @returns {jsPDF} 
310 		 * @name jsPDF.addPage
311 		 */
312 		addPage: function() {
313 			_addPage();
314 			return _jsPDF;
315 		},
316 		/**
317 		 * Adds text to page. Supports adding multiline text when 'text' argument is an Array of Strings. 
318 		 * @param {Number} x Coordinate (in units declared at inception of PDF document) against left edge of the page
319 		 * @param {Number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page
320 		 * @param {String|Array} text String or array of strings to be added to the page. Each line is shifted one line down per font, spacing settings declared before this call.
321 		 * @function
322 		 * @returns {jsPDF}
323 		 * @name jsPDF.text
324 		 */
325 		text: function(x, y, text) {
326 			/**
327 			 * Inserts something like this into PDF
328 				BT 
329 				/F1 16 Tf  % Font name + size
330 				16 TL % How many units down for next line in multiline text
331 				0 g % color
332 				28.35 813.54 Td % position
333 				(line one) Tj 
334 				T* (line two) Tj
335 				T* (line three) Tj
336 				ET
337 		 	*/
338 			
339 			var newtext, str
340 
341 			if (typeof text === 'string'){
342 				str = pdfEscape(text)
343 			} else /* Array */{
344 				// we don't want to destroy  original text array, so cloning it
345 				newtext = text.concat()
346 				// we do array.join('text that must not be PDFescaped")
347 				// thus, pdfEscape eash component separately
348 				for ( var i = newtext.length - 1; i !== -1 ; i--) {
349 					newtext[i] = pdfEscape( newtext[i] )
350 				}
351 				str = newtext.join( ") Tj\nT* (" )
352 			}
353 			// Using "'" ("go next line and render text" mark) would save space but would complicate our rendering code, templates 
354 			
355 			// BT .. ET does NOT have default settings for Tf. You must state that explicitely every time for BT .. ET
356 			// if you want text transformation matrix (+ multiline) to work reliably (which reads sizes of things from font declarations) 
357 			// Thus, there is NO useful, *reliable* concept of "default" font for a page. 
358 			// The fact that "default" (reuse font used before) font worked before in basic cases is an accident
359 			// - readers dealing smartly with brokenness of jsPDF's markup.
360 			out( 
361 				'BT\n/' +
362 				getFont() + ' ' + fontSize + ' Tf\n' +
363 				fontSize + ' TL\n' +
364 				textColor + 
365 				sprintf('\n%.2f %.2f Td\n(', x * k, (pageHeight - y) * k) + 
366 				str +
367 				') Tj\nET'
368 			)
369 			return _jsPDF;
370 		},
371 		line: function(x1, y1, x2, y2) {
372 			var str = sprintf('%.2f %.2f m %.2f %.2f l S',x1 * k, (pageHeight - y1) * k, x2 * k, (pageHeight - y2) * k);
373 			out(str);
374 			return _jsPDF;
375 		},
376 		rect: function(x, y, w, h, style) {
377 			var op = 'S';
378 			if (style === 'F') {
379 				op = 'f';
380 			} else if (style === 'FD' || style === 'DF') {
381 				op = 'B';
382 			}
383 			out(sprintf('%.2f %.2f %.2f %.2f re %s', x * k, (pageHeight - y) * k, w * k, -h * k, op));
384 			return _jsPDF;
385 		},
386 		ellipse: function(x, y, rx, ry, style) {
387 			var op = 'S';
388 			if (style === 'F') {
389 				op = 'f';
390 			} else if (style === 'FD' || style === 'DF') {
391 				op = 'B';
392 			}
393 			var lx = 4/3*(Math.SQRT2-1)*rx;
394 			var ly = 4/3*(Math.SQRT2-1)*ry;
395 			out(sprintf('%.2f %.2f m %.2f %.2f %.2f %.2f %.2f %.2f c',
396 			        (x+rx)*k, (pageHeight-y)*k, 
397 			        (x+rx)*k, (pageHeight-(y-ly))*k, 
398 			        (x+lx)*k, (pageHeight-(y-ry))*k, 
399 			        x*k, (pageHeight-(y-ry))*k));
400 			out(sprintf('%.2f %.2f %.2f %.2f %.2f %.2f c',
401 			        (x-lx)*k, (pageHeight-(y-ry))*k, 
402 			        (x-rx)*k, (pageHeight-(y-ly))*k, 
403 			        (x-rx)*k, (pageHeight-y)*k));			
404 			out(sprintf('%.2f %.2f %.2f %.2f %.2f %.2f c',
405 			        (x-rx)*k, (pageHeight-(y+ly))*k, 
406 			        (x-lx)*k, (pageHeight-(y+ry))*k, 
407 			        x*k, (pageHeight-(y+ry))*k));
408 			out(sprintf('%.2f %.2f %.2f %.2f %.2f %.2f c %s',
409 			        (x+lx)*k, (pageHeight-(y+ry))*k, 
410 			        (x+rx)*k, (pageHeight-(y+ly))*k, 
411 			        (x+rx)*k, (pageHeight-y)*k, 
412 			        op));
413 			return _jsPDF;
414 		},
415 		circle: function(x, y, r, style) {
416 			return this.ellipse(x, y, r, r, style);
417 		},
418 		setProperties: function(properties) {
419 			documentProperties = properties;
420 			return _jsPDF;
421 		},
422 		addImage: function(imageData, format, x, y, w, h) {
423 			return _jsPDF;
424 		},
425 		output: function(type, options) {
426 			if(type == undefined) {
427 				return buildDocument();
428 			}
429 			else if(type == 'datauri') {
430 				document.location.href = 'data:application/pdf;base64,' + Base64.encode(buildDocument());
431 			}
432 			// @TODO: Add different output options
433 		},
434 		setFontSize: function(size) {
435 			fontSize = size;
436 			return _jsPDF;
437 		},
438 		setFont: function(name) {
439 			switch(name.toLowerCase()) {
440 				case HELVETICA:
441 				case TIMES:
442 				case COURIER:
443 					fontName = name.toLowerCase();
444 					break;
445 				default:
446 					// do nothing
447 					break;
448 			}
449 			return _jsPDF;
450 		},
451 		setFontType: function(type) {
452 			switch(type.toLowerCase()) {
453 				case NORMAL:
454 				case BOLD:
455 				case ITALIC:
456 				case BOLD_ITALIC:
457 					fontType = type.toLowerCase();
458 					break;
459 				default:
460 					// do nothing
461 					break;
462 			}
463 			return _jsPDF;
464 		},
465 		setLineWidth: function(width) {
466 			out(sprintf('%.2f w', (width * k)));
467 			return _jsPDF;
468 		},
469 		setDrawColor: function(r,g,b) {
470 			var color;
471 			if ((r===0 && g===0 && b===0) || (typeof g === 'undefined')) {
472 				color = sprintf('%.3f G', r/255);
473 			} else {
474 				color = sprintf('%.3f %.3f %.3f RG', r/255, g/255, b/255);
475 			}
476 			out(color);
477 			return _jsPDF;
478 		},
479 		setFillColor: function(r,g,b) {
480 			var color;
481 			if ((r===0 && g===0 && b===0) || (typeof g === 'undefined')) {
482 				color = sprintf('%.3f g', r/255);
483 			} else {
484 				color = sprintf('%.3f %.3f %.3f rg', r/255, g/255, b/255);
485 			}
486 			out(color);
487 			return _jsPDF;
488 		},
489 		setTextColor: function(r,g,b) {
490 			if ((r===0 && g===0 && b===0) || (typeof g === 'undefined')) {
491 				textColor = sprintf('%.3f g', r/255);
492 			} else {
493 				textColor = sprintf('%.3f %.3f %.3f rg', r/255, g/255, b/255);
494 			}
495 			return _jsPDF;
496 		}
497 	};
498 
499 	/////////////////////////////////////////
500 	// Initilisation if jsPDF Document object
501 	/////////////////////////////////////////
502 	
503 	if (unit == 'pt') {
504 		k = 1;
505 	} else if(unit == 'mm') {
506 		k = 72/25.4;
507 	} else if(unit == 'cm') {
508 		k = 72/2.54;
509 	} else if(unit == 'in') {
510 		k = 72;
511 	} else {
512 		throw('Invalid unit: ' + unit)
513 	}
514 	
515 	// Dimensions are stored as user units and converted to points on output
516 	if (format_as_string in pageFormats) {
517 		pageHeight = pageFormats[format_as_string][1] / k;
518 		pageWidth = pageFormats[format_as_string][0] / k;
519 	} else {
520 		try {
521 			pageHeight = format[1];
522 			pageWidth = format[0];
523 		} 
524 		catch(err) {
525 			throw('Invalid format: ' + format);
526 		}
527 	}
528 	
529 	if (orientation === 'p' || orientation === 'portrait') {
530 		orientation = 'p';
531 	} else if (orientation === 'l' || orientation === 'landscape') {
532 		orientation = 'l';
533 		var tmp = pageWidth;
534 		pageWidth = pageHeight;
535 		pageHeight = tmp;
536 	} else {
537 		throw('Invalid orientation: ' + orientation);
538 	}
539 
540 	// Add the first page automatically
541 	addFonts();
542 	_addPage();	
543 	
544 	return _jsPDF;
545 };