Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / gallery / js / image_editor / exif_encoder.js
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 // TODO:(kaznacheev) Share the EXIF constants with exif_parser.js
6 var EXIF_MARK_SOS = 0xffda;  // Start of "stream" (the actual image data).
7 var EXIF_MARK_SOI = 0xffd8;  // Start of image data.
8 var EXIF_MARK_EOI = 0xffd9;  // End of image data.
9
10 var EXIF_MARK_APP0 = 0xffe0;  // APP0 block, most commonly JFIF data.
11 var EXIF_MARK_EXIF = 0xffe1;  // Start of exif block.
12
13 var EXIF_ALIGN_LITTLE = 0x4949;  // Indicates little endian exif data.
14 var EXIF_ALIGN_BIG = 0x4d4d;  // Indicates big endian exif data.
15
16 var EXIF_TAG_TIFF = 0x002a;  // First directory containing TIFF data.
17 var EXIF_TAG_GPSDATA = 0x8825;  // Pointer from TIFF to the GPS directory.
18 var EXIF_TAG_EXIFDATA = 0x8769;  // Pointer from TIFF to the EXIF IFD.
19
20 var EXIF_TAG_JPG_THUMB_OFFSET = 0x0201;  // Pointer from TIFF to thumbnail.
21 var EXIF_TAG_JPG_THUMB_LENGTH = 0x0202;  // Length of thumbnail data.
22
23 var EXIF_TAG_IMAGE_WIDTH = 0x0100;
24 var EXIF_TAG_IMAGE_HEIGHT = 0x0101;
25
26 var EXIF_TAG_ORIENTATION = 0x0112;
27 var EXIF_TAG_X_DIMENSION = 0xA002;
28 var EXIF_TAG_Y_DIMENSION = 0xA003;
29
30 /**
31  * The Exif metadata encoder.
32  * Uses the metadata format as defined by ExifParser.
33  * @param {Object} original_metadata Metadata to encode.
34  * @constructor
35  * @extends {ImageEncoder.MetadataEncoder}
36  */
37 function ExifEncoder(original_metadata) {
38   ImageEncoder.MetadataEncoder.apply(this, arguments);
39
40   this.ifd_ = this.metadata_.ifd;
41   if (!this.ifd_)
42     this.ifd_ = this.metadata_.ifd = {};
43 }
44
45 ExifEncoder.prototype = {__proto__: ImageEncoder.MetadataEncoder.prototype};
46
47 ImageEncoder.registerMetadataEncoder(ExifEncoder, 'image/jpeg');
48
49 /**
50  * @param {HTMLCanvasElement|Object} canvas Canvas or anything with
51  *                                          width and height properties.
52  */
53 ExifEncoder.prototype.setImageData = function(canvas) {
54   var image = this.ifd_.image;
55   if (!image)
56     image = this.ifd_.image = {};
57
58   // Only update width/height in this directory if they are present.
59   if (image[EXIF_TAG_IMAGE_WIDTH] && image[EXIF_TAG_IMAGE_HEIGHT]) {
60     image[EXIF_TAG_IMAGE_WIDTH].value = canvas.width;
61     image[EXIF_TAG_IMAGE_HEIGHT].value = canvas.height;
62   }
63
64   var exif = this.ifd_.exif;
65   if (!exif)
66     exif = this.ifd_.exif = {};
67   ExifEncoder.findOrCreateTag(image, EXIF_TAG_EXIFDATA);
68   ExifEncoder.findOrCreateTag(exif, EXIF_TAG_X_DIMENSION).value = canvas.width;
69   ExifEncoder.findOrCreateTag(exif, EXIF_TAG_Y_DIMENSION).value = canvas.height;
70
71   this.metadata_.width = canvas.width;
72   this.metadata_.height = canvas.height;
73
74   // Always save in default orientation.
75   delete this.metadata_.imageTransform;
76   ExifEncoder.findOrCreateTag(image, EXIF_TAG_ORIENTATION).value = 1;
77 };
78
79
80 /**
81  * @param {HTMLCanvasElement} canvas Thumbnail canvas.
82  * @param {number} quality (0..1] Thumbnail encoding quality.
83  */
84 ExifEncoder.prototype.setThumbnailData = function(canvas, quality) {
85   // Empirical formula with reasonable behavior:
86   // 10K for 1Mpix, 30K for 5Mpix, 50K for 9Mpix and up.
87   var pixelCount = this.metadata_.width * this.metadata_.height;
88   var maxEncodedSize = 5000 * Math.min(10, 1 + pixelCount / 1000000);
89
90   var DATA_URL_PREFIX = 'data:' + this.mimeType + ';base64,';
91   var BASE64_BLOAT = 4 / 3;
92   var maxDataURLLength =
93       DATA_URL_PREFIX.length + Math.ceil(maxEncodedSize * BASE64_BLOAT);
94
95   for (;; quality *= 0.8) {
96     ImageEncoder.MetadataEncoder.prototype.setThumbnailData.call(
97         this, canvas, quality);
98     if (this.metadata_.thumbnailURL.length <= maxDataURLLength || quality < 0.2)
99       break;
100   }
101
102   if (this.metadata_.thumbnailURL.length <= maxDataURLLength) {
103     var thumbnail = this.ifd_.thumbnail;
104     if (!thumbnail)
105       thumbnail = this.ifd_.thumbnail = {};
106
107     ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_IMAGE_WIDTH).value =
108         canvas.width;
109
110     ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_IMAGE_HEIGHT).value =
111         canvas.height;
112
113     // The values for these tags will be set in ExifWriter.encode.
114     ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_JPG_THUMB_OFFSET);
115     ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_JPG_THUMB_LENGTH);
116
117     // Always save in default orientation.
118     ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_ORIENTATION).value = 1;
119   } else {
120     console.warn(
121         'Thumbnail URL too long: ' + this.metadata_.thumbnailURL.length);
122     // Delete thumbnail ifd so that it is not written out to a file, but
123     // keep thumbnailURL for display purposes.
124     if (this.ifd_.thumbnail) {
125       delete this.ifd_.thumbnail;
126     }
127   }
128   delete this.metadata_.thumbnailTransform;
129 };
130
131 /**
132  * Return a range where the metadata is (or should be) located.
133  * @param {string} encodedImage Raw image data to look for metadata.
134  * @return {Object} An object with from and to properties.
135  */
136 ExifEncoder.prototype.findInsertionRange = function(encodedImage) {
137   function getWord(pos) {
138     if (pos + 2 > encodedImage.length)
139       throw 'Reading past the buffer end @' + pos;
140     return encodedImage.charCodeAt(pos) << 8 | encodedImage.charCodeAt(pos + 1);
141   }
142
143   if (getWord(0) != EXIF_MARK_SOI)
144     throw new Error('Jpeg data starts from 0x' + getWord(0).toString(16));
145
146   var sectionStart = 2;
147
148   // Default: an empty range right after SOI.
149   // Will be returned in absence of APP0 or Exif sections.
150   var range = {from: sectionStart, to: sectionStart};
151
152   for (;;) {
153     var tag = getWord(sectionStart);
154
155     if (tag == EXIF_MARK_SOS)
156       break;
157
158     var nextSectionStart = sectionStart + 2 + getWord(sectionStart + 2);
159     if (nextSectionStart <= sectionStart ||
160         nextSectionStart > encodedImage.length)
161       throw new Error('Invalid section size in jpeg data');
162
163     if (tag == EXIF_MARK_APP0) {
164       // Assert that we have not seen the Exif section yet.
165       if (range.from != range.to)
166         throw new Error('APP0 section found after EXIF section');
167       // An empty range right after the APP0 segment.
168       range.from = range.to = nextSectionStart;
169     } else if (tag == EXIF_MARK_EXIF) {
170       // A range containing the existing EXIF section.
171       range.from = sectionStart;
172       range.to = nextSectionStart;
173     }
174     sectionStart = nextSectionStart;
175   }
176
177   return range;
178 };
179
180 /**
181  * @return {ArrayBuffer} serialized metadata ready to write to an image file.
182  */
183 ExifEncoder.prototype.encode = function() {
184   var HEADER_SIZE = 10;
185
186   // Allocate the largest theoretically possible size.
187   var bytes = new Uint8Array(0x10000);
188
189   // Serialize header
190   var hw = new ByteWriter(bytes.buffer, 0, HEADER_SIZE);
191   hw.writeScalar(EXIF_MARK_EXIF, 2);
192   hw.forward('size', 2);
193   hw.writeString('Exif\0\0');  // Magic string.
194
195   // First serialize the content of the exif section.
196   // Use a ByteWriter starting at HEADER_SIZE offset so that tell() positions
197   // can be directly mapped to offsets as encoded in the dictionaries.
198   var bw = new ByteWriter(bytes.buffer, HEADER_SIZE);
199
200   if (this.metadata_.littleEndian) {
201     bw.setByteOrder(ByteWriter.LITTLE_ENDIAN);
202     bw.writeScalar(EXIF_ALIGN_LITTLE, 2);
203   } else {
204     bw.setByteOrder(ByteWriter.BIG_ENDIAN);
205     bw.writeScalar(EXIF_ALIGN_BIG, 2);
206   }
207
208   bw.writeScalar(EXIF_TAG_TIFF, 2);
209
210   bw.forward('image-dir', 4);  // The pointer should point right after itself.
211   bw.resolveOffset('image-dir');
212
213   ExifEncoder.encodeDirectory(bw, this.ifd_.image,
214       [EXIF_TAG_EXIFDATA, EXIF_TAG_GPSDATA], 'thumb-dir');
215
216   if (this.ifd_.exif) {
217     bw.resolveOffset(EXIF_TAG_EXIFDATA);
218     ExifEncoder.encodeDirectory(bw, this.ifd_.exif);
219   } else {
220     if (EXIF_TAG_EXIFDATA in this.ifd_.image)
221       throw new Error('Corrupt exif dictionary reference');
222   }
223
224   if (this.ifd_.gps) {
225     bw.resolveOffset(EXIF_TAG_GPSDATA);
226     ExifEncoder.encodeDirectory(bw, this.ifd_.gps);
227   } else {
228     if (EXIF_TAG_GPSDATA in this.ifd_.image)
229       throw new Error('Missing gps dictionary reference');
230   }
231
232   if (this.ifd_.thumbnail) {
233     bw.resolveOffset('thumb-dir');
234     ExifEncoder.encodeDirectory(
235         bw,
236         this.ifd_.thumbnail,
237         [EXIF_TAG_JPG_THUMB_OFFSET, EXIF_TAG_JPG_THUMB_LENGTH]);
238
239     var thumbnailDecoded =
240         ImageEncoder.decodeDataURL(this.metadata_.thumbnailURL);
241     bw.resolveOffset(EXIF_TAG_JPG_THUMB_OFFSET);
242     bw.resolve(EXIF_TAG_JPG_THUMB_LENGTH, thumbnailDecoded.length);
243     bw.writeString(thumbnailDecoded);
244   } else {
245     bw.resolve('thumb-dir', 0);
246   }
247
248   bw.checkResolved();
249
250   var totalSize = HEADER_SIZE + bw.tell();
251   hw.resolve('size', totalSize - 2);  // The marker is excluded.
252   hw.checkResolved();
253
254   var subarray = new Uint8Array(totalSize);
255   for (var i = 0; i != totalSize; i++) {
256     subarray[i] = bytes[i];
257   }
258   return subarray.buffer;
259 };
260
261 /*
262  * Static methods.
263  */
264
265 /**
266  * Write the contents of an IFD directory.
267  * @param {ByteWriter} bw ByteWriter to use.
268  * @param {Object} directory A directory map as created by ExifParser.
269  * @param {Array} resolveLater An array of tag ids for which the values will be
270  *                resolved later.
271  * @param {string} nextDirPointer A forward key for the pointer to the next
272  *                 directory. If omitted the pointer is set to 0.
273  */
274 ExifEncoder.encodeDirectory = function(
275     bw, directory, resolveLater, nextDirPointer) {
276
277   var longValues = [];
278
279   bw.forward('dir-count', 2);
280   var count = 0;
281
282   for (var key in directory) {
283     var tag = directory[key];
284     bw.writeScalar(tag.id, 2);
285     bw.writeScalar(tag.format, 2);
286     bw.writeScalar(tag.componentCount, 4);
287
288     var width = ExifEncoder.getComponentWidth(tag) * tag.componentCount;
289
290     if (resolveLater && (resolveLater.indexOf(tag.id) >= 0)) {
291       // The actual value depends on further computations.
292       if (tag.componentCount != 1 || width > 4)
293         throw new Error('Cannot forward the pointer for ' + tag.id);
294       bw.forward(tag.id, width);
295     } else if (width <= 4) {
296       // The value fits into 4 bytes, write it immediately.
297       ExifEncoder.writeValue(bw, tag);
298     } else {
299       // The value does not fit, forward the 4 byte offset to the actual value.
300       width = 4;
301       bw.forward(tag.id, width);
302       longValues.push(tag);
303     }
304     bw.skip(4 - width);  // Align so that the value take up exactly 4 bytes.
305     count++;
306   }
307
308   bw.resolve('dir-count', count);
309
310   if (nextDirPointer) {
311     bw.forward(nextDirPointer, 4);
312   } else {
313     bw.writeScalar(0, 4);
314   }
315
316   // Write out the long values and resolve pointers.
317   for (var i = 0; i != longValues.length; i++) {
318     var longValue = longValues[i];
319     bw.resolveOffset(longValue.id);
320     ExifEncoder.writeValue(bw, longValue);
321   }
322 };
323
324 /**
325  * @param {{format:number, id:number}} tag EXIF tag object.
326  * @return {number} Width in bytes of the data unit associated with this tag.
327  * TODO(kaznacheev): Share with ExifParser?
328  */
329 ExifEncoder.getComponentWidth = function(tag) {
330   switch (tag.format) {
331     case 1:  // Byte
332     case 2:  // String
333     case 7:  // Undefined
334       return 1;
335
336     case 3:  // Short
337       return 2;
338
339     case 4:  // Long
340     case 9:  // Signed Long
341       return 4;
342
343     case 5:  // Rational
344     case 10:  // Signed Rational
345       return 8;
346
347     default:  // ???
348       console.warn('Unknown tag format 0x' +
349           Number(tag.id).toString(16) + ': ' + tag.format);
350       return 4;
351   }
352 };
353
354 /**
355  * Writes out the tag value.
356  * @param {ByteWriter} bw Writer to use.
357  * @param {Object} tag Tag, which value to write.
358  */
359 ExifEncoder.writeValue = function(bw, tag) {
360   if (tag.format == 2) {  // String
361     if (tag.componentCount != tag.value.length) {
362       throw new Error(
363           'String size mismatch for 0x' + Number(tag.id).toString(16));
364     }
365     bw.writeString(tag.value);
366   } else {  // Scalar or rational
367     var width = ExifEncoder.getComponentWidth(tag);
368
369     var writeComponent = function(value, signed) {
370       if (width == 8) {
371         bw.writeScalar(value[0], 4, signed);
372         bw.writeScalar(value[1], 4, signed);
373       } else {
374         bw.writeScalar(value, width, signed);
375       }
376     };
377
378     var signed = (tag.format == 9 || tag.format == 10);
379     if (tag.componentCount == 1) {
380       writeComponent(tag.value, signed);
381     } else {
382       for (var i = 0; i != tag.componentCount; i++) {
383         writeComponent(tag.value[i], signed);
384       }
385     }
386   }
387 };
388
389 /**
390  * @param {{Object.<number,Object>}} directory EXIF directory.
391  * @param {number} id Tag id.
392  * @param {number} format Tag format
393  *                        (used in {@link ExifEncoder#getComponentWidth}).
394  * @param {number} componentCount Number of components in this tag.
395  * @return {{id:number, format:number, componentCount:number}}
396  *     Tag found or created.
397  */
398 ExifEncoder.findOrCreateTag = function(directory, id, format, componentCount) {
399   if (!(id in directory)) {
400     directory[id] = {
401       id: id,
402       format: format || 3,  // Short
403       componentCount: componentCount || 1
404     };
405   }
406   return directory[id];
407 };
408
409 /**
410  * ByteWriter class.
411  * @param {ArrayBuffer} arrayBuffer Underlying buffer to use.
412  * @param {number} offset Offset at which to start writing.
413  * @param {number} length Maximum length to use.
414  * @class
415  * @constructor
416  */
417 function ByteWriter(arrayBuffer, offset, length) {
418   length = length || (arrayBuffer.byteLength - offset);
419   this.view_ = new DataView(arrayBuffer, offset, length);
420   this.littleEndian_ = false;
421   this.pos_ = 0;
422   this.forwards_ = {};
423 }
424
425 /**
426  * Little endian byte order.
427  * @type {number}
428  */
429 ByteWriter.LITTLE_ENDIAN = 0;
430
431 /**
432  * Bug endian byte order.
433  * @type {number}
434  */
435 ByteWriter.BIG_ENDIAN = 1;
436
437 /**
438  * Set the byte ordering for future writes.
439  * @param {number} order ByteOrder to use {ByteWriter.LITTLE_ENDIAN}
440  *   or {ByteWriter.BIG_ENDIAN}.
441  */
442 ByteWriter.prototype.setByteOrder = function(order) {
443   this.littleEndian_ = (order == ByteWriter.LITTLE_ENDIAN);
444 };
445
446 /**
447  * @return {number} the current write position.
448  */
449 ByteWriter.prototype.tell = function() { return this.pos_ };
450
451 /**
452  * Skips desired amount of bytes in output stream.
453  * @param {number} count Byte count to skip.
454  */
455 ByteWriter.prototype.skip = function(count) {
456   this.validateWrite(count);
457   this.pos_ += count;
458 };
459
460 /**
461  * Check if the buffer has enough room to read 'width' bytes. Throws an error
462  * if it has not.
463  * @param {number} width Amount of bytes to check.
464  */
465 ByteWriter.prototype.validateWrite = function(width) {
466   if (this.pos_ + width > this.view_.byteLength)
467     throw new Error('Writing past the end of the buffer');
468 };
469
470 /**
471  * Writes scalar value to output stream.
472  * @param {number} value Value to write.
473  * @param {number} width Desired width of written value.
474  * @param {boolean=} opt_signed True if value represents signed number.
475  */
476 ByteWriter.prototype.writeScalar = function(value, width, opt_signed) {
477   var method;
478   // The below switch is so verbose for two reasons:
479   // 1. V8 is faster on method names which are 'symbols'.
480   // 2. Method names are discoverable by full text search.
481   switch (width) {
482     case 1:
483       method = opt_signed ? 'setInt8' : 'setUint8';
484       break;
485
486     case 2:
487       method = opt_signed ? 'setInt16' : 'setUint16';
488       break;
489
490     case 4:
491       method = opt_signed ? 'setInt32' : 'setUint32';
492       break;
493
494     case 8:
495       method = opt_signed ? 'setInt64' : 'setUint64';
496       break;
497
498     default:
499       throw new Error('Invalid width: ' + width);
500       break;
501   }
502
503   this.validateWrite(width);
504   this.view_[method](this.pos_, value, this.littleEndian_);
505   this.pos_ += width;
506 };
507
508 /**
509  * Writes string.
510  * @param {string} str String to write.
511  */
512 ByteWriter.prototype.writeString = function(str) {
513   this.validateWrite(str.length);
514   for (var i = 0; i != str.length; i++) {
515     this.view_.setUint8(this.pos_++, str.charCodeAt(i));
516   }
517 };
518
519 /**
520  * Allocate the space for 'width' bytes for the value that will be set later.
521  * To be followed by a 'resolve' call with the same key.
522  * @param {string} key A key to identify the value.
523  * @param {number} width Width of the value in bytes.
524  */
525 ByteWriter.prototype.forward = function(key, width) {
526   if (key in this.forwards_)
527     throw new Error('Duplicate forward key ' + key);
528   this.validateWrite(width);
529   this.forwards_[key] = {
530     pos: this.pos_,
531     width: width
532   };
533   this.pos_ += width;
534 };
535
536 /**
537  * Set the value previously allocated with a 'forward' call.
538  * @param {string} key A key to identify the value.
539  * @param {number} value value to write in pre-allocated space.
540  */
541 ByteWriter.prototype.resolve = function(key, value) {
542   if (!(key in this.forwards_))
543     throw new Error('Undeclared forward key ' + key.toString(16));
544   var forward = this.forwards_[key];
545   var curPos = this.pos_;
546   this.pos_ = forward.pos;
547   this.writeScalar(value, forward.width);
548   this.pos_ = curPos;
549   delete this.forwards_[key];
550 };
551
552 /**
553  * A shortcut to resolve the value to the current write position.
554  * @param {string} key A key to identify pre-allocated position.
555  */
556 ByteWriter.prototype.resolveOffset = function(key) {
557   this.resolve(key, this.tell());
558 };
559
560 /**
561  * Check if every forward has been resolved, throw and error if not.
562  */
563 ByteWriter.prototype.checkResolved = function() {
564   for (var key in this.forwards_) {
565     throw new Error('Unresolved forward pointer ' + key.toString(16));
566   }
567 };