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