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