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