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