/** @preserve * jsPDF addImage plugin * Copyright (c) 2012 Jason Siefken, https://github.com/siefkenj/ * 2013 Chris Dowling, https://github.com/gingerchris * 2013 Trinh Ho, https://github.com/ineedfat * 2013 Edwin Alejandro Perez, https://github.com/eaparango * 2013 Norah Smith, https://github.com/burnburnrocket * 2014 Diego Casorran, https://github.com/diegocr * 2014 James Robb, https://github.com/jamesbrobb * * 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. */ ;(function(jsPDFAPI) { 'use strict' var namespace = 'addImage_', supported_image_types = ['jpeg', 'jpg', 'png']; // Image functionality ported from pdf.js var putImage = function(img) { var objectNumber = this.internal.newObject() , out = this.internal.write , putStream = this.internal.putStream img['n'] = objectNumber out('<>'); } if ('trns' in img && img['trns'].constructor == Array) { var trns = '', i = 0, len = img['trns'].length; for (; i < len; i++) trns += (img['trns'][i] + ' ' + img['trns'][i] + ' '); out('/Mask [' + trns + ']'); } if ('smask' in img) { out('/SMask ' + (objectNumber + 1) + ' 0 R'); } out('/Length ' + img['data'].length + '>>'); putStream(img['data']); out('endobj'); // Soft mask if ('smask' in img) { var dp = '/Predictor 15 /Colors 1 /BitsPerComponent ' + img['bpc'] + ' /Columns ' + img['w']; var smask = {'w': img['w'], 'h': img['h'], 'cs': 'DeviceGray', 'bpc': img['bpc'], 'dp': dp, 'data': img['smask']}; if ('f' in img) smask.f = img['f']; putImage.call(this, smask); } //Palette if (img['cs'] === this.color_spaces.INDEXED) { this.internal.newObject(); //out('<< /Filter / ' + img['f'] +' /Length ' + img['pal'].length + '>>'); //putStream(zlib.compress(img['pal'])); out('<< /Length ' + img['pal'].length + '>>'); putStream(this.arrayBufferToBinaryString(new Uint8Array(img['pal']))); out('endobj'); } } , putResourcesCallback = function() { var images = this.internal.collections[namespace + 'images'] for ( var i in images ) { putImage.call(this, images[i]) } } , putXObjectsDictCallback = function(){ var images = this.internal.collections[namespace + 'images'] , out = this.internal.write , image for (var i in images) { image = images[i] out( '/I' + image['i'] , image['n'] , '0' , 'R' ) } } , checkCompressValue = function(value) { if(value && typeof value === 'string') value = value.toUpperCase(); return value in jsPDFAPI.image_compression ? value : jsPDFAPI.image_compression.NONE; } , getImages = function() { var images = this.internal.collections[namespace + 'images']; //first run, so initialise stuff if(!images) { this.internal.collections[namespace + 'images'] = images = {}; this.internal.events.subscribe('putResources', putResourcesCallback); this.internal.events.subscribe('putXobjectDict', putXObjectsDictCallback); } return images; } , getImageIndex = function(images) { var imageIndex = 0; if (images){ // this is NOT the first time this method is ran on this instance of jsPDF object. imageIndex = Object.keys ? Object.keys(images).length : (function(o){ var i = 0 for (var e in o){if(o.hasOwnProperty(e)){ i++ }} return i })(images) } return imageIndex; } , notDefined = function(value) { return typeof value === 'undefined' || value === null; } , generateAliasFromData = function(data) { return typeof data === 'string' && jsPDFAPI.sHashCode(data); } , doesNotSupportImageType = function(type) { return supported_image_types.indexOf(type) === -1; } , processMethodNotEnabled = function(type) { return typeof jsPDFAPI['process' + type.toUpperCase()] !== 'function'; } , isDOMElement = function(object) { return typeof object === 'object' && object.nodeType === 1; } , createDataURIFromElement = function(element, format, angle) { //if element is an image which uses data url defintion, just return the dataurl if (element.nodeName === 'IMG' && element.hasAttribute('src')) { var src = ''+element.getAttribute('src'); if (!angle && src.indexOf('data:image/') === 0) return src; // only if the user doesn't care about a format if (!format && /\.png(?:[?#].*)?$/i.test(src)) format = 'png'; } if(element.nodeName === 'CANVAS') { var canvas = element; } else { var canvas = document.createElement('canvas'); canvas.width = element.clientWidth || element.width; canvas.height = element.clientHeight || element.height; var ctx = canvas.getContext('2d'); if (!ctx) { throw ('addImage requires canvas to be supported by browser.'); } if (angle) { var x, y, b, c, s, w, h, to_radians = Math.PI/180, angleInRadians; if (typeof angle === 'object') { x = angle.x; y = angle.y; b = angle.bg; angle = angle.angle; } angleInRadians = angle*to_radians; c = Math.abs(Math.cos(angleInRadians)); s = Math.abs(Math.sin(angleInRadians)); w = canvas.width; h = canvas.height; canvas.width = h * s + w * c; canvas.height = h * c + w * s; if (isNaN(x)) x = canvas.width / 2; if (isNaN(y)) y = canvas.height / 2; ctx.clearRect(0,0,canvas.width, canvas.height); ctx.fillStyle = b || 'white'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.save(); ctx.translate(x, y); ctx.rotate(angleInRadians); ctx.drawImage(element, -(w/2), -(h/2)); ctx.rotate(-angleInRadians); ctx.translate(-x, -y); ctx.restore(); } else { ctx.drawImage(element, 0, 0, canvas.width, canvas.height); } } return canvas.toDataURL((''+format).toLowerCase() == 'png' ? 'image/png' : 'image/jpeg'); } ,checkImagesForAlias = function(alias, images) { var cached_info; if(images) { for(var e in images) { if(alias === images[e].alias) { cached_info = images[e]; break; } } } return cached_info; } ,determineWidthAndHeight = function(w, h, info) { if (!w && !h) { w = -96; h = -96; } if (w < 0) { w = (-1) * info['w'] * 72 / w / this.internal.scaleFactor; } if (h < 0) { h = (-1) * info['h'] * 72 / h / this.internal.scaleFactor; } if (w === 0) { w = h * info['w'] / info['h']; } if (h === 0) { h = w * info['h'] / info['w']; } return [w, h]; } , writeImageToPDF = function(x, y, w, h, info, index, images) { var dims = determineWidthAndHeight.call(this, w, h, info), coord = this.internal.getCoordinateString, vcoord = this.internal.getVerticalCoordinateString; w = dims[0]; h = dims[1]; images[index] = info; this.internal.write( 'q' , coord(w) , '0 0' , coord(h) // TODO: check if this should be shifted by vcoord , coord(x) , vcoord(y + h) , 'cm /I'+info['i'] , 'Do Q' ) }; /** * COLOR SPACES */ jsPDFAPI.color_spaces = { DEVICE_RGB:'DeviceRGB', DEVICE_GRAY:'DeviceGray', DEVICE_CMYK:'DeviceCMYK', CAL_GREY:'CalGray', CAL_RGB:'CalRGB', LAB:'Lab', ICC_BASED:'ICCBased', INDEXED:'Indexed', PATTERN:'Pattern', SEPERATION:'Seperation', DEVICE_N:'DeviceN' }; /** * DECODE METHODS */ jsPDFAPI.decode = { DCT_DECODE:'DCTDecode', FLATE_DECODE:'FlateDecode', LZW_DECODE:'LZWDecode', JPX_DECODE:'JPXDecode', JBIG2_DECODE:'JBIG2Decode', ASCII85_DECODE:'ASCII85Decode', ASCII_HEX_DECODE:'ASCIIHexDecode', RUN_LENGTH_DECODE:'RunLengthDecode', CCITT_FAX_DECODE:'CCITTFaxDecode' }; /** * IMAGE COMPRESSION TYPES */ jsPDFAPI.image_compression = { NONE: 'NONE', FAST: 'FAST', MEDIUM: 'MEDIUM', SLOW: 'SLOW' }; jsPDFAPI.sHashCode = function(str) { return Array.prototype.reduce && str.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0); }; jsPDFAPI.isString = function(object) { return typeof object === 'string'; }; /** * Strips out and returns info from a valid base64 data URI * @param {String[dataURI]} a valid data URI of format 'data:[][;base64],' * @returns an Array containing the following * [0] the complete data URI * [1] * [2] format - the second part of the mime-type i.e 'png' in 'image/png' * [4] */ jsPDFAPI.extractInfoFromBase64DataURI = function(dataURI) { return /^data:([\w]+?\/([\w]+?));base64,(.+?)$/g.exec(dataURI); }; /** * Check to see if ArrayBuffer is supported */ jsPDFAPI.supportsArrayBuffer = function() { return typeof ArrayBuffer !== 'undefined' && typeof Uint8Array !== 'undefined'; }; /** * Tests supplied object to determine if ArrayBuffer * @param {Object[object]} */ jsPDFAPI.isArrayBuffer = function(object) { if(!this.supportsArrayBuffer()) return false; return object instanceof ArrayBuffer; }; /** * Tests supplied object to determine if it implements the ArrayBufferView (TypedArray) interface * @param {Object[object]} */ jsPDFAPI.isArrayBufferView = function(object) { if(!this.supportsArrayBuffer()) return false; if(typeof Uint32Array === 'undefined') return false; return (object instanceof Int8Array || object instanceof Uint8Array || (typeof Uint8ClampedArray !== 'undefined' && object instanceof Uint8ClampedArray) || object instanceof Int16Array || object instanceof Uint16Array || object instanceof Int32Array || object instanceof Uint32Array || object instanceof Float32Array || object instanceof Float64Array ); }; /** * Exactly what it says on the tin */ jsPDFAPI.binaryStringToUint8Array = function(binary_string) { /* * not sure how efficient this will be will bigger files. Is there a native method? */ var len = binary_string.length; var bytes = new Uint8Array( len ); for (var i = 0; i < len; i++) { bytes[i] = binary_string.charCodeAt(i); } return bytes; }; /** * @see this discussion * http://stackoverflow.com/questions/6965107/converting-between-strings-and-arraybuffers * * As stated, i imagine the method below is highly inefficent for large files. * * Also of note from Mozilla, * * "However, this is slow and error-prone, due to the need for multiple conversions (especially if the binary data is not actually byte-format data, but, for example, 32-bit integers or floats)." * * https://developer.mozilla.org/en-US/Add-ons/Code_snippets/StringView * * Although i'm strugglig to see how StringView solves this issue? Doesn't appear to be a direct method for conversion? * * Async method using Blob and FileReader could be best, but i'm not sure how to fit it into the flow? */ jsPDFAPI.arrayBufferToBinaryString = function(buffer) { if(this.isArrayBuffer(buffer)) buffer = new Uint8Array(buffer); var binary_string = ''; var len = buffer.byteLength; for (var i = 0; i < len; i++) { binary_string += String.fromCharCode(buffer[i]); } return binary_string; /* * Another solution is the method below - convert array buffer straight to base64 and then use atob */ //return atob(this.arrayBufferToBase64(buffer)); }; /** * Converts an ArrayBuffer directly to base64 * * Taken from here * * http://jsperf.com/encoding-xhr-image-data/31 * * Need to test if this is a better solution for larger files * */ jsPDFAPI.arrayBufferToBase64 = function(arrayBuffer) { var base64 = '' var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' var bytes = new Uint8Array(arrayBuffer) var byteLength = bytes.byteLength var byteRemainder = byteLength % 3 var mainLength = byteLength - byteRemainder var a, b, c, d var chunk // Main loop deals with bytes in chunks of 3 for (var i = 0; i < mainLength; i = i + 3) { // Combine the three bytes into a single integer chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2] // Use bitmasks to extract 6-bit segments from the triplet a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18 b = (chunk & 258048) >> 12 // 258048 = (2^6 - 1) << 12 c = (chunk & 4032) >> 6 // 4032 = (2^6 - 1) << 6 d = chunk & 63 // 63 = 2^6 - 1 // Convert the raw binary segments to the appropriate ASCII encoding base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d] } // Deal with the remaining bytes and padding if (byteRemainder == 1) { chunk = bytes[mainLength] a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2 // Set the 4 least significant bits to zero b = (chunk & 3) << 4 // 3 = 2^2 - 1 base64 += encodings[a] + encodings[b] + '==' } else if (byteRemainder == 2) { chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1] a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10 b = (chunk & 1008) >> 4 // 1008 = (2^6 - 1) << 4 // Set the 2 least significant bits to zero c = (chunk & 15) << 2 // 15 = 2^4 - 1 base64 += encodings[a] + encodings[b] + encodings[c] + '=' } return base64 }; jsPDFAPI.createImageInfo = function(data, wd, ht, cs, bpc, f, imageIndex, alias, dp, trns, pal, smask) { var info = { alias:alias, w : wd, h : ht, cs : cs, bpc : bpc, i : imageIndex, data : data // n: objectNumber will be added by putImage code }; if(f) info.f = f; if(dp) info.dp = dp; if(trns) info.trns = trns; if(pal) info.pal = pal; if(smask) info.smask = smask; return info; }; jsPDFAPI.addImage = function(imageData, format, x, y, w, h, alias, compression, rotation) { 'use strict' if(typeof format !== 'string') { var tmp = h; h = w; w = y; y = x; x = format; format = tmp; } if (typeof imageData === 'object' && !isDOMElement(imageData) && "imageData" in imageData) { var options = imageData; imageData = options.imageData; format = options.format || format; x = options.x || x || 0; y = options.y || y || 0; w = options.w || w; h = options.h || h; alias = options.alias || alias; compression = options.compression || compression; rotation = options.rotation || options.angle || rotation; } if (isNaN(x) || isNaN(y)) { console.error('jsPDF.addImage: Invalid coordinates', arguments); throw new Error('Invalid coordinates passed to jsPDF.addImage'); } var images = getImages.call(this), info; if (!(info = checkImagesForAlias(imageData, images))) { var dataAsBinaryString; if(isDOMElement(imageData)) imageData = createDataURIFromElement(imageData, format, rotation); if(notDefined(alias)) alias = generateAliasFromData(imageData); if (!(info = checkImagesForAlias(alias, images))) { if(this.isString(imageData)) { var base64Info = this.extractInfoFromBase64DataURI(imageData); if(base64Info) { format = base64Info[2]; imageData = atob(base64Info[3]);//convert to binary string } else { if (imageData.charCodeAt(0) === 0x89 && imageData.charCodeAt(1) === 0x50 && imageData.charCodeAt(2) === 0x4e && imageData.charCodeAt(3) === 0x47 ) format = 'png'; } } format = (format || 'JPEG').toLowerCase(); if(doesNotSupportImageType(format)) throw new Error('addImage currently only supports formats ' + supported_image_types + ', not \''+format+'\''); if(processMethodNotEnabled(format)) throw new Error('please ensure that the plugin for \''+format+'\' support is added'); /** * need to test if it's more efficent to convert all binary strings * to TypedArray - or should we just leave and process as string? */ if(this.supportsArrayBuffer()) { dataAsBinaryString = imageData; imageData = this.binaryStringToUint8Array(imageData); } info = this['process' + format.toUpperCase()]( imageData, getImageIndex(images), alias, checkCompressValue(compression), dataAsBinaryString ); if(!info) throw new Error('An unkwown error occurred whilst processing the image'); } } writeImageToPDF.call(this, x, y, w, h, info, info.i, images); return this }; /** * JPEG SUPPORT **/ //takes a string imgData containing the raw bytes of //a jpeg image and returns [width, height] //Algorithm from: http://www.64lines.com/jpeg-width-height var getJpegSize = function(imgData) { 'use strict' var width, height, numcomponents; // Verify we have a valid jpeg header 0xff,0xd8,0xff,0xe0,?,?,'J','F','I','F',0x00 if (!imgData.charCodeAt(0) === 0xff || !imgData.charCodeAt(1) === 0xd8 || !imgData.charCodeAt(2) === 0xff || !imgData.charCodeAt(3) === 0xe0 || !imgData.charCodeAt(6) === 'J'.charCodeAt(0) || !imgData.charCodeAt(7) === 'F'.charCodeAt(0) || !imgData.charCodeAt(8) === 'I'.charCodeAt(0) || !imgData.charCodeAt(9) === 'F'.charCodeAt(0) || !imgData.charCodeAt(10) === 0x00) { throw new Error('getJpegSize requires a binary string jpeg file') } var blockLength = imgData.charCodeAt(4)*256 + imgData.charCodeAt(5); var i = 4, len = imgData.length; while ( i < len ) { i += blockLength; if (imgData.charCodeAt(i) !== 0xff) { throw new Error('getJpegSize could not find the size of the image'); } if (imgData.charCodeAt(i+1) === 0xc0 || //(SOF) Huffman - Baseline DCT imgData.charCodeAt(i+1) === 0xc1 || //(SOF) Huffman - Extended sequential DCT imgData.charCodeAt(i+1) === 0xc2 || // Progressive DCT (SOF2) imgData.charCodeAt(i+1) === 0xc3 || // Spatial (sequential) lossless (SOF3) imgData.charCodeAt(i+1) === 0xc4 || // Differential sequential DCT (SOF5) imgData.charCodeAt(i+1) === 0xc5 || // Differential progressive DCT (SOF6) imgData.charCodeAt(i+1) === 0xc6 || // Differential spatial (SOF7) imgData.charCodeAt(i+1) === 0xc7) { height = imgData.charCodeAt(i+5)*256 + imgData.charCodeAt(i+6); width = imgData.charCodeAt(i+7)*256 + imgData.charCodeAt(i+8); numcomponents = imgData.charCodeAt(i+9); return [width, height, numcomponents]; } else { i += 2; blockLength = imgData.charCodeAt(i)*256 + imgData.charCodeAt(i+1) } } } , getJpegSizeFromBytes = function(data) { var hdr = (data[0] << 8) | data[1]; if(hdr !== 0xFFD8) throw new Error('Supplied data is not a JPEG'); var len = data.length, block = (data[4] << 8) + data[5], pos = 4, bytes, width, height, numcomponents; while(pos < len) { pos += block; bytes = readBytes(data, pos); block = (bytes[2] << 8) + bytes[3]; if((bytes[1] === 0xC0 || bytes[1] === 0xC2) && bytes[0] === 0xFF && block > 7) { bytes = readBytes(data, pos + 5); width = (bytes[2] << 8) + bytes[3]; height = (bytes[0] << 8) + bytes[1]; numcomponents = bytes[4]; return {width:width, height:height, numcomponents: numcomponents}; } pos+=2; } throw new Error('getJpegSizeFromBytes could not find the size of the image'); } , readBytes = function(data, offset) { return data.subarray(offset, offset+ 5); }; jsPDFAPI.processJPEG = function(data, index, alias, compression, dataAsBinaryString) { 'use strict' var colorSpace = this.color_spaces.DEVICE_RGB, filter = this.decode.DCT_DECODE, bpc = 8, dims; if(this.isString(data)) { dims = getJpegSize(data); return this.createImageInfo(data, dims[0], dims[1], dims[3] == 1 ? this.color_spaces.DEVICE_GRAY:colorSpace, bpc, filter, index, alias); } if(this.isArrayBuffer(data)) data = new Uint8Array(data); if(this.isArrayBufferView(data)) { dims = getJpegSizeFromBytes(data); // if we already have a stored binary string rep use that data = dataAsBinaryString || this.arrayBufferToBinaryString(data); return this.createImageInfo(data, dims.width, dims.height, dims.numcomponents == 1 ? this.color_spaces.DEVICE_GRAY:colorSpace, bpc, filter, index, alias); } return null; }; jsPDFAPI.processJPG = function(/*data, index, alias, compression, dataAsBinaryString*/) { return this.processJPEG.apply(this, arguments); } })(jsPDF.API);