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.
6 * The Exif metadata encoder.
7 * Uses the metadata format as defined by ExifParser.
8 * @param {!MetadataItem} originalMetadata Metadata to encode.
10 * @extends {ImageEncoder.MetadataEncoder}
13 function ExifEncoder(originalMetadata) {
14 ImageEncoder.MetadataEncoder.apply(this, arguments);
16 * Image File Directory obtained from EXIF header.
20 this.ifd_ = /** @type {!Object} */(
21 JSON.parse(JSON.stringify(originalMetadata.ifd || {})));
24 * Note use little endian if the original metadata does not have the
29 this.exifLittleEndian_ = !!originalMetadata.exifLittleEndian;
32 * Modification time to be stored in EXIF header.
36 this.modificationTime_ = assert(originalMetadata.modificationTime);
39 ExifEncoder.prototype = {__proto__: ImageEncoder.MetadataEncoder.prototype};
41 ImageEncoder.registerMetadataEncoder(ExifEncoder, 'image/jpeg');
44 * Software name of Gallery.app.
48 ExifEncoder.SOFTWARE = 'Chrome OS Gallery App\0';
51 * Maximum size of exif data.
54 ExifEncoder.MAXIMUM_EXIF_DATA_SIZE = 0x10000;
57 * Size of metadata for thumbnail.
60 ExifEncoder.THUMBNAIL_METADATA_SIZE = 76;
63 * @param {!HTMLCanvasElement} canvas
66 ExifEncoder.prototype.setImageData = function(canvas) {
67 ImageEncoder.MetadataEncoder.prototype.setImageData.call(this, canvas);
69 var image = this.ifd_.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;
79 var exif = this.ifd_.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) {
103 var dateTimeTag = ExifEncoder.findOrCreateTag(image, Exif.Tag.DATETIME, 2);
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;
117 ExifEncoder.prototype.setThumbnailData = function(canvas, quality) {
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 = '';
139 if (this.thumbnailDataUrl) {
140 var thumbnail = this.ifd_.thumbnail;
142 thumbnail = this.ifd_.thumbnail = {};
144 ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.IMAGE_WIDTH).value =
147 ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.IMAGE_HEIGHT).value =
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 =
161 if (this.ifd_.thumbnail)
162 delete this.ifd_.thumbnail;
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);
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};
186 var tag = getWord(sectionStart);
188 if (tag == Exif.Mark.SOS)
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;
207 sectionStart = nextSectionStart;
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);
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);
237 bw.setByteOrder(ByteWriter.ByteOrder.BIG_ENDIAN);
238 bw.writeScalar(Exif.Align.BIG, 2);
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);
253 if (Exif.Tag.EXIFDATA in this.ifd_.image)
254 throw new Error('Corrupt exif dictionary reference');
258 bw.resolveOffset(Exif.Tag.GPSDATA);
259 ExifEncoder.encodeDirectory(bw, this.ifd_.gps);
261 if (Exif.Tag.GPSDATA in this.ifd_.image)
262 throw new Error('Missing gps dictionary reference');
265 // Since thumbnail data can be large, check enough space has left for
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(
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);
282 bw.resolve('thumb-dir', 0);
287 var totalSize = HEADER_SIZE + bw.tell();
288 hw.resolve('size', totalSize - 2); // The marker is excluded.
291 var subarray = new Uint8Array(totalSize);
292 for (var i = 0; i != totalSize; i++) {
293 subarray[i] = bytes[i];
295 return subarray.buffer;
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
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.
312 ExifEncoder.encodeDirectory = function(
313 bw, directory, opt_resolveLater, opt_nextDirPointer) {
317 bw.forward('dir-count', 2);
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);
337 // The value does not fit, forward the 4 byte offset to the actual value.
339 bw.forward(tag.id, width);
340 longValues.push(tag);
342 bw.skip(4 - width); // Align so that the value take up exactly 4 bytes.
346 bw.resolve('dir-count', count);
348 if (opt_nextDirPointer) {
349 bw.forward(opt_nextDirPointer, 4);
351 bw.writeScalar(0, 4);
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);
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?
367 ExifEncoder.getComponentWidth = function(tag) {
368 switch (tag.format) {
378 case 9: // Signed Long
382 case 10: // Signed Rational
386 console.warn('Unknown tag format 0x' +
387 Number(tag.id).toString(16) + ': ' + tag.format);
393 * Writes out the tag value.
394 * @param {!ByteWriter} bw Writer to use.
395 * @param {ExifEntry} tag Tag, which value to write.
397 ExifEncoder.writeValue = function(bw, tag) {
398 if (tag.format === 2) { // String
399 if (tag.componentCount !== tag.value.length) {
401 'String size mismatch for 0x' + Number(tag.id).toString(16));
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) {
413 bw.writeScalar(value[0], 4, signed);
414 bw.writeScalar(value[1], 4, signed);
416 bw.writeScalar(value, width, signed);
420 var signed = (tag.format == 9 || tag.format == 10);
421 if (tag.componentCount == 1) {
422 writeComponent(tag.value, signed);
424 for (var i = 0; i != tag.componentCount; i++) {
425 writeComponent(tag.value[i], signed);
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.
442 ExifEncoder.findOrCreateTag = function(directory, id, opt_format,
443 opt_componentCount) {
444 if (!(id in directory)) {
447 format: opt_format || 3, // Short
448 componentCount: opt_componentCount || 1,
452 return directory[id];
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.
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;
475 ByteWriter.ByteOrder = {
476 // Little endian byte order.
478 // Big endian byte order.
483 * Set the byte ordering for future writes.
484 * @param {ByteWriter.ByteOrder} order ByteOrder to use
485 * {ByteWriter.LITTLE_ENDIAN} or {ByteWriter.BIG_ENDIAN}.
487 ByteWriter.prototype.setByteOrder = function(order) {
488 this.littleEndian_ = (order === ByteWriter.ByteOrder.LITTLE_ENDIAN);
492 * @return {number} the current write position.
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.
500 ByteWriter.prototype.skip = function(count) {
501 this.validateWrite(count);
506 * Check if the buffer has enough room to read 'width' bytes. Throws an error
508 * @param {number} width Amount of bytes to check.
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.
521 ByteWriter.prototype.writeScalar = function(value, width, opt_signed) {
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.
528 method = opt_signed ? 'setInt8' : 'setUint8';
532 method = opt_signed ? 'setInt16' : 'setUint16';
536 method = opt_signed ? 'setInt32' : 'setUint32';
540 method = opt_signed ? 'setInt64' : 'setUint64';
544 throw new Error('Invalid width: ' + width);
548 this.validateWrite(width);
549 this.view_[method](this.pos_, value, this.littleEndian_);
555 * @param {string} str String to write.
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));
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.
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] = {
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.
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);
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.
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.
608 ByteWriter.prototype.checkResolved = function() {
609 for (var key in this.forwards_) {
610 throw new Error('Unresolved forward pointer ' +
611 ByteWriter.prettyKeyFormat(key));
616 * If key is a number, format it in hex style.
617 * @param {!(string|Exif.Tag)} key A key.
618 * @return {string} Formatted representation.
620 ByteWriter.prettyKeyFormat = function(key) {
621 if (typeof key === 'number') {
622 return '0x' + key.toString(16);