Home | History | Annotate | Download | only in image_editor
      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  */
     10 function ImageEncoder() {}
     11 
     12 /**
     13  * @type {Array.<Object>}
     14  */
     15 ImageEncoder.metadataEncoders = {};
     16 
     17 /**
     18  * @param {function(new:ImageEncoder.MetadataEncoder)} constructor
     19  *     // TODO(JSDOC).
     20  * @param {string} mimeType  // TODO(JSDOC).
     21  */
     22 ImageEncoder.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  */
     34 ImageEncoder.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  */
     51 ImageEncoder.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  */
     66 ImageEncoder.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  */
    121 ImageEncoder.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  */
    134 ImageEncoder.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  */
    160 ImageEncoder.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  */
    178 ImageEncoder.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  */
    191 ImageEncoder.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  */
    199 ImageEncoder.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  */
    208 ImageEncoder.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  */
    220 ImageEncoder.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  */
    228 ImageEncoder.MetadataEncoder.prototype.encode = function() {
    229   return new Uint8Array(0).buffer;
    230 };
    231