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 * @param {!HTMLCanvasElement} canvas
54 ExifEncoder
.prototype.setImageData = function(canvas
) {
55 ImageEncoder
.MetadataEncoder
.prototype.setImageData
.call(this, canvas
);
57 var image
= this.ifd_
.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
;
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
) {
91 var dateTimeTag
= ExifEncoder
.findOrCreateTag(image
, Exif
.Tag
.DATETIME
, 2);
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;
105 ExifEncoder
.prototype.setThumbnailData = function(canvas
, quality
) {
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
= '';
127 if (this.thumbnailDataUrl
) {
128 var thumbnail
= this.ifd_
.thumbnail
;
130 thumbnail
= this.ifd_
.thumbnail
= {};
132 ExifEncoder
.findOrCreateTag(thumbnail
, Exif
.Tag
.IMAGE_WIDTH
).value
=
135 ExifEncoder
.findOrCreateTag(thumbnail
, Exif
.Tag
.IMAGE_HEIGHT
).value
=
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
=
149 if (this.ifd_
.thumbnail
)
150 delete this.ifd_
.thumbnail
;
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
};
174 var tag
= getWord(sectionStart
);
176 if (tag
== Exif
.Mark
.SOS
)
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
;
204 ExifEncoder
.prototype.encode = function() {
205 var HEADER_SIZE
= 10;
207 // Allocate the largest theoretically possible size.
208 var bytes
= new Uint8Array(0x10000);
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);
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
);
241 if (Exif
.Tag
.EXIFDATA
in this.ifd_
.image
)
242 throw new Error('Corrupt exif dictionary reference');
246 bw
.resolveOffset(Exif
.Tag
.GPSDATA
);
247 ExifEncoder
.encodeDirectory(bw
, this.ifd_
.gps
);
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(
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
);
265 bw
.resolve('thumb-dir', 0);
270 var totalSize
= HEADER_SIZE
+ bw
.tell();
271 hw
.resolve('size', totalSize
- 2); // The marker is excluded.
274 var subarray
= new Uint8Array(totalSize
);
275 for (var i
= 0; i
!= totalSize
; i
++) {
276 subarray
[i
] = bytes
[i
];
278 return subarray
.buffer
;
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
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
) {
300 bw
.forward('dir-count', 2);
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
);
320 // The value does not fit, forward the 4 byte offset to the actual value.
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.
329 bw
.resolve('dir-count', count
);
331 if (opt_nextDirPointer
) {
332 bw
.forward(opt_nextDirPointer
, 4);
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
) {
361 case 9: // Signed Long
365 case 10: // Signed Rational
369 console
.warn('Unknown tag format 0x' +
370 Number(tag
.id
).toString(16) + ': ' + tag
.format
);
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
) {
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
) {
396 bw
.writeScalar(value
[0], 4, signed
);
397 bw
.writeScalar(value
[1], 4, signed
);
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
);
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
)) {
430 format
: opt_format
|| 3, // Short
431 componentCount
: opt_componentCount
|| 1,
435 return directory
[id
];
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.
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;
458 ByteWriter
.ByteOrder
= {
459 // Little endian byte order.
461 // Big endian byte order.
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
);
489 * Check if the buffer has enough room to read 'width' bytes. Throws an error
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
) {
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.
511 method
= opt_signed
? 'setInt8' : 'setUint8';
515 method
= opt_signed
? 'setInt16' : 'setUint16';
519 method
= opt_signed
? 'setInt32' : 'setUint32';
523 method
= opt_signed
? 'setInt64' : 'setUint64';
527 throw new Error('Invalid width: ' + width
);
531 this.validateWrite(width
);
532 this.view_
[method
](this.pos_
, value
, this.littleEndian_
);
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
] = {
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
);
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);