1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5'use strict'; 6 7/** 8 * A namespace class for image encoding functions. All methods are static. 9 */ 10function ImageEncoder() {} 11 12/** 13 * @type {Array.<Object>} 14 */ 15ImageEncoder.metadataEncoders = {}; 16 17/** 18 * @param {function(new:ImageEncoder.MetadataEncoder)} constructor 19 * // TODO(JSDOC). 20 * @param {string} mimeType // TODO(JSDOC). 21 */ 22ImageEncoder.registerMetadataEncoder = function(constructor, mimeType) { 23 ImageEncoder.metadataEncoders[mimeType] = constructor; 24}; 25 26/** 27 * Create a metadata encoder. 28 * 29 * The encoder will own and modify a copy of the original metadata. 30 * 31 * @param {Object} metadata Original metadata. 32 * @return {ImageEncoder.MetadataEncoder} Created metadata encoder. 33 */ 34ImageEncoder.createMetadataEncoder = function(metadata) { 35 var constructor = 36 (metadata && ImageEncoder.metadataEncoders[metadata.mimeType]) || 37 ImageEncoder.MetadataEncoder; 38 return new constructor(metadata); 39}; 40 41 42/** 43 * Create a metadata encoder object holding a copy of metadata 44 * modified according to the properties of the supplied image. 45 * 46 * @param {Object} metadata Original metadata. 47 * @param {HTMLCanvasElement} canvas Canvas to use for metadata. 48 * @param {number} quality Encoding quality (defaults to 1). 49 * @return {ImageEncoder.MetadataEncoder} Encoder with encoded metadata. 50 */ 51ImageEncoder.encodeMetadata = function(metadata, canvas, quality) { 52 var encoder = ImageEncoder.createMetadataEncoder(metadata); 53 encoder.setImageData(canvas); 54 encoder.setThumbnailData(ImageEncoder.createThumbnail(canvas), quality || 1); 55 return encoder; 56}; 57 58 59/** 60 * Return a blob with the encoded image with metadata inserted. 61 * @param {HTMLCanvasElement} canvas The canvas with the image to be encoded. 62 * @param {ImageEncoder.MetadataEncoder} metadataEncoder Encoder to use. 63 * @param {number} quality (0..1], Encoding quality, defaults to 0.9. 64 * @return {Blob} encoded data. 65 */ 66ImageEncoder.getBlob = function(canvas, metadataEncoder, quality) { 67 // Contrary to what one might think 1.0 is not a good default. Opening and 68 // saving an typical photo taken with consumer camera increases its file size 69 // by 50-100%. 70 // Experiments show that 0.9 is much better. It shrinks some photos a bit, 71 // keeps others about the same size, but does not visibly lower the quality. 72 quality = quality || 0.9; 73 74 ImageUtil.trace.resetTimer('dataurl'); 75 // WebKit does not support canvas.toBlob yet so canvas.toDataURL is 76 // the only way to use the Chrome built-in image encoder. 77 var dataURL = 78 canvas.toDataURL(metadataEncoder.getMetadata().mimeType, quality); 79 ImageUtil.trace.reportTimer('dataurl'); 80 81 var encodedImage = ImageEncoder.decodeDataURL(dataURL); 82 83 var encodedMetadata = metadataEncoder.encode(); 84 85 var slices = []; 86 87 // TODO(kaznacheev): refactor |stringToArrayBuffer| and |encode| to return 88 // arrays instead of array buffers. 89 function appendSlice(arrayBuffer) { 90 slices.push(new DataView(arrayBuffer)); 91 } 92 93 ImageUtil.trace.resetTimer('blob'); 94 if (encodedMetadata.byteLength != 0) { 95 var metadataRange = metadataEncoder.findInsertionRange(encodedImage); 96 appendSlice(ImageEncoder.stringToArrayBuffer( 97 encodedImage, 0, metadataRange.from)); 98 99 appendSlice(metadataEncoder.encode()); 100 101 appendSlice(ImageEncoder.stringToArrayBuffer( 102 encodedImage, metadataRange.to, encodedImage.length)); 103 } else { 104 appendSlice(ImageEncoder.stringToArrayBuffer( 105 encodedImage, 0, encodedImage.length)); 106 } 107 var blob = new Blob(slices, {type: metadataEncoder.getMetadata().mimeType}); 108 ImageUtil.trace.reportTimer('blob'); 109 return blob; 110}; 111 112/** 113 * Decode a dataURL into a binary string containing the encoded image. 114 * 115 * Why return a string? Calling atob and having the rest of the code deal 116 * with a string is several times faster than decoding base64 in Javascript. 117 * 118 * @param {string} dataURL Data URL to decode. 119 * @return {string} A binary string (char codes are the actual byte values). 120 */ 121ImageEncoder.decodeDataURL = function(dataURL) { 122 // Skip the prefix ('data:image/<type>;base64,') 123 var base64string = dataURL.substring(dataURL.indexOf(',') + 1); 124 return atob(base64string); 125}; 126 127/** 128 * Return a thumbnail for an image. 129 * @param {HTMLCanvasElement} canvas Original image. 130 * @param {number=} opt_shrinkage Thumbnail should be at least this much smaller 131 * than the original image (in each dimension). 132 * @return {HTMLCanvasElement} Thumbnail canvas. 133 */ 134ImageEncoder.createThumbnail = function(canvas, opt_shrinkage) { 135 var MAX_THUMBNAIL_DIMENSION = 320; 136 137 opt_shrinkage = Math.max(opt_shrinkage || 4, 138 canvas.width / MAX_THUMBNAIL_DIMENSION, 139 canvas.height / MAX_THUMBNAIL_DIMENSION); 140 141 var thumbnailCanvas = canvas.ownerDocument.createElement('canvas'); 142 thumbnailCanvas.width = Math.round(canvas.width / opt_shrinkage); 143 thumbnailCanvas.height = Math.round(canvas.height / opt_shrinkage); 144 145 var context = thumbnailCanvas.getContext('2d'); 146 context.drawImage(canvas, 147 0, 0, canvas.width, canvas.height, 148 0, 0, thumbnailCanvas.width, thumbnailCanvas.height); 149 150 return thumbnailCanvas; 151}; 152 153/** 154 * TODO(JSDOC) 155 * @param {string} string // TODO(JSDOC). 156 * @param {number} from // TODO(JSDOC). 157 * @param {number} to // TODO(JSDOC). 158 * @return {ArrayBuffer} // TODO(JSDOC). 159 */ 160ImageEncoder.stringToArrayBuffer = function(string, from, to) { 161 var size = to - from; 162 var array = new Uint8Array(size); 163 for (var i = 0; i != size; i++) { 164 array[i] = string.charCodeAt(from + i); 165 } 166 return array.buffer; 167}; 168 169/** 170 * A base class for a metadata encoder. 171 * 172 * Serves as a default metadata encoder for images that none of the metadata 173 * parsers recognized. 174 * 175 * @param {Object} original_metadata Starting metadata. 176 * @constructor 177 */ 178ImageEncoder.MetadataEncoder = function(original_metadata) { 179 this.metadata_ = MetadataCache.cloneMetadata(original_metadata) || {}; 180 if (this.metadata_.mimeType != 'image/jpeg') { 181 // Chrome can only encode JPEG and PNG. Force PNG mime type so that we 182 // can save to file and generate a thumbnail. 183 this.metadata_.mimeType = 'image/png'; 184 } 185}; 186 187/** 188 * TODO(JSDOC) 189 * @return {Object} // TODO(JSDOC). 190 */ 191ImageEncoder.MetadataEncoder.prototype.getMetadata = function() { 192 return this.metadata_; 193}; 194 195/** 196 * @param {HTMLCanvasElement|Object} canvas Canvas or or anything with 197 * width and height properties. 198 */ 199ImageEncoder.MetadataEncoder.prototype.setImageData = function(canvas) { 200 this.metadata_.width = canvas.width; 201 this.metadata_.height = canvas.height; 202}; 203 204/** 205 * @param {HTMLCanvasElement} canvas Canvas to use as thumbnail. 206 * @param {number} quality Thumbnail quality. 207 */ 208ImageEncoder.MetadataEncoder.prototype.setThumbnailData = 209 function(canvas, quality) { 210 this.metadata_.thumbnailURL = 211 canvas.toDataURL(this.metadata_.mimeType, quality); 212 delete this.metadata_.thumbnailTransform; 213}; 214 215/** 216 * Return a range where the metadata is (or should be) located. 217 * @param {string} encodedImage // TODO(JSDOC). 218 * @return {Object} An object with from and to properties. 219 */ 220ImageEncoder.MetadataEncoder.prototype. 221 findInsertionRange = function(encodedImage) { return {from: 0, to: 0}; }; 222 223/** 224 * Return serialized metadata ready to write to an image file. 225 * The return type is optimized for passing to Blob.append. 226 * @return {ArrayBuffer} // TODO(JSDOC). 227 */ 228ImageEncoder.MetadataEncoder.prototype.encode = function() { 229 return new Uint8Array(0).buffer; 230}; 231