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