diff --git a/examples/images.html b/examples/images.html new file mode 100644 index 000000000..ee42b76a6 --- /dev/null +++ b/examples/images.html @@ -0,0 +1,159 @@ + + + + + jsPDF png test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/images/24_bit.png b/examples/images/24_bit.png new file mode 100644 index 000000000..3f4b22b85 Binary files /dev/null and b/examples/images/24_bit.png differ diff --git a/examples/images/32_bit.png b/examples/images/32_bit.png new file mode 100644 index 000000000..132547469 Binary files /dev/null and b/examples/images/32_bit.png differ diff --git a/examples/images/RGBA_16bpc.png b/examples/images/RGBA_16bpc.png new file mode 100644 index 000000000..14810a0ca Binary files /dev/null and b/examples/images/RGBA_16bpc.png differ diff --git a/examples/images/RGB_16bpc.png b/examples/images/RGB_16bpc.png new file mode 100644 index 000000000..427bd0432 Binary files /dev/null and b/examples/images/RGB_16bpc.png differ diff --git a/examples/images/grayscale_16bpc.png b/examples/images/grayscale_16bpc.png new file mode 100644 index 000000000..ced8fabe0 Binary files /dev/null and b/examples/images/grayscale_16bpc.png differ diff --git a/examples/images/grayscale_8bpc.png b/examples/images/grayscale_8bpc.png new file mode 100644 index 000000000..f4ca224e0 Binary files /dev/null and b/examples/images/grayscale_8bpc.png differ diff --git a/examples/images/grayscale_alpha_16_bpc.png b/examples/images/grayscale_alpha_16_bpc.png new file mode 100644 index 000000000..a62f333f3 Binary files /dev/null and b/examples/images/grayscale_alpha_16_bpc.png differ diff --git a/examples/images/grayscale_alpha_8bpc.png b/examples/images/grayscale_alpha_8bpc.png new file mode 100644 index 000000000..49c3037eb Binary files /dev/null and b/examples/images/grayscale_alpha_8bpc.png differ diff --git a/examples/images/grid.png b/examples/images/grid.png new file mode 100644 index 000000000..d4f71e344 Binary files /dev/null and b/examples/images/grid.png differ diff --git a/examples/images/jpg.jpg b/examples/images/jpg.jpg new file mode 100644 index 000000000..b2edf05a9 Binary files /dev/null and b/examples/images/jpg.jpg differ diff --git a/examples/images/png8_flat.png b/examples/images/png8_flat.png new file mode 100644 index 000000000..57071de06 Binary files /dev/null and b/examples/images/png8_flat.png differ diff --git a/examples/images/png8_trans.png b/examples/images/png8_trans.png new file mode 100644 index 000000000..e84cf5ab4 Binary files /dev/null and b/examples/images/png8_trans.png differ diff --git a/examples/images/tiny_png_indexed.png b/examples/images/tiny_png_indexed.png new file mode 100644 index 000000000..f78c4ba17 Binary files /dev/null and b/examples/images/tiny_png_indexed.png differ diff --git a/jspdf.plugin.addimage.js b/jspdf.plugin.addimage.js index 5db9d5f37..f97678be7 100644 --- a/jspdf.plugin.addimage.js +++ b/jspdf.plugin.addimage.js @@ -1,5 +1,5 @@ /** @preserve - * jsPDF addImage plugin (JPEG only at this time) + * 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 @@ -28,238 +28,623 @@ */ ;(function(jsPDFAPI) { -'use strict' - -var namespace = 'addImage_' - -// 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; - // 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 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); - return [width, height]; + + 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'); } } -} -// Image functionality ported from pdf.js -, putImage = function(img) { - var objectNumber = this.internal.newObject() - , out = this.internal.write - , putStream = this.internal.putStream - - img['n'] = objectNumber - - out('<>'); + , checkCompressValue = function(value) { + if(value && typeof value === 'string') + value = value.toUpperCase(); + return value in jsPDFAPI.image_compression ? value : jsPDFAPI.image_compression.NONE; } - if ('trns' in img && img['trns'].constructor == Array) { - var trns = ''; - for ( var i = 0; i < img['trns'].length; i++) { - trns += (img[trns][i] + ' ' + img['trns'][i] + ' '); - out('/Mask [' + trns + ']'); + , 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; } - if ('smask' in img) { - out('/SMask ' + (objectNumber + 1) + ' 0 R'); + , notDefined = function(value) { + return typeof value === 'undefined' || value === null; } - out('/Length ' + img['data'].length + '>>'); - - putStream(img['data']); - - out('endobj'); -} -, putResourcesCallback = function() { - var images = this.internal.collections[namespace + 'images'] - for ( var i in images ) { - putImage.call(this, images[i]) + , generateAliasFromData = function(data) { + // TODO: Alias dynamic generation from imageData's checksum/hash + return undefined; } -} -, 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' - ) + , doesNotSupportImageType = function(type) { + return supported_image_types.indexOf(type) === -1; } -} - -jsPDFAPI.addImage = function(imageData, format, x, y, w, h, alias) { - 'use strict' - var images = this.internal.collections[namespace + 'images'], - cached_info; - - if(typeof format === 'number') { - var tmp = h; - h = w; - w = y; - y = x; - x = format; - format = tmp || 'JPEG'; + , processMethodNotEnabled = function(type) { + return typeof jsPDFAPI['process' + type.toUpperCase()] !== 'function'; } - if(typeof alias === 'undefined') { - // TODO: Alias dynamic generation from imageData's checksum/hash + , isDOMElement = function(object) { + return typeof object === 'object' && object.nodeType === 1; } + , createDataURIFromElement = function(imageData, format) { + + var canvas = document.createElement('canvas'); + canvas.width = imageData.clientWidth || imageData.width; + canvas.height = imageData.clientHeight || imageData.height; + + var ctx = canvas.getContext('2d'); + if (!ctx) { + throw ('addImage requires canvas to be supported by browser.'); + } + ctx.drawImage(imageData, 0, 0, canvas.width, canvas.height); + + return canvas.toDataURL(format == 'png' ? 'image/png' : 'image/jpeg'); + } + ,checkImagesForAlias = function(imageData, images) { + var cached_info; + if(images) { + for(var e in images) { + if(imageData === images[e].alias) { + cached_info = images[e]; + break; + } + } + } + return cached_info; + } + ,determineWidthAndHeight = function(w, h) { + 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(w, h), + 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.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 === 'function'; + }; + + /** + * 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; + return (object instanceof Int8Array || + object instanceof Uint8Array || + 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+/' - if(typeof imageData === 'string') { - if(imageData.charCodeAt(0) !== 0xff && imageData.substr(0,5) !== 'data:') { - // This is neither raw jpeg-data nor a data uri; alias? - if(images) { + var bytes = new Uint8Array(arrayBuffer) + var byteLength = bytes.byteLength + var byteRemainder = byteLength % 3 + var mainLength = byteLength - byteRemainder - for(var e in images) { + var a, b, c, d + var chunk - if(imageData === images[e].alias) { - cached_info = images[e]; - break; - } - } - } + // 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] - } else if(imageData.substr(0,14) === 'data:image/jpg') { - imageData = imageData.replace('data:image/jpg','data:image/jpeg'); + // 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] } - } - if (typeof imageData === 'object' && imageData.nodeType === 1) { - var canvas = document.createElement('canvas'); - canvas.width = imageData.clientWidth || imageData.width; - canvas.height = imageData.clientHeight || imageData.height; - var ctx = canvas.getContext('2d'); - if (!ctx) { - throw ('addImage requires canvas to be supported by browser.'); - } - ctx.drawImage(imageData, 0, 0, canvas.width, canvas.height); - imageData = canvas.toDataURL('image/jpeg'); - format = "JPEG"; - } - if (format.toUpperCase() !== 'JPEG') { - throw new Error('addImage currently only supports format \'JPEG\', not \''+format+'\''); - } + // Deal with the remaining bytes and padding + if (byteRemainder == 1) { + chunk = bytes[mainLength] - var imageIndex - , coord = this.internal.getCoordinateString - , vcoord = this.internal.getVerticalCoordinateString; + a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2 - // Detect if the imageData is raw binary or Data URL - if (imageData.substring(0, 23) === 'data:image/jpeg;base64,') { - imageData = atob(imageData.replace('data:image/jpeg;base64,', '')); - } + // Set the 4 least significant bits to zero + b = (chunk & 3) << 4 // 3 = 2^2 - 1 - 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) - } else { - // this is the first time this method is ran on this instance of jsPDF object. - imageIndex = 0 - this.internal.collections[namespace + 'images'] = images = {} - this.internal.events.subscribe('putResources', putResourcesCallback) - this.internal.events.subscribe('putXobjectDict', putXObjectsDictCallback) - } + base64 += encodings[a] + encodings[b] + '==' + } else if (byteRemainder == 2) { + chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1] - var info = cached_info || (function(dims) { - return images[imageIndex] = { - alias:alias, - w : dims[0], - h : dims[1], - cs : 'DeviceRGB', - bpc : 8, - f : 'DCTDecode', - i : imageIndex, - data : imageData - // n: objectNumber will be added by putImage code - }; - })(getJpegSize(imageData)); + a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10 + b = (chunk & 1008) >> 4 // 1008 = (2^6 - 1) << 4 - 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; + // 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) { + 'use strict' + + if(typeof format === 'number') { + var tmp = h; + h = w; + w = y; + y = x; + x = format; + format = tmp || 'jpeg'; + } + + var images = getImages.call(this),//initalises internals and events on first run + cached_info, + dataAsBinaryString; + + compression = checkCompressValue(compression); + format = format.toLowerCase(); + + if(notDefined(alias)) + alias = generateAliasFromData(imageData); + + if(isDOMElement(imageData)) + imageData = createDataURIFromElement(imageData, format); + + if(this.isString(imageData)) { + + var base64Info = this.extractInfoFromBase64DataURI(imageData); + + if(base64Info) { + + format = base64Info[2]; + imageData = atob(base64Info[3]);//convert to binary string + + /* + * 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); + } + + }else{ + // This is neither raw jpeg-data nor a data uri; alias? + if(imageData.charCodeAt(0) !== 0xff) + cached_info = checkImagesForAlias(imageData, images); + } + } + + 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'); + + var imageIndex = getImageIndex(images), + info = cached_info; + + if(!info) + info = this['process' + format.toUpperCase()](imageData, imageIndex, alias, compression, dataAsBinaryString); + + if(!info) + throw new Error('An unkwown error occurred whilst processing the image'); + + writeImageToPDF.call(this, x, y, w, h, info, imageIndex, 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; + // 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); + return [width, height]; + } else { + i += 2; + blockLength = imgData.charCodeAt(i)*256 + imgData.charCodeAt(i+1) + } + } } - if (w === 0) { - w = h * info['w'] / info['h']; + , 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; + + 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]; + return {width:width, height:height}; + } + + pos+=2; + } + + throw new Error('getJpegSizeFromBytes could not find the size of the image'); } - if (h === 0) { - h = w * info['h'] / info['w']; + , readBytes = function(data, offset) { + return data.subarray(offset, offset+ 4); + }; + + + 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], 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, colorSpace, bpc, filter, index, alias); + } + + return null; + }; + + jsPDFAPI.processJPG = function(data, index, alias, compression, dataAsBinaryString) { + return this.processJPEG(data, index, alias, compression, dataAsBinaryString); } - 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' - ) - - return this -} })(jsPDF.API); diff --git a/jspdf.plugin.png_support.js b/jspdf.plugin.png_support.js new file mode 100755 index 000000000..391ab70f0 --- /dev/null +++ b/jspdf.plugin.png_support.js @@ -0,0 +1,517 @@ +/**@preserve + * ==================================================================== + * jsPDF [NAME] plugin + * Copyright (c) 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' + + /* + * @see http://www.w3.org/TR/PNG-Chunks.html + * + Color Allowed Interpretation + Type Bit Depths + + 0 1,2,4,8,16 Each pixel is a grayscale sample. + + 2 8,16 Each pixel is an R,G,B triple. + + 3 1,2,4,8 Each pixel is a palette index; + a PLTE chunk must appear. + + 4 8,16 Each pixel is a grayscale sample, + followed by an alpha sample. + + 6 8,16 Each pixel is an R,G,B triple, + followed by an alpha sample. + */ + + /* + * PNG filter method types + * + * @see http://www.w3.org/TR/PNG-Filters.html + * @see http://www.libpng.org/pub/png/book/chapter09.html + * + * This is what the value 'Predictor' in decode params relates to + * + * 15 is "optimal prediction", which means the prediction algorithm can change from line to line. + * In that case, you actually have to read the first byte off each line for the prediction algorthim (which should be 0-4, corresponding to PDF 10-14) and select the appropriate unprediction algorithm based on that byte. + * + 0 None + 1 Sub + 2 Up + 3 Average + 4 Paeth + */ + + var doesNotHavePngJS = function() { + return typeof PNG !== 'function' || typeof FlateStream !== 'function'; + } + , canCompress = function(value) { + return value !== jsPDFAPI.image_compression.NONE && hasCompressionJS(); + } + , hasCompressionJS = function() { + var inst = typeof Deflater === 'function'; + if(!inst) + throw new Error("requires deflate.js for compression") + return inst; + } + , compressBytes = function(bytes, lineLength, colorsPerPixel, compression) { + + var level = 5, + filter_method = filterUp; + + switch(compression) { + + case jsPDFAPI.image_compression.FAST: + + level = 3; + filter_method = filterSub; + break; + + case jsPDFAPI.image_compression.MEDIUM: + + level = 6; + filter_method = filterAverage; + break; + + case jsPDFAPI.image_compression.SLOW: + + level = 9; + filter_method = filterPaeth;//uses to sum to choose best filter for each line + break; + } + + bytes = applyPngFilterMethod(bytes, lineLength, colorsPerPixel, filter_method); + + var header = new Uint8Array(createZlibHeader(level)); + var checksum = adler32(bytes); + + var deflate = new Deflater(level); + var a = deflate.append(bytes); + var cBytes = deflate.flush(); + + var len = header.length + a.length + cBytes.length; + + var cmpd = new Uint8Array(len + 4); + cmpd.set(header); + cmpd.set(a, header.length); + cmpd.set(cBytes, header.length + a.length); + + cmpd[len++] = (checksum >>> 24) & 0xff; + cmpd[len++] = (checksum >>> 16) & 0xff; + cmpd[len++] = (checksum >>> 8) & 0xff; + cmpd[len++] = checksum & 0xff; + + return jsPDFAPI.arrayBufferToBinaryString(cmpd); + } + , createZlibHeader = function(bytes, level){ + /* + * @see http://www.ietf.org/rfc/rfc1950.txt for zlib header + */ + var cm = 8; + var cinfo = Math.LOG2E * Math.log(0x8000) - 8; + var cmf = (cinfo << 4) | cm; + + var hdr = cmf << 8; + var flevel = Math.min(3, ((level - 1) & 0xff) >> 1); + + hdr |= (flevel << 6); + hdr |= 0;//FDICT + hdr += 31 - (hdr % 31); + + return [cmf, (hdr & 0xff) & 0xff]; + } + , adler32 = function(array, param) { + var adler = 1; + var s1 = adler & 0xffff, + s2 = (adler >>> 16) & 0xffff; + var len = array.length; + var tlen; + var i = 0; + + while (len > 0) { + tlen = len > param ? param : len; + len -= tlen; + do { + s1 += array[i++]; + s2 += s1; + } while (--tlen); + + s1 %= 65521; + s2 %= 65521; + } + + return ((s2 << 16) | s1) >>> 0; + } + , applyPngFilterMethod = function(bytes, lineLength, colorsPerPixel, filter_method) { + var lines = bytes.length / lineLength, + result = new Uint8Array(bytes.length + lines), + filter_methods = getFilterMethods(), + i = 0, line, prevLine, offset; + + for(; i < lines; i++) { + offset = i * lineLength; + line = bytes.subarray(offset, offset + lineLength); + + if(filter_method) { + result.set(filter_method(line, colorsPerPixel, prevLine), offset + i); + + }else{ + + var j = 0, + len = filter_methods.length, + results = []; + + for(; j < len; j++) + results[j] = filter_methods[j](line, colorsPerPixel, prevLine); + + var ind = getIndexOfSmallestSum(results.concat()); + + result.set(results[ind], offset + i); + } + + prevLine = line; + } + + return result; + } + , filterNone = function(line, colorsPerPixel, prevLine) { + /*var result = new Uint8Array(line.length + 1); + result[0] = 0; + result.set(line, 1);*/ + + var result = Array.apply([], line); + result.unshift(0); + + return result; + } + , filterSub = function(line, colorsPerPixel, prevLine) { + var result = [], + i = 0, + len = line.length, + left; + + result[0] = 1; + + for(; i < len; i++) { + left = line[i - colorsPerPixel] || 0; + result[i + 1] = (line[i] - left + 0x0100) & 0xff; + } + + return result; + } + , filterUp = function(line, colorsPerPixel, prevLine) { + var result = [], + i = 0, + len = line.length, + up; + + result[0] = 2; + + for(; i < len; i++) { + up = prevLine && prevLine[i] || 0; + result[i + 1] = (line[i] - up + 0x0100) & 0xff; + } + + return result; + } + , filterAverage = function(line, colorsPerPixel, prevLine) { + var result = [], + i = 0, + len = line.length, + left, + up; + + result[0] = 3; + + for(; i < len; i++) { + left = line[i - colorsPerPixel] || 0; + up = prevLine && prevLine[i] || 0; + result[i + 1] = (line[i] + 0x0100 - ((left + up) >>> 1)) & 0xff; + } + + return result; + } + , filterPaeth = function(line, colorsPerPixel, prevLine) { + var result = [], + i = 0, + len = line.length, + left, + up, + upLeft, + paeth; + + result[0] = 4; + + for(; i < len; i++) { + left = line[i - colorsPerPixel] || 0; + up = prevLine && prevLine[i] || 0; + upLeft = prevLine && prevLine[i - colorsPerPixel] || 0; + paeth = paethPredictor(left, up, upLeft); + result[i + 1] = (line[i] - paeth + 0x0100) & 0xff; + } + + return result; + } + ,paethPredictor = function(left, up, upLeft) { + + var p = left + up - upLeft, + pLeft = Math.abs(p - left), + pUp = Math.abs(p - up), + pUpLeft = Math.abs(p - upLeft); + + return (pLeft <= pUp && pLeft <= pUpLeft) ? left : (pUp <= pUpLeft) ? up : upLeft; + } + , getFilterMethods = function() { + return [filterNone, filterSub, filterUp, filterAverage, filterPaeth]; + } + ,getIndexOfSmallestSum = function(arrays) { + var i = 0, + len = arrays.length, + sum, min, ind; + + while(i < len) { + sum = absSum(arrays[i].slice(1)); + + if(sum < min || !min) { + min = sum; + ind = i; + } + + i++; + } + + return ind; + } + , absSum = function(array) { + var i = 0, + len = array.length, + sum = 0; + + while(i < len) + sum += Math.abs(array[i++]); + + return sum; + } + , logImg = function(img) { + console.log("width: " + img.width); + console.log("height: " + img.height); + console.log("bits: " + img.bits); + console.log("colorType: " + img.colorType); + console.log("transparency:"); + console.log(img.transparency); + console.log("text:"); + console.log(img.text); + console.log("compressionMethod: " + img.compressionMethod); + console.log("filterMethod: " + img.filterMethod); + console.log("interlaceMethod: " + img.interlaceMethod); + console.log("imgData:"); + console.log(img.imgData); + console.log("palette:"); + console.log(img.palette); + console.log("colors: " + img.colors); + console.log("colorSpace: " + img.colorSpace); + console.log("pixelBitlength: " + img.pixelBitlength); + console.log("hasAlphaChannel: " + img.hasAlphaChannel); + }; + + + + + jsPDFAPI.processPNG = function(imageData, imageIndex, alias, compression, dataAsBinaryString) { + 'use strict' + + var colorSpace = this.color_spaces.DEVICE_RGB, + decode = this.decode.FLATE_DECODE, + bpc = 8, + img, dp, trns, + colors, pal, smask; + + if(this.isString(imageData)) { + + } + + if(this.isArrayBuffer(imageData)) + imageData = new Uint8Array(imageData); + + if(this.isArrayBufferView(imageData)) { + + if(doesNotHavePngJS()) + throw new Error("PNG support requires png.js and zlib.js"); + + img = new PNG(imageData); + imageData = img.imgData; + bpc = img.bits; + colorSpace = img.colorSpace; + colors = img.colors; + + //logImg(img); + + /* + * colorType 6 - Each pixel is an R,G,B triple, followed by an alpha sample. + * + * colorType 4 - Each pixel is a grayscale sample, followed by an alpha sample. + * + * Extract alpha to create two separate images, using the alpha as a sMask + */ + if([4,6].indexOf(img.colorType) !== -1) { + + /* + * processes 8 bit RGBA and grayscale + alpha images + */ + if(img.bits === 8) { + + var pixelsArrayType = window['Uint' + img.pixelBitlength + 'Array'], + pixels = new pixelsArrayType(img.decodePixels().buffer), + len = pixels.length, + imgData = new Uint8Array(len * img.colors), + alphaData = new Uint8Array(len), + pDiff = img.pixelBitlength - img.bits, + i = 0, n = 0, pixel, pbl; + + for(; i < len; i++) { + pixel = pixels[i]; + pbl = 0; + + while(pbl < pDiff) { + + imgData[n++] = ( pixel >>> pbl ) & 0xff; + pbl = pbl + img.bits; + } + + alphaData[i] = ( pixel >>> pbl ) & 0xff; + } + } + + /* + * processes 16 bit RGBA and grayscale + alpha images + */ + if(img.bits === 16) { + + var pixels = new Uint32Array(img.decodePixels().buffer), + len = pixels.length, + imgData = new Uint8Array((len * (32 / img.pixelBitlength) ) * img.colors), + alphaData = new Uint8Array(len * (32 / img.pixelBitlength) ), + hasColors = img.colors > 1, + i = 0, n = 0, a = 0, pixel; + + while(i < len) { + pixel = pixels[i++]; + + imgData[n++] = (pixel >>> 0) & 0xFF; + + if(hasColors) { + imgData[n++] = (pixel >>> 16) & 0xFF; + + pixel = pixels[i++]; + imgData[n++] = (pixel >>> 0) & 0xFF; + } + + alphaData[a++] = (pixel >>> 16) & 0xFF; + } + + bpc = 8; + } + + if(canCompress(compression)) { + + imageData = compressBytes(imgData, img.width * img.colors, img.colors, compression); + smask = compressBytes(alphaData, img.width, 1, compression); + + }else{ + + imageData = imgData; + smask = alphaData; + decode = null; + } + } + + /* + * Indexed png. Each pixel is a palette index. + */ + if(img.colorType === 3) { + + colorSpace = this.color_spaces.INDEXED; + pal = img.palette; + + if(img.transparency.indexed) { + + var trans = img.transparency.indexed; + + var total = 0, + i = 0, + len = trans.length; + + for(; i chunkSize; i = 0 <= chunkSize ? ++_i : --_i) { + data.push(this.data[this.pos++]); + } + break; + case 'tRNS': + this.transparency = {}; + switch (this.colorType) { + case 3: + palLen = this.palette.length/3; + this.transparency.indexed = this.read(chunkSize); + if(this.transparency.indexed.length > palLen) + throw new Error('More transparent colors than palette size'); + /* + * According to the PNG spec trns should be increased to the same size as palette if shorter + */ + //short = 255 - this.transparency.indexed.length; + short = palLen - this.transparency.indexed.length; + if (short > 0) { + for (i = _j = 0; 0 <= short ? _j < short : _j > short; i = 0 <= short ? ++_j : --_j) { + this.transparency.indexed.push(255); + } + } + break; + case 0: + this.transparency.grayscale = this.read(chunkSize)[0]; + break; + case 2: + this.transparency.rgb = this.read(chunkSize); + } + break; + case 'tEXt': + text = this.read(chunkSize); + index = text.indexOf(0); + key = String.fromCharCode.apply(String, text.slice(0, index)); + this.text[key] = String.fromCharCode.apply(String, text.slice(index + 1)); + break; + case 'IEND': + if (frame) { + this.animation.frames.push(frame); + } + this.colors = (function() { + switch (this.colorType) { + case 0: + case 3: + case 4: + return 1; + case 2: + case 6: + return 3; + } + }).call(this); + this.hasAlphaChannel = (_ref = this.colorType) === 4 || _ref === 6; + colors = this.colors + (this.hasAlphaChannel ? 1 : 0); + this.pixelBitlength = this.bits * colors; + this.colorSpace = (function() { + switch (this.colors) { + case 1: + return 'DeviceGray'; + case 3: + return 'DeviceRGB'; + } + }).call(this); + this.imgData = new Uint8Array(this.imgData); + return; + default: + this.pos += chunkSize; + } + this.pos += 4; + if (this.pos > this.data.length) { + throw new Error("Incomplete or corrupt PNG file"); + } + } + return; + } + + PNG.prototype.read = function(bytes) { + var i, _i, _results; + _results = []; + for (i = _i = 0; 0 <= bytes ? _i < bytes : _i > bytes; i = 0 <= bytes ? ++_i : --_i) { + _results.push(this.data[this.pos++]); + } + return _results; + }; + + PNG.prototype.readUInt32 = function() { + var b1, b2, b3, b4; + b1 = this.data[this.pos++] << 24; + b2 = this.data[this.pos++] << 16; + b3 = this.data[this.pos++] << 8; + b4 = this.data[this.pos++]; + return b1 | b2 | b3 | b4; + }; + + PNG.prototype.readUInt16 = function() { + var b1, b2; + b1 = this.data[this.pos++] << 8; + b2 = this.data[this.pos++]; + return b1 | b2; + }; + + PNG.prototype.decodePixels = function(data) { + var byte, c, col, i, left, length, p, pa, paeth, pb, pc, pixelBytes, pixels, pos, row, scanlineLength, upper, upperLeft, _i, _j, _k, _l, _m; + if (data == null) { + data = this.imgData; + } + if (data.length === 0) { + return new Uint8Array(0); + } + data = new FlateStream(data); + data = data.getBytes(); + pixelBytes = this.pixelBitlength / 8; + scanlineLength = pixelBytes * this.width; + pixels = new Uint8Array(scanlineLength * this.height); + length = data.length; + row = 0; + pos = 0; + c = 0; + while (pos < length) { + switch (data[pos++]) { + case 0: + for (i = _i = 0; _i < scanlineLength; i = _i += 1) { + pixels[c++] = data[pos++]; + } + break; + case 1: + for (i = _j = 0; _j < scanlineLength; i = _j += 1) { + byte = data[pos++]; + left = i < pixelBytes ? 0 : pixels[c - pixelBytes]; + pixels[c++] = (byte + left) % 256; + } + break; + case 2: + for (i = _k = 0; _k < scanlineLength; i = _k += 1) { + byte = data[pos++]; + col = (i - (i % pixelBytes)) / pixelBytes; + upper = row && pixels[(row - 1) * scanlineLength + col * pixelBytes + (i % pixelBytes)]; + pixels[c++] = (upper + byte) % 256; + } + break; + case 3: + for (i = _l = 0; _l < scanlineLength; i = _l += 1) { + byte = data[pos++]; + col = (i - (i % pixelBytes)) / pixelBytes; + left = i < pixelBytes ? 0 : pixels[c - pixelBytes]; + upper = row && pixels[(row - 1) * scanlineLength + col * pixelBytes + (i % pixelBytes)]; + pixels[c++] = (byte + Math.floor((left + upper) / 2)) % 256; + } + break; + case 4: + for (i = _m = 0; _m < scanlineLength; i = _m += 1) { + byte = data[pos++]; + col = (i - (i % pixelBytes)) / pixelBytes; + left = i < pixelBytes ? 0 : pixels[c - pixelBytes]; + if (row === 0) { + upper = upperLeft = 0; + } else { + upper = pixels[(row - 1) * scanlineLength + col * pixelBytes + (i % pixelBytes)]; + upperLeft = col && pixels[(row - 1) * scanlineLength + (col - 1) * pixelBytes + (i % pixelBytes)]; + } + p = left + upper - upperLeft; + pa = Math.abs(p - left); + pb = Math.abs(p - upper); + pc = Math.abs(p - upperLeft); + if (pa <= pb && pa <= pc) { + paeth = left; + } else if (pb <= pc) { + paeth = upper; + } else { + paeth = upperLeft; + } + pixels[c++] = (byte + paeth) % 256; + } + break; + default: + throw new Error("Invalid filter algorithm: " + data[pos - 1]); + } + row++; + } + return pixels; + }; + + PNG.prototype.decodePalette = function() { + var c, i, length, palette, pos, ret, transparency, _i, _ref, _ref1; + palette = this.palette; + transparency = this.transparency.indexed || []; + ret = new Uint8Array((transparency.length || 0) + palette.length); + pos = 0; + length = palette.length; + c = 0; + for (i = _i = 0, _ref = palette.length; _i < _ref; i = _i += 3) { + ret[pos++] = palette[i]; + ret[pos++] = palette[i + 1]; + ret[pos++] = palette[i + 2]; + ret[pos++] = (_ref1 = transparency[c++]) != null ? _ref1 : 255; + } + return ret; + }; + + PNG.prototype.copyToImageData = function(imageData, pixels) { + var alpha, colors, data, i, input, j, k, length, palette, v, _ref; + colors = this.colors; + palette = null; + alpha = this.hasAlphaChannel; + if (this.palette.length) { + palette = (_ref = this._decodedPalette) != null ? _ref : this._decodedPalette = this.decodePalette(); + colors = 4; + alpha = true; + } + data = imageData.data || imageData; + length = data.length; + input = palette || pixels; + i = j = 0; + if (colors === 1) { + while (i < length) { + k = palette ? pixels[i / 4] * 4 : j; + v = input[k++]; + data[i++] = v; + data[i++] = v; + data[i++] = v; + data[i++] = alpha ? input[k++] : 255; + j = k; + } + } else { + while (i < length) { + k = palette ? pixels[i / 4] * 4 : j; + data[i++] = input[k++]; + data[i++] = input[k++]; + data[i++] = input[k++]; + data[i++] = alpha ? input[k++] : 255; + j = k; + } + } + }; + + PNG.prototype.decode = function() { + var ret; + ret = new Uint8Array(this.width * this.height * 4); + this.copyToImageData(ret, this.decodePixels()); + return ret; + }; + + scratchCanvas = document.createElement('canvas'); + + scratchCtx = scratchCanvas.getContext('2d'); + + makeImage = function(imageData) { + var img; + scratchCtx.width = imageData.width; + scratchCtx.height = imageData.height; + scratchCtx.clearRect(0, 0, imageData.width, imageData.height); + scratchCtx.putImageData(imageData, 0, 0); + img = new Image; + img.src = scratchCanvas.toDataURL(); + return img; + }; + + PNG.prototype.decodeFrames = function(ctx) { + var frame, i, imageData, pixels, _i, _len, _ref, _results; + if (!this.animation) { + return; + } + _ref = this.animation.frames; + _results = []; + for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { + frame = _ref[i]; + imageData = ctx.createImageData(frame.width, frame.height); + pixels = this.decodePixels(new Uint8Array(frame.data)); + this.copyToImageData(imageData, pixels); + frame.imageData = imageData; + _results.push(frame.image = makeImage(imageData)); + } + return _results; + }; + + PNG.prototype.renderFrame = function(ctx, number) { + var frame, frames, prev; + frames = this.animation.frames; + frame = frames[number]; + prev = frames[number - 1]; + if (number === 0) { + ctx.clearRect(0, 0, this.width, this.height); + } + if ((prev != null ? prev.disposeOp : void 0) === APNG_DISPOSE_OP_BACKGROUND) { + ctx.clearRect(prev.xOffset, prev.yOffset, prev.width, prev.height); + } else if ((prev != null ? prev.disposeOp : void 0) === APNG_DISPOSE_OP_PREVIOUS) { + ctx.putImageData(prev.imageData, prev.xOffset, prev.yOffset); + } + if (frame.blendOp === APNG_BLEND_OP_SOURCE) { + ctx.clearRect(frame.xOffset, frame.yOffset, frame.width, frame.height); + } + return ctx.drawImage(frame.image, frame.xOffset, frame.yOffset); + }; + + PNG.prototype.animate = function(ctx) { + var doFrame, frameNumber, frames, numFrames, numPlays, _ref, + _this = this; + frameNumber = 0; + _ref = this.animation, numFrames = _ref.numFrames, frames = _ref.frames, numPlays = _ref.numPlays; + return (doFrame = function() { + var f, frame; + f = frameNumber++ % numFrames; + frame = frames[f]; + _this.renderFrame(ctx, f); + if (numFrames > 1 && frameNumber / numFrames < numPlays) { + return _this.animation._timeout = setTimeout(doFrame, frame.delay); + } + })(); + }; + + PNG.prototype.stopAnimation = function() { + var _ref; + return clearTimeout((_ref = this.animation) != null ? _ref._timeout : void 0); + }; + + PNG.prototype.render = function(canvas) { + var ctx, data; + if (canvas._png) { + canvas._png.stopAnimation(); + } + canvas._png = this; + canvas.width = this.width; + canvas.height = this.height; + ctx = canvas.getContext("2d"); + if (this.animation) { + this.decodeFrames(ctx); + return this.animate(ctx); + } else { + data = ctx.createImageData(this.width, this.height); + this.copyToImageData(data, this.decodePixels()); + return ctx.putImageData(data, 0, 0); + } + }; + + return PNG; + + })(); + + window.PNG = PNG; + +}).call(this); diff --git a/libs/png_support/zlib.js b/libs/png_support/zlib.js new file mode 100755 index 000000000..2d0f40b26 --- /dev/null +++ b/libs/png_support/zlib.js @@ -0,0 +1,464 @@ +/* + * Extracted from pdf.js + * https://github.com/andreasgal/pdf.js + * + * Copyright (c) 2011 Mozilla Foundation + * + * Contributors: Andreas Gal + * Chris G Jones + * Shaon Barman + * Vivien Nicolas <21@vingtetun.org> + * Justin D'Arcangelo + * Yury Delendik + * + * 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. + */ + +var DecodeStream = (function() { + function constructor() { + this.pos = 0; + this.bufferLength = 0; + this.eof = false; + this.buffer = null; + } + + constructor.prototype = { + ensureBuffer: function decodestream_ensureBuffer(requested) { + var buffer = this.buffer; + var current = buffer ? buffer.byteLength : 0; + if (requested < current) + return buffer; + var size = 512; + while (size < requested) + size <<= 1; + var buffer2 = new Uint8Array(size); + for (var i = 0; i < current; ++i) + buffer2[i] = buffer[i]; + return this.buffer = buffer2; + }, + getByte: function decodestream_getByte() { + var pos = this.pos; + while (this.bufferLength <= pos) { + if (this.eof) + return null; + this.readBlock(); + } + return this.buffer[this.pos++]; + }, + getBytes: function decodestream_getBytes(length) { + var pos = this.pos; + + if (length) { + this.ensureBuffer(pos + length); + var end = pos + length; + + while (!this.eof && this.bufferLength < end) + this.readBlock(); + + var bufEnd = this.bufferLength; + if (end > bufEnd) + end = bufEnd; + } else { + while (!this.eof) + this.readBlock(); + + var end = this.bufferLength; + } + + this.pos = end; + return this.buffer.subarray(pos, end); + }, + lookChar: function decodestream_lookChar() { + var pos = this.pos; + while (this.bufferLength <= pos) { + if (this.eof) + return null; + this.readBlock(); + } + return String.fromCharCode(this.buffer[this.pos]); + }, + getChar: function decodestream_getChar() { + var pos = this.pos; + while (this.bufferLength <= pos) { + if (this.eof) + return null; + this.readBlock(); + } + return String.fromCharCode(this.buffer[this.pos++]); + }, + makeSubStream: function decodestream_makeSubstream(start, length, dict) { + var end = start + length; + while (this.bufferLength <= end && !this.eof) + this.readBlock(); + return new Stream(this.buffer, start, length, dict); + }, + skip: function decodestream_skip(n) { + if (!n) + n = 1; + this.pos += n; + }, + reset: function decodestream_reset() { + this.pos = 0; + } + }; + + return constructor; +})(); + +var FlateStream = (function() { + var codeLenCodeMap = new Uint32Array([ + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 + ]); + + var lengthDecode = new Uint32Array([ + 0x00003, 0x00004, 0x00005, 0x00006, 0x00007, 0x00008, 0x00009, 0x0000a, + 0x1000b, 0x1000d, 0x1000f, 0x10011, 0x20013, 0x20017, 0x2001b, 0x2001f, + 0x30023, 0x3002b, 0x30033, 0x3003b, 0x40043, 0x40053, 0x40063, 0x40073, + 0x50083, 0x500a3, 0x500c3, 0x500e3, 0x00102, 0x00102, 0x00102 + ]); + + var distDecode = new Uint32Array([ + 0x00001, 0x00002, 0x00003, 0x00004, 0x10005, 0x10007, 0x20009, 0x2000d, + 0x30011, 0x30019, 0x40021, 0x40031, 0x50041, 0x50061, 0x60081, 0x600c1, + 0x70101, 0x70181, 0x80201, 0x80301, 0x90401, 0x90601, 0xa0801, 0xa0c01, + 0xb1001, 0xb1801, 0xc2001, 0xc3001, 0xd4001, 0xd6001 + ]); + + var fixedLitCodeTab = [new Uint32Array([ + 0x70100, 0x80050, 0x80010, 0x80118, 0x70110, 0x80070, 0x80030, 0x900c0, + 0x70108, 0x80060, 0x80020, 0x900a0, 0x80000, 0x80080, 0x80040, 0x900e0, + 0x70104, 0x80058, 0x80018, 0x90090, 0x70114, 0x80078, 0x80038, 0x900d0, + 0x7010c, 0x80068, 0x80028, 0x900b0, 0x80008, 0x80088, 0x80048, 0x900f0, + 0x70102, 0x80054, 0x80014, 0x8011c, 0x70112, 0x80074, 0x80034, 0x900c8, + 0x7010a, 0x80064, 0x80024, 0x900a8, 0x80004, 0x80084, 0x80044, 0x900e8, + 0x70106, 0x8005c, 0x8001c, 0x90098, 0x70116, 0x8007c, 0x8003c, 0x900d8, + 0x7010e, 0x8006c, 0x8002c, 0x900b8, 0x8000c, 0x8008c, 0x8004c, 0x900f8, + 0x70101, 0x80052, 0x80012, 0x8011a, 0x70111, 0x80072, 0x80032, 0x900c4, + 0x70109, 0x80062, 0x80022, 0x900a4, 0x80002, 0x80082, 0x80042, 0x900e4, + 0x70105, 0x8005a, 0x8001a, 0x90094, 0x70115, 0x8007a, 0x8003a, 0x900d4, + 0x7010d, 0x8006a, 0x8002a, 0x900b4, 0x8000a, 0x8008a, 0x8004a, 0x900f4, + 0x70103, 0x80056, 0x80016, 0x8011e, 0x70113, 0x80076, 0x80036, 0x900cc, + 0x7010b, 0x80066, 0x80026, 0x900ac, 0x80006, 0x80086, 0x80046, 0x900ec, + 0x70107, 0x8005e, 0x8001e, 0x9009c, 0x70117, 0x8007e, 0x8003e, 0x900dc, + 0x7010f, 0x8006e, 0x8002e, 0x900bc, 0x8000e, 0x8008e, 0x8004e, 0x900fc, + 0x70100, 0x80051, 0x80011, 0x80119, 0x70110, 0x80071, 0x80031, 0x900c2, + 0x70108, 0x80061, 0x80021, 0x900a2, 0x80001, 0x80081, 0x80041, 0x900e2, + 0x70104, 0x80059, 0x80019, 0x90092, 0x70114, 0x80079, 0x80039, 0x900d2, + 0x7010c, 0x80069, 0x80029, 0x900b2, 0x80009, 0x80089, 0x80049, 0x900f2, + 0x70102, 0x80055, 0x80015, 0x8011d, 0x70112, 0x80075, 0x80035, 0x900ca, + 0x7010a, 0x80065, 0x80025, 0x900aa, 0x80005, 0x80085, 0x80045, 0x900ea, + 0x70106, 0x8005d, 0x8001d, 0x9009a, 0x70116, 0x8007d, 0x8003d, 0x900da, + 0x7010e, 0x8006d, 0x8002d, 0x900ba, 0x8000d, 0x8008d, 0x8004d, 0x900fa, + 0x70101, 0x80053, 0x80013, 0x8011b, 0x70111, 0x80073, 0x80033, 0x900c6, + 0x70109, 0x80063, 0x80023, 0x900a6, 0x80003, 0x80083, 0x80043, 0x900e6, + 0x70105, 0x8005b, 0x8001b, 0x90096, 0x70115, 0x8007b, 0x8003b, 0x900d6, + 0x7010d, 0x8006b, 0x8002b, 0x900b6, 0x8000b, 0x8008b, 0x8004b, 0x900f6, + 0x70103, 0x80057, 0x80017, 0x8011f, 0x70113, 0x80077, 0x80037, 0x900ce, + 0x7010b, 0x80067, 0x80027, 0x900ae, 0x80007, 0x80087, 0x80047, 0x900ee, + 0x70107, 0x8005f, 0x8001f, 0x9009e, 0x70117, 0x8007f, 0x8003f, 0x900de, + 0x7010f, 0x8006f, 0x8002f, 0x900be, 0x8000f, 0x8008f, 0x8004f, 0x900fe, + 0x70100, 0x80050, 0x80010, 0x80118, 0x70110, 0x80070, 0x80030, 0x900c1, + 0x70108, 0x80060, 0x80020, 0x900a1, 0x80000, 0x80080, 0x80040, 0x900e1, + 0x70104, 0x80058, 0x80018, 0x90091, 0x70114, 0x80078, 0x80038, 0x900d1, + 0x7010c, 0x80068, 0x80028, 0x900b1, 0x80008, 0x80088, 0x80048, 0x900f1, + 0x70102, 0x80054, 0x80014, 0x8011c, 0x70112, 0x80074, 0x80034, 0x900c9, + 0x7010a, 0x80064, 0x80024, 0x900a9, 0x80004, 0x80084, 0x80044, 0x900e9, + 0x70106, 0x8005c, 0x8001c, 0x90099, 0x70116, 0x8007c, 0x8003c, 0x900d9, + 0x7010e, 0x8006c, 0x8002c, 0x900b9, 0x8000c, 0x8008c, 0x8004c, 0x900f9, + 0x70101, 0x80052, 0x80012, 0x8011a, 0x70111, 0x80072, 0x80032, 0x900c5, + 0x70109, 0x80062, 0x80022, 0x900a5, 0x80002, 0x80082, 0x80042, 0x900e5, + 0x70105, 0x8005a, 0x8001a, 0x90095, 0x70115, 0x8007a, 0x8003a, 0x900d5, + 0x7010d, 0x8006a, 0x8002a, 0x900b5, 0x8000a, 0x8008a, 0x8004a, 0x900f5, + 0x70103, 0x80056, 0x80016, 0x8011e, 0x70113, 0x80076, 0x80036, 0x900cd, + 0x7010b, 0x80066, 0x80026, 0x900ad, 0x80006, 0x80086, 0x80046, 0x900ed, + 0x70107, 0x8005e, 0x8001e, 0x9009d, 0x70117, 0x8007e, 0x8003e, 0x900dd, + 0x7010f, 0x8006e, 0x8002e, 0x900bd, 0x8000e, 0x8008e, 0x8004e, 0x900fd, + 0x70100, 0x80051, 0x80011, 0x80119, 0x70110, 0x80071, 0x80031, 0x900c3, + 0x70108, 0x80061, 0x80021, 0x900a3, 0x80001, 0x80081, 0x80041, 0x900e3, + 0x70104, 0x80059, 0x80019, 0x90093, 0x70114, 0x80079, 0x80039, 0x900d3, + 0x7010c, 0x80069, 0x80029, 0x900b3, 0x80009, 0x80089, 0x80049, 0x900f3, + 0x70102, 0x80055, 0x80015, 0x8011d, 0x70112, 0x80075, 0x80035, 0x900cb, + 0x7010a, 0x80065, 0x80025, 0x900ab, 0x80005, 0x80085, 0x80045, 0x900eb, + 0x70106, 0x8005d, 0x8001d, 0x9009b, 0x70116, 0x8007d, 0x8003d, 0x900db, + 0x7010e, 0x8006d, 0x8002d, 0x900bb, 0x8000d, 0x8008d, 0x8004d, 0x900fb, + 0x70101, 0x80053, 0x80013, 0x8011b, 0x70111, 0x80073, 0x80033, 0x900c7, + 0x70109, 0x80063, 0x80023, 0x900a7, 0x80003, 0x80083, 0x80043, 0x900e7, + 0x70105, 0x8005b, 0x8001b, 0x90097, 0x70115, 0x8007b, 0x8003b, 0x900d7, + 0x7010d, 0x8006b, 0x8002b, 0x900b7, 0x8000b, 0x8008b, 0x8004b, 0x900f7, + 0x70103, 0x80057, 0x80017, 0x8011f, 0x70113, 0x80077, 0x80037, 0x900cf, + 0x7010b, 0x80067, 0x80027, 0x900af, 0x80007, 0x80087, 0x80047, 0x900ef, + 0x70107, 0x8005f, 0x8001f, 0x9009f, 0x70117, 0x8007f, 0x8003f, 0x900df, + 0x7010f, 0x8006f, 0x8002f, 0x900bf, 0x8000f, 0x8008f, 0x8004f, 0x900ff + ]), 9]; + + var fixedDistCodeTab = [new Uint32Array([ + 0x50000, 0x50010, 0x50008, 0x50018, 0x50004, 0x50014, 0x5000c, 0x5001c, + 0x50002, 0x50012, 0x5000a, 0x5001a, 0x50006, 0x50016, 0x5000e, 0x00000, + 0x50001, 0x50011, 0x50009, 0x50019, 0x50005, 0x50015, 0x5000d, 0x5001d, + 0x50003, 0x50013, 0x5000b, 0x5001b, 0x50007, 0x50017, 0x5000f, 0x00000 + ]), 5]; + + function error(e) { + throw new Error(e) + } + + function constructor(bytes) { + //var bytes = stream.getBytes(); + var bytesPos = 0; + + var cmf = bytes[bytesPos++]; + var flg = bytes[bytesPos++]; + if (cmf == -1 || flg == -1) + error('Invalid header in flate stream'); + if ((cmf & 0x0f) != 0x08) + error('Unknown compression method in flate stream'); + if ((((cmf << 8) + flg) % 31) != 0) + error('Bad FCHECK in flate stream'); + if (flg & 0x20) + error('FDICT bit set in flate stream'); + + this.bytes = bytes; + this.bytesPos = bytesPos; + + this.codeSize = 0; + this.codeBuf = 0; + + DecodeStream.call(this); + } + + constructor.prototype = Object.create(DecodeStream.prototype); + + constructor.prototype.getBits = function(bits) { + var codeSize = this.codeSize; + var codeBuf = this.codeBuf; + var bytes = this.bytes; + var bytesPos = this.bytesPos; + + var b; + while (codeSize < bits) { + if (typeof (b = bytes[bytesPos++]) == 'undefined') + error('Bad encoding in flate stream'); + codeBuf |= b << codeSize; + codeSize += 8; + } + b = codeBuf & ((1 << bits) - 1); + this.codeBuf = codeBuf >> bits; + this.codeSize = codeSize -= bits; + this.bytesPos = bytesPos; + return b; + }; + + constructor.prototype.getCode = function(table) { + var codes = table[0]; + var maxLen = table[1]; + var codeSize = this.codeSize; + var codeBuf = this.codeBuf; + var bytes = this.bytes; + var bytesPos = this.bytesPos; + + while (codeSize < maxLen) { + var b; + if (typeof (b = bytes[bytesPos++]) == 'undefined') + error('Bad encoding in flate stream'); + codeBuf |= (b << codeSize); + codeSize += 8; + } + var code = codes[codeBuf & ((1 << maxLen) - 1)]; + var codeLen = code >> 16; + var codeVal = code & 0xffff; + if (codeSize == 0 || codeSize < codeLen || codeLen == 0) + error('Bad encoding in flate stream'); + this.codeBuf = (codeBuf >> codeLen); + this.codeSize = (codeSize - codeLen); + this.bytesPos = bytesPos; + return codeVal; + }; + + constructor.prototype.generateHuffmanTable = function(lengths) { + var n = lengths.length; + + // find max code length + var maxLen = 0; + for (var i = 0; i < n; ++i) { + if (lengths[i] > maxLen) + maxLen = lengths[i]; + } + + // build the table + var size = 1 << maxLen; + var codes = new Uint32Array(size); + for (var len = 1, code = 0, skip = 2; + len <= maxLen; + ++len, code <<= 1, skip <<= 1) { + for (var val = 0; val < n; ++val) { + if (lengths[val] == len) { + // bit-reverse the code + var code2 = 0; + var t = code; + for (var i = 0; i < len; ++i) { + code2 = (code2 << 1) | (t & 1); + t >>= 1; + } + + // fill the table entries + for (var i = code2; i < size; i += skip) + codes[i] = (len << 16) | val; + + ++code; + } + } + } + + return [codes, maxLen]; + }; + + constructor.prototype.readBlock = function() { + function repeat(stream, array, len, offset, what) { + var repeat = stream.getBits(len) + offset; + while (repeat-- > 0) + array[i++] = what; + } + + // read block header + var hdr = this.getBits(3); + if (hdr & 1) + this.eof = true; + hdr >>= 1; + + if (hdr == 0) { // uncompressed block + var bytes = this.bytes; + var bytesPos = this.bytesPos; + var b; + + if (typeof (b = bytes[bytesPos++]) == 'undefined') + error('Bad block header in flate stream'); + var blockLen = b; + if (typeof (b = bytes[bytesPos++]) == 'undefined') + error('Bad block header in flate stream'); + blockLen |= (b << 8); + if (typeof (b = bytes[bytesPos++]) == 'undefined') + error('Bad block header in flate stream'); + var check = b; + if (typeof (b = bytes[bytesPos++]) == 'undefined') + error('Bad block header in flate stream'); + check |= (b << 8); + if (check != (~blockLen & 0xffff)) + error('Bad uncompressed block length in flate stream'); + + this.codeBuf = 0; + this.codeSize = 0; + + var bufferLength = this.bufferLength; + var buffer = this.ensureBuffer(bufferLength + blockLen); + var end = bufferLength + blockLen; + this.bufferLength = end; + for (var n = bufferLength; n < end; ++n) { + if (typeof (b = bytes[bytesPos++]) == 'undefined') { + this.eof = true; + break; + } + buffer[n] = b; + } + this.bytesPos = bytesPos; + return; + } + + var litCodeTable; + var distCodeTable; + if (hdr == 1) { // compressed block, fixed codes + litCodeTable = fixedLitCodeTab; + distCodeTable = fixedDistCodeTab; + } else if (hdr == 2) { // compressed block, dynamic codes + var numLitCodes = this.getBits(5) + 257; + var numDistCodes = this.getBits(5) + 1; + var numCodeLenCodes = this.getBits(4) + 4; + + // build the code lengths code table + var codeLenCodeLengths = Array(codeLenCodeMap.length); + var i = 0; + while (i < numCodeLenCodes) + codeLenCodeLengths[codeLenCodeMap[i++]] = this.getBits(3); + var codeLenCodeTab = this.generateHuffmanTable(codeLenCodeLengths); + + // build the literal and distance code tables + var len = 0; + var i = 0; + var codes = numLitCodes + numDistCodes; + var codeLengths = new Array(codes); + while (i < codes) { + var code = this.getCode(codeLenCodeTab); + if (code == 16) { + repeat(this, codeLengths, 2, 3, len); + } else if (code == 17) { + repeat(this, codeLengths, 3, 3, len = 0); + } else if (code == 18) { + repeat(this, codeLengths, 7, 11, len = 0); + } else { + codeLengths[i++] = len = code; + } + } + + litCodeTable = + this.generateHuffmanTable(codeLengths.slice(0, numLitCodes)); + distCodeTable = + this.generateHuffmanTable(codeLengths.slice(numLitCodes, codes)); + } else { + error('Unknown block type in flate stream'); + } + + var buffer = this.buffer; + var limit = buffer ? buffer.length : 0; + var pos = this.bufferLength; + while (true) { + var code1 = this.getCode(litCodeTable); + if (code1 < 256) { + if (pos + 1 >= limit) { + buffer = this.ensureBuffer(pos + 1); + limit = buffer.length; + } + buffer[pos++] = code1; + continue; + } + if (code1 == 256) { + this.bufferLength = pos; + return; + } + code1 -= 257; + code1 = lengthDecode[code1]; + var code2 = code1 >> 16; + if (code2 > 0) + code2 = this.getBits(code2); + var len = (code1 & 0xffff) + code2; + code1 = this.getCode(distCodeTable); + code1 = distDecode[code1]; + code2 = code1 >> 16; + if (code2 > 0) + code2 = this.getBits(code2); + var dist = (code1 & 0xffff) + code2; + if (pos + len >= limit) { + buffer = this.ensureBuffer(pos + len); + limit = buffer.length; + } + for (var k = 0; k < len; ++k, ++pos) + buffer[pos] = buffer[pos - dist]; + } + }; + + return constructor; +})(); \ No newline at end of file