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 |