Home | History | Annotate | Download | only in image_editor
      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
      8 var EXIF_MARK_SOS = 0xffda;  // Start of "stream" (the actual image data).
      9 var EXIF_MARK_SOI = 0xffd8;  // Start of image data.
     10 var EXIF_MARK_EOI = 0xffd9;  // End of image data.
     11 
     12 var EXIF_MARK_APP0 = 0xffe0;  // APP0 block, most commonly JFIF data.
     13 var EXIF_MARK_EXIF = 0xffe1;  // Start of exif block.
     14 
     15 var EXIF_ALIGN_LITTLE = 0x4949;  // Indicates little endian exif data.
     16 var EXIF_ALIGN_BIG = 0x4d4d;  // Indicates big endian exif data.
     17 
     18 var EXIF_TAG_TIFF = 0x002a;  // First directory containing TIFF data.
     19 var EXIF_TAG_GPSDATA = 0x8825;  // Pointer from TIFF to the GPS directory.
     20 var EXIF_TAG_EXIFDATA = 0x8769;  // Pointer from TIFF to the EXIF IFD.
     21 
     22 var EXIF_TAG_JPG_THUMB_OFFSET = 0x0201;  // Pointer from TIFF to thumbnail.
     23 var EXIF_TAG_JPG_THUMB_LENGTH = 0x0202;  // Length of thumbnail data.
     24 
     25 var EXIF_TAG_IMAGE_WIDTH = 0x0100;
     26 var EXIF_TAG_IMAGE_HEIGHT = 0x0101;
     27 
     28 var EXIF_TAG_ORIENTATION = 0x0112;
     29 var EXIF_TAG_X_DIMENSION = 0xA002;
     30 var 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  */
     39 function 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 
     47 ExifEncoder.prototype = {__proto__: ImageEncoder.MetadataEncoder.prototype};
     48 
     49 ImageEncoder.registerMetadataEncoder(ExifEncoder, 'image/jpeg');
     50 
     51 /**
     52  * @param {HTMLCanvasElement|Object} canvas Canvas or anything with
     53  *                                          width and height properties.
     54  */
     55 ExifEncoder.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  */
     86 ExifEncoder.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  */
    138 ExifEncoder.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  */
    185 ExifEncoder.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  */
    276 ExifEncoder.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  */
    331 ExifEncoder.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  */
    361 ExifEncoder.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  */
    400 ExifEncoder.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  */
    419 function 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  */
    431 ByteWriter.LITTLE_ENDIAN = 0;
    432 
    433 /**
    434  * Bug endian byte order.
    435  * @type {number}
    436  */
    437 ByteWriter.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  */
    444 ByteWriter.prototype.setByteOrder = function(order) {
    445   this.littleEndian_ = (order == ByteWriter.LITTLE_ENDIAN);
    446 };
    447 
    448 /**
    449  * @return {number} the current write position.
    450  */
    451 ByteWriter.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  */
    457 ByteWriter.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  */
    467 ByteWriter.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  */
    478 ByteWriter.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  */
    514 ByteWriter.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  */
    527 ByteWriter.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  */
    543 ByteWriter.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  */
    558 ByteWriter.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  */
    565 ByteWriter.prototype.checkResolved = function() {
    566   for (var key in this.forwards_) {
    567     throw new Error('Unresolved forward pointer ' + key.toString(16));
    568   }
    569 };
    570