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