      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 };