From 50d0b1fb6744a79f478af29a06cc8db93616f8f6 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 8 Apr 2014 05:13:54 +0000 Subject: [PATCH] Add jsPDF(with SVG plugin)-based PDF export (courtesy of https://github.com/brettz9/jsPDF forked from https://github.com/MrRio/jsPDF => https://github.com/ahwolf/jsPDF ); provides data: URL where blob conversion is supported, and a blob URL otherwise. git-svn-id: http://svg-edit.googlecode.com/svn/trunk@2792 eee81c28-f429-11dd-99c0-75d572ba1ddd --- editor/jspdf/jspdf.js | 1888 +++++++++++++++++++++++++ editor/jspdf/jspdf.plugin.svgToPdf.js | 669 +++++++++ editor/jspdf/underscore-min.js | 6 + editor/svg-editor.html | 4 + editor/svg-editor.js | 45 +- 5 files changed, 2607 insertions(+), 5 deletions(-) create mode 100644 editor/jspdf/jspdf.js create mode 100644 editor/jspdf/jspdf.plugin.svgToPdf.js create mode 100644 editor/jspdf/underscore-min.js diff --git a/editor/jspdf/jspdf.js b/editor/jspdf/jspdf.js new file mode 100644 index 00000000..de45a91c --- /dev/null +++ b/editor/jspdf/jspdf.js @@ -0,0 +1,1888 @@ +/** @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. +*/ + +/* + * 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 + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * 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. + * ==================================================================== + */ + + +/** +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 + }; + + if (unit === 'pt') { + k = 1; + } else if (unit === 'mm') { + k = 72 / 25.4; + } else if (unit === 'cm') { + k = 72 / 2.54; + } else if (unit === 'in') { + k = 72; + } else { + 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; +}()); diff --git a/editor/jspdf/jspdf.plugin.svgToPdf.js b/editor/jspdf/jspdf.plugin.svgToPdf.js new file mode 100644 index 00000000..8bc9157d --- /dev/null +++ b/editor/jspdf/jspdf.plugin.svgToPdf.js @@ -0,0 +1,669 @@ +/* + * svgToPdf.js + * + * Copyright 2012 Florian Hülsmann + * Updated in 2013 by Datascope Analytics + * This script is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this file. If not, see . + * + */ + + +// These are all of the currently supported attributes of the svg elements +// that we are converting to PDF. The path is empty and taken care of in a +// different way below. +var pdfSvgAttr = { + // allowed attributes. all others are removed from the preview. + g: ['stroke', 'fill', 'stroke-width'], + line: ['x1', 'y1', 'x2', 'y2', 'stroke', 'stroke-width', 'fill'], + rect: ['x', 'y', 'width', 'height', 'stroke', 'fill', 'stroke-width'], + ellipse: ['cx', 'cy', 'rx', 'ry', 'stroke', 'fill', 'stroke-width'], + circle: ['cx', 'cy', 'r', 'stroke', 'fill', 'stroke-width'], + text: ['x', 'y', 'font-size', 'font-family', 'text-anchor', 'font-weight', 'font-style', 'fill'], + path: [] +}; + + +var svgElementToPdf = function(element, pdf, options) { + // pdf is a jsPDF object + var remove = (typeof(options.removeInvalid) == 'undefined' ? remove = false : options.removeInvalid); + var k = (typeof(options.removeInvalid) == 'undefined' ? 1.0 : options.scale); + + var x_offset = (typeof(options.x_offset) == 'undefined' ? 0 : options.x_offset); + var y_offset = (typeof(options.y_offset) == 'undefined' ? 0 : options.y_offset); + var ego_or_link = (typeof(options.ego_or_link) == 'undefined' ? false : options.ego_or_link); + var colorMode = null; + $(element).children().each(function(i, node) { + var n = $(node); + if (n.is("g")) { + if (n.attr('transform') != null) { + var matrix = n.attr('transform').replace(/,/g, ' ').replace('matrix(', '').replace(')', ' cm'); + pdf.internal.write('q'); + pdf.internal.write(matrix); + svgElementToPdf(n, pdf, options); + pdf.internal.write('Q'); + } else { + svgElementToPdf(n, pdf, options); + } + } + + var hasFillColor = false; + var hasStrokeColor = false; + if (n.is('g,line,rect,ellipse,circle,text,path')) { + + var fillColor = n.attr('fill'); + if (fillColor != null) { + var fillRGB = new RGBColor(fillColor); + + if (fillRGB.ok) { + //console.log("fillRGB is okay"); + hasFillColor = true; + colorMode = 'F'; + } else { + colorMode = null; + } + } + } + + if (hasFillColor) { + + pdf.setFillColor(fillRGB.r, fillRGB.g, fillRGB.b); + } + + var strokeColor = n.attr('stroke'); + if (strokeColor != null && strokeColor !== 'none') { + + var strokeRGB = new RGBColor(strokeColor); + if (strokeRGB.ok) { + hasStrokeColor = true; + + pdf.setDrawColor(strokeRGB.r, strokeRGB.g, strokeRGB.b); + if (colorMode == 'F') { + colorMode = 'FD'; + } else { + + colorMode = "S"; + } + } else { + + colorMode = null; + } + } + switch (n.get(0).tagName.toLowerCase()) { + case 'line': + pdf.line( + k * parseInt(n.attr('x1')), + k * parseInt(n.attr('y1')), + k * parseInt(n.attr('x2')), + k * parseInt(n.attr('y2'))); + + // $.each(node.attributes, function(i, a) { + // if(a != null && pdfSvgAttr.line.indexOf(a.name.toLowerCase()) == -1) { + // node.removeAttribute(a.name); + // } + // }); + break; + case 'rect': + //console.log("in rectangle"); + pdf.rect( + k * (parseInt(n.attr('x')) + x_offset), + k * (parseInt(n.attr('y')) + y_offset), + k * parseInt(n.attr('width')), + k * parseInt(n.attr('height')), + colorMode); + // $.each(node.attributes, function(i, a) { + // if(a != null && pdfSvgAttr.rect.indexOf(a.name.toLowerCase()) == -1) { + // node.removeAttribute(a.name); + // } + // }); + break; + case 'ellipse': + pdf.ellipse( + k * parseInt(n.attr('cx')), + k * parseInt(n.attr('cy')), + k * parseInt(n.attr('rx')), + k * parseInt(n.attr('ry')), + colorMode); + // $.each(node.attributes, function(i, a) { + // if(a != null && pdfSvgAttr.ellipse.indexOf(a.name.toLowerCase()) == -1) { + // node.removeAttribute(a.name); + // } + // }); + break; + case 'circle': + pdf.circle( + k * (parseInt(n.attr('cx')) + x_offset), + k * (parseInt(n.attr('cy')) + y_offset), + k * parseInt(n.attr('r')), + colorMode); + // $.each(node.attributes, function(i, a) { + // if(a != null && pdfSvgAttr.circle.indexOf(a.name.toLowerCase()) == -1) { + // node.removeAttribute(a.name); + // } + // }); + break; + case 'text': + var fontFamily = 'helvetica'; + if (node.hasAttribute('font-family')) { + + switch (n.attr('font-family').toLowerCase().split(',')[0]) { + case 'times': + var fontFamily = 'times'; + pdf.setFont('times'); + break; + case 'courier': + var fontFamily = 'courier'; + pdf.setFont('courier'); + break; + + default: + pdf.setFont('helvetica'); + } + } + + // If no font family, we set the font to the times by default + else { + pdf.setFont('helvetica'); + } + + // if (colorMode !== "FD" || colorMode !== 'F') { + // console.log("I'm setting text color"); + // pdf.setTextColor(fillRGB.r, fillRGB.g, fillRGB.b); + // } + + var fontType = "normal"; + if (node.hasAttribute('font-weight')) { + if (n.attr('font-weight') == "bold") { + fontType = "bold"; + } else { + node.removeAttribute('font-weight'); + } + } + + if (node.hasAttribute('font-style')) { + if (n.attr('font-style') == "italic") { + fontType += "italic"; + } else { + node.removeAttribute('font-style'); + } + } + + pdf.setFontType(fontType); + var pdfFontSize = 9; + if (node.hasAttribute('font-size')) { + pdfFontSize = parseInt(n.attr('font-size')); + } + + var fontMetrics = pdf.internal.getFont(fontFamily, fontType) + .metadata.Unicode; + + var text_value = n.text(); + var name_length = pdf.getStringUnitWidth( + text_value, + fontMetrics) * pdfFontSize; + + + //FIXME: use more accurate positioning!! + var label_offset = 0; + var x = parseInt(n.attr('x')) + x_offset - label_offset; + var y = parseInt(n.attr('y')) + y_offset; + + + + if (node.hasAttribute('text-anchor')) { + switch (n.attr('text-anchor')) { + case 'end': + break; + case 'middle': + label_offset = name_length / 2; + break; + case 'start': + label_offset = 0; + break; + case 'default': + label_offset = 0; + break; + } + x = parseInt(n.attr('x')) + x_offset - label_offset; + y = parseInt(n.attr('y')) + y_offset; + } + + + + pdf.setFontSize(pdfFontSize).text( + k * x, + k * y, + text_value); + // $.each(node.attributes, function(i, a) { + // if(a != null && pdfSvgAttr.text.indexOf(a.name.toLowerCase()) == -1) { + // node.removeAttribute(a.name); + // } + // }); + break; + case 'path': + // //console.log("node is: ",node.attributes) + // $.each(node.attributes, function(i, a) { + // //console.log(pdfSvgAttr.path.indexOf(a.name.toLowerCase())); + // }); + + var updated_path = n.attr('d'); + // Separate the svg 'd' string to a list of letter + // and number elements. Iterate on this list. + // console.log('path before',path); + svg_regex = /(m|l|h|v|c|s|a|z)/gi; + // console.log('TESTING REGULAR + // EXPRESSION',svg_regex.test('m')); The crazy ie9 case + // where they take our path and make some crazy scientific + // notation + updated_path = updated_path.replace(/(e)?-/g, function($0, $1) { + return $1 ? $0 : ' -'; + }) + // .replace(/-/g, ' -') + .replace(svg_regex, ' $1 ') + .replace(/,+/g, ' ') + .replace(/^\s+|\s+$/g, '') + .split(/[\s]+/); + + var svg_element = null; + var i = 0; + + // mx and my will define the starting points from each m/M case + var mx = null; + var my = null; + // x and y will redefine the starting points + var x = null; + var y = null; + + // big list contains the large list of lists to pass to + // jspdf to render the appropriate path + var big_list = []; + + // for S/s shorthand bezier calculations of 2nd control pts + var previous_element = { + element: null, + prev_numbers: [], + point: [] + }; + var m_flag = 0; + // Go through our list until we are done with the updated_path + while (i < updated_path.length) { + + // Numbers will hold the list of numbers for the + // appropriate updated_path + var numbers = []; + + // svg_element is a letter corresponding to the type + // of updated_path to draw + var sci_regex = /[+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?/; + var svg_element = updated_path[i]; + if (sci_regex.test(svg_element)) { + svg_element = String(Number(svg_element)); + } + i++ + //if svg_element is s/S, need to find 1st control pts + if (/s/i.test(svg_element)) { + previous_element.point = find_s_points(svg_element, + previous_element); + } + + // for some reason z followed by another letter + // i.e. 'z m' skips that 2nd letter, so added if + // statement to get around that. + if (/z/i.test(svg_element) == false) { + + // Parse through the updated_path until we find the next + // letter or we are at the end of the updated_path + while ((svg_regex.test(updated_path[i]) == false) && (i != updated_path.length)) { + numbers.push(k * parseFloat(updated_path[i])); + i++; + } + } + + + switch (svg_element) { + case 'm': + //paths and subpaths must always start with m/M. + //thus we call pdf.lines + if (big_list.length != 0) { + pdf.lines(big_list, mx, my, [1, 1], null); + } + big_list = []; + + // check if this is 1st command in the path + if (previous_element.element == null) { + x = numbers[0]; + mx = numbers[0]; + y = numbers[1]; + my = numbers[1]; + } else { + x += numbers[0]; + mx += numbers[0]; + y += numbers[1]; + my += numbers[1]; + } + if (numbers.length != 2) { + var lines_numbers = numbers.slice(2, numbers.length); + + var new_numbers = change_numbers(lines_numbers, + x, y, true); + _.each(new_numbers, function(num) { + big_list.push(num); + }); + // pdf.lines(new_numbers,x,y,[1,1],null); + x += sums(new_numbers, true); + y += sums(new_numbers, false); + } + break; + case 'M': + if (big_list.length != 0) { + pdf.lines(big_list, mx, my, [1, 1], null); + } + big_list = []; + + x = numbers[0]; + mx = numbers[0] + x_offset; + y = numbers[1]; + my = numbers[1] + y_offset; + + if (numbers.length != 2) { + x = numbers[0]; + y = numbers[1]; + var lines_numbers = numbers.slice(2, numbers.length); + var new_numbers = change_numbers(lines_numbers, + x, y, false); + pdf.lines(new_numbers, x, y, [1, 1], null); + x += new_numbers[new_numbers.length - 1][0]; + y += new_numbers[new_numbers.length - 1][1]; + + } + break; + case 'l': + var new_numbers = change_numbers(numbers, x, y, true); + _.each(new_numbers, function(num) { + big_list.push(num); + }); + // pdf.lines(new_numbers,x,y,[1,1],null); + x += sums(new_numbers, true); + y += sums(new_numbers, false); + break; + case 'L': + var new_numbers = change_numbers(numbers, x, y, false); + _.each(new_numbers, function(num) { + big_list.push(num); + }); + //pdf.lines(new_numbers,x,y,[1,1],null); + x += new_numbers[new_numbers.length - 1][0]; + y += new_numbers[new_numbers.length - 1][1]; + break; + case 'h': + // x does not change. Only y changes + var sum = _.reduce(numbers, + + function(memo, num) { + return memo + num; + }, 0); + var new_numbers = [ + [sum, 0] + ]; + _.each(new_numbers, function(num) { + big_list.push(num); + }); + // pdf.lines([[sum,0]],x,y,[1,1],null); + x += sum; + break; + case 'H': + big_list.push([numbers[numbers.length - 1] - x, 0]); + // pdf.lines([[numbers[numbers.length-1]-x,0]], + // x,y,[1,1],null); + x = numbers[numbers.length - 1]; + break; + case 'v': + var sum = _.reduce(numbers, + + function(memo, num) { + return memo + num; + }, 0); + var new_numbers = [ + [0, sum] + ]; + _.each(new_numbers, function(num) { + big_list.push(num); + }); + // pdf.lines([[0,sum]],x,y,[1,1],null); + y += sum; + break; + case 'V': + big_list.push([0, numbers[numbers.length - 1] - y]); + // pdf.lines([[0,numbers[numbers.length-1]-y]], + // x,y,[1,1],null); + y = numbers[numbers.length - 1]; + break; + case 'c': + var new_numbers = bezier_numbers(numbers, x, y, true); + _.each(new_numbers, function(num) { + big_list.push(num); + }); + // pdf.lines(new_numbers,x,y,[1,1],null); + x += sums(new_numbers, true); + y += sums(new_numbers, false); + break; + case 'C': + var new_numbers = bezier_numbers(numbers, x, y, false); + _.each(new_numbers, function(num) { + big_list.push(num); + }); + // pdf.lines(new_numbers,x,y,[1,1],null); + x = numbers[numbers.length - 2]; + y = numbers[numbers.length - 1]; + break; + case 's': + var new_numbers = s_bezier_numbers(numbers, x, y, + true, previous_element); + _.each(new_numbers, function(num) { + big_list.push(num); + }); + // pdf.lines(new_numbers,x,y,[1,1],null); + x += sums(new_numbers, true); + y += sums(new_numbers, false); + break; + case 'S': + var new_numbers = s_bezier_numbers(numbers, x, y, false, + previous_element); + _.each(new_numbers, function(num) { + big_list.push(num); + }); + // pdf.lines(new_numbers,x,y,[1,1],null); + x = numbers[numbers.length - 2]; + y = numbers[numbers.length - 1]; + break; + case 'A': + // for now a hack.treat this as a line + + + break; + case 'a': + + break; + case 'z': + big_list.push([mx - x, my - y]); + x = mx; + y = my; + // pdf.lines([[mx-x,my-y]],x,y,[1,1],null); + break; + case 'Z': + big_list.push([mx - x, my - y]); + x = mx; + y = my; + // pdf.lines([[mx-x,my-y]],x,y,[1,1],null); + break; + default: + console.log('Sorry, the', svg_element, 'svg command is not yet available.'); + } + previous_element.element = svg_element; + previous_element.prev_numbers = numbers; + } + pdf.lines(big_list, mx, my, [1, 1], colorMode); + var numbs = null; + break; + //TODO: image + default: + //console.log("cannot identify: ", node); + if (remove) { + //console.log("can't translate to pdf:", node); + n.remove(); + } + } + }); + return pdf; +} + + function sums(ListOfLists, is_x) { + if (is_x) { + var sum = _.reduce(ListOfLists, + + function(memo, num) { + return memo + num[num.length - 2]; + }, 0); + } else { + var sum = _.reduce(ListOfLists, + + function(memo, num) { + return memo + num[num.length - 1]; + }, 0); + } + return sum; + } + + function change_numbers(numbers, x, y, relative) { + var i = 0; + var prev_x = x; + var prev_y = y; + var new_numbers = []; + while (i < numbers.length) { + if (relative) { + x = numbers[i]; + y = numbers[i + 1]; + } else { + x = numbers[i] - prev_x; + y = numbers[i + 1] - prev_y; + } + prev_x = numbers[i]; + prev_y = numbers[i + 1]; + new_numbers.push([x, y]); + i += 2; + } + return new_numbers; + } + + function bezier_numbers(numbers, x, y, relative) { + // the bezier numbers are ALL relative to the + // previous case line's (x,y), not all relative to + // each other. + var i = 0; + var prev_x = x; + var prev_y = y; + var new_numbers = []; + while (i < numbers.length) { + if (relative) { + var numbers_to_push = numbers.slice(i, i + 6); + } else { + var numbers_to_push = []; + for (var k = i; k < i + 6; k = k + 2) { + numbers_to_push.push( + numbers[k] - prev_x, + numbers[k + 1] - prev_y); + } + } + prev_x = numbers[i + 4]; + prev_y = numbers[i + 5]; + new_numbers.push(numbers_to_push); + i += 6; + } + return new_numbers; + } + + function s_bezier_numbers(numbers, x, y, relative, + previous_element) { + var i = 0; + var prev_x = x; + var prev_y = y; + var new_numbers = []; + while (i < numbers.length) { + var numbers_to_push = []; + //need to check if relative for the 4 s/S numbers + if (relative) { + //find that 1st control point + if (i < 4 && (previous_element.element == 'c' || previous_element.element == 's')) { + // case 1: there was a prev c/C/s/S + // outside this numbers segment + numbers_to_push.push(previous_element.point[0], + previous_element.point[1]); + } else if (i >= 4) { + //case 1: there was a prev s/S + //within this numbers segment + numbers_to_push.push(numbers[i - 2] - numbers[i - 4], + numbers[i - 1] - numbers[i - 3]); + } else { + // case 2: no prev c/C/s/S, therefore + // 1st control pt = current pt. + numbers_to_push.push(prev_x, prev_y); + } + //then add the rest of the s numbers + for (var k = i; k < i + 4; k++) { + numbers_to_push.push(numbers[k]); + } + } else { + //find that 1st control point + if (i < 4 && (previous_element.element == 'C' || previous_element.element == 'S')) { + // case 1: there was a prev c/C/s/S + // outside this numbers segment + numbers_to_push.push(previous_element.point[0] - prev_x, + previous_element.point[1] - prev_y); + } else if (i >= 4) { + //case 1: there was a prev s/S + //within this numbers segment + numbers_to_push.push(numbers[i - 2] + numbers[i - 2] - numbers[i - 4], + numbers[i - 1] + numbers[i - 1] - numbers[i - 3]); + } else { + // case 2: no prev c/C/s/S, therefore + // 1st control pt = current pt. + numbers_to_push.push(prev_x, prev_y); + } + //then add the rest of the s numbers + for (var k = i; k < i + 4; k = k + 2) { + numbers_to_push.push(numbers[k] - prev_x); + numbers_to_push.push(numbers[k + 1] - prev_y); + } + } + + prev_x = numbers[i + 2]; + prev_y = numbers[i + 3]; + + new_numbers.push(numbers_to_push); + i += 4; + } + return new_numbers; + } + + function find_s_points(svg_element, previous_element) { + if (/(s|c)/.test(previous_element.element)) { + numbs = previous_element.prev_numbers; + previous_element.point = [numbs[numbs.length - 2] - numbs[numbs.length - 4], + numbs[numbs.length - 1] - numbs[numbs.length - 3]]; + } else if (/(S|C)/.test(previous_element.element)) { + numbs = previous_element.prev_numbers; + previous_element.point = [2 * numbs[numbs.length - 2] - numbs[numbs.length - 4], + 2 * numbs[numbs.length - 1] - numbs[numbs.length - 3]]; + } + return previous_element.point + } diff --git a/editor/jspdf/underscore-min.js b/editor/jspdf/underscore-min.js new file mode 100644 index 00000000..3434d6c5 --- /dev/null +++ b/editor/jspdf/underscore-min.js @@ -0,0 +1,6 @@ +// Underscore.js 1.6.0 +// http://underscorejs.org +// (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. +(function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,h=e.reduce,v=e.reduceRight,g=e.filter,d=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,w=Object.keys,_=i.bind,j=function(n){return n instanceof j?n:this instanceof j?void(this._wrapped=n):new j(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=j),exports._=j):n._=j,j.VERSION="1.6.0";var A=j.each=j.forEach=function(n,t,e){if(null==n)return n;if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a=j.keys(n),u=0,i=a.length;i>u;u++)if(t.call(e,n[a[u]],a[u],n)===r)return;return n};j.map=j.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e.push(t.call(r,n,u,i))}),e)};var O="Reduce of empty array with no initial value";j.reduce=j.foldl=j.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduce===h)return e&&(t=j.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(O);return r},j.reduceRight=j.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduceRight===v)return e&&(t=j.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=j.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(O);return r},j.find=j.detect=function(n,t,r){var e;return k(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},j.filter=j.select=function(n,t,r){var e=[];return null==n?e:g&&n.filter===g?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&e.push(n)}),e)},j.reject=function(n,t,r){return j.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},j.every=j.all=function(n,t,e){t||(t=j.identity);var u=!0;return null==n?u:d&&n.every===d?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var k=j.some=j.any=function(n,t,e){t||(t=j.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};j.contains=j.include=function(n,t){return null==n?!1:y&&n.indexOf===y?n.indexOf(t)!=-1:k(n,function(n){return n===t})},j.invoke=function(n,t){var r=o.call(arguments,2),e=j.isFunction(t);return j.map(n,function(n){return(e?t:n[t]).apply(n,r)})},j.pluck=function(n,t){return j.map(n,j.property(t))},j.where=function(n,t){return j.filter(n,j.matches(t))},j.findWhere=function(n,t){return j.find(n,j.matches(t))},j.max=function(n,t,r){if(!t&&j.isArray(n)&&n[0]===+n[0]&&n.length<65535)return Math.max.apply(Math,n);var e=-1/0,u=-1/0;return A(n,function(n,i,a){var o=t?t.call(r,n,i,a):n;o>u&&(e=n,u=o)}),e},j.min=function(n,t,r){if(!t&&j.isArray(n)&&n[0]===+n[0]&&n.length<65535)return Math.min.apply(Math,n);var e=1/0,u=1/0;return A(n,function(n,i,a){var o=t?t.call(r,n,i,a):n;u>o&&(e=n,u=o)}),e},j.shuffle=function(n){var t,r=0,e=[];return A(n,function(n){t=j.random(r++),e[r-1]=e[t],e[t]=n}),e},j.sample=function(n,t,r){return null==t||r?(n.length!==+n.length&&(n=j.values(n)),n[j.random(n.length-1)]):j.shuffle(n).slice(0,Math.max(0,t))};var E=function(n){return null==n?j.identity:j.isFunction(n)?n:j.property(n)};j.sortBy=function(n,t,r){return t=E(t),j.pluck(j.map(n,function(n,e,u){return{value:n,index:e,criteria:t.call(r,n,e,u)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={};return r=E(r),A(t,function(i,a){var o=r.call(e,i,a,t);n(u,o,i)}),u}};j.groupBy=F(function(n,t,r){j.has(n,t)?n[t].push(r):n[t]=[r]}),j.indexBy=F(function(n,t,r){n[t]=r}),j.countBy=F(function(n,t){j.has(n,t)?n[t]++:n[t]=1}),j.sortedIndex=function(n,t,r,e){r=E(r);for(var u=r.call(e,t),i=0,a=n.length;a>i;){var o=i+a>>>1;r.call(e,n[o])t?[]:o.call(n,0,t)},j.initial=function(n,t,r){return o.call(n,0,n.length-(null==t||r?1:t))},j.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:o.call(n,Math.max(n.length-t,0))},j.rest=j.tail=j.drop=function(n,t,r){return o.call(n,null==t||r?1:t)},j.compact=function(n){return j.filter(n,j.identity)};var M=function(n,t,r){return t&&j.every(n,j.isArray)?c.apply(r,n):(A(n,function(n){j.isArray(n)||j.isArguments(n)?t?a.apply(r,n):M(n,t,r):r.push(n)}),r)};j.flatten=function(n,t){return M(n,t,[])},j.without=function(n){return j.difference(n,o.call(arguments,1))},j.partition=function(n,t){var r=[],e=[];return A(n,function(n){(t(n)?r:e).push(n)}),[r,e]},j.uniq=j.unique=function(n,t,r,e){j.isFunction(t)&&(e=r,r=t,t=!1);var u=r?j.map(n,r,e):n,i=[],a=[];return A(u,function(r,e){(t?e&&a[a.length-1]===r:j.contains(a,r))||(a.push(r),i.push(n[e]))}),i},j.union=function(){return j.uniq(j.flatten(arguments,!0))},j.intersection=function(n){var t=o.call(arguments,1);return j.filter(j.uniq(n),function(n){return j.every(t,function(t){return j.contains(t,n)})})},j.difference=function(n){var t=c.apply(e,o.call(arguments,1));return j.filter(n,function(n){return!j.contains(t,n)})},j.zip=function(){for(var n=j.max(j.pluck(arguments,"length").concat(0)),t=new Array(n),r=0;n>r;r++)t[r]=j.pluck(arguments,""+r);return t},j.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},j.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=j.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},j.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},j.range=function(n,t,r){arguments.length<=1&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=new Array(e);e>u;)i[u++]=n,n+=r;return i};var R=function(){};j.bind=function(n,t){var r,e;if(_&&n.bind===_)return _.apply(n,o.call(arguments,1));if(!j.isFunction(n))throw new TypeError;return r=o.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(o.call(arguments)));R.prototype=n.prototype;var u=new R;R.prototype=null;var i=n.apply(u,r.concat(o.call(arguments)));return Object(i)===i?i:u}},j.partial=function(n){var t=o.call(arguments,1);return function(){for(var r=0,e=t.slice(),u=0,i=e.length;i>u;u++)e[u]===j&&(e[u]=arguments[r++]);for(;r=f?(clearTimeout(a),a=null,o=l,i=n.apply(e,u),e=u=null):a||r.trailing===!1||(a=setTimeout(c,f)),i}},j.debounce=function(n,t,r){var e,u,i,a,o,c=function(){var l=j.now()-a;t>l?e=setTimeout(c,t-l):(e=null,r||(o=n.apply(i,u),i=u=null))};return function(){i=this,u=arguments,a=j.now();var l=r&&!e;return e||(e=setTimeout(c,t)),l&&(o=n.apply(i,u),i=u=null),o}},j.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},j.wrap=function(n,t){return j.partial(t,n)},j.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},j.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},j.keys=function(n){if(!j.isObject(n))return[];if(w)return w(n);var t=[];for(var r in n)j.has(n,r)&&t.push(r);return t},j.values=function(n){for(var t=j.keys(n),r=t.length,e=new Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},j.pairs=function(n){for(var t=j.keys(n),r=t.length,e=new Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},j.invert=function(n){for(var t={},r=j.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},j.functions=j.methods=function(n){var t=[];for(var r in n)j.isFunction(n[r])&&t.push(r);return t.sort()},j.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},j.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},j.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)j.contains(r,u)||(t[u]=n[u]);return t},j.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]===void 0&&(n[r]=t[r])}),n},j.clone=function(n){return j.isObject(n)?j.isArray(n)?n.slice():j.extend({},n):n},j.tap=function(n,t){return t(n),n};var S=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof j&&(n=n._wrapped),t instanceof j&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==String(t);case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;var a=n.constructor,o=t.constructor;if(a!==o&&!(j.isFunction(a)&&a instanceof a&&j.isFunction(o)&&o instanceof o)&&"constructor"in n&&"constructor"in t)return!1;r.push(n),e.push(t);var c=0,f=!0;if("[object Array]"==u){if(c=n.length,f=c==t.length)for(;c--&&(f=S(n[c],t[c],r,e)););}else{for(var s in n)if(j.has(n,s)&&(c++,!(f=j.has(t,s)&&S(n[s],t[s],r,e))))break;if(f){for(s in t)if(j.has(t,s)&&!c--)break;f=!c}}return r.pop(),e.pop(),f};j.isEqual=function(n,t){return S(n,t,[],[])},j.isEmpty=function(n){if(null==n)return!0;if(j.isArray(n)||j.isString(n))return 0===n.length;for(var t in n)if(j.has(n,t))return!1;return!0},j.isElement=function(n){return!(!n||1!==n.nodeType)},j.isArray=x||function(n){return"[object Array]"==l.call(n)},j.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){j["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),j.isArguments(arguments)||(j.isArguments=function(n){return!(!n||!j.has(n,"callee"))}),"function"!=typeof/./&&(j.isFunction=function(n){return"function"==typeof n}),j.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},j.isNaN=function(n){return j.isNumber(n)&&n!=+n},j.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},j.isNull=function(n){return null===n},j.isUndefined=function(n){return n===void 0},j.has=function(n,t){return f.call(n,t)},j.noConflict=function(){return n._=t,this},j.identity=function(n){return n},j.constant=function(n){return function(){return n}},j.property=function(n){return function(t){return t[n]}},j.matches=function(n){return function(t){if(t===n)return!0;for(var r in n)if(n[r]!==t[r])return!1;return!0}},j.times=function(n,t,r){for(var e=Array(Math.max(0,n)),u=0;n>u;u++)e[u]=t.call(r,u);return e},j.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},j.now=Date.now||function(){return(new Date).getTime()};var T={escape:{"&":"&","<":"<",">":">",'"':""","'":"'"}};T.unescape=j.invert(T.escape);var I={escape:new RegExp("["+j.keys(T.escape).join("")+"]","g"),unescape:new RegExp("("+j.keys(T.unescape).join("|")+")","g")};j.each(["escape","unescape"],function(n){j[n]=function(t){return null==t?"":(""+t).replace(I[n],function(t){return T[n][t]})}}),j.result=function(n,t){if(null==n)return void 0;var r=n[t];return j.isFunction(r)?r.call(n):r},j.mixin=function(n){A(j.functions(n),function(t){var r=j[t]=n[t];j.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),z.call(this,r.apply(j,n))}})};var N=0;j.uniqueId=function(n){var t=++N+"";return n?n+t:t},j.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var q=/(.)^/,B={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\t|\u2028|\u2029/g;j.template=function(n,t,r){var e;r=j.defaults({},r,j.templateSettings);var u=new RegExp([(r.escape||q).source,(r.interpolate||q).source,(r.evaluate||q).source].join("|")+"|$","g"),i=0,a="__p+='";n.replace(u,function(t,r,e,u,o){return a+=n.slice(i,o).replace(D,function(n){return"\\"+B[n]}),r&&(a+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(a+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),u&&(a+="';\n"+u+"\n__p+='"),i=o+t.length,t}),a+="';\n",r.variable||(a="with(obj||{}){\n"+a+"}\n"),a="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+a+"return __p;\n";try{e=new Function(r.variable||"obj","_",a)}catch(o){throw o.source=a,o}if(t)return e(t,j);var c=function(n){return e.call(this,n,j)};return c.source="function("+(r.variable||"obj")+"){\n"+a+"}",c},j.chain=function(n){return j(n).chain()};var z=function(n){return this._chain?j(n).chain():n};j.mixin(j),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];j.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],z.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];j.prototype[n]=function(){return z.call(this,t.apply(this._wrapped,arguments))}}),j.extend(j.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}}),"function"==typeof define&&define.amd&&define("underscore",[],function(){return j})}).call(this); +//# sourceMappingURL=underscore-min.map \ No newline at end of file diff --git a/editor/svg-editor.html b/editor/svg-editor.html index 0c4b4a67..76dd81c2 100644 --- a/editor/svg-editor.html +++ b/editor/svg-editor.html @@ -25,6 +25,10 @@ + + + + diff --git a/editor/svg-editor.js b/editor/svg-editor.js index 8d103726..a54b44a2 100644 --- a/editor/svg-editor.js +++ b/editor/svg-editor.js @@ -1,4 +1,4 @@ -/*globals svgEditor:true, globalStorage, widget, svgedit, canvg, jQuery, $, DOMParser, FileReader */ +/*globals saveAs:true, svgEditor:true, globalStorage, widget, svgedit, canvg, jsPDF, svgElementToPdf, jQuery, $, DOMParser, FileReader, URL */ /*jslint vars: true, eqeq: true, todo: true, forin: true, continue: true, regexp: true */ /* * svg-editor.js @@ -391,7 +391,7 @@ TODOS * - inform user of any issues supplied via the "issues" property * - convert the "svg" property SVG string into an image for export; * utilize the properties "type" (currently 'PNG', 'JPEG', 'BMP', - * 'WEBP'), "mimeType", and "quality" (for 'JPEG' and 'WEBP' + * 'WEBP', 'PDF'), "mimeType", and "quality" (for 'JPEG' and 'WEBP' * types) to determine the proper output. */ editor.setCustomHandlers = function (opts) { @@ -1069,7 +1069,37 @@ TODOS } }; - var exportHandler = function(win, data) { + // Export global for use by jsPDF + saveAs = function (blob, options) { + var blobUrl = URL.createObjectURL(blob); + try { + // This creates a bookmarkable data URL, + // but it doesn't currently work in + // Firefox, and although it works in Chrome, + // Chrome doesn't make the full "data:" URL + // visible unless you right-click to "Inspect + // element" and then right-click on the element + // to "Copy link address". + var xhr = new XMLHttpRequest(); + xhr.responseType = 'blob'; + xhr.onload = function() { + var recoveredBlob = xhr.response; + var reader = new FileReader(); + reader.onload = function() { + var blobAsDataUrl = reader.result; + exportWindow.location.href = blobAsDataUrl; + }; + reader.readAsDataURL(recoveredBlob); + }; + xhr.open('GET', blobUrl); + xhr.send(); + } + catch (e) { + exportWindow.location.href = blobUrl; + } + }; + + var exportHandler = function(win, data) { var issues = data.issues, type = data.type || 'PNG', dataURLType = (type === 'ICO' ? 'BMP' : type).toLowerCase(); @@ -1078,7 +1108,12 @@ TODOS $('', {id: 'export_canvas'}).hide().appendTo('body'); } var c = $('#export_canvas')[0]; - + if (type === 'PDF') { + var doc = new jsPDF(); + svgElementToPdf(data.svg, doc, {}); + doc.save(svgCanvas.getDocumentTitle() + '.pdf'); + return; + } c.width = svgCanvas.contentW; c.height = svgCanvas.contentH; canvg(c, data.svg, {renderCallback: function() { @@ -3581,7 +3616,7 @@ TODOS // See http://kangax.github.io/jstests/toDataUrl_mime_type_test/ for a useful list of MIME types and browser support // 'ICO', // Todo: Find a way to preserve transparency in SVG-Edit if not working presently and do full packaging for x-icon; then switch back to position after 'PNG' 'PNG', - 'JPEG', 'BMP', 'WEBP' + 'JPEG', 'BMP', 'WEBP', 'PDF' ], function (imgType) { // todo: replace hard-coded msg with uiStrings.notification. if (!imgType) { return;