Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ui / file_manager / gallery / js / image_editor / exif_encoder.js
blob67085f08f8f4a18ecef5856574faa05c04c908cd
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.
5 /**
6  * The Exif metadata encoder.
7  * Uses the metadata format as defined by ExifParser.
8  * @param {!MetadataItem} originalMetadata Metadata to encode.
9  * @constructor
10  * @extends {ImageEncoder.MetadataEncoder}
11  * @struct
12  */
13 function ExifEncoder(originalMetadata) {
14   ImageEncoder.MetadataEncoder.apply(this, arguments);
15   /**
16    * Image File Directory obtained from EXIF header.
17    * @private {!Object}
18    * @const
19    */
20   this.ifd_ = /** @type {!Object} */(
21       JSON.parse(JSON.stringify(originalMetadata.ifd || {})));
23   /**
24    * Note use little endian if the original metadata does not have the
25    * information.
26    * @private {boolean}
27    * @const
28    */
29   this.exifLittleEndian_ = !!originalMetadata.exifLittleEndian;
31   /**
32    * Modification time to be stored in EXIF header.
33    * @private {!Date}
34    * @const
35    */
36   this.modificationTime_ = assert(originalMetadata.modificationTime);
39 ExifEncoder.prototype = {__proto__: ImageEncoder.MetadataEncoder.prototype};
41 ImageEncoder.registerMetadataEncoder(ExifEncoder, 'image/jpeg');
43 /**
44  * Software name of Gallery.app.
45  * @type {string}
46  * @const
47  */
48 ExifEncoder.SOFTWARE = 'Chrome OS Gallery App\0';
50 /**
51  * Maximum size of exif data.
52  * @const {number}
53  */
54 ExifEncoder.MAXIMUM_EXIF_DATA_SIZE = 0x10000;
56 /**
57  * Size of metadata for thumbnail.
58  * @const {number}
59  */
60 ExifEncoder.THUMBNAIL_METADATA_SIZE = 76;
62 /**
63  * @param {!HTMLCanvasElement} canvas
64  * @override
65  */
66 ExifEncoder.prototype.setImageData = function(canvas) {
67   ImageEncoder.MetadataEncoder.prototype.setImageData.call(this, canvas);
69   var image = this.ifd_.image;
70   if (!image)
71     image = this.ifd_.image = {};
73   // Only update width/height in this directory if they are present.
74   if (image[Exif.Tag.IMAGE_WIDTH] && image[Exif.Tag.IMAGE_HEIGHT]) {
75     image[Exif.Tag.IMAGE_WIDTH].value = canvas.width;
76     image[Exif.Tag.IMAGE_HEIGHT].value = canvas.height;
77   }
79   var exif = this.ifd_.exif;
80   if (!exif)
81     exif = this.ifd_.exif = {};
82   ExifEncoder.findOrCreateTag(image, Exif.Tag.EXIFDATA);
83   ExifEncoder.findOrCreateTag(exif, Exif.Tag.X_DIMENSION).value = canvas.width;
84   ExifEncoder.findOrCreateTag(exif, Exif.Tag.Y_DIMENSION).value = canvas.height;
86   // Always save in default orientation.
87   ExifEncoder.findOrCreateTag(image, Exif.Tag.ORIENTATION).value = 1;
89   // Update software name.
90   var softwareTag = ExifEncoder.findOrCreateTag(image, Exif.Tag.SOFTWARE, 2);
91   softwareTag.value = ExifEncoder.SOFTWARE;
92   softwareTag.componentCount = ExifEncoder.SOFTWARE.length;
94   // Update modification date time.
95   var padNumWithZero = function(num, length) {
96     var str = num.toString();
97     while (str.length < length) {
98       str = '0' + str;
99     }
100     return str;
101   };
103   var dateTimeTag = ExifEncoder.findOrCreateTag(image, Exif.Tag.DATETIME, 2);
104   dateTimeTag.value =
105       padNumWithZero(this.modificationTime_.getFullYear(), 4) + ':' +
106       padNumWithZero(this.modificationTime_.getMonth() + 1, 2) + ':' +
107       padNumWithZero(this.modificationTime_.getDate(), 2) + ' ' +
108       padNumWithZero(this.modificationTime_.getHours(), 2) + ':' +
109       padNumWithZero(this.modificationTime_.getMinutes(), 2) + ':' +
110       padNumWithZero(this.modificationTime_.getSeconds(), 2) + '\0';
111   dateTimeTag.componentCount = 20;
115  * @override
116  */
117 ExifEncoder.prototype.setThumbnailData = function(canvas, quality) {
118   if (canvas) {
119     // Empirical formula with reasonable behavior:
120     // 10K for 1Mpix, 30K for 5Mpix, 50K for 9Mpix and up.
121     var pixelCount = this.imageWidth * this.imageHeight;
122     var maxEncodedSize = 5000 * Math.min(10, 1 + pixelCount / 1000000);
123     var DATA_URL_PREFIX = 'data:image/jpeg;base64,';
124     var BASE64_BLOAT = 4 / 3;
125     var maxDataURLLength =
126         DATA_URL_PREFIX.length + Math.ceil(maxEncodedSize * BASE64_BLOAT);
127     for (; quality > 0.2; quality *= 0.8) {
128       ImageEncoder.MetadataEncoder.prototype.setThumbnailData.call(
129           this, canvas, quality);
130       // If the obtained thumbnail URL is too long, reset the URL and try again
131       // with less quality value.
132       if (this.thumbnailDataUrl.length > maxDataURLLength) {
133         this.thumbnailDataUrl = '';
134         continue;
135       }
136       break;
137     }
138   }
139   if (this.thumbnailDataUrl) {
140     var thumbnail = this.ifd_.thumbnail;
141     if (!thumbnail)
142       thumbnail = this.ifd_.thumbnail = {};
144     ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.IMAGE_WIDTH).value =
145         canvas.width;
147     ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.IMAGE_HEIGHT).value =
148         canvas.height;
150     // The values for these tags will be set in ExifWriter.encode.
151     ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.JPG_THUMB_OFFSET);
152     ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.JPG_THUMB_LENGTH);
154     // Always save in default orientation.
155     ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.ORIENTATION).value = 1;
157     // When thumbnail is compressed with JPEG, compression must be set as 6.
158     ExifEncoder.findOrCreateTag(this.ifd_.image, Exif.Tag.COMPRESSION).value =
159         6;
160   } else {
161     if (this.ifd_.thumbnail)
162       delete this.ifd_.thumbnail;
163   }
167  * @override
168  */
169 ExifEncoder.prototype.findInsertionRange = function(encodedImage) {
170   function getWord(pos) {
171     if (pos + 2 > encodedImage.length)
172       throw 'Reading past the buffer end @' + pos;
173     return encodedImage.charCodeAt(pos) << 8 | encodedImage.charCodeAt(pos + 1);
174   }
176   if (getWord(0) != Exif.Mark.SOI)
177     throw new Error('Jpeg data starts from 0x' + getWord(0).toString(16));
179   var sectionStart = 2;
181   // Default: an empty range right after SOI.
182   // Will be returned in absence of APP0 or Exif sections.
183   var range = {from: sectionStart, to: sectionStart};
185   for (;;) {
186     var tag = getWord(sectionStart);
188     if (tag == Exif.Mark.SOS)
189       break;
191     var nextSectionStart = sectionStart + 2 + getWord(sectionStart + 2);
192     if (nextSectionStart <= sectionStart ||
193         nextSectionStart > encodedImage.length)
194       throw new Error('Invalid section size in jpeg data');
196     if (tag == Exif.Mark.APP0) {
197       // Assert that we have not seen the Exif section yet.
198       if (range.from != range.to)
199         throw new Error('APP0 section found after EXIF section');
200       // An empty range right after the APP0 segment.
201       range.from = range.to = nextSectionStart;
202     } else if (tag == Exif.Mark.EXIF) {
203       // A range containing the existing EXIF section.
204       range.from = sectionStart;
205       range.to = nextSectionStart;
206     }
207     sectionStart = nextSectionStart;
208   }
210   return range;
214  * @override
215  */
216 ExifEncoder.prototype.encode = function() {
217   var HEADER_SIZE = 10;
219   // Allocate the largest theoretically possible size.
220   var bytes = new Uint8Array(ExifEncoder.MAXIMUM_EXIF_DATA_SIZE);
222   // Serialize header
223   var hw = new ByteWriter(bytes.buffer, 0, HEADER_SIZE);
224   hw.writeScalar(Exif.Mark.EXIF, 2);
225   hw.forward('size', 2);
226   hw.writeString('Exif\0\0');  // Magic string.
228   // First serialize the content of the exif section.
229   // Use a ByteWriter starting at HEADER_SIZE offset so that tell() positions
230   // can be directly mapped to offsets as encoded in the dictionaries.
231   var bw = new ByteWriter(bytes.buffer, HEADER_SIZE);
233   if (this.exifLittleEndian_) {
234     bw.setByteOrder(ByteWriter.ByteOrder.LITTLE_ENDIAN);
235     bw.writeScalar(Exif.Align.LITTLE, 2);
236   } else {
237     bw.setByteOrder(ByteWriter.ByteOrder.BIG_ENDIAN);
238     bw.writeScalar(Exif.Align.BIG, 2);
239   }
241   bw.writeScalar(Exif.Tag.TIFF, 2);
243   bw.forward('image-dir', 4);  // The pointer should point right after itself.
244   bw.resolveOffset('image-dir');
246   ExifEncoder.encodeDirectory(bw, this.ifd_.image,
247       [Exif.Tag.EXIFDATA, Exif.Tag.GPSDATA], 'thumb-dir');
249   if (this.ifd_.exif) {
250     bw.resolveOffset(Exif.Tag.EXIFDATA);
251     ExifEncoder.encodeDirectory(bw, this.ifd_.exif);
252   } else {
253     if (Exif.Tag.EXIFDATA in this.ifd_.image)
254       throw new Error('Corrupt exif dictionary reference');
255   }
257   if (this.ifd_.gps) {
258     bw.resolveOffset(Exif.Tag.GPSDATA);
259     ExifEncoder.encodeDirectory(bw, this.ifd_.gps);
260   } else {
261     if (Exif.Tag.GPSDATA in this.ifd_.image)
262       throw new Error('Missing gps dictionary reference');
263   }
265   // Since thumbnail data can be large, check enough space has left for
266   // thumbnail data.
267   var thumbnailDecoded = this.ifd_.thumbnail ?
268       ImageEncoder.decodeDataURL(this.thumbnailDataUrl) : '';
269   if (this.ifd_.thumbnail &&
270       (ExifEncoder.MAXIMUM_EXIF_DATA_SIZE - bw.tell() >=
271        thumbnailDecoded.length + ExifEncoder.THUMBNAIL_METADATA_SIZE)) {
272     bw.resolveOffset('thumb-dir');
273     ExifEncoder.encodeDirectory(
274         bw,
275         this.ifd_.thumbnail,
276         [Exif.Tag.JPG_THUMB_OFFSET, Exif.Tag.JPG_THUMB_LENGTH]);
278     bw.resolveOffset(Exif.Tag.JPG_THUMB_OFFSET);
279     bw.resolve(Exif.Tag.JPG_THUMB_LENGTH, thumbnailDecoded.length);
280     bw.writeString(thumbnailDecoded);
281   } else {
282     bw.resolve('thumb-dir', 0);
283   }
285   bw.checkResolved();
287   var totalSize = HEADER_SIZE + bw.tell();
288   hw.resolve('size', totalSize - 2);  // The marker is excluded.
289   hw.checkResolved();
291   var subarray = new Uint8Array(totalSize);
292   for (var i = 0; i != totalSize; i++) {
293     subarray[i] = bytes[i];
294   }
295   return subarray.buffer;
299  * Static methods.
300  */
303  * Write the contents of an IFD directory.
304  * @param {!ByteWriter} bw ByteWriter to use.
305  * @param {!Object<!Exif.Tag, ExifEntry>} directory A directory map as created
306  *     by ExifParser.
307  * @param {Array=} opt_resolveLater An array of tag ids for which the values
308  *     will be resolved later.
309  * @param {string=} opt_nextDirPointer A forward key for the pointer to the next
310  *     directory. If omitted the pointer is set to 0.
311  */
312 ExifEncoder.encodeDirectory = function(
313     bw, directory, opt_resolveLater, opt_nextDirPointer) {
315   var longValues = [];
317   bw.forward('dir-count', 2);
318   var count = 0;
320   for (var key in directory) {
321     var tag = directory[/** @type {!Exif.Tag} */ (parseInt(key, 10))];
322     bw.writeScalar(/** @type {number}*/ (tag.id), 2);
323     bw.writeScalar(tag.format, 2);
324     bw.writeScalar(tag.componentCount, 4);
326     var width = ExifEncoder.getComponentWidth(tag) * tag.componentCount;
328     if (opt_resolveLater && (opt_resolveLater.indexOf(tag.id) >= 0)) {
329       // The actual value depends on further computations.
330       if (tag.componentCount != 1 || width > 4)
331         throw new Error('Cannot forward the pointer for ' + tag.id);
332       bw.forward(tag.id, width);
333     } else if (width <= 4) {
334       // The value fits into 4 bytes, write it immediately.
335       ExifEncoder.writeValue(bw, tag);
336     } else {
337       // The value does not fit, forward the 4 byte offset to the actual value.
338       width = 4;
339       bw.forward(tag.id, width);
340       longValues.push(tag);
341     }
342     bw.skip(4 - width);  // Align so that the value take up exactly 4 bytes.
343     count++;
344   }
346   bw.resolve('dir-count', count);
348   if (opt_nextDirPointer) {
349     bw.forward(opt_nextDirPointer, 4);
350   } else {
351     bw.writeScalar(0, 4);
352   }
354   // Write out the long values and resolve pointers.
355   for (var i = 0; i != longValues.length; i++) {
356     var longValue = longValues[i];
357     bw.resolveOffset(longValue.id);
358     ExifEncoder.writeValue(bw, longValue);
359   }
363  * @param {ExifEntry} tag EXIF tag object.
364  * @return {number} Width in bytes of the data unit associated with this tag.
365  * TODO(kaznacheev): Share with ExifParser?
366  */
367 ExifEncoder.getComponentWidth = function(tag) {
368   switch (tag.format) {
369     case 1:  // Byte
370     case 2:  // String
371     case 7:  // Undefined
372       return 1;
374     case 3:  // Short
375       return 2;
377     case 4:  // Long
378     case 9:  // Signed Long
379       return 4;
381     case 5:  // Rational
382     case 10:  // Signed Rational
383       return 8;
385     default:  // ???
386       console.warn('Unknown tag format 0x' +
387           Number(tag.id).toString(16) + ': ' + tag.format);
388       return 4;
389   }
393  * Writes out the tag value.
394  * @param {!ByteWriter} bw Writer to use.
395  * @param {ExifEntry} tag Tag, which value to write.
396  */
397 ExifEncoder.writeValue = function(bw, tag) {
398   if (tag.format === 2) {  // String
399     if (tag.componentCount !== tag.value.length) {
400       throw new Error(
401           'String size mismatch for 0x' + Number(tag.id).toString(16));
402     }
404     if (tag.value.charAt(tag.value.length - 1) !== '\0')
405       throw new Error('String must end with null character.');
407     bw.writeString(/** @type {string} */ (tag.value));
408   } else {  // Scalar or rational
409     var width = ExifEncoder.getComponentWidth(tag);
411     var writeComponent = function(value, signed) {
412       if (width == 8) {
413         bw.writeScalar(value[0], 4, signed);
414         bw.writeScalar(value[1], 4, signed);
415       } else {
416         bw.writeScalar(value, width, signed);
417       }
418     };
420     var signed = (tag.format == 9 || tag.format == 10);
421     if (tag.componentCount == 1) {
422       writeComponent(tag.value, signed);
423     } else {
424       for (var i = 0; i != tag.componentCount; i++) {
425         writeComponent(tag.value[i], signed);
426       }
427     }
428   }
432  * Finds a tag. If not exist, creates a tag.
434  * @param {!Object<!Exif.Tag, ExifEntry>} directory EXIF directory.
435  * @param {!Exif.Tag} id Tag id.
436  * @param {number=} opt_format Tag format
437  *     (used in {@link ExifEncoder#getComponentWidth}).
438  * @param {number=} opt_componentCount Number of components in this tag.
439  * @return {ExifEntry}
440  *     Tag found or created.
441  */
442 ExifEncoder.findOrCreateTag = function(directory, id, opt_format,
443     opt_componentCount) {
444   if (!(id in directory)) {
445     directory[id] = {
446       id: id,
447       format: opt_format || 3,  // Short
448       componentCount: opt_componentCount || 1,
449       value: 0
450     };
451   }
452   return directory[id];
456  * ByteWriter class.
457  * @param {!ArrayBuffer} arrayBuffer Underlying buffer to use.
458  * @param {number} offset Offset at which to start writing.
459  * @param {number=} opt_length Maximum length to use.
460  * @constructor
461  * @struct
462  */
463 function ByteWriter(arrayBuffer, offset, opt_length) {
464   var length = opt_length || (arrayBuffer.byteLength - offset);
465   this.view_ = new DataView(arrayBuffer, offset, length);
466   this.littleEndian_ = false;
467   this.pos_ = 0;
468   this.forwards_ = {};
472  * Byte order.
473  * @enum {number}
474  */
475 ByteWriter.ByteOrder = {
476   // Little endian byte order.
477   LITTLE_ENDIAN: 0,
478   // Big endian byte order.
479   BIG_ENDIAN: 1
483  * Set the byte ordering for future writes.
484  * @param {ByteWriter.ByteOrder} order ByteOrder to use
485  *     {ByteWriter.LITTLE_ENDIAN} or {ByteWriter.BIG_ENDIAN}.
486  */
487 ByteWriter.prototype.setByteOrder = function(order) {
488   this.littleEndian_ = (order === ByteWriter.ByteOrder.LITTLE_ENDIAN);
492  * @return {number} the current write position.
493  */
494 ByteWriter.prototype.tell = function() { return this.pos_ };
497  * Skips desired amount of bytes in output stream.
498  * @param {number} count Byte count to skip.
499  */
500 ByteWriter.prototype.skip = function(count) {
501   this.validateWrite(count);
502   this.pos_ += count;
506  * Check if the buffer has enough room to read 'width' bytes. Throws an error
507  * if it has not.
508  * @param {number} width Amount of bytes to check.
509  */
510 ByteWriter.prototype.validateWrite = function(width) {
511   if (this.pos_ + width > this.view_.byteLength)
512     throw new Error('Writing past the end of the buffer');
516  * Writes scalar value to output stream.
517  * @param {number} value Value to write.
518  * @param {number} width Desired width of written value.
519  * @param {boolean=} opt_signed True if value represents signed number.
520  */
521 ByteWriter.prototype.writeScalar = function(value, width, opt_signed) {
522   var method;
523   // The below switch is so verbose for two reasons:
524   // 1. V8 is faster on method names which are 'symbols'.
525   // 2. Method names are discoverable by full text search.
526   switch (width) {
527     case 1:
528       method = opt_signed ? 'setInt8' : 'setUint8';
529       break;
531     case 2:
532       method = opt_signed ? 'setInt16' : 'setUint16';
533       break;
535     case 4:
536       method = opt_signed ? 'setInt32' : 'setUint32';
537       break;
539     case 8:
540       method = opt_signed ? 'setInt64' : 'setUint64';
541       break;
543     default:
544       throw new Error('Invalid width: ' + width);
545       break;
546   }
548   this.validateWrite(width);
549   this.view_[method](this.pos_, value, this.littleEndian_);
550   this.pos_ += width;
554  * Writes string.
555  * @param {string} str String to write.
556  */
557 ByteWriter.prototype.writeString = function(str) {
558   this.validateWrite(str.length);
559   for (var i = 0; i != str.length; i++) {
560     this.view_.setUint8(this.pos_++, str.charCodeAt(i));
561   }
565  * Allocate the space for 'width' bytes for the value that will be set later.
566  * To be followed by a 'resolve' call with the same key.
567  * @param {(string|Exif.Tag)} key A key to identify the value.
568  * @param {number} width Width of the value in bytes.
569  */
570 ByteWriter.prototype.forward = function(key, width) {
571   if (key in this.forwards_)
572     throw new Error('Duplicate forward key ' + key);
573   this.validateWrite(width);
574   this.forwards_[key] = {
575     pos: this.pos_,
576     width: width
577   };
578   this.pos_ += width;
582  * Set the value previously allocated with a 'forward' call.
583  * @param {(string|Exif.Tag)} key A key to identify the value.
584  * @param {number} value value to write in pre-allocated space.
585  */
586 ByteWriter.prototype.resolve = function(key, value) {
587   if (!(key in this.forwards_))
588     throw new Error('Undeclared forward key ' + key.toString(16));
589   var forward = this.forwards_[key];
590   var curPos = this.pos_;
591   this.pos_ = forward.pos;
592   this.writeScalar(value, forward.width);
593   this.pos_ = curPos;
594   delete this.forwards_[key];
598  * A shortcut to resolve the value to the current write position.
599  * @param {(string|Exif.Tag)} key A key to identify pre-allocated position.
600  */
601 ByteWriter.prototype.resolveOffset = function(key) {
602   this.resolve(key, this.tell());
606  * Check if every forward has been resolved, throw and error if not.
607  */
608 ByteWriter.prototype.checkResolved = function() {
609   for (var key in this.forwards_) {
610     throw new Error('Unresolved forward pointer ' +
611         ByteWriter.prettyKeyFormat(key));
612   }
616  * If key is a number, format it in hex style.
617  * @param {!(string|Exif.Tag)} key A key.
618  * @return {string} Formatted representation.
619  */
620 ByteWriter.prettyKeyFormat = function(key) {
621   if (typeof key === 'number') {
622     return '0x' + key.toString(16);
623   } else {
624     return key;
625   }