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