| OLD | NEW |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 /** | 5 /** |
| 6 * The Exif metadata encoder. | 6 * The Exif metadata encoder. |
| 7 * Uses the metadata format as defined by ExifParser. | 7 * Uses the metadata format as defined by ExifParser. |
| 8 * @param {!Object} originalMetadata Metadata to encode. | 8 * @param {!MetadataItem} originalMetadata Metadata to encode. |
| 9 * @constructor | 9 * @constructor |
| 10 * @extends {ImageEncoder.MetadataEncoder} | 10 * @extends {ImageEncoder.MetadataEncoder} |
| 11 * @struct | 11 * @struct |
| 12 */ | 12 */ |
| 13 function ExifEncoder(originalMetadata) { | 13 function ExifEncoder(originalMetadata) { |
| 14 ImageEncoder.MetadataEncoder.apply(this, arguments); | 14 ImageEncoder.MetadataEncoder.apply(this, arguments); |
| 15 /** |
| 16 * Image File Directory obtained from EXIF header. |
| 17 * @private {!Object} |
| 18 * @const |
| 19 */ |
| 20 this.ifd_ = /** @type {!Object} */( |
| 21 JSON.parse(JSON.stringify(originalMetadata.ifd || {}))); |
| 15 | 22 |
| 16 if (this.metadata_.media && this.metadata_.media.ifd) | 23 /** |
| 17 this.ifd_ = this.metadata_.media.ifd; | 24 * Note use little endian if the original metadata does not have the |
| 18 else | 25 * information. |
| 19 this.ifd_ = {}; | 26 * @private {boolean} |
| 27 * @const |
| 28 */ |
| 29 this.exifLittleEndian_ = !!originalMetadata.exifLittleEndian; |
| 30 |
| 31 /** |
| 32 * Modification time to be stored in EXIF header. |
| 33 * @private {!Date} |
| 34 * @const |
| 35 */ |
| 36 this.modificationTime_ = assert(originalMetadata.modificationTime); |
| 20 } | 37 } |
| 21 | 38 |
| 22 ExifEncoder.prototype = {__proto__: ImageEncoder.MetadataEncoder.prototype}; | 39 ExifEncoder.prototype = {__proto__: ImageEncoder.MetadataEncoder.prototype}; |
| 23 | 40 |
| 24 ImageEncoder.registerMetadataEncoder(ExifEncoder, 'image/jpeg'); | 41 ImageEncoder.registerMetadataEncoder(ExifEncoder, 'image/jpeg'); |
| 25 | 42 |
| 26 /** | 43 /** |
| 27 * Software name of Gallery.app. | 44 * Software name of Gallery.app. |
| 28 * @type {string} | 45 * @type {string} |
| 29 * @const | 46 * @const |
| 30 */ | 47 */ |
| 31 ExifEncoder.SOFTWARE = 'Chrome OS Gallery App\0'; | 48 ExifEncoder.SOFTWARE = 'Chrome OS Gallery App\0'; |
| 32 | 49 |
| 33 /** | 50 /** |
| 34 * @param {!HTMLCanvasElement} canvas | 51 * @param {!HTMLCanvasElement} canvas |
| 35 * @param {Date=} opt_modificationDateTime | |
| 36 * @override | 52 * @override |
| 37 */ | 53 */ |
| 38 ExifEncoder.prototype.setImageData = | 54 ExifEncoder.prototype.setImageData = function(canvas) { |
| 39 function(canvas, opt_modificationDateTime) { | 55 ImageEncoder.MetadataEncoder.prototype.setImageData.call(this, canvas); |
| 56 |
| 40 var image = this.ifd_.image; | 57 var image = this.ifd_.image; |
| 41 if (!image) | 58 if (!image) |
| 42 image = this.ifd_.image = {}; | 59 image = this.ifd_.image = {}; |
| 43 | 60 |
| 44 // Only update width/height in this directory if they are present. | 61 // Only update width/height in this directory if they are present. |
| 45 if (image[Exif.Tag.IMAGE_WIDTH] && image[Exif.Tag.IMAGE_HEIGHT]) { | 62 if (image[Exif.Tag.IMAGE_WIDTH] && image[Exif.Tag.IMAGE_HEIGHT]) { |
| 46 image[Exif.Tag.IMAGE_WIDTH].value = canvas.width; | 63 image[Exif.Tag.IMAGE_WIDTH].value = canvas.width; |
| 47 image[Exif.Tag.IMAGE_HEIGHT].value = canvas.height; | 64 image[Exif.Tag.IMAGE_HEIGHT].value = canvas.height; |
| 48 } | 65 } |
| 49 | 66 |
| 50 var exif = this.ifd_.exif; | 67 var exif = this.ifd_.exif; |
| 51 if (!exif) | 68 if (!exif) |
| 52 exif = this.ifd_.exif = {}; | 69 exif = this.ifd_.exif = {}; |
| 53 ExifEncoder.findOrCreateTag(image, Exif.Tag.EXIFDATA); | 70 ExifEncoder.findOrCreateTag(image, Exif.Tag.EXIFDATA); |
| 54 ExifEncoder.findOrCreateTag(exif, Exif.Tag.X_DIMENSION).value = canvas.width; | 71 ExifEncoder.findOrCreateTag(exif, Exif.Tag.X_DIMENSION).value = canvas.width; |
| 55 ExifEncoder.findOrCreateTag(exif, Exif.Tag.Y_DIMENSION).value = canvas.height; | 72 ExifEncoder.findOrCreateTag(exif, Exif.Tag.Y_DIMENSION).value = canvas.height; |
| 56 | 73 |
| 57 this.metadata_.width = canvas.width; | |
| 58 this.metadata_.height = canvas.height; | |
| 59 | |
| 60 // Always save in default orientation. | 74 // Always save in default orientation. |
| 61 delete this.metadata_['imageTransform']; | |
| 62 ExifEncoder.findOrCreateTag(image, Exif.Tag.ORIENTATION).value = 1; | 75 ExifEncoder.findOrCreateTag(image, Exif.Tag.ORIENTATION).value = 1; |
| 63 | 76 |
| 64 // Update software name. | 77 // Update software name. |
| 65 var softwareTag = ExifEncoder.findOrCreateTag(image, Exif.Tag.SOFTWARE, 2); | 78 var softwareTag = ExifEncoder.findOrCreateTag(image, Exif.Tag.SOFTWARE, 2); |
| 66 softwareTag.value = ExifEncoder.SOFTWARE; | 79 softwareTag.value = ExifEncoder.SOFTWARE; |
| 67 softwareTag.componentCount = ExifEncoder.SOFTWARE.length; | 80 softwareTag.componentCount = ExifEncoder.SOFTWARE.length; |
| 68 | 81 |
| 69 // Update modification date time. | 82 // Update modification date time. |
| 70 var padNumWithZero = function(num, length) { | 83 var padNumWithZero = function(num, length) { |
| 71 var str = num.toString(); | 84 var str = num.toString(); |
| 72 while (str.length < length) { | 85 while (str.length < length) { |
| 73 str = '0' + str; | 86 str = '0' + str; |
| 74 } | 87 } |
| 75 return str; | 88 return str; |
| 76 }; | 89 }; |
| 77 | 90 |
| 78 var modificationDateTime = opt_modificationDateTime || new Date(); | |
| 79 var dateTimeTag = ExifEncoder.findOrCreateTag(image, Exif.Tag.DATETIME, 2); | 91 var dateTimeTag = ExifEncoder.findOrCreateTag(image, Exif.Tag.DATETIME, 2); |
| 80 dateTimeTag.value = | 92 dateTimeTag.value = |
| 81 padNumWithZero(modificationDateTime.getFullYear(), 4) + ':' + | 93 padNumWithZero(this.modificationTime_.getFullYear(), 4) + ':' + |
| 82 padNumWithZero(modificationDateTime.getMonth() + 1, 2) + ':' + | 94 padNumWithZero(this.modificationTime_.getMonth() + 1, 2) + ':' + |
| 83 padNumWithZero(modificationDateTime.getDate(), 2) + ' ' + | 95 padNumWithZero(this.modificationTime_.getDate(), 2) + ' ' + |
| 84 padNumWithZero(modificationDateTime.getHours(), 2) + ':' + | 96 padNumWithZero(this.modificationTime_.getHours(), 2) + ':' + |
| 85 padNumWithZero(modificationDateTime.getMinutes(), 2) + ':' + | 97 padNumWithZero(this.modificationTime_.getMinutes(), 2) + ':' + |
| 86 padNumWithZero(modificationDateTime.getSeconds(), 2) + '\0'; | 98 padNumWithZero(this.modificationTime_.getSeconds(), 2) + '\0'; |
| 87 dateTimeTag.componentCount = 20; | 99 dateTimeTag.componentCount = 20; |
| 88 }; | 100 }; |
| 89 | 101 |
| 90 /** | 102 /** |
| 91 * @override | 103 * @override |
| 92 */ | 104 */ |
| 93 ExifEncoder.prototype.setThumbnailData = function(canvas, quality) { | 105 ExifEncoder.prototype.setThumbnailData = function(canvas, quality) { |
| 94 // Empirical formula with reasonable behavior: | 106 if (canvas) { |
| 95 // 10K for 1Mpix, 30K for 5Mpix, 50K for 9Mpix and up. | 107 // Empirical formula with reasonable behavior: |
| 96 var pixelCount = this.metadata_.width * this.metadata_.height; | 108 // 10K for 1Mpix, 30K for 5Mpix, 50K for 9Mpix and up. |
| 97 var maxEncodedSize = 5000 * Math.min(10, 1 + pixelCount / 1000000); | 109 var pixelCount = this.imageWidth * this.imageHeight; |
| 98 | 110 var maxEncodedSize = 5000 * Math.min(10, 1 + pixelCount / 1000000); |
| 99 var DATA_URL_PREFIX = 'data:' + this.metadata_.media.mimeType + ';base64,'; | 111 var DATA_URL_PREFIX = 'data:image/jpeg;base64,'; |
| 100 var BASE64_BLOAT = 4 / 3; | 112 var BASE64_BLOAT = 4 / 3; |
| 101 var maxDataURLLength = | 113 var maxDataURLLength = |
| 102 DATA_URL_PREFIX.length + Math.ceil(maxEncodedSize * BASE64_BLOAT); | 114 DATA_URL_PREFIX.length + Math.ceil(maxEncodedSize * BASE64_BLOAT); |
| 103 | 115 for (; quality > 0.2; quality *= 0.8) { |
| 104 for (;; quality *= 0.8) { | 116 ImageEncoder.MetadataEncoder.prototype.setThumbnailData.call( |
| 105 ImageEncoder.MetadataEncoder.prototype.setThumbnailData.call( | 117 this, canvas, quality); |
| 106 this, canvas, quality); | 118 // If the obtained thumbnail URL is too long, reset the URL and try again |
| 107 if (this.metadata_.thumbnailURL.length <= maxDataURLLength || quality < 0.2) | 119 // with less quality value. |
| 120 if (this.thumbnailDataUrl.length > maxDataURLLength) { |
| 121 this.thumbnailDataUrl = ''; |
| 122 continue; |
| 123 } |
| 108 break; | 124 break; |
| 125 } |
| 109 } | 126 } |
| 110 | 127 if (this.thumbnailDataUrl) { |
| 111 if (canvas && this.metadata_.thumbnailURL.length <= maxDataURLLength) { | |
| 112 var thumbnail = this.ifd_.thumbnail; | 128 var thumbnail = this.ifd_.thumbnail; |
| 113 if (!thumbnail) | 129 if (!thumbnail) |
| 114 thumbnail = this.ifd_.thumbnail = {}; | 130 thumbnail = this.ifd_.thumbnail = {}; |
| 115 | 131 |
| 116 ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.IMAGE_WIDTH).value = | 132 ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.IMAGE_WIDTH).value = |
| 117 canvas.width; | 133 canvas.width; |
| 118 | 134 |
| 119 ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.IMAGE_HEIGHT).value = | 135 ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.IMAGE_HEIGHT).value = |
| 120 canvas.height; | 136 canvas.height; |
| 121 | 137 |
| 122 // The values for these tags will be set in ExifWriter.encode. | 138 // The values for these tags will be set in ExifWriter.encode. |
| 123 ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.JPG_THUMB_OFFSET); | 139 ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.JPG_THUMB_OFFSET); |
| 124 ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.JPG_THUMB_LENGTH); | 140 ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.JPG_THUMB_LENGTH); |
| 125 | 141 |
| 126 // Always save in default orientation. | 142 // Always save in default orientation. |
| 127 ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.ORIENTATION).value = 1; | 143 ExifEncoder.findOrCreateTag(thumbnail, Exif.Tag.ORIENTATION).value = 1; |
| 128 | 144 |
| 129 // When thumbnail is compressed with JPEG, compression must be set as 6. | 145 // When thumbnail is compressed with JPEG, compression must be set as 6. |
| 130 ExifEncoder.findOrCreateTag(this.ifd_.image, Exif.Tag.COMPRESSION).value = | 146 ExifEncoder.findOrCreateTag(this.ifd_.image, Exif.Tag.COMPRESSION).value = |
| 131 6; | 147 6; |
| 132 } else { | 148 } else { |
| 133 console.warn( | 149 if (this.ifd_.thumbnail) |
| 134 'Thumbnail URL too long: ' + this.metadata_.thumbnailURL.length); | |
| 135 // Delete thumbnail ifd so that it is not written out to a file, but | |
| 136 // keep thumbnailURL for display purposes. | |
| 137 if (this.ifd_.thumbnail) { | |
| 138 delete this.ifd_.thumbnail; | 150 delete this.ifd_.thumbnail; |
| 139 } | |
| 140 } | 151 } |
| 141 }; | 152 }; |
| 142 | 153 |
| 143 /** | 154 /** |
| 144 * @override | 155 * @override |
| 145 */ | 156 */ |
| 146 ExifEncoder.prototype.findInsertionRange = function(encodedImage) { | 157 ExifEncoder.prototype.findInsertionRange = function(encodedImage) { |
| 147 function getWord(pos) { | 158 function getWord(pos) { |
| 148 if (pos + 2 > encodedImage.length) | 159 if (pos + 2 > encodedImage.length) |
| 149 throw 'Reading past the buffer end @' + pos; | 160 throw 'Reading past the buffer end @' + pos; |
| (...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 200 var hw = new ByteWriter(bytes.buffer, 0, HEADER_SIZE); | 211 var hw = new ByteWriter(bytes.buffer, 0, HEADER_SIZE); |
| 201 hw.writeScalar(Exif.Mark.EXIF, 2); | 212 hw.writeScalar(Exif.Mark.EXIF, 2); |
| 202 hw.forward('size', 2); | 213 hw.forward('size', 2); |
| 203 hw.writeString('Exif\0\0'); // Magic string. | 214 hw.writeString('Exif\0\0'); // Magic string. |
| 204 | 215 |
| 205 // First serialize the content of the exif section. | 216 // First serialize the content of the exif section. |
| 206 // Use a ByteWriter starting at HEADER_SIZE offset so that tell() positions | 217 // Use a ByteWriter starting at HEADER_SIZE offset so that tell() positions |
| 207 // can be directly mapped to offsets as encoded in the dictionaries. | 218 // can be directly mapped to offsets as encoded in the dictionaries. |
| 208 var bw = new ByteWriter(bytes.buffer, HEADER_SIZE); | 219 var bw = new ByteWriter(bytes.buffer, HEADER_SIZE); |
| 209 | 220 |
| 210 if (this.metadata_.littleEndian) { | 221 if (this.exifLittleEndian_) { |
| 211 bw.setByteOrder(ByteWriter.ByteOrder.LITTLE_ENDIAN); | 222 bw.setByteOrder(ByteWriter.ByteOrder.LITTLE_ENDIAN); |
| 212 bw.writeScalar(Exif.Align.LITTLE, 2); | 223 bw.writeScalar(Exif.Align.LITTLE, 2); |
| 213 } else { | 224 } else { |
| 214 bw.setByteOrder(ByteWriter.ByteOrder.BIG_ENDIAN); | 225 bw.setByteOrder(ByteWriter.ByteOrder.BIG_ENDIAN); |
| 215 bw.writeScalar(Exif.Align.BIG, 2); | 226 bw.writeScalar(Exif.Align.BIG, 2); |
| 216 } | 227 } |
| 217 | 228 |
| 218 bw.writeScalar(Exif.Tag.TIFF, 2); | 229 bw.writeScalar(Exif.Tag.TIFF, 2); |
| 219 | 230 |
| 220 bw.forward('image-dir', 4); // The pointer should point right after itself. | 231 bw.forward('image-dir', 4); // The pointer should point right after itself. |
| (...skipping 18 matching lines...) Expand all Loading... |
| 239 throw new Error('Missing gps dictionary reference'); | 250 throw new Error('Missing gps dictionary reference'); |
| 240 } | 251 } |
| 241 | 252 |
| 242 if (this.ifd_.thumbnail) { | 253 if (this.ifd_.thumbnail) { |
| 243 bw.resolveOffset('thumb-dir'); | 254 bw.resolveOffset('thumb-dir'); |
| 244 ExifEncoder.encodeDirectory( | 255 ExifEncoder.encodeDirectory( |
| 245 bw, | 256 bw, |
| 246 this.ifd_.thumbnail, | 257 this.ifd_.thumbnail, |
| 247 [Exif.Tag.JPG_THUMB_OFFSET, Exif.Tag.JPG_THUMB_LENGTH]); | 258 [Exif.Tag.JPG_THUMB_OFFSET, Exif.Tag.JPG_THUMB_LENGTH]); |
| 248 | 259 |
| 249 var thumbnailDecoded = | 260 var thumbnailDecoded = ImageEncoder.decodeDataURL(this.thumbnailDataUrl); |
| 250 ImageEncoder.decodeDataURL(this.metadata_.thumbnailURL); | |
| 251 bw.resolveOffset(Exif.Tag.JPG_THUMB_OFFSET); | 261 bw.resolveOffset(Exif.Tag.JPG_THUMB_OFFSET); |
| 252 bw.resolve(Exif.Tag.JPG_THUMB_LENGTH, thumbnailDecoded.length); | 262 bw.resolve(Exif.Tag.JPG_THUMB_LENGTH, thumbnailDecoded.length); |
| 253 bw.writeString(thumbnailDecoded); | 263 bw.writeString(thumbnailDecoded); |
| 254 } else { | 264 } else { |
| 255 bw.resolve('thumb-dir', 0); | 265 bw.resolve('thumb-dir', 0); |
| 256 } | 266 } |
| 257 | 267 |
| 258 bw.checkResolved(); | 268 bw.checkResolved(); |
| 259 | 269 |
| 260 var totalSize = HEADER_SIZE + bw.tell(); | 270 var totalSize = HEADER_SIZE + bw.tell(); |
| (...skipping 329 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 590 * @param {!(string|Exif.Tag)} key A key. | 600 * @param {!(string|Exif.Tag)} key A key. |
| 591 * @return {string} Formatted representation. | 601 * @return {string} Formatted representation. |
| 592 */ | 602 */ |
| 593 ByteWriter.prettyKeyFormat = function(key) { | 603 ByteWriter.prettyKeyFormat = function(key) { |
| 594 if (typeof key === 'number') { | 604 if (typeof key === 'number') { |
| 595 return '0x' + key.toString(16); | 605 return '0x' + key.toString(16); |
| 596 } else { | 606 } else { |
| 597 return key; | 607 return key; |
| 598 } | 608 } |
| 599 }; | 609 }; |
| OLD | NEW |