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