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 |