diff --git a/editor/jspdf/jspdf.js b/editor/jspdf/jspdf.js index 14281e5d..f6b40e6a 100644 --- a/editor/jspdf/jspdf.js +++ b/editor/jspdf/jspdf.js @@ -1,10 +1,23 @@ -/** @preserve jsPDF 0.9.0rc2 ( ${buildDate} ${commitID} ) -Copyright (c) 2010-2012 James Hall, james@snapshotmedia.co.uk, https://github.com/MrRio/jsPDF -Copyright (c) 2012 Willow Systems Corporation, willow-systems.com -MIT license. -*/ - -/* +/** @preserve + * jsPDF - PDF Document creation from JavaScript + * Version ${versionID} + * CommitID ${commitID} + * + * Copyright (c) 2010-2014 James Hall, https://github.com/MrRio/jsPDF + * 2010 Aaron Spike, https://github.com/acspike + * 2012 Willow Systems Corporation, willow-systems.com + * 2012 Pablo Hess, https://github.com/pablohess + * 2012 Florian Jenett, https://github.com/fjenett + * 2013 Warren Weckesser, https://github.com/warrenweckesser + * 2013 Youssef Beddad, https://github.com/lifof + * 2013 Lee Driscoll, https://github.com/lsdriscoll + * 2013 Stefan Slonevskiy, https://github.com/stefslon + * 2013 Jeremy Morel, https://github.com/jmorel + * 2013 Christoph Hartmann, https://github.com/chris-rock + * 2014 Juan Pablo Gaviria, https://github.com/juanpgaviria + * 2014 James Makes, https://github.com/dollaruw + * 2014 Diego Casorran, https://github.com/diegocr + * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including @@ -23,972 +36,758 @@ MIT license. * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * ==================================================================== + * + * Contributor(s): + * siefkenj, ahwolf, rickygu, Midnith, saintclair, eaparango, + * kim3er, mfo, alnorth, */ - /** -Creates new jsPDF document object instance -@class -@param orientation One of "portrait" or "landscape" (or shortcuts "p" (Default), "l") -@param unit Measurement unit to be used when coordinates are specified. One of "pt" (points), "mm" (Default), "cm", "in" -@param format One of 'a3', 'a4' (Default),'a5' ,'letter' ,'legal' -@returns {jsPDF} -@name jsPDF -*/ -var jsPDF = (function () { - 'use strict'; - /*jslint browser:true, plusplus: true, bitwise: true, nomen: true */ - /*global document: false, btoa, atob, zpipe, Uint8Array, ArrayBuffer, Blob, saveAs, adler32cs, Deflater */ - -// this will run on <=IE9, possibly some niche browsers -// new webkit-based, FireFox, IE10 already have native version of this. - if (typeof btoa === 'undefined') { - window.btoa = function (data) { - // DO NOT ADD UTF8 ENCODING CODE HERE!!!! - - // UTF8 encoding encodes bytes over char code 128 - // and, essentially, turns an 8-bit binary streams - // (that base64 can deal with) into 7-bit binary streams. - // (by default server does not know that and does not recode the data back to 8bit) - // You destroy your data. - - // binary streams like jpeg image data etc, while stored in JavaScript strings, - // (which are 16bit arrays) are in 8bit format already. - // You do NOT need to char-encode that before base64 encoding. - - // if you, by act of fate - // have string which has individual characters with code - // above 255 (pure unicode chars), encode that BEFORE you base64 here. - // you can use absolutely any approch there, as long as in the end, - // base64 gets an 8bit (char codes 0 - 255) stream. - // when you get it on the server after un-base64, you must - // UNencode it too, to get back to 16, 32bit or whatever original bin stream. - - // Note, Yes, JavaScript strings are, in most cases UCS-2 - - // 16-bit character arrays. This does not mean, however, - // that you always have to UTF8 it before base64. - // it means that if you have actual characters anywhere in - // that string that have char code above 255, you need to - // recode *entire* string from 16-bit (or 32bit) to 8-bit array. - // You can do binary split to UTF16 (BE or LE) - // you can do utf8, you can split the thing by hand and prepend BOM to it, - // but whatever you do, make sure you mirror the opposite on - // the server. If server does not expect to post-process un-base64 - // 8-bit binary stream, think very very hard about messing around with encoding. - - // so, long story short: - // DO NOT ADD UTF8 ENCODING CODE HERE!!!! - - /* @preserve - ==================================================================== - base64 encoder - MIT, GPL - - version: 1109.2015 - discuss at: http://phpjs.org/functions/base64_encode - + original by: Tyler Akins (http://rumkin.com) - + improved by: Bayron Guevara - + improved by: Thunder.m - + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - + bugfixed by: Pellentesque Malesuada - + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - + improved by: Rafal Kukawski (http://kukawski.pl) - + Daniel Dotsenko, Willow Systems Corp, willow-systems.com - ==================================================================== - */ - - var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", - b64a = b64.split(''), - o1, - o2, - o3, - h1, - h2, - h3, - h4, - bits, - i = 0, - ac = 0, - enc = "", - tmp_arr = [], - r; - - do { // pack three octets into four hexets - o1 = data.charCodeAt(i++); - o2 = data.charCodeAt(i++); - o3 = data.charCodeAt(i++); - - bits = o1 << 16 | o2 << 8 | o3; - - h1 = bits >> 18 & 0x3f; - h2 = bits >> 12 & 0x3f; - h3 = bits >> 6 & 0x3f; - h4 = bits & 0x3f; - - // use hexets to index into b64, and append result to encoded string - tmp_arr[ac++] = b64a[h1] + b64a[h2] + b64a[h3] + b64a[h4]; - } while (i < data.length); - - enc = tmp_arr.join(''); - r = data.length % 3; - return (r ? enc.slice(0, r - 3) : enc) + '==='.slice(r || 3); - // end of base64 encoder MIT, GPL - }; - } - - if (typeof atob === 'undefined') { - window.atob = function (data) { - // http://kevin.vanzonneveld.net - // + original by: Tyler Akins (http://rumkin.com) - // + improved by: Thunder.m - // + input by: Aman Gupta - // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // + bugfixed by: Onno Marsman - // + bugfixed by: Pellentesque Malesuada - // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // + input by: Brett Zamir (http://brett-zamir.me) - // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // * example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA=='); - // * returns 1: 'Kevin van Zonneveld' - // mozilla has this native - // - but breaks in 2.0.0.12! - //if (typeof this.window['atob'] == 'function') { - // return atob(data); - //} - var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", - o1, - o2, - o3, - h1, - h2, - h3, - h4, - bits, - i = 0, - ac = 0, - dec = "", - tmp_arr = []; - - if (!data) { - return data; - } - - data += ''; - - do { // unpack four hexets into three octets using index points in b64 - h1 = b64.indexOf(data.charAt(i++)); - h2 = b64.indexOf(data.charAt(i++)); - h3 = b64.indexOf(data.charAt(i++)); - h4 = b64.indexOf(data.charAt(i++)); - - bits = h1 << 18 | h2 << 12 | h3 << 6 | h4; - - o1 = bits >> 16 & 0xff; - o2 = bits >> 8 & 0xff; - o3 = bits & 0xff; - - if (h3 === 64) { - tmp_arr[ac++] = String.fromCharCode(o1); - } else if (h4 === 64) { - tmp_arr[ac++] = String.fromCharCode(o1, o2); - } else { - tmp_arr[ac++] = String.fromCharCode(o1, o2, o3); - } - } while (i < data.length); - dec = tmp_arr.join(''); - return dec; - }; - } - - var getObjectLength = typeof Object.keys === 'function' ? - function (object) { - return Object.keys(object).length; - } : - function (object) { - var i = 0, e; - for (e in object) { - if (object.hasOwnProperty(e)) { - i++; - } - } - return i; - }, - -/** -PubSub implementation - -@class -@name PubSub -*/ - PubSub = function (context) { - /** @preserve - ----------------------------------------------------------------------------------------------- - JavaScript PubSub library - 2012 (c) ddotsenko@willowsystems.com - based on Peter Higgins (dante@dojotoolkit.org) - Loosely based on Dojo publish/subscribe API, limited in scope. Rewritten blindly. - Original is (c) Dojo Foundation 2004-2010. Released under either AFL or new BSD, see: - http://dojofoundation.org/license for more information. - ----------------------------------------------------------------------------------------------- - */ - /** - @private - @fieldOf PubSub - */ - this.topics = {}; - /** - Stores what will be `this` within the callback functions. - - @private - @fieldOf PubSub# - */ - this.context = context; - /** - Allows caller to emit an event and pass arguments to event listeners. - @public - @function - @param topic {String} Name of the channel on which to voice this event - @param args Any number of arguments you want to pass to the listeners of this event. - @methodOf PubSub# - @name publish - */ - this.publish = function (topic, args) { - if (this.topics[topic]) { - var currentTopic = this.topics[topic], - toremove = [], - fn, - i, - l, - pair, - emptyFunc = function () {}; - args = Array.prototype.slice.call(arguments, 1); - for (i = 0, l = currentTopic.length; i < l; i++) { - pair = currentTopic[i]; // this is a [function, once_flag] array - fn = pair[0]; - if (pair[1]) { /* 'run once' flag set */ - pair[0] = emptyFunc; - toremove.push(i); - } - fn.apply(this.context, args); - } - for (i = 0, l = toremove.length; i < l; i++) { - currentTopic.splice(toremove[i], 1); - } - } - }; - /** - Allows listener code to subscribe to channel and be called when data is available - @public - @function - @param topic {String} Name of the channel on which to voice this event - @param callback {Function} Executable (function pointer) that will be ran when event is voiced on this channel. - @param once {Boolean} (optional. False by default) Flag indicating if the function is to be triggered only once. - @returns {Object} A token object that cen be used for unsubscribing. - @methodOf PubSub# - @name subscribe - */ - this.subscribe = function (topic, callback, once) { - if (!this.topics[topic]) { - this.topics[topic] = [[callback, once]]; - } else { - this.topics[topic].push([callback, once]); - } - return { - "topic": topic, - "callback": callback - }; - }; - /** - Allows listener code to unsubscribe from a channel - @public - @function - @param token {Object} A token object that was returned by `subscribe` method - @methodOf PubSub# - @name unsubscribe - */ - this.unsubscribe = function (token) { - if (this.topics[token.topic]) { - var currentTopic = this.topics[token.topic], i, l; - - for (i = 0, l = currentTopic.length; i < l; i++) { - if (currentTopic[i][0] === token.callback) { - currentTopic.splice(i, 1); - } - } - } - }; - }; - - -/** -@constructor -@private -*/ - function jsPDF(orientation, unit, format, compressPdf) { /** String orientation, String unit, String format, Boolean compressed */ - - // Default parameter values - if (typeof orientation === 'undefined') { - orientation = 'p'; - } else { - orientation = orientation.toString().toLowerCase(); - } - if (typeof unit === 'undefined') { unit = 'mm'; } - if (typeof format === 'undefined') { format = 'a4'; } - if (typeof compressPdf === 'undefined' && typeof zpipe === 'undefined') { compressPdf = false; } - - var format_as_string = format.toString().toLowerCase(), - version = '0.9.0rc2', - content = [], - content_length = 0, - compress = compressPdf, - pdfVersion = '1.3', // PDF Version - pageFormats = { // Size in pt of various paper formats - 'a3': [841.89, 1190.55], - 'a4': [595.28, 841.89], - 'a5': [420.94, 595.28], - 'letter': [612, 792], - 'legal': [612, 1008] - }, - textColor = '0 g', - drawColor = '0 G', - page = 0, - pages = [], - objectNumber = 2, // 'n' Current object number - outToPages = false, // switches where out() prints. outToPages true = push to pages obj. outToPages false = doc builder content - offsets = [], // List of offsets. Activated and reset by buildDocument(). Pupulated by various calls buildDocument makes. - fonts = {}, // collection of font objects, where key is fontKey - a dynamically created label for a given font. - fontmap = {}, // mapping structure fontName > fontStyle > font key - performance layer. See addFont() - activeFontSize = 16, - activeFontKey, // will be string representing the KEY of the font as combination of fontName + fontStyle - lineWidth = 0.200025, // 2mm - pageHeight, - pageWidth, - k, // Scale factor - documentProperties = {'title': '', 'subject': '', 'author': '', 'keywords': '', 'creator': ''}, - lineCapID = 0, - lineJoinID = 0, - API = {}, - events = new PubSub(API), - tmp, - plugin, - ///////////////////// - // Private functions - ///////////////////// - // simplified (speedier) replacement for sprintf's %.2f conversion - f2 = function (number) { - return number.toFixed(2); - }, - // simplified (speedier) replacement for sprintf's %.3f conversion - f3 = function (number) { - return number.toFixed(3); - }, - // simplified (speedier) replacement for sprintf's %02d - padd2 = function (number) { - var n = (number).toFixed(0); - if (number < 10) { - return '0' + n; - } else { - return n; - } - }, - // simplified (speedier) replacement for sprintf's %02d - padd10 = function (number) { - var n = (number).toFixed(0); - if (n.length < 10) { - return new Array( 11 - n.length ).join('0') + n; - } else { - return n; - } - }, - out = function (string) { - if (outToPages) { /* set by beginPage */ - pages[page].push(string); - } else { - content.push(string); - content_length += string.length + 1; // +1 is for '\n' that will be used to join contents of content - } - }, - newObject = function () { - // Begin a new object - objectNumber++; - offsets[objectNumber] = content_length; - out(objectNumber + ' 0 obj'); - return objectNumber; - }, - putStream = function (str) { - out('stream'); - out(str); - out('endstream'); - }, - wPt, - hPt, - kids, - i, - putPages = function () { - wPt = pageWidth * k; - hPt = pageHeight * k; - - // outToPages = false as set in endDocument(). out() writes to content. - - var n, p, arr, uint, i, deflater, adler32; - for (n = 1; n <= page; n++) { - newObject(); - out('<>'); - out('endobj'); - - // Page content - p = pages[n].join('\n'); - newObject(); - if (compress) { - arr = []; - for (i = 0; i < p.length; ++i) { - arr[i] = p.charCodeAt(i); - } - adler32 = adler32cs.from(p); - deflater = new Deflater(6); - deflater.append(new Uint8Array(arr)); - p = deflater.flush(); - arr = [new Uint8Array([120, 156]), new Uint8Array(p), - new Uint8Array([adler32 & 0xFF, (adler32 >> 8) & 0xFF, (adler32 >> 16) & 0xFF, (adler32 >> 24) & 0xFF])]; - p = ''; - for (i in arr) { - if (arr.hasOwnProperty(i)) { - p += String.fromCharCode.apply(null, arr[i]); - } - } - out('<>'); - } else { - out('<>'); - } - putStream(p); - out('endobj'); - } - offsets[1] = content_length; - out('1 0 obj'); - out('<>'); - out('endobj'); - }, - putFont = function (font) { - font.objectNumber = newObject(); - out('<>'); - out('endobj'); - }, - putFonts = function () { - var fontKey; - for (fontKey in fonts) { - if (fonts.hasOwnProperty(fontKey)) { - putFont(fonts[fontKey]); - } - } - }, - putXobjectDict = function () { - // Loop through images, or other data objects - events.publish('putXobjectDict'); - }, - putResourceDictionary = function () { - out('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]'); - out('/Font <<'); - // Do this for each font, the '1' bit is the index of the font - var fontKey; - for (fontKey in fonts) { - if (fonts.hasOwnProperty(fontKey)) { - out('/' + fontKey + ' ' + fonts[fontKey].objectNumber + ' 0 R'); - } - } - out('>>'); - out('/XObject <<'); - putXobjectDict(); - out('>>'); - }, - putResources = function () { - putFonts(); - events.publish('putResources'); - // Resource dictionary - offsets[2] = content_length; - out('2 0 obj'); - out('<<'); - putResourceDictionary(); - out('>>'); - out('endobj'); - events.publish('postPutResources'); - }, - addToFontDictionary = function (fontKey, fontName, fontStyle) { - // this is mapping structure for quick font key lookup. - // returns the KEY of the font (ex: "F1") for a given pair of font name and type (ex: "Arial". "Italic") - var undef; - if (fontmap[fontName] === undef) { - fontmap[fontName] = {}; // fontStyle is a var interpreted and converted to appropriate string. don't wrap in quotes. - } - fontmap[fontName][fontStyle] = fontKey; - }, - /** - FontObject describes a particular font as member of an instnace of jsPDF - - It's a collection of properties like 'id' (to be used in PDF stream), - 'fontName' (font's family name), 'fontStyle' (font's style variant label) - - @class - @public - @property id {String} PDF-document-instance-specific label assinged to the font. - @property PostScriptName {String} PDF specification full name for the font - @property encoding {Object} Encoding_name-to-Font_metrics_object mapping. - @name FontObject - */ - FontObject = {}, - addFont = function (PostScriptName, fontName, fontStyle, encoding) { - var fontKey = 'F' + (getObjectLength(fonts) + 1).toString(10), - // This is FontObject - font = fonts[fontKey] = { - 'id': fontKey, - // , 'objectNumber': will be set by putFont() - 'PostScriptName': PostScriptName, - 'fontName': fontName, - 'fontStyle': fontStyle, - 'encoding': encoding, - 'metadata': {} - }; - - addToFontDictionary(fontKey, fontName, fontStyle); - - events.publish('addFont', font); - - return fontKey; - }, - addFonts = function () { - - var HELVETICA = "helvetica", - TIMES = "times", - COURIER = "courier", - NORMAL = "normal", - BOLD = "bold", - ITALIC = "italic", - BOLD_ITALIC = "bolditalic", - encoding = 'StandardEncoding', - standardFonts = [ - ['Helvetica', HELVETICA, NORMAL], - ['Helvetica-Bold', HELVETICA, BOLD], - ['Helvetica-Oblique', HELVETICA, ITALIC], - ['Helvetica-BoldOblique', HELVETICA, BOLD_ITALIC], - ['Courier', COURIER, NORMAL], - ['Courier-Bold', COURIER, BOLD], - ['Courier-Oblique', COURIER, ITALIC], - ['Courier-BoldOblique', COURIER, BOLD_ITALIC], - ['Times-Roman', TIMES, NORMAL], - ['Times-Bold', TIMES, BOLD], - ['Times-Italic', TIMES, ITALIC], - ['Times-BoldItalic', TIMES, BOLD_ITALIC] - ], - i, - l, - fontKey, - parts; - for (i = 0, l = standardFonts.length; i < l; i++) { - fontKey = addFont( - standardFonts[i][0], - standardFonts[i][1], - standardFonts[i][2], - encoding - ); - - // adding aliases for standard fonts, this time matching the capitalization - parts = standardFonts[i][0].split('-'); - addToFontDictionary(fontKey, parts[0], parts[1] || ''); - } - - events.publish('addFonts', {'fonts': fonts, 'dictionary': fontmap}); - }, - /** - - @public - @function - @param text {String} - @param flags {Object} Encoding flags. - @returns {String} Encoded string - */ - to8bitStream = function (text, flags) { - /* PDF 1.3 spec: - "For text strings encoded in Unicode, the first two bytes must be 254 followed by - 255, representing the Unicode byte order marker, U+FEFF. (This sequence conflicts - with the PDFDocEncoding character sequence thorn ydieresis, which is unlikely - to be a meaningful beginning of a word or phrase.) The remainder of the - string consists of Unicode character codes, according to the UTF-16 encoding - specified in the Unicode standard, version 2.0. Commonly used Unicode values - are represented as 2 bytes per character, with the high-order byte appearing first - in the string." - - In other words, if there are chars in a string with char code above 255, we - recode the string to UCS2 BE - string doubles in length and BOM is prepended. - - HOWEVER! - Actual *content* (body) text (as opposed to strings used in document properties etc) - does NOT expect BOM. There, it is treated as a literal GID (Glyph ID) - - Because of Adobe's focus on "you subset your fonts!" you are not supposed to have - a font that maps directly Unicode (UCS2 / UTF16BE) code to font GID, but you could - fudge it with "Identity-H" encoding and custom CIDtoGID map that mimics Unicode - code page. There, however, all characters in the stream are treated as GIDs, - including BOM, which is the reason we need to skip BOM in content text (i.e. that - that is tied to a font). - - To signal this "special" PDFEscape / to8bitStream handling mode, - API.text() function sets (unless you overwrite it with manual values - given to API.text(.., flags) ) - flags.autoencode = true - flags.noBOM = true - - */ - - /* - `flags` properties relied upon: - .sourceEncoding = string with encoding label. - "Unicode" by default. = encoding of the incoming text. - pass some non-existing encoding name - (ex: 'Do not touch my strings! I know what I am doing.') - to make encoding code skip the encoding step. - .outputEncoding = Either valid PDF encoding name - (must be supported by jsPDF font metrics, otherwise no encoding) - or a JS object, where key = sourceCharCode, value = outputCharCode - missing keys will be treated as: sourceCharCode === outputCharCode - .noBOM - See comment higher above for explanation for why this is important - .autoencode - See comment higher above for explanation for why this is important - */ - - var i, l, undef, sourceEncoding, encodingBlock, outputEncoding, newtext, isUnicode, ch, bch; - - if (flags === undef) { - flags = {}; - } - - sourceEncoding = flags.sourceEncoding ? sourceEncoding : 'Unicode'; - - outputEncoding = flags.outputEncoding; - - // This 'encoding' section relies on font metrics format - // attached to font objects by, among others, - // "Willow Systems' standard_font_metrics plugin" - // see jspdf.plugin.standard_font_metrics.js for format - // of the font.metadata.encoding Object. - // It should be something like - // .encoding = {'codePages':['WinANSI....'], 'WinANSI...':{code:code, ...}} - // .widths = {0:width, code:width, ..., 'fof':divisor} - // .kerning = {code:{previous_char_code:shift, ..., 'fof':-divisor},...} - if ((flags.autoencode || outputEncoding) && - fonts[activeFontKey].metadata && - fonts[activeFontKey].metadata[sourceEncoding] && - fonts[activeFontKey].metadata[sourceEncoding].encoding - ) { - encodingBlock = fonts[activeFontKey].metadata[sourceEncoding].encoding; - - // each font has default encoding. Some have it clearly defined. - if (!outputEncoding && fonts[activeFontKey].encoding) { - outputEncoding = fonts[activeFontKey].encoding; - } - - // Hmmm, the above did not work? Let's try again, in different place. - if (!outputEncoding && encodingBlock.codePages) { - outputEncoding = encodingBlock.codePages[0]; // let's say, first one is the default - } - - if (typeof outputEncoding === 'string') { - outputEncoding = encodingBlock[outputEncoding]; - } - // we want output encoding to be a JS Object, where - // key = sourceEncoding's character code and - // value = outputEncoding's character code. - if (outputEncoding) { - isUnicode = false; - newtext = []; - for (i = 0, l = text.length; i < l; i++) { - ch = outputEncoding[text.charCodeAt(i)]; - if (ch) { - newtext.push( - String.fromCharCode(ch) - ); - } else { - newtext.push( - text[i] - ); - } - - // since we are looping over chars anyway, might as well - // check for residual unicodeness - if (newtext[i].charCodeAt(0) >> 8) { /* more than 255 */ - isUnicode = true; - } - } - text = newtext.join(''); - } - } - - i = text.length; - // isUnicode may be set to false above. Hence the triple-equal to undefined - while (isUnicode === undef && i !== 0) { - if (text.charCodeAt(i - 1) >> 8) { /* more than 255 */ - isUnicode = true; - } - i--; - } - if (!isUnicode) { - return text; - } else { - newtext = flags.noBOM ? [] : [254, 255]; - for (i = 0, l = text.length; i < l; i++) { - ch = text.charCodeAt(i); - bch = ch >> 8; // divide by 256 - if (bch >> 8) { /* something left after dividing by 256 second time */ - throw new Error("Character at position " + i.toString(10) + " of string '" + text + "' exceeds 16bits. Cannot be encoded into UCS-2 BE"); - } - newtext.push(bch); - newtext.push(ch - (bch << 8)); - } - return String.fromCharCode.apply(undef, newtext); - } - }, - // Replace '/', '(', and ')' with pdf-safe versions - pdfEscape = function (text, flags) { - // doing to8bitStream does NOT make this PDF display unicode text. For that - // we also need to reference a unicode font and embed it - royal pain in the rear. - - // There is still a benefit to to8bitStream - PDF simply cannot handle 16bit chars, - // which JavaScript Strings are happy to provide. So, while we still cannot display - // 2-byte characters property, at least CONDITIONALLY converting (entire string containing) - // 16bit chars to (USC-2-BE) 2-bytes per char + BOM streams we ensure that entire PDF - // is still parseable. - // This will allow immediate support for unicode in document properties strings. - return to8bitStream(text, flags).replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)'); - }, - putInfo = function () { - out('/Producer (jsPDF ' + version + ')'); - if (documentProperties.title) { - out('/Title (' + pdfEscape(documentProperties.title) + ')'); - } - if (documentProperties.subject) { - out('/Subject (' + pdfEscape(documentProperties.subject) + ')'); - } - if (documentProperties.author) { - out('/Author (' + pdfEscape(documentProperties.author) + ')'); - } - if (documentProperties.keywords) { - out('/Keywords (' + pdfEscape(documentProperties.keywords) + ')'); - } - if (documentProperties.creator) { - out('/Creator (' + pdfEscape(documentProperties.creator) + ')'); - } - var created = new Date(); - out('/CreationDate (D:' + - [ - created.getFullYear(), - padd2(created.getMonth() + 1), - padd2(created.getDate()), - padd2(created.getHours()), - padd2(created.getMinutes()), - padd2(created.getSeconds()) - ].join('') + - ')' - ); - }, - putCatalog = function () { - out('/Type /Catalog'); - out('/Pages 1 0 R'); - // @TODO: Add zoom and layout modes - out('/OpenAction [3 0 R /FitH null]'); - out('/PageLayout /OneColumn'); - events.publish('putCatalog'); - }, - putTrailer = function () { - out('/Size ' + (objectNumber + 1)); - out('/Root ' + objectNumber + ' 0 R'); - out('/Info ' + (objectNumber - 1) + ' 0 R'); - }, - beginPage = function () { - page++; - // Do dimension stuff - outToPages = true; - pages[page] = []; - }, - _addPage = function () { - beginPage(); - // Set line width - out(f2(lineWidth * k) + ' w'); - // Set draw color - out(drawColor); - // resurrecting non-default line caps, joins - if (lineCapID !== 0) { - out(lineCapID.toString(10) + ' J'); - } - if (lineJoinID !== 0) { - out(lineJoinID.toString(10) + ' j'); - } - events.publish('addPage', {'pageNumber': page}); - }, - /** - Returns a document-specific font key - a label assigned to a - font name + font type combination at the time the font was added - to the font inventory. - - Font key is used as label for the desired font for a block of text - to be added to the PDF document stream. - @private - @function - @param fontName {String} can be undefined on "falthy" to indicate "use current" - @param fontStyle {String} can be undefined on "falthy" to indicate "use current" - @returns {String} Font key. - */ - getFont = function (fontName, fontStyle) { - var key, undef; - - if (fontName === undef) { - fontName = fonts[activeFontKey].fontName; - } - if (fontStyle === undef) { - fontStyle = fonts[activeFontKey].fontStyle; - } - - try { - key = fontmap[fontName][fontStyle]; // returns a string like 'F3' - the KEY corresponding tot he font + type combination. - } catch (e) { - key = undef; - } - if (!key) { - throw new Error("Unable to look up font label for font '" + fontName + "', '" + fontStyle + "'. Refer to getFontList() for available fonts."); - } - - return key; - }, - buildDocument = function () { - - outToPages = false; // switches out() to content - content = []; - offsets = []; - - // putHeader() - out('%PDF-' + pdfVersion); - - putPages(); - - putResources(); - - // Info - newObject(); - out('<<'); - putInfo(); - out('>>'); - out('endobj'); - - // Catalog - newObject(); - out('<<'); - putCatalog(); - out('>>'); - out('endobj'); - - // Cross-ref - var o = content_length, i; - out('xref'); - out('0 ' + (objectNumber + 1)); - out('0000000000 65535 f '); - for (i = 1; i <= objectNumber; i++) { - out(padd10(offsets[i]) + ' 00000 n '); - } - // Trailer - out('trailer'); - out('<<'); - putTrailer(); - out('>>'); - out('startxref'); - out(o); - out('%%EOF'); - - outToPages = true; - - return content.join('\n'); - }, - getStyle = function (style) { - // see Path-Painting Operators of PDF spec - var op = 'S'; // stroke - if (style === 'F') { - op = 'f'; // fill - } else if (style === 'FD' || style === 'DF') { - op = 'B'; // both - } - return op; - }, - - /** - Generates the PDF document. - Possible values: - datauristring (alias dataurlstring) - Data-Url-formatted data returned as string. - datauri (alias datauri) - Data-Url-formatted data pushed into current window's location (effectively reloading the window with contents of the PDF). - - If `type` argument is undefined, output is raw body of resulting PDF returned as a string. - - @param {String} type A string identifying one of the possible output types. - @param {Object} options An object providing some additional signalling to PDF generator. - @function - @returns {jsPDF} - @methodOf jsPDF# - @name output - */ - output = function (type, options) { - var undef, data, length, array, i, blob; - switch (type) { - case undef: - return buildDocument(); - case 'save': - if (navigator.getUserMedia) { - if (window.URL === undefined) { - return API.output('dataurlnewwindow'); - } else if (window.URL.createObjectURL === undefined) { - return API.output('dataurlnewwindow'); - } - } - data = buildDocument(); - - // Need to add the file to BlobBuilder as a Uint8Array - length = data.length; - array = new Uint8Array(new ArrayBuffer(length)); - - for (i = 0; i < length; i++) { - array[i] = data.charCodeAt(i); - } - - blob = new Blob([array], {type: "application/pdf"}); - - saveAs(blob, options); - break; - case 'datauristring': - case 'dataurlstring': - return 'data:application/pdf;base64,' + btoa(buildDocument()); - case 'datauri': - case 'dataurl': - document.location.href = 'data:application/pdf;base64,' + btoa(buildDocument()); - break; - case 'dataurlnewwindow': - window.open('data:application/pdf;base64,' + btoa(buildDocument())); - break; - default: - throw new Error('Output type "' + type + '" is not supported.'); - } - // @TODO: Add different output options - }; + * Creates new jsPDF document object instance. + * + * @class + * @param orientation One of "portrait" or "landscape" (or shortcuts "p" (Default), "l") + * @param unit Measurement unit to be used when coordinates are specified. + * One of "pt" (points), "mm" (Default), "cm", "in" + * @param format One of 'pageFormats' as shown below, default: a4 + * @returns {jsPDF} + * @name jsPDF + */ +var jsPDF = (function(global) { + 'use strict'; + var pdfVersion = '1.3', + pageFormats = { // Size in pt of various paper formats + 'a0' : [2383.94, 3370.39], 'a1' : [1683.78, 2383.94], + 'a2' : [1190.55, 1683.78], 'a3' : [ 841.89, 1190.55], + 'a4' : [ 595.28, 841.89], 'a5' : [ 419.53, 595.28], + 'a6' : [ 297.64, 419.53], 'a7' : [ 209.76, 297.64], + 'a8' : [ 147.40, 209.76], 'a9' : [ 104.88, 147.40], + 'a10' : [ 73.70, 104.88], 'b0' : [2834.65, 4008.19], + 'b1' : [2004.09, 2834.65], 'b2' : [1417.32, 2004.09], + 'b3' : [1000.63, 1417.32], 'b4' : [ 708.66, 1000.63], + 'b5' : [ 498.90, 708.66], 'b6' : [ 354.33, 498.90], + 'b7' : [ 249.45, 354.33], 'b8' : [ 175.75, 249.45], + 'b9' : [ 124.72, 175.75], 'b10' : [ 87.87, 124.72], + 'c0' : [2599.37, 3676.54], 'c1' : [1836.85, 2599.37], + 'c2' : [1298.27, 1836.85], 'c3' : [ 918.43, 1298.27], + 'c4' : [ 649.13, 918.43], 'c5' : [ 459.21, 649.13], + 'c6' : [ 323.15, 459.21], 'c7' : [ 229.61, 323.15], + 'c8' : [ 161.57, 229.61], 'c9' : [ 113.39, 161.57], + 'c10' : [ 79.37, 113.39], 'dl' : [ 311.81, 623.62], + 'letter' : [612, 792], + 'government-letter' : [576, 756], + 'legal' : [612, 1008], + 'junior-legal' : [576, 360], + 'ledger' : [1224, 792], + 'tabloid' : [792, 1224], + 'credit-card' : [153, 243] + }; + + /** + * jsPDF's Internal PubSub Implementation. + * See mrrio.github.io/jsPDF/doc/symbols/PubSub.html + * Backward compatible rewritten on 2014 by + * Diego Casorran, https://github.com/diegocr + * + * @class + * @name PubSub + */ + function PubSub(context) { + var topics = {}; + + this.subscribe = function(topic, callback, once) { + if(typeof callback !== 'function') { + return false; + } + + if(!topics.hasOwnProperty(topic)) { + topics[topic] = {}; + } + + var id = Math.random().toString(35); + topics[topic][id] = [callback,!!once]; + + return id; + }; + + this.unsubscribe = function(token) { + for(var topic in topics) { + if(topics[topic][token]) { + delete topics[topic][token]; + return true; + } + } + return false; + }; + + this.publish = function(topic) { + if(topics.hasOwnProperty(topic)) { + var args = Array.prototype.slice.call(arguments, 1), idr = []; + + for(var id in topics[topic]) { + var sub = topics[topic][id]; + try { + sub[0].apply(context, args); + } catch(ex) { + if(global.console) { + console.error('jsPDF PubSub Error', ex.message, ex); + } + } + if(sub[1]) idr.push(id); + } + if(idr.length) idr.forEach(this.unsubscribe); + } + }; + } + + /** + * @constructor + * @private + */ + function jsPDF(orientation, unit, format, compressPdf) { + var options = {}; + + if (typeof orientation === 'object') { + options = orientation; + + orientation = options.orientation; + unit = options.unit || unit; + format = options.format || format; + compressPdf = options.compress || options.compressPdf || compressPdf; + } + + // Default options + unit = unit || 'mm'; + format = format || 'a4'; + orientation = ('' + (orientation || 'P')).toLowerCase(); + + var format_as_string = ('' + format).toLowerCase(), + compress = !!compressPdf && typeof Uint8Array === 'function', + textColor = options.textColor || '0 g', + drawColor = options.drawColor || '0 G', + activeFontSize = options.fontSize || 16, + lineHeightProportion = options.lineHeight || 1.15, + lineWidth = options.lineWidth || 0.200025, // 2mm + objectNumber = 2, // 'n' Current object number + outToPages = !1, // switches where out() prints. outToPages true = push to pages obj. outToPages false = doc builder content + offsets = [], // List of offsets. Activated and reset by buildDocument(). Pupulated by various calls buildDocument makes. + fonts = {}, // collection of font objects, where key is fontKey - a dynamically created label for a given font. + fontmap = {}, // mapping structure fontName > fontStyle > font key - performance layer. See addFont() + activeFontKey, // will be string representing the KEY of the font as combination of fontName + fontStyle + k, // Scale factor + tmp, + page = 0, + pages = [], + content = [], + lineCapID = 0, + lineJoinID = 0, + content_length = 0, + pageWidth, + pageHeight, + documentProperties = { + 'title' : '', + 'subject' : '', + 'author' : '', + 'keywords' : '', + 'creator' : '' + }, + API = {}, + events = new PubSub(API), + + ///////////////////// + // Private functions + ///////////////////// + f2 = function(number) { + return number.toFixed(2); // Ie, %.2f + }, + f3 = function(number) { + return number.toFixed(3); // Ie, %.3f + }, + padd2 = function(number) { + return ('0' + parseInt(number)).slice(-2); + }, + out = function(string) { + if (outToPages) { + /* set by beginPage */ + pages[page].push(string); + } else { + // +1 for '\n' that will be used to join 'content' + content_length += string.length + 1; + content.push(string); + } + }, + newObject = function() { + // Begin a new object + objectNumber++; + offsets[objectNumber] = content_length; + out(objectNumber + ' 0 obj'); + return objectNumber; + }, + putStream = function(str) { + out('stream'); + out(str); + out('endstream'); + }, + putPages = function() { + var n,p,arr,i,deflater,adler32,wPt = pageWidth * k, hPt = pageHeight * k, adler32cs; + + adler32cs = global.adler32cs || jsPDF.adler32cs; + if (compress && typeof adler32cs === 'undefined') { + compress = false; + } + + // outToPages = false as set in endDocument(). out() writes to content. + + for (n = 1; n <= page; n++) { + newObject(); + out('<>'); + out('endobj'); + + // Page content + p = pages[n].join('\n'); + newObject(); + if (compress) { + arr = []; + i = p.length; + while(i--) { + arr[i] = p.charCodeAt(i); + } + adler32 = adler32cs.from(p); + deflater = new Deflater(6); + deflater.append(new Uint8Array(arr)); + p = deflater.flush(); + arr = new Uint8Array(p.length + 6); + arr.set(new Uint8Array([120, 156])), + arr.set(p, 2); + arr.set(new Uint8Array([adler32 & 0xFF, (adler32 >> 8) & 0xFF, (adler32 >> 16) & 0xFF, (adler32 >> 24) & 0xFF]), p.length+2); + p = String.fromCharCode.apply(null, arr); + out('<>'); + } else { + out('<>'); + } + putStream(p); + out('endobj'); + } + offsets[1] = content_length; + out('1 0 obj'); + out('<>'); + out('endobj'); + }, + putFont = function(font) { + font.objectNumber = newObject(); + out('<>'); + out('endobj'); + }, + putFonts = function() { + for (var fontKey in fonts) { + if (fonts.hasOwnProperty(fontKey)) { + putFont(fonts[fontKey]); + } + } + }, + putXobjectDict = function() { + // Loop through images, or other data objects + events.publish('putXobjectDict'); + }, + putResourceDictionary = function() { + out('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]'); + out('/Font <<'); + + // Do this for each font, the '1' bit is the index of the font + for (var fontKey in fonts) { + if (fonts.hasOwnProperty(fontKey)) { + out('/' + fontKey + ' ' + fonts[fontKey].objectNumber + ' 0 R'); + } + } + out('>>'); + out('/XObject <<'); + putXobjectDict(); + out('>>'); + }, + putResources = function() { + putFonts(); + events.publish('putResources'); + // Resource dictionary + offsets[2] = content_length; + out('2 0 obj'); + out('<<'); + putResourceDictionary(); + out('>>'); + out('endobj'); + events.publish('postPutResources'); + }, + addToFontDictionary = function(fontKey, fontName, fontStyle) { + // this is mapping structure for quick font key lookup. + // returns the KEY of the font (ex: "F1") for a given + // pair of font name and type (ex: "Arial". "Italic") + if (!fontmap.hasOwnProperty(fontName)) { + fontmap[fontName] = {}; + } + fontmap[fontName][fontStyle] = fontKey; + }, + /** + * FontObject describes a particular font as member of an instnace of jsPDF + * + * It's a collection of properties like 'id' (to be used in PDF stream), + * 'fontName' (font's family name), 'fontStyle' (font's style variant label) + * + * @class + * @public + * @property id {String} PDF-document-instance-specific label assinged to the font. + * @property PostScriptName {String} PDF specification full name for the font + * @property encoding {Object} Encoding_name-to-Font_metrics_object mapping. + * @name FontObject + */ + addFont = function(PostScriptName, fontName, fontStyle, encoding) { + var fontKey = 'F' + (Object.keys(fonts).length + 1).toString(10), + // This is FontObject + font = fonts[fontKey] = { + 'id' : fontKey, + 'PostScriptName' : PostScriptName, + 'fontName' : fontName, + 'fontStyle' : fontStyle, + 'encoding' : encoding, + 'metadata' : {} + }; + addToFontDictionary(fontKey, fontName, fontStyle); + events.publish('addFont', font); + + return fontKey; + }, + addFonts = function() { + + var HELVETICA = "helvetica", + TIMES = "times", + COURIER = "courier", + NORMAL = "normal", + BOLD = "bold", + ITALIC = "italic", + BOLD_ITALIC = "bolditalic", + encoding = 'StandardEncoding', + standardFonts = [ + ['Helvetica', HELVETICA, NORMAL], + ['Helvetica-Bold', HELVETICA, BOLD], + ['Helvetica-Oblique', HELVETICA, ITALIC], + ['Helvetica-BoldOblique', HELVETICA, BOLD_ITALIC], + ['Courier', COURIER, NORMAL], + ['Courier-Bold', COURIER, BOLD], + ['Courier-Oblique', COURIER, ITALIC], + ['Courier-BoldOblique', COURIER, BOLD_ITALIC], + ['Times-Roman', TIMES, NORMAL], + ['Times-Bold', TIMES, BOLD], + ['Times-Italic', TIMES, ITALIC], + ['Times-BoldItalic', TIMES, BOLD_ITALIC] + ]; + + for (var i = 0, l = standardFonts.length; i < l; i++) { + var fontKey = addFont( + standardFonts[i][0], + standardFonts[i][1], + standardFonts[i][2], + encoding); + + // adding aliases for standard fonts, this time matching the capitalization + var parts = standardFonts[i][0].split('-'); + addToFontDictionary(fontKey, parts[0], parts[1] || ''); + } + events.publish('addFonts', { fonts : fonts, dictionary : fontmap }); + }, + SAFE = function(fn) { + fn.foo = function() { + try { + return fn.apply(this, arguments); + } catch (e) { + var stack = e.stack || ''; + if(~stack.indexOf(' at ')) stack = stack.split(" at ")[1]; + var m = "Error in function " + stack.split("\n")[0].split('<')[0] + ": " + e.message; + if(global.console) { + console.log(m, e); + if(global.alert) alert(m); + console.trace(); + } else { + throw new Error(m); + } + } + }; + fn.foo.bar = fn; + return fn.foo; + }, + to8bitStream = function(text, flags) { + /** + * PDF 1.3 spec: + * "For text strings encoded in Unicode, the first two bytes must be 254 followed by + * 255, representing the Unicode byte order marker, U+FEFF. (This sequence conflicts + * with the PDFDocEncoding character sequence thorn ydieresis, which is unlikely + * to be a meaningful beginning of a word or phrase.) The remainder of the + * string consists of Unicode character codes, according to the UTF-16 encoding + * specified in the Unicode standard, version 2.0. Commonly used Unicode values + * are represented as 2 bytes per character, with the high-order byte appearing first + * in the string." + * + * In other words, if there are chars in a string with char code above 255, we + * recode the string to UCS2 BE - string doubles in length and BOM is prepended. + * + * HOWEVER! + * Actual *content* (body) text (as opposed to strings used in document properties etc) + * does NOT expect BOM. There, it is treated as a literal GID (Glyph ID) + * + * Because of Adobe's focus on "you subset your fonts!" you are not supposed to have + * a font that maps directly Unicode (UCS2 / UTF16BE) code to font GID, but you could + * fudge it with "Identity-H" encoding and custom CIDtoGID map that mimics Unicode + * code page. There, however, all characters in the stream are treated as GIDs, + * including BOM, which is the reason we need to skip BOM in content text (i.e. that + * that is tied to a font). + * + * To signal this "special" PDFEscape / to8bitStream handling mode, + * API.text() function sets (unless you overwrite it with manual values + * given to API.text(.., flags) ) + * flags.autoencode = true + * flags.noBOM = true + * + * =================================================================================== + * `flags` properties relied upon: + * .sourceEncoding = string with encoding label. + * "Unicode" by default. = encoding of the incoming text. + * pass some non-existing encoding name + * (ex: 'Do not touch my strings! I know what I am doing.') + * to make encoding code skip the encoding step. + * .outputEncoding = Either valid PDF encoding name + * (must be supported by jsPDF font metrics, otherwise no encoding) + * or a JS object, where key = sourceCharCode, value = outputCharCode + * missing keys will be treated as: sourceCharCode === outputCharCode + * .noBOM + * See comment higher above for explanation for why this is important + * .autoencode + * See comment higher above for explanation for why this is important + */ + + var i,l,sourceEncoding,encodingBlock,outputEncoding,newtext,isUnicode,ch,bch; + + flags = flags || {}; + sourceEncoding = flags.sourceEncoding || 'Unicode'; + outputEncoding = flags.outputEncoding; + + // This 'encoding' section relies on font metrics format + // attached to font objects by, among others, + // "Willow Systems' standard_font_metrics plugin" + // see jspdf.plugin.standard_font_metrics.js for format + // of the font.metadata.encoding Object. + // It should be something like + // .encoding = {'codePages':['WinANSI....'], 'WinANSI...':{code:code, ...}} + // .widths = {0:width, code:width, ..., 'fof':divisor} + // .kerning = {code:{previous_char_code:shift, ..., 'fof':-divisor},...} + if ((flags.autoencode || outputEncoding) && + fonts[activeFontKey].metadata && + fonts[activeFontKey].metadata[sourceEncoding] && + fonts[activeFontKey].metadata[sourceEncoding].encoding) { + encodingBlock = fonts[activeFontKey].metadata[sourceEncoding].encoding; + + // each font has default encoding. Some have it clearly defined. + if (!outputEncoding && fonts[activeFontKey].encoding) { + outputEncoding = fonts[activeFontKey].encoding; + } + + // Hmmm, the above did not work? Let's try again, in different place. + if (!outputEncoding && encodingBlock.codePages) { + outputEncoding = encodingBlock.codePages[0]; // let's say, first one is the default + } + + if (typeof outputEncoding === 'string') { + outputEncoding = encodingBlock[outputEncoding]; + } + // we want output encoding to be a JS Object, where + // key = sourceEncoding's character code and + // value = outputEncoding's character code. + if (outputEncoding) { + isUnicode = false; + newtext = []; + for (i = 0, l = text.length; i < l; i++) { + ch = outputEncoding[text.charCodeAt(i)]; + if (ch) { + newtext.push( + String.fromCharCode(ch)); + } else { + newtext.push( + text[i]); + } + + // since we are looping over chars anyway, might as well + // check for residual unicodeness + if (newtext[i].charCodeAt(0) >> 8) { + /* more than 255 */ + isUnicode = true; + } + } + text = newtext.join(''); + } + } + + i = text.length; + // isUnicode may be set to false above. Hence the triple-equal to undefined + while (isUnicode === undefined && i !== 0) { + if (text.charCodeAt(i - 1) >> 8) { + /* more than 255 */ + isUnicode = true; + } + i--; + } + if (!isUnicode) { + return text; + } + + newtext = flags.noBOM ? [] : [254, 255]; + for (i = 0, l = text.length; i < l; i++) { + ch = text.charCodeAt(i); + bch = ch >> 8; // divide by 256 + if (bch >> 8) { + /* something left after dividing by 256 second time */ + throw new Error("Character at position " + i + " of string '" + + text + "' exceeds 16bits. Cannot be encoded into UCS-2 BE"); + } + newtext.push(bch); + newtext.push(ch - (bch << 8)); + } + return String.fromCharCode.apply(undefined, newtext); + }, + pdfEscape = function(text, flags) { + /** + * Replace '/', '(', and ')' with pdf-safe versions + * + * Doing to8bitStream does NOT make this PDF display unicode text. For that + * we also need to reference a unicode font and embed it - royal pain in the rear. + * + * There is still a benefit to to8bitStream - PDF simply cannot handle 16bit chars, + * which JavaScript Strings are happy to provide. So, while we still cannot display + * 2-byte characters property, at least CONDITIONALLY converting (entire string containing) + * 16bit chars to (USC-2-BE) 2-bytes per char + BOM streams we ensure that entire PDF + * is still parseable. + * This will allow immediate support for unicode in document properties strings. + */ + return to8bitStream(text, flags).replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)'); + }, + putInfo = function() { + out('/Producer (jsPDF ' + jsPDF.version + ')'); + for(var key in documentProperties) { + if(documentProperties.hasOwnProperty(key) && documentProperties[key]) { + out('/'+key.substr(0,1).toUpperCase() + key.substr(1) + +' (' + pdfEscape(documentProperties[key]) + ')'); + } + } + var created = new Date(); + out(['/CreationDate (D:', + created.getFullYear(), + padd2(created.getMonth() + 1), + padd2(created.getDate()), + padd2(created.getHours()), + padd2(created.getMinutes()), + padd2(created.getSeconds()), ')'].join('')); + }, + putCatalog = function() { + out('/Type /Catalog'); + out('/Pages 1 0 R'); + // @TODO: Add zoom and layout modes + out('/OpenAction [3 0 R /FitH null]'); + out('/PageLayout /OneColumn'); + events.publish('putCatalog'); + }, + putTrailer = function() { + out('/Size ' + (objectNumber + 1)); + out('/Root ' + objectNumber + ' 0 R'); + out('/Info ' + (objectNumber - 1) + ' 0 R'); + }, + beginPage = function() { + page++; + // Do dimension stuff + outToPages = true; + pages[page] = []; + }, + _addPage = function() { + beginPage(); + // Set line width + out(f2(lineWidth * k) + ' w'); + // Set draw color + out(drawColor); + // resurrecting non-default line caps, joins + if (lineCapID !== 0) { + out(lineCapID + ' J'); + } + if (lineJoinID !== 0) { + out(lineJoinID + ' j'); + } + events.publish('addPage', { pageNumber : page }); + }, + /** + * Returns a document-specific font key - a label assigned to a + * font name + font type combination at the time the font was added + * to the font inventory. + * + * Font key is used as label for the desired font for a block of text + * to be added to the PDF document stream. + * @private + * @function + * @param fontName {String} can be undefined on "falthy" to indicate "use current" + * @param fontStyle {String} can be undefined on "falthy" to indicate "use current" + * @returns {String} Font key. + */ + getFont = function(fontName, fontStyle) { + var key; + + fontName = fontName !== undefined ? fontName : fonts[activeFontKey].fontName; + fontStyle = fontStyle !== undefined ? fontStyle : fonts[activeFontKey].fontStyle; + + try { + // get a string like 'F3' - the KEY corresponding tot he font + type combination. + key = fontmap[fontName][fontStyle]; + } catch (e) {} + + if (!key) { + throw new Error("Unable to look up font label for font '" + fontName + "', '" + + fontStyle + "'. Refer to getFontList() for available fonts."); + } + return key; + }, + buildDocument = function() { + + outToPages = false; // switches out() to content + objectNumber = 2; + content = []; + offsets = []; + + // putHeader() + out('%PDF-' + pdfVersion); + + putPages(); + + putResources(); + + // Info + newObject(); + out('<<'); + putInfo(); + out('>>'); + out('endobj'); + + // Catalog + newObject(); + out('<<'); + putCatalog(); + out('>>'); + out('endobj'); + + // Cross-ref + var o = content_length, i, p = "0000000000"; + out('xref'); + out('0 ' + (objectNumber + 1)); + out(p+' 65535 f '); + for (i = 1; i <= objectNumber; i++) { + out((p + offsets[i]).slice(-10) + ' 00000 n '); + } + // Trailer + out('trailer'); + out('<<'); + putTrailer(); + out('>>'); + out('startxref'); + out(o); + out('%%EOF'); + + outToPages = true; + + return content.join('\n'); + }, + getStyle = function(style) { + // see path-painting operators in PDF spec + var op = 'S'; // stroke + if (style === 'F') { + op = 'f'; // fill + } else if (style === 'FD' || style === 'DF') { + op = 'B'; // both + } else if (style === 'f' || style === 'f*' || style === 'B' || style === 'B*') { + /* + Allow direct use of these PDF path-painting operators: + - f fill using nonzero winding number rule + - f* fill using even-odd rule + - B fill then stroke with fill using non-zero winding number rule + - B* fill then stroke with fill using even-odd rule + */ + op = style; + } + return op; + }, + getArrayBuffer = function() { + var data = buildDocument(), len = data.length, + ab = new ArrayBuffer(len), u8 = new Uint8Array(ab); + + while(len--) u8[len] = data.charCodeAt(len); + return ab; + }, + getBlob = function() { + return new Blob([getArrayBuffer()], { type : "application/pdf" }); + }, + /** + * Generates the PDF document. + * + * If `type` argument is undefined, output is raw body of resulting PDF returned as a string. + * + * @param {String} type A string identifying one of the possible output types. + * @param {Object} options An object providing some additional signalling to PDF generator. + * @function + * @returns {jsPDF} + * @methodOf jsPDF# + * @name output + */ + output = SAFE(function(type, options) { + switch (type) { + case undefined: + return buildDocument(); + case 'save': + if (navigator.getUserMedia) { + if (global.URL === undefined + || global.URL.createObjectURL === undefined) { + return API.output('dataurlnewwindow'); + } + } + saveAs(getBlob(), options); + if(typeof saveAs.unload === 'function') { + if(global.setTimeout) { + setTimeout(saveAs.unload,70); + } + } + break; + case 'arraybuffer': + return getArrayBuffer(); + case 'blob': + return getBlob(); + case 'datauristring': + case 'dataurlstring': + return 'data:application/pdf;base64,' + btoa(buildDocument()); + case 'datauri': + case 'dataurl': + global.document.location.href = 'data:application/pdf;base64,' + btoa(buildDocument()); + break; + case 'dataurlnewwindow': + global.open('data:application/pdf;base64,' + btoa(buildDocument())); + break; + default: + throw new Error('Output type "' + type + '" is not supported.'); + } + // @TODO: Add different output options + }); switch (unit) { case 'pt': k = 1; break; @@ -1003,887 +802,905 @@ PubSub implementation throw ('Invalid unit: ' + unit); } - // Dimensions are stored as user units and converted to points on output - if (pageFormats.hasOwnProperty(format_as_string)) { - pageHeight = pageFormats[format_as_string][1] / k; - pageWidth = pageFormats[format_as_string][0] / k; - } else { - try { - pageHeight = format[1]; - pageWidth = format[0]; - } catch (err) { - throw ('Invalid format: ' + format); - } - } - - if (orientation === 'p' || orientation === 'portrait') { - orientation = 'p'; - if (pageWidth > pageHeight) { - tmp = pageWidth; - pageWidth = pageHeight; - pageHeight = tmp; - } - } else if (orientation === 'l' || orientation === 'landscape') { - orientation = 'l'; - if (pageHeight > pageWidth) { - tmp = pageWidth; - pageWidth = pageHeight; - pageHeight = tmp; - } - } else { - throw ('Invalid orientation: ' + orientation); - } - - - - //--------------------------------------- - // Public API - - /* - Object exposing internal API to plugins - @public - */ - API.internal = { - 'pdfEscape': pdfEscape, - 'getStyle': getStyle, - /** - Returns {FontObject} describing a particular font. - @public - @function - @param fontName {String} (Optional) Font's family name - @param fontStyle {String} (Optional) Font's style variation name (Example:"Italic") - @returns {FontObject} - */ - 'getFont': function () { return fonts[getFont.apply(API, arguments)]; }, - 'getFontSize': function () { return activeFontSize; }, - 'btoa': btoa, - 'write': function (string1, string2, string3, etc) { - out( - arguments.length === 1 ? string1 : Array.prototype.join.call(arguments, ' ') - ); - }, - 'getCoordinateString': function (value) { - return f2(value * k); - }, - 'getVerticalCoordinateString': function (value) { - return f2((pageHeight - value) * k); - }, - 'collections': {}, - 'newObject': newObject, - 'putStream': putStream, - 'events': events, - // ratio that you use in multiplication of a given "size" number to arrive to 'point' - // units of measurement. - // scaleFactor is set at initialization of the document and calculated against the stated - // default measurement units for the document. - // If default is "mm", k is the number that will turn number in 'mm' into 'points' number. - // through multiplication. - 'scaleFactor': k, - 'pageSize': {'width': pageWidth, 'height': pageHeight}, - 'output': function (type, options) { - return output(type, options); - } - }; - - /** - Adds (and transfers the focus to) new page to the PDF document. - @function - @returns {jsPDF} - - @methodOf jsPDF# - @name addPage - */ - API.addPage = function () { - _addPage(); - return this; - }; - - /** - Adds text to page. Supports adding multiline text when 'text' argument is an Array of Strings. - @function - @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. - @param {Number} x Coordinate (in units declared at inception of PDF document) against left edge of the page - @param {Number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page - @param {Object} flags Collection of settings signalling how the text must be encoded. Defaults are sane. If you think you want to pass some flags, you likely can read the source. - @returns {jsPDF} - @methodOf jsPDF# - @name text - */ - API.text = function (text, x, y, flags) { - /** - * Inserts something like this into PDF - BT - /F1 16 Tf % Font name + size - 16 TL % How many units down for next line in multiline text - 0 g % color - 28.35 813.54 Td % position - (line one) Tj - T* (line two) Tj - T* (line three) Tj - ET - */ - - var undef, _first, _second, _third, newtext, str, i; - // Pre-August-2012 the order of arguments was function(x, y, text, flags) - // in effort to make all calls have similar signature like - // function(data, coordinates... , miscellaneous) - // this method had its args flipped. - // code below allows backward compatibility with old arg order. - if (typeof text === 'number') { - _first = y; - _second = text; - _third = x; - - text = _first; - x = _second; - y = _third; - } - - // If there are any newlines in text, we assume - // the user wanted to print multiple lines, so break the - // text up into an array. If the text is already an array, - // we assume the user knows what they are doing. - if (typeof text === 'string' && text.match(/[\n\r]/)) { - text = text.split(/\r\n|\r|\n/g); - } - - if (typeof flags === 'undefined') { - flags = {'noBOM': true, 'autoencode': true}; - } else { - - if (flags.noBOM === undef) { - flags.noBOM = true; - } - - if (flags.autoencode === undef) { - flags.autoencode = true; - } - - } - - if (typeof text === 'string') { - str = pdfEscape(text, flags); - } else if (text instanceof Array) { /* Array */ - // we don't want to destroy original text array, so cloning it - newtext = text.concat(); - // we do array.join('text that must not be PDFescaped") - // thus, pdfEscape each component separately - for (i = newtext.length - 1; i !== -1; i--) { - newtext[i] = pdfEscape(newtext[i], flags); - } - str = newtext.join(") Tj\nT* ("); - } else { - throw new Error('Type of text must be string or Array. "' + text + '" is not recognized.'); - } - // Using "'" ("go next line and render text" mark) would save space but would complicate our rendering code, templates - - // BT .. ET does NOT have default settings for Tf. You must state that explicitely every time for BT .. ET - // if you want text transformation matrix (+ multiline) to work reliably (which reads sizes of things from font declarations) - // Thus, there is NO useful, *reliable* concept of "default" font for a page. - // The fact that "default" (reuse font used before) font worked before in basic cases is an accident - // - readers dealing smartly with brokenness of jsPDF's markup. - out( - 'BT\n/' + - activeFontKey + ' ' + activeFontSize + ' Tf\n' + // font face, style, size - activeFontSize + ' TL\n' + // line spacing - textColor + - '\n' + f2(x * k) + ' ' + f2((pageHeight - y) * k) + ' Td\n(' + - str + - ') Tj\nET' - ); - return this; - }; - - API.line = function (x1, y1, x2, y2) { - out( - f2(x1 * k) + ' ' + f2((pageHeight - y1) * k) + ' m ' + - f2(x2 * k) + ' ' + f2((pageHeight - y2) * k) + ' l S' - ); - return this; - }; - - /** - Adds series of curves (straight lines or cubic bezier curves) to canvas, starting at `x`, `y` coordinates. - All data points in `lines` are relative to last line origin. - `x`, `y` become x1,y1 for first line / curve in the set. - For lines you only need to specify [x2, y2] - (ending point) vector against x1, y1 starting point. - For bezier curves you need to specify [x2,y2,x3,y3,x4,y4] - vectors to control points 1, 2, ending point. All vectors are against the start of the curve - x1,y1. - - @example .lines([[2,2],[-2,2],[1,1,2,2,3,3],[2,1]], 212,110, 10) // line, line, bezier curve, line - @param {Array} lines Array of *vector* shifts as pairs (lines) or sextets (cubic bezier curves). - @param {Number} x Coordinate (in units declared at inception of PDF document) against left edge of the page - @param {Number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page - @param {Number} scale (Defaults to [1.0,1.0]) x,y Scaling factor for all vectors. Elements can be any floating number Sub-one makes drawing smaller. Over-one grows the drawing. Negative flips the direction. - @function - @returns {jsPDF} - @methodOf jsPDF# - @name lines - */ - API.lines = function (lines, x, y, scale, style) { - var undef, _first, _second, _third, scalex, scaley, i, l, leg, x2, y2, x3, y3, x4, y4; - - // Pre-August-2012 the order of arguments was function(x, y, lines, scale, style) - // in effort to make all calls have similar signature like - // function(content, coordinateX, coordinateY , miscellaneous) - // this method had its args flipped. - // code below allows backward compatibility with old arg order. - if (typeof lines === 'number') { - _first = y; - _second = lines; - _third = x; - - lines = _first; - x = _second; - y = _third; - } - - style = getStyle(style); - scale = scale === undef ? [1, 1] : scale; - - // starting point - out(f3(x * k) + ' ' + f3((pageHeight - y) * k) + ' m '); - - scalex = scale[0]; - scaley = scale[1]; - l = lines.length; - //, x2, y2 // bezier only. In page default measurement "units", *after* scaling - //, x3, y3 // bezier only. In page default measurement "units", *after* scaling - // ending point for all, lines and bezier. . In page default measurement "units", *after* scaling - x4 = x; // last / ending point = starting point for first item. - y4 = y; // last / ending point = starting point for first item. - - for (i = 0; i < l; i++) { - leg = lines[i]; - if (leg.length === 2) { - // simple line - x4 = leg[0] * scalex + x4; // here last x4 was prior ending point - y4 = leg[1] * scaley + y4; // here last y4 was prior ending point - out(f3(x4 * k) + ' ' + f3((pageHeight - y4) * k) + ' l'); - } else { - // bezier curve - x2 = leg[0] * scalex + x4; // here last x4 is prior ending point - y2 = leg[1] * scaley + y4; // here last y4 is prior ending point - x3 = leg[2] * scalex + x4; // here last x4 is prior ending point - y3 = leg[3] * scaley + y4; // here last y4 is prior ending point - x4 = leg[4] * scalex + x4; // here last x4 was prior ending point - y4 = leg[5] * scaley + y4; // here last y4 was prior ending point - out( - f3(x2 * k) + ' ' + - f3((pageHeight - y2) * k) + ' ' + - f3(x3 * k) + ' ' + - f3((pageHeight - y3) * k) + ' ' + - f3(x4 * k) + ' ' + - f3((pageHeight - y4) * k) + ' c' - ); - } - } - // stroking / filling / both the path - out(style); - return this; - }; - - /** - Adds a rectangle to PDF - - @param {Number} x Coordinate (in units declared at inception of PDF document) against left edge of the page - @param {Number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page - @param {Number} w Width (in units declared at inception of PDF document) - @param {Number} h Height (in units declared at inception of PDF document) - @param {String} style (Defaults to active fill/stroke style) A string signalling if stroke, fill or both are to be applied. - @function - @returns {jsPDF} - @methodOf jsPDF# - @name rect - */ - API.rect = function (x, y, w, h, style) { - var op = getStyle(style); - out([ - f2(x * k), - f2((pageHeight - y) * k), - f2(w * k), - f2(-h * k), - 're', - op - ].join(' ')); - return this; - }; - - /** - Adds a triangle to PDF - - @param {Number} x1 Coordinate (in units declared at inception of PDF document) against left edge of the page - @param {Number} y1 Coordinate (in units declared at inception of PDF document) against upper edge of the page - @param {Number} x2 Coordinate (in units declared at inception of PDF document) against left edge of the page - @param {Number} y2 Coordinate (in units declared at inception of PDF document) against upper edge of the page - @param {Number} x3 Coordinate (in units declared at inception of PDF document) against left edge of the page - @param {Number} y3 Coordinate (in units declared at inception of PDF document) against upper edge of the page - @param {String} style (Defaults to active fill/stroke style) A string signalling if stroke, fill or both are to be applied. - @function - @returns {jsPDF} - @methodOf jsPDF# - @name triangle - */ - API.triangle = function (x1, y1, x2, y2, x3, y3, style) { - this.lines( - [ - [ x2 - x1, y2 - y1 ], // vector to point 2 - [ x3 - x2, y3 - y2 ], // vector to point 3 - [ x1 - x3, y1 - y3 ] // closing vector back to point 1 - ], - x1, - y1, // start of path - [1, 1], - style - ); - return this; - }; - - /** - Adds a rectangle with rounded corners to PDF - - @param {Number} x Coordinate (in units declared at inception of PDF document) against left edge of the page - @param {Number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page - @param {Number} w Width (in units declared at inception of PDF document) - @param {Number} h Height (in units declared at inception of PDF document) - @param {Number} rx Radius along x axis (in units declared at inception of PDF document) - @param {Number} rx Radius along y axis (in units declared at inception of PDF document) - @param {String} style (Defaults to active fill/stroke style) A string signalling if stroke, fill or both are to be applied. - @function - @returns {jsPDF} - @methodOf jsPDF# - @name roundedRect - */ - API.roundedRect = function (x, y, w, h, rx, ry, style) { - var MyArc = 4 / 3 * (Math.SQRT2 - 1); - this.lines( - [ - [ (w - 2 * rx), 0 ], - [ (rx * MyArc), 0, rx, ry - (ry * MyArc), rx, ry ], - [ 0, (h - 2 * ry) ], - [ 0, (ry * MyArc), -(rx * MyArc), ry, -rx, ry], - [ (-w + 2 * rx), 0], - [ -(rx * MyArc), 0, -rx, -(ry * MyArc), -rx, -ry], - [ 0, (-h + 2 * ry)], - [ 0, -(ry * MyArc), (rx * MyArc), -ry, rx, -ry] - ], - x + rx, - y, // start of path - [1, 1], - style - ); - return this; - }; - - /** - Adds an ellipse to PDF - - @param {Number} x Coordinate (in units declared at inception of PDF document) against left edge of the page - @param {Number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page - @param {Number} rx Radius along x axis (in units declared at inception of PDF document) - @param {Number} rx Radius along y axis (in units declared at inception of PDF document) - @param {String} style (Defaults to active fill/stroke style) A string signalling if stroke, fill or both are to be applied. - @function - @returns {jsPDF} - @methodOf jsPDF# - @name ellipse - */ - API.ellipse = function (x, y, rx, ry, style) { - var op = getStyle(style), - lx = 4 / 3 * (Math.SQRT2 - 1) * rx, - ly = 4 / 3 * (Math.SQRT2 - 1) * ry; - - out([ - f2((x + rx) * k), - f2((pageHeight - y) * k), - 'm', - f2((x + rx) * k), - f2((pageHeight - (y - ly)) * k), - f2((x + lx) * k), - f2((pageHeight - (y - ry)) * k), - f2(x * k), - f2((pageHeight - (y - ry)) * k), - 'c' - ].join(' ')); - out([ - f2((x - lx) * k), - f2((pageHeight - (y - ry)) * k), - f2((x - rx) * k), - f2((pageHeight - (y - ly)) * k), - f2((x - rx) * k), - f2((pageHeight - y) * k), - 'c' - ].join(' ')); - out([ - f2((x - rx) * k), - f2((pageHeight - (y + ly)) * k), - f2((x - lx) * k), - f2((pageHeight - (y + ry)) * k), - f2(x * k), - f2((pageHeight - (y + ry)) * k), - 'c' - ].join(' ')); - out([ - f2((x + lx) * k), - f2((pageHeight - (y + ry)) * k), - f2((x + rx) * k), - f2((pageHeight - (y + ly)) * k), - f2((x + rx) * k), - f2((pageHeight - y) * k), - 'c', - op - ].join(' ')); - return this; - }; - - /** - Adds an circle to PDF - - @param {Number} x Coordinate (in units declared at inception of PDF document) against left edge of the page - @param {Number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page - @param {Number} r Radius (in units declared at inception of PDF document) - @param {String} style (Defaults to active fill/stroke style) A string signalling if stroke, fill or both are to be applied. - @function - @returns {jsPDF} - @methodOf jsPDF# - @name circle - */ - API.circle = function (x, y, r, style) { - return this.ellipse(x, y, r, r, style); - }; - - /** - Adds a properties to the PDF document - - @param {Object} A property_name-to-property_value object structure. - @function - @returns {jsPDF} - @methodOf jsPDF# - @name setProperties - */ - API.setProperties = function (properties) { - // copying only those properties we can render. - var property; - for (property in documentProperties) { - if (documentProperties.hasOwnProperty(property) && properties[property]) { - documentProperties[property] = properties[property]; - } - } - return this; - }; - - /** - Sets font size for upcoming text elements. - - @param {Number} size Font size in points. - @function - @returns {jsPDF} - @methodOf jsPDF# - @name setFontSize - */ - API.setFontSize = function (size) { - activeFontSize = size; - return this; - }; - - /** - Sets text font face, variant for upcoming text elements. - See output of jsPDF.getFontList() for possible font names, styles. - - @param {String} fontName Font name or family. Example: "times" - @param {String} fontStyle Font style or variant. Example: "italic" - @function - @returns {jsPDF} - @methodOf jsPDF# - @name setFont - */ - API.setFont = function (fontName, fontStyle) { - activeFontKey = getFont(fontName, fontStyle); - // if font is not found, the above line blows up and we never go further - return this; - }; - - /** - Switches font style or variant for upcoming text elements, - while keeping the font face or family same. - See output of jsPDF.getFontList() for possible font names, styles. - - @param {String} style Font style or variant. Example: "italic" - @function - @returns {jsPDF} - @methodOf jsPDF# - @name setFontStyle - */ - API.setFontStyle = API.setFontType = function (style) { - var undef; - activeFontKey = getFont(undef, style); - // if font is not found, the above line blows up and we never go further - return this; - }; - - /** - Returns an object - a tree of fontName to fontStyle relationships available to - active PDF document. - - @public - @function - @returns {Object} Like {'times':['normal', 'italic', ... ], 'arial':['normal', 'bold', ... ], ... } - @methodOf jsPDF# - @name getFontList - */ - API.getFontList = function () { - // TODO: iterate over fonts array or return copy of fontmap instead in case more are ever added. - var list = {}, - fontName, - fontStyle, - tmp; - - for (fontName in fontmap) { - if (fontmap.hasOwnProperty(fontName)) { - list[fontName] = tmp = []; - for (fontStyle in fontmap[fontName]) { - if (fontmap[fontName].hasOwnProperty(fontStyle)) { - tmp.push(fontStyle); - } - } - } - } - - return list; - }; - - /** - Sets line width for upcoming lines. - - @param {Number} width Line width (in units declared at inception of PDF document) - @function - @returns {jsPDF} - @methodOf jsPDF# - @name setLineWidth - */ - API.setLineWidth = function (width) { - out((width * k).toFixed(2) + ' w'); - return this; - }; - - /** - Sets the stroke color for upcoming elements. - - Depending on the number of arguments given, Gray, RGB, or CMYK - color space is implied. - - When only ch1 is given, "Gray" color space is implied and it - must be a value in the range from 0.00 (solid black) to to 1.00 (white) - if values are communicated as String types, or in range from 0 (black) - to 255 (white) if communicated as Number type. - The RGB-like 0-255 range is provided for backward compatibility. - - When only ch1,ch2,ch3 are given, "RGB" color space is implied and each - value must be in the range from 0.00 (minimum intensity) to to 1.00 - (max intensity) if values are communicated as String types, or - from 0 (min intensity) to to 255 (max intensity) if values are communicated - as Number types. - The RGB-like 0-255 range is provided for backward compatibility. - - When ch1,ch2,ch3,ch4 are given, "CMYK" color space is implied and each - value must be a in the range from 0.00 (0% concentration) to to - 1.00 (100% concentration) - - Because JavaScript treats fixed point numbers badly (rounds to - floating point nearest to binary representation) it is highly advised to - communicate the fractional numbers as String types, not JavaScript Number type. - - @param {Number|String} ch1 Color channel value - @param {Number|String} ch2 Color channel value - @param {Number|String} ch3 Color channel value - @param {Number|String} ch4 Color channel value - - @function - @returns {jsPDF} - @methodOf jsPDF# - @name setDrawColor - */ - API.setDrawColor = function (ch1, ch2, ch3, ch4) { - var color; - if (ch2 === undefined || (ch4 === undefined && ch1 === ch2 === ch3)) { - // Gray color space. - if (typeof ch1 === 'string') { - color = ch1 + ' G'; - } else { - color = f2(ch1 / 255) + ' G'; - } - } else if (ch4 === undefined) { - // RGB - if (typeof ch1 === 'string') { - color = [ch1, ch2, ch3, 'RG'].join(' '); - } else { - color = [f2(ch1 / 255), f2(ch2 / 255), f2(ch3 / 255), 'RG'].join(' '); - } - } else { - // CMYK - if (typeof ch1 === 'string') { - color = [ch1, ch2, ch3, ch4, 'K'].join(' '); - } else { - color = [f2(ch1), f2(ch2), f2(ch3), f2(ch4), 'K'].join(' '); - } - } - - out(color); - return this; - }; - - /** - Sets the fill color for upcoming elements. - - Depending on the number of arguments given, Gray, RGB, or CMYK - color space is implied. - - When only ch1 is given, "Gray" color space is implied and it - must be a value in the range from 0.00 (solid black) to to 1.00 (white) - if values are communicated as String types, or in range from 0 (black) - to 255 (white) if communicated as Number type. - The RGB-like 0-255 range is provided for backward compatibility. - - When only ch1,ch2,ch3 are given, "RGB" color space is implied and each - value must be in the range from 0.00 (minimum intensity) to to 1.00 - (max intensity) if values are communicated as String types, or - from 0 (min intensity) to to 255 (max intensity) if values are communicated - as Number types. - The RGB-like 0-255 range is provided for backward compatibility. - - When ch1,ch2,ch3,ch4 are given, "CMYK" color space is implied and each - value must be a in the range from 0.00 (0% concentration) to to - 1.00 (100% concentration) - - Because JavaScript treats fixed point numbers badly (rounds to - floating point nearest to binary representation) it is highly advised to - communicate the fractional numbers as String types, not JavaScript Number type. - - @param {Number|String} ch1 Color channel value - @param {Number|String} ch2 Color channel value - @param {Number|String} ch3 Color channel value - @param {Number|String} ch4 Color channel value - - @function - @returns {jsPDF} - @methodOf jsPDF# - @name setFillColor - */ - API.setFillColor = function (ch1, ch2, ch3, ch4) { - var color; - - if (ch2 === undefined || (ch4 === undefined && ch1 === ch2 === ch3)) { - // Gray color space. - if (typeof ch1 === 'string') { - color = ch1 + ' g'; - } else { - color = f2(ch1 / 255) + ' g'; - } - } else if (ch4 === undefined) { - // RGB - if (typeof ch1 === 'string') { - color = [ch1, ch2, ch3, 'rg'].join(' '); - } else { - color = [f2(ch1 / 255), f2(ch2 / 255), f2(ch3 / 255), 'rg'].join(' '); - } - } else { - // CMYK - if (typeof ch1 === 'string') { - color = [ch1, ch2, ch3, ch4, 'k'].join(' '); - } else { - color = [f2(ch1), f2(ch2), f2(ch3), f2(ch4), 'k'].join(' '); - } - } - - out(color); - return this; - }; - - /** - Sets the text color for upcoming elements. - If only one, first argument is given, - treats the value as gray-scale color value. - - @param {Number} r Red channel color value in range 0-255 - @param {Number} g Green channel color value in range 0-255 - @param {Number} b Blue channel color value in range 0-255 - @function - @returns {jsPDF} - @methodOf jsPDF# - @name setTextColor - */ - API.setTextColor = function (r, g, b) { - if ((r === 0 && g === 0 && b === 0) || (typeof g === 'undefined')) { - textColor = f3(r / 255) + ' g'; - } else { - textColor = [f3(r / 255), f3(g / 255), f3(b / 255), 'rg'].join(' '); - } - return this; - }; - - /** - Is an Object providing a mapping from human-readable to - integer flag values designating the varieties of line cap - and join styles. - - @returns {Object} - @fieldOf jsPDF# - @name CapJoinStyles - */ - API.CapJoinStyles = { - 0: 0, - 'butt': 0, - 'but': 0, - 'bevel': 0, - 1: 1, - 'round': 1, - 'rounded': 1, - 'circle': 1, - 2: 2, - 'projecting': 2, - 'project': 2, - 'square': 2, - 'milter': 2 - }; - - /** - Sets the line cap styles - See {jsPDF.CapJoinStyles} for variants - - @param {String|Number} style A string or number identifying the type of line cap - @function - @returns {jsPDF} - @methodOf jsPDF# - @name setLineCap - */ - API.setLineCap = function (style) { - var id = this.CapJoinStyles[style]; - if (id === undefined) { - throw new Error("Line cap style of '" + style + "' is not recognized. See or extend .CapJoinStyles property for valid styles"); - } - lineCapID = id; - out(id.toString(10) + ' J'); - - return this; - }; - - /** - Sets the line join styles - See {jsPDF.CapJoinStyles} for variants - - @param {String|Number} style A string or number identifying the type of line join - @function - @returns {jsPDF} - @methodOf jsPDF# - @name setLineJoin - */ - API.setLineJoin = function (style) { - var id = this.CapJoinStyles[style]; - if (id === undefined) { - throw new Error("Line join style of '" + style + "' is not recognized. See or extend .CapJoinStyles property for valid styles"); - } - lineJoinID = id; - out(id.toString(10) + ' j'); - - return this; - }; - - // Output is both an internal (for plugins) and external function - API.output = output; - - /** - * Saves as PDF document. An alias of jsPDF.output('save', 'filename.pdf') - * @param {String} filename The filename including extension. - * - * @function - * @returns {jsPDF} - * @methodOf jsPDF# - * @name save - */ - API.save = function (filename) { - API.output('save', filename); - }; - - // applying plugins (more methods) ON TOP of built-in API. - // this is intentional as we allow plugins to override - // built-ins - for (plugin in jsPDF.API) { - if (jsPDF.API.hasOwnProperty(plugin)) { - if (plugin === 'events' && jsPDF.API.events.length) { - (function (events, newEvents) { - - // jsPDF.API.events is a JS Array of Arrays - // where each Array is a pair of event name, handler - // Events were added by plugins to the jsPDF instantiator. - // These are always added to the new instance and some ran - // during instantiation. - - var eventname, handler_and_args, i; - - for (i = newEvents.length - 1; i !== -1; i--) { - // subscribe takes 3 args: 'topic', function, runonce_flag - // if undefined, runonce is false. - // users can attach callback directly, - // or they can attach an array with [callback, runonce_flag] - // that's what the "apply" magic is for below. - eventname = newEvents[i][0]; - handler_and_args = newEvents[i][1]; - events.subscribe.apply( - events, - [eventname].concat( - typeof handler_and_args === 'function' ? - [ handler_and_args ] : - handler_and_args - ) - ); - } - }(events, jsPDF.API.events)); - } else { - API[plugin] = jsPDF.API[plugin]; - } - } - } - - ///////////////////////////////////////// - // continuing initilisation of jsPDF Document object - ///////////////////////////////////////// - - - // Add the first page automatically - addFonts(); - activeFontKey = 'F1'; - _addPage(); - - events.publish('initialized'); - - return API; - } - -/** -jsPDF.API is a STATIC property of jsPDF class. -jsPDF.API is an object you can add methods and properties to. -The methods / properties you add will show up in new jsPDF objects. - -One property is prepopulated. It is the 'events' Object. Plugin authors can add topics, callbacks to this object. These will be reassigned to all new instances of jsPDF. -Examples: - jsPDF.API.events['initialized'] = function(){ 'this' is API object } - jsPDF.API.events['addFont'] = function(added_font_object){ 'this' is API object } - -@static -@public -@memberOf jsPDF -@name API - -@example - jsPDF.API.mymethod = function(){ - // 'this' will be ref to internal API object. see jsPDF source - // , so you can refer to built-in methods like so: - // this.line(....) - // this.text(....) - } - var pdfdoc = new jsPDF() - pdfdoc.mymethod() // <- !!!!!! -*/ - jsPDF.API = {'events': []}; - - return jsPDF; -}()); + // Dimensions are stored as user units and converted to points on output + if (pageFormats.hasOwnProperty(format_as_string)) { + pageHeight = pageFormats[format_as_string][1] / k; + pageWidth = pageFormats[format_as_string][0] / k; + } else { + try { + pageHeight = format[1]; + pageWidth = format[0]; + } catch (err) { + throw new Error('Invalid format: ' + format); + } + } + + if (orientation === 'p' || orientation === 'portrait') { + orientation = 'p'; + if (pageWidth > pageHeight) { + tmp = pageWidth; + pageWidth = pageHeight; + pageHeight = tmp; + } + } else if (orientation === 'l' || orientation === 'landscape') { + orientation = 'l'; + if (pageHeight > pageWidth) { + tmp = pageWidth; + pageWidth = pageHeight; + pageHeight = tmp; + } + } else { + throw('Invalid orientation: ' + orientation); + } + + //--------------------------------------- + // Public API + + /** + * Object exposing internal API to plugins + * @public + */ + API.internal = { + 'pdfEscape' : pdfEscape, + 'getStyle' : getStyle, + /** + * Returns {FontObject} describing a particular font. + * @public + * @function + * @param fontName {String} (Optional) Font's family name + * @param fontStyle {String} (Optional) Font's style variation name (Example:"Italic") + * @returns {FontObject} + */ + 'getFont' : function() { + return fonts[getFont.apply(API, arguments)]; + }, + 'getFontSize' : function() { + return activeFontSize; + }, + 'getLineHeight' : function() { + return activeFontSize * lineHeightProportion; + }, + 'write' : function(string1 /*, string2, string3, etc */) { + out(arguments.length === 1 ? string1 : Array.prototype.join.call(arguments, ' ')); + }, + 'getCoordinateString' : function(value) { + return f2(value * k); + }, + 'getVerticalCoordinateString' : function(value) { + return f2((pageHeight - value) * k); + }, + 'collections' : {}, + 'newObject' : newObject, + 'putStream' : putStream, + 'events' : events, + // ratio that you use in multiplication of a given "size" number to arrive to 'point' + // units of measurement. + // scaleFactor is set at initialization of the document and calculated against the stated + // default measurement units for the document. + // If default is "mm", k is the number that will turn number in 'mm' into 'points' number. + // through multiplication. + 'scaleFactor' : k, + 'pageSize' : { + 'width' : pageWidth, + 'height' : pageHeight + }, + 'output' : function(type, options) { + return output(type, options); + }, + 'getNumberOfPages' : function() { + return pages.length - 1; + }, + 'pages' : pages + }; + + /** + * Adds (and transfers the focus to) new page to the PDF document. + * @function + * @returns {jsPDF} + * + * @methodOf jsPDF# + * @name addPage + */ + API.addPage = function() { + _addPage(); + return this; + }; + + /** + * Adds text to page. Supports adding multiline text when 'text' argument is an Array of Strings. + * + * @function + * @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. + * @param {Number} x Coordinate (in units declared at inception of PDF document) against left edge of the page + * @param {Number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page + * @param {Object} flags Collection of settings signalling how the text must be encoded. Defaults are sane. If you think you want to pass some flags, you likely can read the source. + * @returns {jsPDF} + * @methodOf jsPDF# + * @name text + */ + API.text = function(text, x, y, flags, angle) { + /** + * Inserts something like this into PDF + * BT + * /F1 16 Tf % Font name + size + * 16 TL % How many units down for next line in multiline text + * 0 g % color + * 28.35 813.54 Td % position + * (line one) Tj + * T* (line two) Tj + * T* (line three) Tj + * ET + */ + + // Pre-August-2012 the order of arguments was function(x, y, text, flags) + // in effort to make all calls have similar signature like + // function(data, coordinates... , miscellaneous) + // this method had its args flipped. + // code below allows backward compatibility with old arg order. + if (typeof text === 'number') { + tmp = y; + y = x; + x = text; + text = tmp; + } + + // If there are any newlines in text, we assume + // the user wanted to print multiple lines, so break the + // text up into an array. If the text is already an array, + // we assume the user knows what they are doing. + if (typeof text === 'string' && text.match(/[\n\r]/)) { + text = text.split(/\r\n|\r|\n/g); + } + if (typeof flags === 'number') { + angle = flags; + flags = null; + } + var xtra = '',mode = 'Td'; + if (angle) { + angle *= (Math.PI / 180); + var c = Math.cos(angle), + s = Math.sin(angle); + xtra = [f2(c), f2(s), f2(s * -1), f2(c), ''].join(" "); + mode = 'Tm'; + } + flags = flags || {}; + if (!('noBOM' in flags)) + flags.noBOM = true; + if (!('autoencode' in flags)) + flags.autoencode = true; + + if (typeof text === 'string') { + text = pdfEscape(text, flags); + } else if (text instanceof Array) { + // we don't want to destroy original text array, so cloning it + var sa = text.concat(), da = [], len = sa.length; + // we do array.join('text that must not be PDFescaped") + // thus, pdfEscape each component separately + while (len--) { + da.push(pdfEscape(sa.shift(), flags)); + } + text = da.join(") Tj\nT* ("); + } else { + throw new Error('Type of text must be string or Array. "' + text + '" is not recognized.'); + } + // Using "'" ("go next line and render text" mark) would save space but would complicate our rendering code, templates + + // BT .. ET does NOT have default settings for Tf. You must state that explicitely every time for BT .. ET + // if you want text transformation matrix (+ multiline) to work reliably (which reads sizes of things from font declarations) + // Thus, there is NO useful, *reliable* concept of "default" font for a page. + // The fact that "default" (reuse font used before) font worked before in basic cases is an accident + // - readers dealing smartly with brokenness of jsPDF's markup. + out( + 'BT\n/' + + activeFontKey + ' ' + activeFontSize + ' Tf\n' + // font face, style, size + (activeFontSize * lineHeightProportion) + ' TL\n' + // line spacing + textColor + + '\n' + xtra + f2(x * k) + ' ' + f2((pageHeight - y) * k) + ' ' + mode + '\n(' + + text + + ') Tj\nET'); + return this; + }; + + API.line = function(x1, y1, x2, y2) { + return this.lines([[x2 - x1, y2 - y1]], x1, y1); + }; + + /** + * Adds series of curves (straight lines or cubic bezier curves) to canvas, starting at `x`, `y` coordinates. + * All data points in `lines` are relative to last line origin. + * `x`, `y` become x1,y1 for first line / curve in the set. + * For lines you only need to specify [x2, y2] - (ending point) vector against x1, y1 starting point. + * For bezier curves you need to specify [x2,y2,x3,y3,x4,y4] - vectors to control points 1, 2, ending point. All vectors are against the start of the curve - x1,y1. + * + * @example .lines([[2,2],[-2,2],[1,1,2,2,3,3],[2,1]], 212,110, 10) // line, line, bezier curve, line + * @param {Array} lines Array of *vector* shifts as pairs (lines) or sextets (cubic bezier curves). + * @param {Number} x Coordinate (in units declared at inception of PDF document) against left edge of the page + * @param {Number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page + * @param {Number} scale (Defaults to [1.0,1.0]) x,y Scaling factor for all vectors. Elements can be any floating number Sub-one makes drawing smaller. Over-one grows the drawing. Negative flips the direction. + * @param {String} style A string specifying the painting style or null. Valid styles include: 'S' [default] - stroke, 'F' - fill, and 'DF' (or 'FD') - fill then stroke. A null value postpones setting the style so that a shape may be composed using multiple method calls. The last drawing method call used to define the shape should not have a null style argument. + * @param {Boolean} closed If true, the path is closed with a straight line from the end of the last curve to the starting point. + * @function + * @returns {jsPDF} + * @methodOf jsPDF# + * @name lines + */ + API.lines = function(lines, x, y, scale, style, closed) { + var scalex,scaley,i,l,leg,x2,y2,x3,y3,x4,y4; + + // Pre-August-2012 the order of arguments was function(x, y, lines, scale, style) + // in effort to make all calls have similar signature like + // function(content, coordinateX, coordinateY , miscellaneous) + // this method had its args flipped. + // code below allows backward compatibility with old arg order. + if (typeof lines === 'number') { + tmp = y; + y = x; + x = lines; + lines = tmp; + } + + scale = scale || [1, 1]; + + // starting point + out(f3(x * k) + ' ' + f3((pageHeight - y) * k) + ' m '); + + scalex = scale[0]; + scaley = scale[1]; + l = lines.length; + //, x2, y2 // bezier only. In page default measurement "units", *after* scaling + //, x3, y3 // bezier only. In page default measurement "units", *after* scaling + // ending point for all, lines and bezier. . In page default measurement "units", *after* scaling + x4 = x; // last / ending point = starting point for first item. + y4 = y; // last / ending point = starting point for first item. + + for (i = 0; i < l; i++) { + leg = lines[i]; + if (leg.length === 2) { + // simple line + x4 = leg[0] * scalex + x4; // here last x4 was prior ending point + y4 = leg[1] * scaley + y4; // here last y4 was prior ending point + out(f3(x4 * k) + ' ' + f3((pageHeight - y4) * k) + ' l'); + } else { + // bezier curve + x2 = leg[0] * scalex + x4; // here last x4 is prior ending point + y2 = leg[1] * scaley + y4; // here last y4 is prior ending point + x3 = leg[2] * scalex + x4; // here last x4 is prior ending point + y3 = leg[3] * scaley + y4; // here last y4 is prior ending point + x4 = leg[4] * scalex + x4; // here last x4 was prior ending point + y4 = leg[5] * scaley + y4; // here last y4 was prior ending point + out( + f3(x2 * k) + ' ' + + f3((pageHeight - y2) * k) + ' ' + + f3(x3 * k) + ' ' + + f3((pageHeight - y3) * k) + ' ' + + f3(x4 * k) + ' ' + + f3((pageHeight - y4) * k) + ' c'); + } + } + + if (closed) { + out(' h'); + } + + // stroking / filling / both the path + if (style !== null) { + out(getStyle(style)); + } + return this; + }; + + /** + * Adds a rectangle to PDF + * + * @param {Number} x Coordinate (in units declared at inception of PDF document) against left edge of the page + * @param {Number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page + * @param {Number} w Width (in units declared at inception of PDF document) + * @param {Number} h Height (in units declared at inception of PDF document) + * @param {String} style A string specifying the painting style or null. Valid styles include: 'S' [default] - stroke, 'F' - fill, and 'DF' (or 'FD') - fill then stroke. A null value postpones setting the style so that a shape may be composed using multiple method calls. The last drawing method call used to define the shape should not have a null style argument. + * @function + * @returns {jsPDF} + * @methodOf jsPDF# + * @name rect + */ + API.rect = function(x, y, w, h, style) { + var op = getStyle(style); + out([ + f2(x * k), + f2((pageHeight - y) * k), + f2(w * k), + f2(-h * k), + 're' + ].join(' ')); + + if (style !== null) { + out(getStyle(style)); + } + + return this; + }; + + /** + * Adds a triangle to PDF + * + * @param {Number} x1 Coordinate (in units declared at inception of PDF document) against left edge of the page + * @param {Number} y1 Coordinate (in units declared at inception of PDF document) against upper edge of the page + * @param {Number} x2 Coordinate (in units declared at inception of PDF document) against left edge of the page + * @param {Number} y2 Coordinate (in units declared at inception of PDF document) against upper edge of the page + * @param {Number} x3 Coordinate (in units declared at inception of PDF document) against left edge of the page + * @param {Number} y3 Coordinate (in units declared at inception of PDF document) against upper edge of the page + * @param {String} style A string specifying the painting style or null. Valid styles include: 'S' [default] - stroke, 'F' - fill, and 'DF' (or 'FD') - fill then stroke. A null value postpones setting the style so that a shape may be composed using multiple method calls. The last drawing method call used to define the shape should not have a null style argument. + * @function + * @returns {jsPDF} + * @methodOf jsPDF# + * @name triangle + */ + API.triangle = function(x1, y1, x2, y2, x3, y3, style) { + this.lines( + [ + [x2 - x1, y2 - y1], // vector to point 2 + [x3 - x2, y3 - y2], // vector to point 3 + [x1 - x3, y1 - y3]// closing vector back to point 1 + ], + x1, + y1, // start of path + [1, 1], + style, + true); + return this; + }; + + /** + * Adds a rectangle with rounded corners to PDF + * + * @param {Number} x Coordinate (in units declared at inception of PDF document) against left edge of the page + * @param {Number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page + * @param {Number} w Width (in units declared at inception of PDF document) + * @param {Number} h Height (in units declared at inception of PDF document) + * @param {Number} rx Radius along x axis (in units declared at inception of PDF document) + * @param {Number} rx Radius along y axis (in units declared at inception of PDF document) + * @param {String} style A string specifying the painting style or null. Valid styles include: 'S' [default] - stroke, 'F' - fill, and 'DF' (or 'FD') - fill then stroke. A null value postpones setting the style so that a shape may be composed using multiple method calls. The last drawing method call used to define the shape should not have a null style argument. + * @function + * @returns {jsPDF} + * @methodOf jsPDF# + * @name roundedRect + */ + API.roundedRect = function(x, y, w, h, rx, ry, style) { + var MyArc = 4 / 3 * (Math.SQRT2 - 1); + this.lines( + [ + [(w - 2 * rx), 0], + [(rx * MyArc), 0, rx, ry - (ry * MyArc), rx, ry], + [0, (h - 2 * ry)], + [0, (ry * MyArc), - (rx * MyArc), ry, -rx, ry], + [(-w + 2 * rx), 0], + [ - (rx * MyArc), 0, -rx, - (ry * MyArc), -rx, -ry], + [0, (-h + 2 * ry)], + [0, - (ry * MyArc), (rx * MyArc), -ry, rx, -ry] + ], + x + rx, + y, // start of path + [1, 1], + style); + return this; + }; + + /** + * Adds an ellipse to PDF + * + * @param {Number} x Coordinate (in units declared at inception of PDF document) against left edge of the page + * @param {Number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page + * @param {Number} rx Radius along x axis (in units declared at inception of PDF document) + * @param {Number} rx Radius along y axis (in units declared at inception of PDF document) + * @param {String} style A string specifying the painting style or null. Valid styles include: 'S' [default] - stroke, 'F' - fill, and 'DF' (or 'FD') - fill then stroke. A null value postpones setting the style so that a shape may be composed using multiple method calls. The last drawing method call used to define the shape should not have a null style argument. + * @function + * @returns {jsPDF} + * @methodOf jsPDF# + * @name ellipse + */ + API.ellipse = function(x, y, rx, ry, style) { + var lx = 4 / 3 * (Math.SQRT2 - 1) * rx, + ly = 4 / 3 * (Math.SQRT2 - 1) * ry; + + out([ + f2((x + rx) * k), + f2((pageHeight - y) * k), + 'm', + f2((x + rx) * k), + f2((pageHeight - (y - ly)) * k), + f2((x + lx) * k), + f2((pageHeight - (y - ry)) * k), + f2(x * k), + f2((pageHeight - (y - ry)) * k), + 'c' + ].join(' ')); + out([ + f2((x - lx) * k), + f2((pageHeight - (y - ry)) * k), + f2((x - rx) * k), + f2((pageHeight - (y - ly)) * k), + f2((x - rx) * k), + f2((pageHeight - y) * k), + 'c' + ].join(' ')); + out([ + f2((x - rx) * k), + f2((pageHeight - (y + ly)) * k), + f2((x - lx) * k), + f2((pageHeight - (y + ry)) * k), + f2(x * k), + f2((pageHeight - (y + ry)) * k), + 'c' + ].join(' ')); + out([ + f2((x + lx) * k), + f2((pageHeight - (y + ry)) * k), + f2((x + rx) * k), + f2((pageHeight - (y + ly)) * k), + f2((x + rx) * k), + f2((pageHeight - y) * k), + 'c' + ].join(' ')); + + if (style !== null) { + out(getStyle(style)); + } + + return this; + }; + + /** + * Adds an circle to PDF + * + * @param {Number} x Coordinate (in units declared at inception of PDF document) against left edge of the page + * @param {Number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page + * @param {Number} r Radius (in units declared at inception of PDF document) + * @param {String} style A string specifying the painting style or null. Valid styles include: 'S' [default] - stroke, 'F' - fill, and 'DF' (or 'FD') - fill then stroke. A null value postpones setting the style so that a shape may be composed using multiple method calls. The last drawing method call used to define the shape should not have a null style argument. + * @function + * @returns {jsPDF} + * @methodOf jsPDF# + * @name circle + */ + API.circle = function(x, y, r, style) { + return this.ellipse(x, y, r, r, style); + }; + + /** + * Adds a properties to the PDF document + * + * @param {Object} A property_name-to-property_value object structure. + * @function + * @returns {jsPDF} + * @methodOf jsPDF# + * @name setProperties + */ + API.setProperties = function(properties) { + // copying only those properties we can render. + for (var property in documentProperties) { + if (documentProperties.hasOwnProperty(property) && properties[property]) { + documentProperties[property] = properties[property]; + } + } + return this; + }; + + /** + * Sets font size for upcoming text elements. + * + * @param {Number} size Font size in points. + * @function + * @returns {jsPDF} + * @methodOf jsPDF# + * @name setFontSize + */ + API.setFontSize = function(size) { + activeFontSize = size; + return this; + }; + + /** + * Sets text font face, variant for upcoming text elements. + * See output of jsPDF.getFontList() for possible font names, styles. + * + * @param {String} fontName Font name or family. Example: "times" + * @param {String} fontStyle Font style or variant. Example: "italic" + * @function + * @returns {jsPDF} + * @methodOf jsPDF# + * @name setFont + */ + API.setFont = function(fontName, fontStyle) { + activeFontKey = getFont(fontName, fontStyle); + // if font is not found, the above line blows up and we never go further + return this; + }; + + /** + * Switches font style or variant for upcoming text elements, + * while keeping the font face or family same. + * See output of jsPDF.getFontList() for possible font names, styles. + * + * @param {String} style Font style or variant. Example: "italic" + * @function + * @returns {jsPDF} + * @methodOf jsPDF# + * @name setFontStyle + */ + API.setFontStyle = API.setFontType = function(style) { + activeFontKey = getFont(undefined, style); + // if font is not found, the above line blows up and we never go further + return this; + }; + + /** + * Returns an object - a tree of fontName to fontStyle relationships available to + * active PDF document. + * + * @public + * @function + * @returns {Object} Like {'times':['normal', 'italic', ... ], 'arial':['normal', 'bold', ... ], ... } + * @methodOf jsPDF# + * @name getFontList + */ + API.getFontList = function() { + // TODO: iterate over fonts array or return copy of fontmap instead in case more are ever added. + var list = {},fontName,fontStyle,tmp; + + for (fontName in fontmap) { + if (fontmap.hasOwnProperty(fontName)) { + list[fontName] = tmp = []; + for (fontStyle in fontmap[fontName]) { + if (fontmap[fontName].hasOwnProperty(fontStyle)) { + tmp.push(fontStyle); + } + } + } + } + + return list; + }; + + /** + * Sets line width for upcoming lines. + * + * @param {Number} width Line width (in units declared at inception of PDF document) + * @function + * @returns {jsPDF} + * @methodOf jsPDF# + * @name setLineWidth + */ + API.setLineWidth = function(width) { + out((width * k).toFixed(2) + ' w'); + return this; + }; + + /** + * Sets the stroke color for upcoming elements. + * + * Depending on the number of arguments given, Gray, RGB, or CMYK + * color space is implied. + * + * When only ch1 is given, "Gray" color space is implied and it + * must be a value in the range from 0.00 (solid black) to to 1.00 (white) + * if values are communicated as String types, or in range from 0 (black) + * to 255 (white) if communicated as Number type. + * The RGB-like 0-255 range is provided for backward compatibility. + * + * When only ch1,ch2,ch3 are given, "RGB" color space is implied and each + * value must be in the range from 0.00 (minimum intensity) to to 1.00 + * (max intensity) if values are communicated as String types, or + * from 0 (min intensity) to to 255 (max intensity) if values are communicated + * as Number types. + * The RGB-like 0-255 range is provided for backward compatibility. + * + * When ch1,ch2,ch3,ch4 are given, "CMYK" color space is implied and each + * value must be a in the range from 0.00 (0% concentration) to to + * 1.00 (100% concentration) + * + * Because JavaScript treats fixed point numbers badly (rounds to + * floating point nearest to binary representation) it is highly advised to + * communicate the fractional numbers as String types, not JavaScript Number type. + * + * @param {Number|String} ch1 Color channel value + * @param {Number|String} ch2 Color channel value + * @param {Number|String} ch3 Color channel value + * @param {Number|String} ch4 Color channel value + * + * @function + * @returns {jsPDF} + * @methodOf jsPDF# + * @name setDrawColor + */ + API.setDrawColor = function(ch1, ch2, ch3, ch4) { + var color; + if (ch2 === undefined || (ch4 === undefined && ch1 === ch2 === ch3)) { + // Gray color space. + if (typeof ch1 === 'string') { + color = ch1 + ' G'; + } else { + color = f2(ch1 / 255) + ' G'; + } + } else if (ch4 === undefined) { + // RGB + if (typeof ch1 === 'string') { + color = [ch1, ch2, ch3, 'RG'].join(' '); + } else { + color = [f2(ch1 / 255), f2(ch2 / 255), f2(ch3 / 255), 'RG'].join(' '); + } + } else { + // CMYK + if (typeof ch1 === 'string') { + color = [ch1, ch2, ch3, ch4, 'K'].join(' '); + } else { + color = [f2(ch1), f2(ch2), f2(ch3), f2(ch4), 'K'].join(' '); + } + } + + out(color); + return this; + }; + + /** + * Sets the fill color for upcoming elements. + * + * Depending on the number of arguments given, Gray, RGB, or CMYK + * color space is implied. + * + * When only ch1 is given, "Gray" color space is implied and it + * must be a value in the range from 0.00 (solid black) to to 1.00 (white) + * if values are communicated as String types, or in range from 0 (black) + * to 255 (white) if communicated as Number type. + * The RGB-like 0-255 range is provided for backward compatibility. + * + * When only ch1,ch2,ch3 are given, "RGB" color space is implied and each + * value must be in the range from 0.00 (minimum intensity) to to 1.00 + * (max intensity) if values are communicated as String types, or + * from 0 (min intensity) to to 255 (max intensity) if values are communicated + * as Number types. + * The RGB-like 0-255 range is provided for backward compatibility. + * + * When ch1,ch2,ch3,ch4 are given, "CMYK" color space is implied and each + * value must be a in the range from 0.00 (0% concentration) to to + * 1.00 (100% concentration) + * + * Because JavaScript treats fixed point numbers badly (rounds to + * floating point nearest to binary representation) it is highly advised to + * communicate the fractional numbers as String types, not JavaScript Number type. + * + * @param {Number|String} ch1 Color channel value + * @param {Number|String} ch2 Color channel value + * @param {Number|String} ch3 Color channel value + * @param {Number|String} ch4 Color channel value + * + * @function + * @returns {jsPDF} + * @methodOf jsPDF# + * @name setFillColor + */ + API.setFillColor = function(ch1, ch2, ch3, ch4) { + var color; + + if (ch2 === undefined || (ch4 === undefined && ch1 === ch2 === ch3)) { + // Gray color space. + if (typeof ch1 === 'string') { + color = ch1 + ' g'; + } else { + color = f2(ch1 / 255) + ' g'; + } + } else if (ch4 === undefined) { + // RGB + if (typeof ch1 === 'string') { + color = [ch1, ch2, ch3, 'rg'].join(' '); + } else { + color = [f2(ch1 / 255), f2(ch2 / 255), f2(ch3 / 255), 'rg'].join(' '); + } + } else { + // CMYK + if (typeof ch1 === 'string') { + color = [ch1, ch2, ch3, ch4, 'k'].join(' '); + } else { + color = [f2(ch1), f2(ch2), f2(ch3), f2(ch4), 'k'].join(' '); + } + } + + out(color); + return this; + }; + + /** + * Sets the text color for upcoming elements. + * If only one, first argument is given, + * treats the value as gray-scale color value. + * + * @param {Number} r Red channel color value in range 0-255 or {String} r color value in hexadecimal, example: '#FFFFFF' + * @param {Number} g Green channel color value in range 0-255 + * @param {Number} b Blue channel color value in range 0-255 + * @function + * @returns {jsPDF} + * @methodOf jsPDF# + * @name setTextColor + */ + API.setTextColor = function(r, g, b) { + if ((typeof r === 'string') && /^#[0-9A-Fa-f]{6}$/.test(r)) { + var hex = parseInt(r.substr(1), 16); + r = (hex >> 16) & 255; + g = (hex >> 8) & 255; + b = (hex & 255); + } + + if ((r === 0 && g === 0 && b === 0) || (typeof g === 'undefined')) { + textColor = f3(r / 255) + ' g'; + } else { + textColor = [f3(r / 255), f3(g / 255), f3(b / 255), 'rg'].join(' '); + } + return this; + }; + + /** + * Is an Object providing a mapping from human-readable to + * integer flag values designating the varieties of line cap + * and join styles. + * + * @returns {Object} + * @fieldOf jsPDF# + * @name CapJoinStyles + */ + API.CapJoinStyles = { + 0 : 0, + 'butt' : 0, + 'but' : 0, + 'miter' : 0, + 1 : 1, + 'round' : 1, + 'rounded' : 1, + 'circle' : 1, + 2 : 2, + 'projecting' : 2, + 'project' : 2, + 'square' : 2, + 'bevel' : 2 + }; + + /** + * Sets the line cap styles + * See {jsPDF.CapJoinStyles} for variants + * + * @param {String|Number} style A string or number identifying the type of line cap + * @function + * @returns {jsPDF} + * @methodOf jsPDF# + * @name setLineCap + */ + API.setLineCap = function(style) { + var id = this.CapJoinStyles[style]; + if (id === undefined) { + throw new Error("Line cap style of '" + style + "' is not recognized. See or extend .CapJoinStyles property for valid styles"); + } + lineCapID = id; + out(id + ' J'); + + return this; + }; + + /** + * Sets the line join styles + * See {jsPDF.CapJoinStyles} for variants + * + * @param {String|Number} style A string or number identifying the type of line join + * @function + * @returns {jsPDF} + * @methodOf jsPDF# + * @name setLineJoin + */ + API.setLineJoin = function(style) { + var id = this.CapJoinStyles[style]; + if (id === undefined) { + throw new Error("Line join style of '" + style + "' is not recognized. See or extend .CapJoinStyles property for valid styles"); + } + lineJoinID = id; + out(id + ' j'); + + return this; + }; + + // Output is both an internal (for plugins) and external function + API.output = output; + + /** + * Saves as PDF document. An alias of jsPDF.output('save', 'filename.pdf') + * @param {String} filename The filename including extension. + * + * @function + * @returns {jsPDF} + * @methodOf jsPDF# + * @name save + */ + API.save = function(filename) { + API.output('save', filename); + }; + + // applying plugins (more methods) ON TOP of built-in API. + // this is intentional as we allow plugins to override + // built-ins + for (var plugin in jsPDF.API) { + if (jsPDF.API.hasOwnProperty(plugin)) { + if (plugin === 'events' && jsPDF.API.events.length) { + (function(events, newEvents) { + + // jsPDF.API.events is a JS Array of Arrays + // where each Array is a pair of event name, handler + // Events were added by plugins to the jsPDF instantiator. + // These are always added to the new instance and some ran + // during instantiation. + var eventname,handler_and_args,i; + + for (i = newEvents.length - 1; i !== -1; i--) { + // subscribe takes 3 args: 'topic', function, runonce_flag + // if undefined, runonce is false. + // users can attach callback directly, + // or they can attach an array with [callback, runonce_flag] + // that's what the "apply" magic is for below. + eventname = newEvents[i][0]; + handler_and_args = newEvents[i][1]; + events.subscribe.apply( + events, + [eventname].concat( + typeof handler_and_args === 'function' ? + [handler_and_args] : handler_and_args)); + } + }(events, jsPDF.API.events)); + } else { + API[plugin] = jsPDF.API[plugin]; + } + } + } + + ////////////////////////////////////////////////////// + // continuing initialization of jsPDF Document object + ////////////////////////////////////////////////////// + // Add the first page automatically + addFonts(); + activeFontKey = 'F1'; + _addPage(); + + events.publish('initialized'); + return API; + } + + /** + * jsPDF.API is a STATIC property of jsPDF class. + * jsPDF.API is an object you can add methods and properties to. + * The methods / properties you add will show up in new jsPDF objects. + * + * One property is prepopulated. It is the 'events' Object. Plugin authors can add topics, + * callbacks to this object. These will be reassigned to all new instances of jsPDF. + * Examples: + * jsPDF.API.events['initialized'] = function(){ 'this' is API object } + * jsPDF.API.events['addFont'] = function(added_font_object){ 'this' is API object } + * + * @static + * @public + * @memberOf jsPDF + * @name API + * + * @example + * jsPDF.API.mymethod = function(){ + * // 'this' will be ref to internal API object. see jsPDF source + * // , so you can refer to built-in methods like so: + * // this.line(....) + * // this.text(....) + * } + * var pdfdoc = new jsPDF() + * pdfdoc.mymethod() // <- !!!!!! + */ + jsPDF.API = {events:[]}; + jsPDF.version = "1.0.0-trunk"; + + if (typeof define === 'function') { + define(function() { + return jsPDF; + }); + } else { + global.jsPDF = jsPDF; + } + return jsPDF; +}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this)); diff --git a/editor/svg-editor.html b/editor/svg-editor.html index 76dd81c2..0c4b4a67 100644 --- a/editor/svg-editor.html +++ b/editor/svg-editor.html @@ -25,10 +25,6 @@ - - - - diff --git a/editor/svg-editor.js b/editor/svg-editor.js index 92c4a43d..02d8aa2d 100644 --- a/editor/svg-editor.js +++ b/editor/svg-editor.js @@ -202,19 +202,37 @@ var saveAs; } } - function checkCanvg (callCanvg) { - return function (win, data) { - if (window.canvg) { - callCanvg(win, data); - } else { // Might not be set up yet - $.getScript('canvg/rgbcolor.js', function() { - $.getScript('canvg/canvg.js', function() { - callCanvg(win, data); - }); - }); + /** + * @param {string} globalCheck A global which can be used to determine if the script is already loaded + * @param {array} scripts An array of scripts to preload (in order) + * @param {function} cb The callback to execute upon load. + */ + function executeAfterLoads (globalCheck, scripts, cb) { + return function () { + var args = arguments; + function endCallback () { + cb.apply(null, args); + } + if (window[globalCheck]) { + endCallback(); + } + else { + scripts.reduceRight(function (oldFunc, script) { + return function () { + $.getScript(script, oldFunc); + }; + }, endCallback)(); } }; } + + function checkCanvg (callCanvg) { + return executeAfterLoads('canvg', ['canvg/rgbcolor.js', 'canvg/canvg.js'], callCanvg); + } + + function executeJSPDF (callJSPDF) { + return executeAfterLoads('jsPDF', ['jspdf/underscore-min.js', 'jspdf/jspdf.js', 'jspdf/jspdf.plugin.svgToPdf.js'], callJSPDF)(); + } /** * EXPORTS @@ -1127,17 +1145,20 @@ var saveAs; var res = svgCanvas.getResolution(); var orientation = res.w > res.h ? 'landscape' : 'portrait'; var units = 'pt'; // curConfig.baseUnit; // We could use baseUnit, but that is presumably not intended for export purposes - var doc = new jsPDF(orientation, units, [res.w, res.h]); // Todo: Give options to use predefined jsPDF formats like "a4", etc. from pull-down (with option to keep customizable) - var docTitle = svgCanvas.getDocumentTitle(); - doc.setProperties({ - title: docTitle/*, - subject: '', - author: '', - keywords: '', - creator: ''*/ + + executeJSPDF(function () { + var doc = new jsPDF(orientation, units, [res.w, res.h]); // Todo: Give options to use predefined jsPDF formats like "a4", etc. from pull-down (with option to keep customizable) + var docTitle = svgCanvas.getDocumentTitle(); + doc.setProperties({ + title: docTitle/*, + subject: '', + author: '', + keywords: '', + creator: ''*/ + }); + svgElementToPdf(data.svg, doc, {}); + doc.save(docTitle + '.pdf'); }); - svgElementToPdf(data.svg, doc, {}); - doc.save(docTitle + '.pdf'); return; } c.width = svgCanvas.contentW;