Add ability for NetLogLogger to gather data from more than just NetLog
[chromium-blink-merge.git] / ui / file_manager / gallery / js / image_editor / exif_encoder.js
blob0a1813ccc3433fce73737870f6c6c3734698824e
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
13 function ExifEncoder(originalMetadata) {
14 ImageEncoder.MetadataEncoder.apply(this, arguments);
15 /**
16 * Image File Directory obtained from EXIF header.
17 * @private {!Object}
18 * @const
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
29 this.exifLittleEndian_ = !!originalMetadata.exifLittleEndian;
31 /**
32 * Modification time to be stored in EXIF header.
33 * @private {!Date}
34 * @const
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
48 ExifEncoder.SOFTWARE = 'Chrome OS Gallery App\0';
50 /**
51 * @param {!HTMLCanvasElement} canvas
52 * @override
54 ExifEncoder.prototype.setImageData = function(canvas) {
55 ImageEncoder.MetadataEncoder.prototype.setImageData.call(this, canvas);
57 var image = this.ifd_.image;
58 if (!image)
59 image = this.ifd_.image = {};
61 // Only update width/height in this directory if they are present.
62 if (image[Exif.Tag.IMAGE_WIDTH] && image[Exif.Tag.IMAGE_HEIGHT]) {
63 image[Exif.Tag.IMAGE_WIDTH].value = canvas.width;
64 image[Exif.Tag.IMAGE_HEIGHT].value = canvas.height;
67 var exif = this.ifd_.exif;
68 if (!exif)
69 exif = this.ifd_.exif = {};
70 ExifEncoder.findOrCreateTag(image, Exif.Tag.EXIFDATA);
71 ExifEncoder.findOrCreateTag(exif, Exif.Tag.X_DIMENSION).value = canvas.width;
72 ExifEncoder.findOrCreateTag(exif, Exif.Tag.Y_DIMENSION).value = canvas.height;
74 // Always save in default orientation.
75 ExifEncoder.findOrCreateTag(image, Exif.Tag.ORIENTATION).value = 1;
77 // Update software name.
78 var softwareTag = ExifEncoder.findOrCreateTag(image, Exif.Tag.SOFTWARE, 2);
79 softwareTag.value = ExifEncoder.SOFTWARE;
80 softwareTag.componentCount = ExifEncoder.SOFTWARE.length;
82 // Update modification date time.
83 var padNumWithZero = function(num, length) {
84 var str = num.toString();
85 while (str.length < length) {
86 str = '0' + str;
88 return str;
91 var dateTimeTag = ExifEncoder.findOrCreateTag(image, Exif.Tag.DATETIME, 2);
92 dateTimeTag.value =
93 padNumWithZero(this.modificationTime_.getFullYear(), 4) + ':' +
94 padNumWithZero(this.modificationTime_.getMonth() + 1, 2) + ':' +
95 padNumWithZero(this.modificationTime_.getDate(), 2) + ' ' +
96 padNumWithZero(this.modificationTime_.getHours(), 2) + ':' +
97 padNumWithZero(this.modificationTime_.getMinutes(), 2) + ':' +
98 padNumWithZero(this.modificationTime_.getSeconds(), 2) + '\0';
99 dateTimeTag.componentCount = 20;
103 * @override
105 ExifEncoder.prototype.setThumbnailData = function(canvas, quality) {
106 if (canvas) {
107 // Empirical formula with reasonable behavior:
108 // 10K for 1Mpix, 30K for 5Mpix, 50K for 9Mpix and up.
109 var pixelCount = this.imageWidth * this.imageHeight;
110 var maxEncodedSize = 5000 * Math.min(10, 1 + pixelCount / 1000000);
111 var DATA_URL_PREFIX = 'data:image/jpeg;base64,';
112 var BASE64_BLOAT = 4 / 3;
113 var maxDataURLLength =
114 DATA_URL_PREFIX.length + Math.ceil(maxEncodedSize * BASE64_BLOAT);
115 for (; quality > 0.2; quality *= 0.8) {
116 ImageEncoder.MetadataEncoder.prototype.setThumbnailData.call(
117 this, canvas, quality);
118 // If the obtained thumbnail URL is too long, reset the URL and try again
119 // with less quality value.
120 if (this.thumbnailDataUrl.length > maxDataURLLength) {
121 this.thumbnailDataUrl = '';
122 continue;
124 break;
127 if (this.thumbnailDataUrl) {
128 var thumbnail = this.ifd_.thumbnail;
129 if (!thumbnail)
130 thumbnail = this.ifd_.thumbnail = {};
132 ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.IMAGE_WIDTH).value =
133 canvas.width;
135 ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.IMAGE_HEIGHT).value =
136 canvas.height;
138 // The values for these tags will be set in ExifWriter.encode.
139 ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.JPG_THUMB_OFFSET);
140 ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.JPG_THUMB_LENGTH);
142 // Always save in default orientation.
143 ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.ORIENTATION).value = 1;
145 // When thumbnail is compressed with JPEG, compression must be set as 6.
146 ExifEncoder.findOrCreateTag(this.ifd_.image, Exif.Tag.COMPRESSION).value =
148 } else {
149 if (this.ifd_.thumbnail)
150 delete this.ifd_.thumbnail;
155 * @override
157 ExifEncoder.prototype.findInsertionRange = function(encodedImage) {
158 function getWord(pos) {
159 if (pos + 2 > encodedImage.length)
160 throw 'Reading past the buffer end @' + pos;
161 return encodedImage.charCodeAt(pos) << 8 | encodedImage.charCodeAt(pos + 1);
164 if (getWord(0) != Exif.Mark.SOI)
165 throw new Error('Jpeg data starts from 0x' + getWord(0).toString(16));
167 var sectionStart = 2;
169 // Default: an empty range right after SOI.
170 // Will be returned in absence of APP0 or Exif sections.
171 var range = {from: sectionStart, to: sectionStart};
173 for (;;) {
174 var tag = getWord(sectionStart);
176 if (tag == Exif.Mark.SOS)
177 break;
179 var nextSectionStart = sectionStart + 2 + getWord(sectionStart + 2);
180 if (nextSectionStart <= sectionStart ||
181 nextSectionStart > encodedImage.length)
182 throw new Error('Invalid section size in jpeg data');
184 if (tag == Exif.Mark.APP0) {
185 // Assert that we have not seen the Exif section yet.
186 if (range.from != range.to)
187 throw new Error('APP0 section found after EXIF section');
188 // An empty range right after the APP0 segment.
189 range.from = range.to = nextSectionStart;
190 } else if (tag == Exif.Mark.EXIF) {
191 // A range containing the existing EXIF section.
192 range.from = sectionStart;
193 range.to = nextSectionStart;
195 sectionStart = nextSectionStart;
198 return range;
202 * @override
204 ExifEncoder.prototype.encode = function() {
205 var HEADER_SIZE = 10;
207 // Allocate the largest theoretically possible size.
208 var bytes = new Uint8Array(0x10000);
210 // Serialize header
211 var hw = new ByteWriter(bytes.buffer, 0, HEADER_SIZE);
212 hw.writeScalar(Exif.Mark.EXIF, 2);
213 hw.forward('size', 2);
214 hw.writeString('Exif\0\0'); // Magic string.
216 // First serialize the content of the exif section.
217 // Use a ByteWriter starting at HEADER_SIZE offset so that tell() positions
218 // can be directly mapped to offsets as encoded in the dictionaries.
219 var bw = new ByteWriter(bytes.buffer, HEADER_SIZE);
221 if (this.exifLittleEndian_) {
222 bw.setByteOrder(ByteWriter.ByteOrder.LITTLE_ENDIAN);
223 bw.writeScalar(Exif.Align.LITTLE, 2);
224 } else {
225 bw.setByteOrder(ByteWriter.ByteOrder.BIG_ENDIAN);
226 bw.writeScalar(Exif.Align.BIG, 2);
229 bw.writeScalar(Exif.Tag.TIFF, 2);
231 bw.forward('image-dir', 4); // The pointer should point right after itself.
232 bw.resolveOffset('image-dir');
234 ExifEncoder.encodeDirectory(bw, this.ifd_.image,
235 [Exif.Tag.EXIFDATA, Exif.Tag.GPSDATA], 'thumb-dir');
237 if (this.ifd_.exif) {
238 bw.resolveOffset(Exif.Tag.EXIFDATA);
239 ExifEncoder.encodeDirectory(bw, this.ifd_.exif);
240 } else {
241 if (Exif.Tag.EXIFDATA in this.ifd_.image)
242 throw new Error('Corrupt exif dictionary reference');
245 if (this.ifd_.gps) {
246 bw.resolveOffset(Exif.Tag.GPSDATA);
247 ExifEncoder.encodeDirectory(bw, this.ifd_.gps);
248 } else {
249 if (Exif.Tag.GPSDATA in this.ifd_.image)
250 throw new Error('Missing gps dictionary reference');
253 if (this.ifd_.thumbnail) {
254 bw.resolveOffset('thumb-dir');
255 ExifEncoder.encodeDirectory(
257 this.ifd_.thumbnail,
258 [Exif.Tag.JPG_THUMB_OFFSET, Exif.Tag.JPG_THUMB_LENGTH]);
260 var thumbnailDecoded = ImageEncoder.decodeDataURL(this.thumbnailDataUrl);
261 bw.resolveOffset(Exif.Tag.JPG_THUMB_OFFSET);
262 bw.resolve(Exif.Tag.JPG_THUMB_LENGTH, thumbnailDecoded.length);
263 bw.writeString(thumbnailDecoded);
264 } else {
265 bw.resolve('thumb-dir', 0);
268 bw.checkResolved();
270 var totalSize = HEADER_SIZE + bw.tell();
271 hw.resolve('size', totalSize - 2); // The marker is excluded.
272 hw.checkResolved();
274 var subarray = new Uint8Array(totalSize);
275 for (var i = 0; i != totalSize; i++) {
276 subarray[i] = bytes[i];
278 return subarray.buffer;
282 * Static methods.
286 * Write the contents of an IFD directory.
287 * @param {!ByteWriter} bw ByteWriter to use.
288 * @param {!Object.<!Exif.Tag, ExifEntry>} directory A directory map as created
289 * by ExifParser.
290 * @param {Array=} opt_resolveLater An array of tag ids for which the values
291 * will be resolved later.
292 * @param {string=} opt_nextDirPointer A forward key for the pointer to the next
293 * directory. If omitted the pointer is set to 0.
295 ExifEncoder.encodeDirectory = function(
296 bw, directory, opt_resolveLater, opt_nextDirPointer) {
298 var longValues = [];
300 bw.forward('dir-count', 2);
301 var count = 0;
303 for (var key in directory) {
304 var tag = directory[/** @type {!Exif.Tag} */ (parseInt(key, 10))];
305 bw.writeScalar(/** @type {number}*/ (tag.id), 2);
306 bw.writeScalar(tag.format, 2);
307 bw.writeScalar(tag.componentCount, 4);
309 var width = ExifEncoder.getComponentWidth(tag) * tag.componentCount;
311 if (opt_resolveLater && (opt_resolveLater.indexOf(tag.id) >= 0)) {
312 // The actual value depends on further computations.
313 if (tag.componentCount != 1 || width > 4)
314 throw new Error('Cannot forward the pointer for ' + tag.id);
315 bw.forward(tag.id, width);
316 } else if (width <= 4) {
317 // The value fits into 4 bytes, write it immediately.
318 ExifEncoder.writeValue(bw, tag);
319 } else {
320 // The value does not fit, forward the 4 byte offset to the actual value.
321 width = 4;
322 bw.forward(tag.id, width);
323 longValues.push(tag);
325 bw.skip(4 - width); // Align so that the value take up exactly 4 bytes.
326 count++;
329 bw.resolve('dir-count', count);
331 if (opt_nextDirPointer) {
332 bw.forward(opt_nextDirPointer, 4);
333 } else {
334 bw.writeScalar(0, 4);
337 // Write out the long values and resolve pointers.
338 for (var i = 0; i != longValues.length; i++) {
339 var longValue = longValues[i];
340 bw.resolveOffset(longValue.id);
341 ExifEncoder.writeValue(bw, longValue);
346 * @param {ExifEntry} tag EXIF tag object.
347 * @return {number} Width in bytes of the data unit associated with this tag.
348 * TODO(kaznacheev): Share with ExifParser?
350 ExifEncoder.getComponentWidth = function(tag) {
351 switch (tag.format) {
352 case 1: // Byte
353 case 2: // String
354 case 7: // Undefined
355 return 1;
357 case 3: // Short
358 return 2;
360 case 4: // Long
361 case 9: // Signed Long
362 return 4;
364 case 5: // Rational
365 case 10: // Signed Rational
366 return 8;
368 default: // ???
369 console.warn('Unknown tag format 0x' +
370 Number(tag.id).toString(16) + ': ' + tag.format);
371 return 4;
376 * Writes out the tag value.
377 * @param {!ByteWriter} bw Writer to use.
378 * @param {ExifEntry} tag Tag, which value to write.
380 ExifEncoder.writeValue = function(bw, tag) {
381 if (tag.format === 2) { // String
382 if (tag.componentCount !== tag.value.length) {
383 throw new Error(
384 'String size mismatch for 0x' + Number(tag.id).toString(16));
387 if (tag.value.charAt(tag.value.length - 1) !== '\0')
388 throw new Error('String must end with null character.');
390 bw.writeString(/** @type {string} */ (tag.value));
391 } else { // Scalar or rational
392 var width = ExifEncoder.getComponentWidth(tag);
394 var writeComponent = function(value, signed) {
395 if (width == 8) {
396 bw.writeScalar(value[0], 4, signed);
397 bw.writeScalar(value[1], 4, signed);
398 } else {
399 bw.writeScalar(value, width, signed);
403 var signed = (tag.format == 9 || tag.format == 10);
404 if (tag.componentCount == 1) {
405 writeComponent(tag.value, signed);
406 } else {
407 for (var i = 0; i != tag.componentCount; i++) {
408 writeComponent(tag.value[i], signed);
415 * Finds a tag. If not exist, creates a tag.
417 * @param {!Object.<!Exif.Tag, ExifEntry>} directory EXIF directory.
418 * @param {!Exif.Tag} id Tag id.
419 * @param {number=} opt_format Tag format
420 * (used in {@link ExifEncoder#getComponentWidth}).
421 * @param {number=} opt_componentCount Number of components in this tag.
422 * @return {ExifEntry}
423 * Tag found or created.
425 ExifEncoder.findOrCreateTag = function(directory, id, opt_format,
426 opt_componentCount) {
427 if (!(id in directory)) {
428 directory[id] = {
429 id: id,
430 format: opt_format || 3, // Short
431 componentCount: opt_componentCount || 1,
432 value: 0
435 return directory[id];
439 * ByteWriter class.
440 * @param {!ArrayBuffer} arrayBuffer Underlying buffer to use.
441 * @param {number} offset Offset at which to start writing.
442 * @param {number=} opt_length Maximum length to use.
443 * @constructor
444 * @struct
446 function ByteWriter(arrayBuffer, offset, opt_length) {
447 var length = opt_length || (arrayBuffer.byteLength - offset);
448 this.view_ = new DataView(arrayBuffer, offset, length);
449 this.littleEndian_ = false;
450 this.pos_ = 0;
451 this.forwards_ = {};
455 * Byte order.
456 * @enum {number}
458 ByteWriter.ByteOrder = {
459 // Little endian byte order.
460 LITTLE_ENDIAN: 0,
461 // Big endian byte order.
462 BIG_ENDIAN: 1
466 * Set the byte ordering for future writes.
467 * @param {ByteWriter.ByteOrder} order ByteOrder to use
468 * {ByteWriter.LITTLE_ENDIAN} or {ByteWriter.BIG_ENDIAN}.
470 ByteWriter.prototype.setByteOrder = function(order) {
471 this.littleEndian_ = (order === ByteWriter.ByteOrder.LITTLE_ENDIAN);
475 * @return {number} the current write position.
477 ByteWriter.prototype.tell = function() { return this.pos_ };
480 * Skips desired amount of bytes in output stream.
481 * @param {number} count Byte count to skip.
483 ByteWriter.prototype.skip = function(count) {
484 this.validateWrite(count);
485 this.pos_ += count;
489 * Check if the buffer has enough room to read 'width' bytes. Throws an error
490 * if it has not.
491 * @param {number} width Amount of bytes to check.
493 ByteWriter.prototype.validateWrite = function(width) {
494 if (this.pos_ + width > this.view_.byteLength)
495 throw new Error('Writing past the end of the buffer');
499 * Writes scalar value to output stream.
500 * @param {number} value Value to write.
501 * @param {number} width Desired width of written value.
502 * @param {boolean=} opt_signed True if value represents signed number.
504 ByteWriter.prototype.writeScalar = function(value, width, opt_signed) {
505 var method;
506 // The below switch is so verbose for two reasons:
507 // 1. V8 is faster on method names which are 'symbols'.
508 // 2. Method names are discoverable by full text search.
509 switch (width) {
510 case 1:
511 method = opt_signed ? 'setInt8' : 'setUint8';
512 break;
514 case 2:
515 method = opt_signed ? 'setInt16' : 'setUint16';
516 break;
518 case 4:
519 method = opt_signed ? 'setInt32' : 'setUint32';
520 break;
522 case 8:
523 method = opt_signed ? 'setInt64' : 'setUint64';
524 break;
526 default:
527 throw new Error('Invalid width: ' + width);
528 break;
531 this.validateWrite(width);
532 this.view_[method](this.pos_, value, this.littleEndian_);
533 this.pos_ += width;
537 * Writes string.
538 * @param {string} str String to write.
540 ByteWriter.prototype.writeString = function(str) {
541 this.validateWrite(str.length);
542 for (var i = 0; i != str.length; i++) {
543 this.view_.setUint8(this.pos_++, str.charCodeAt(i));
548 * Allocate the space for 'width' bytes for the value that will be set later.
549 * To be followed by a 'resolve' call with the same key.
550 * @param {(string|Exif.Tag)} key A key to identify the value.
551 * @param {number} width Width of the value in bytes.
553 ByteWriter.prototype.forward = function(key, width) {
554 if (key in this.forwards_)
555 throw new Error('Duplicate forward key ' + key);
556 this.validateWrite(width);
557 this.forwards_[key] = {
558 pos: this.pos_,
559 width: width
561 this.pos_ += width;
565 * Set the value previously allocated with a 'forward' call.
566 * @param {(string|Exif.Tag)} key A key to identify the value.
567 * @param {number} value value to write in pre-allocated space.
569 ByteWriter.prototype.resolve = function(key, value) {
570 if (!(key in this.forwards_))
571 throw new Error('Undeclared forward key ' + key.toString(16));
572 var forward = this.forwards_[key];
573 var curPos = this.pos_;
574 this.pos_ = forward.pos;
575 this.writeScalar(value, forward.width);
576 this.pos_ = curPos;
577 delete this.forwards_[key];
581 * A shortcut to resolve the value to the current write position.
582 * @param {(string|Exif.Tag)} key A key to identify pre-allocated position.
584 ByteWriter.prototype.resolveOffset = function(key) {
585 this.resolve(key, this.tell());
589 * Check if every forward has been resolved, throw and error if not.
591 ByteWriter.prototype.checkResolved = function() {
592 for (var key in this.forwards_) {
593 throw new Error('Unresolved forward pointer ' +
594 ByteWriter.prettyKeyFormat(key));
599 * If key is a number, format it in hex style.
600 * @param {!(string|Exif.Tag)} key A key.
601 * @return {string} Formatted representation.
603 ByteWriter.prettyKeyFormat = function(key) {
604 if (typeof key === 'number') {
605 return '0x' + key.toString(16);
606 } else {
607 return key;