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 const EXIF_MARK_SOI = 0xffd8; // Start of image data. | |
6 const EXIF_MARK_SOS = 0xffda; // Start of "stream" (the actual image data). | |
7 const EXIF_MARK_SOF = 0xffc0; // Start of "frame" | |
8 const EXIF_MARK_EXIF = 0xffe1; // Start of exif block. | |
9 | |
10 const EXIF_ALIGN_LITTLE = 0x4949; // Indicates little endian exif data. | |
11 const EXIF_ALIGN_BIG = 0x4d4d; // Indicates big endian exif data. | |
12 | |
13 const EXIF_TAG_TIFF = 0x002a; // First directory containing TIFF data. | |
14 const EXIF_TAG_GPSDATA = 0x8825; // Pointer from TIFF to the GPS directory. | |
15 const EXIF_TAG_EXIFDATA = 0x8769; // Pointer from TIFF to the EXIF IFD. | |
16 const EXIF_TAG_SUBIFD = 0x014a; // Pointer from TIFF to "Extra" IFDs. | |
17 | |
18 const EXIF_TAG_JPG_THUMB_OFFSET = 0x0201; // Pointer from TIFF to thumbnail. | |
19 const EXIF_TAG_JPG_THUMB_LENGTH = 0x0202; // Length of thumbnail data. | |
20 | |
21 const EXIF_TAG_ORIENTATION = 0x0112; | |
22 const EXIF_TAG_X_DIMENSION = 0xA002; | |
23 const EXIF_TAG_Y_DIMENSION = 0xA003; | |
24 | |
25 function ExifParser(parent) { | |
26 ImageParser.call(this, parent, 'jpeg', /\.jpe?g$/i); | |
27 } | |
28 | |
29 ExifParser.prototype = {__proto__: ImageParser.prototype}; | |
30 | |
31 ExifParser.prototype.parse = function(file, metadata, callback, errorCallback) { | |
32 this.requestSlice(file, callback, errorCallback, metadata, 0); | |
33 }; | |
34 | |
35 ExifParser.prototype.requestSlice = function ( | |
36 file, callback, errorCallback, metadata, filePos, opt_length) { | |
37 // Read at least 1Kb so that we do not issue too many read requests. | |
38 opt_length = Math.max(1024, opt_length || 0); | |
39 | |
40 var self = this; | |
41 var reader = new FileReader(); | |
42 reader.onerror = errorCallback; | |
43 reader.onload = function() { self.parseSlice( | |
44 file, callback, errorCallback, metadata, filePos, reader.result); | |
45 }; | |
46 reader.readAsArrayBuffer(file.webkitSlice(filePos, filePos + opt_length)); | |
47 }; | |
48 | |
49 ExifParser.prototype.parseSlice = function( | |
50 file, callback, errorCallback, metadata, filePos, buf) { | |
51 try { | |
52 var br = new ByteReader(buf); | |
53 | |
54 if (!br.canRead(4)) { | |
55 // We never ask for less than 4 bytes. This can only mean we reached EOF. | |
56 throw new Error('Unexpected EOF @' + (filePos + buf.byteLength)); | |
57 } | |
58 | |
59 if (filePos == 0) { | |
60 // First slice, check for the SOI mark. | |
61 var firstMark = this.readMark(br); | |
62 if (firstMark != EXIF_MARK_SOI) | |
63 throw new Error('Invalid file header: ' + firstMark.toString(16)); | |
64 } | |
65 | |
66 var self = this; | |
67 function reread(opt_offset, opt_bytes) { | |
68 self.requestSlice(file, callback, errorCallback, metadata, | |
69 filePos + br.tell() + (opt_offset || 0), opt_bytes); | |
70 } | |
71 | |
72 while (true) { | |
73 if (!br.canRead(4)) { | |
74 // Cannot read the mark and the length, request a minimum-size slice. | |
75 reread(); | |
76 return; | |
77 } | |
78 | |
79 var mark = this.readMark(br); | |
80 if (mark == EXIF_MARK_SOS) | |
81 throw new Error('SOS marker found before SOF'); | |
82 | |
83 var markLength = this.readMarkLength(br); | |
84 | |
85 var nextSectionStart = br.tell() + markLength; | |
86 if (!br.canRead(markLength)) { | |
87 // Get the entire section. | |
88 if (filePos + br.tell() + markLength > file.size) { | |
89 throw new Error( | |
90 'Invalid section length @' + (filePos + br.tell() - 2)); | |
91 } | |
92 reread(-4, markLength + 4); | |
93 return; | |
94 } | |
95 | |
96 if (mark == EXIF_MARK_EXIF) { | |
97 this.parseExifSection(metadata, buf, br); | |
98 } else if (ExifParser.isSOF_(mark)) { | |
99 // The most reliable size information is encoded in the SOF section. | |
100 br.seek(1, ByteReader.SEEK_CUR); // Skip the precision byte. | |
101 var height = br.readScalar(2); | |
102 var width = br.readScalar(2); | |
103 ExifParser.setImageSize(metadata, width, height); | |
104 callback(metadata); // We are done! | |
105 return; | |
106 } | |
107 | |
108 br.seek(nextSectionStart, ByteReader.SEEK_BEG); | |
109 } | |
110 } catch (e) { | |
111 errorCallback(e.toString()); | |
112 } | |
113 }; | |
114 | |
115 ExifParser.isSOF_ = function(mark) { | |
116 // There are 13 variants of SOF fragment format distinguished by the last | |
117 // hex digit of the mark, but the part we want is always the same. | |
118 if ((mark & ~0xF) != EXIF_MARK_SOF) return false; | |
119 | |
120 // If the last digit is 4, 8 or 12 it is not really a SOF. | |
121 var type = mark & 0xF; | |
122 return (type != 4 && type != 8 && type != 12); | |
123 }; | |
124 | |
125 ExifParser.prototype.parseExifSection = function(metadata, buf, br) { | |
126 var magic = br.readString(6); | |
127 if (magic != 'Exif\0\0') { | |
128 // Some JPEG files may have sections marked with EXIF_MARK_EXIF | |
129 // but containing something else (e.g. XML text). Ignore such sections. | |
130 this.vlog('Invalid EXIF magic: ' + magic + br.readString(100)); | |
131 return; | |
132 } | |
133 | |
134 // Offsets inside the EXIF block are based after the magic string. | |
135 // Create a new ByteReader based on the current position to make offset | |
136 // calculations simpler. | |
137 br = new ByteReader(buf, br.tell()); | |
138 | |
139 var order = br.readScalar(2); | |
140 if (order == EXIF_ALIGN_LITTLE) { | |
141 br.setByteOrder(ByteReader.LITTLE_ENDIAN); | |
142 } else if (order != EXIF_ALIGN_BIG) { | |
143 this.log('Invalid alignment value: ' + order.toString(16)); | |
144 return; | |
145 } | |
146 | |
147 var tag = br.readScalar(2); | |
148 if (tag != EXIF_TAG_TIFF) { | |
149 this.log('Invalid TIFF tag: ' + tag.toString(16)); | |
150 return; | |
151 } | |
152 | |
153 metadata.littleEndian = (order == EXIF_ALIGN_LITTLE); | |
154 metadata.ifd = { | |
155 image: {}, | |
156 thumbnail: {} | |
157 }; | |
158 var directoryOffset = br.readScalar(4); | |
159 | |
160 // Image directory. | |
161 this.vlog('Read image directory.'); | |
162 br.seek(directoryOffset); | |
163 directoryOffset = this.readDirectory(br, metadata.ifd.image); | |
164 metadata.imageTransform = this.parseOrientation(metadata.ifd.image); | |
165 | |
166 // Thumbnail Directory chained from the end of the image directory. | |
167 if (directoryOffset) { | |
168 this.vlog('Read thumbnail directory.'); | |
169 br.seek(directoryOffset); | |
170 this.readDirectory(br, metadata.ifd.thumbnail); | |
171 // If no thumbnail orientation is encoded, assume same orientation as | |
172 // the primary image. | |
173 metadata.thumbnailTransform = | |
174 this.parseOrientation(metadata.ifd.thumbnail) || | |
175 metadata.imageTransform; | |
176 } | |
177 | |
178 // EXIF Directory may be specified as a tag in the image directory. | |
179 if (EXIF_TAG_EXIFDATA in metadata.ifd.image) { | |
180 this.vlog('Read EXIF directory.'); | |
181 directoryOffset = metadata.ifd.image[EXIF_TAG_EXIFDATA].value; | |
182 br.seek(directoryOffset); | |
183 metadata.ifd.exif = {}; | |
184 this.readDirectory(br, metadata.ifd.exif); | |
185 } | |
186 | |
187 // GPS Directory may also be linked from the image directory. | |
188 if (EXIF_TAG_GPSDATA in metadata.ifd.image) { | |
189 this.vlog('Read GPS directory.'); | |
190 directoryOffset = metadata.ifd.image[EXIF_TAG_GPSDATA].value; | |
191 br.seek(directoryOffset); | |
192 metadata.ifd.gps = {}; | |
193 this.readDirectory(br, metadata.ifd.gps); | |
194 } | |
195 | |
196 // Thumbnail may be linked from the image directory. | |
197 if (EXIF_TAG_JPG_THUMB_OFFSET in metadata.ifd.thumbnail && | |
198 EXIF_TAG_JPG_THUMB_LENGTH in metadata.ifd.thumbnail) { | |
199 this.vlog('Read thumbnail image.'); | |
200 br.seek(metadata.ifd.thumbnail[EXIF_TAG_JPG_THUMB_OFFSET].value); | |
201 metadata.thumbnailURL = br.readImage( | |
202 metadata.ifd.thumbnail[EXIF_TAG_JPG_THUMB_LENGTH].value); | |
203 } else { | |
204 this.vlog('Image has EXIF data, but no JPG thumbnail.'); | |
205 } | |
206 }; | |
207 | |
208 ExifParser.setImageSize = function(metadata, width, height) { | |
209 if (metadata.imageTransform && metadata.imageTransform.rotate90) { | |
210 metadata.width = height; | |
211 metadata.height = width; | |
212 } else { | |
213 metadata.width = width; | |
214 metadata.height = height; | |
215 } | |
216 }; | |
217 | |
218 ExifParser.prototype.readMark = function(br) { | |
219 return br.readScalar(2); | |
220 }; | |
221 | |
222 ExifParser.prototype.readMarkLength = function(br) { | |
223 // Length includes the 2 bytes used to store the length. | |
224 return br.readScalar(2) - 2; | |
225 }; | |
226 | |
227 ExifParser.prototype.readDirectory = function(br, tags) { | |
228 var entryCount = br.readScalar(2); | |
229 for (var i = 0; i < entryCount; i++) { | |
230 var tagId = br.readScalar(2); | |
231 var tag = tags[tagId] = {id: tagId}; | |
232 tag.format = br.readScalar(2); | |
233 tag.componentCount = br.readScalar(4); | |
234 this.readTagValue(br, tag); | |
235 } | |
236 | |
237 return br.readScalar(4); | |
238 }; | |
239 | |
240 ExifParser.prototype.readTagValue = function(br, tag) { | |
241 var self = this; | |
242 | |
243 function safeRead(size, readFunction, signed) { | |
244 try { | |
245 unsafeRead(size, readFunction, signed); | |
246 } catch (ex) { | |
247 self.log('error reading tag 0x' + tag.id.toString(16) + '/' + | |
248 tag.format + ', size ' + tag.componentCount + '*' + size + ' ' + | |
249 (ex.stack || '<no stack>') + ': ' + ex); | |
250 tag.value = null; | |
251 } | |
252 } | |
253 | |
254 function unsafeRead(size, readFunction, signed) { | |
255 if (!readFunction) | |
256 readFunction = function(size) { return br.readScalar(size, signed) }; | |
257 | |
258 var totalSize = tag.componentCount * size; | |
259 if (totalSize < 1) { | |
260 // This is probably invalid exif data, skip it. | |
261 tag.componentCount = 1; | |
262 tag.value = br.readScalar(4); | |
263 return; | |
264 } | |
265 | |
266 if (totalSize > 4) { | |
267 // If the total size is > 4, the next 4 bytes will be a pointer to the | |
268 // actual data. | |
269 br.pushSeek(br.readScalar(4)); | |
270 } | |
271 | |
272 if (tag.componentCount == 1) { | |
273 tag.value = readFunction(size); | |
274 } else { | |
275 // Read multiple components into an array. | |
276 tag.value = []; | |
277 for (var i = 0; i < tag.componentCount; i++) | |
278 tag.value[i] = readFunction(size); | |
279 } | |
280 | |
281 if (totalSize > 4) { | |
282 // Go back to the previous position if we had to jump to the data. | |
283 br.popSeek(); | |
284 } else if (totalSize < 4) { | |
285 // Otherwise, if the value wasn't exactly 4 bytes, skip over the | |
286 // unread data. | |
287 br.seek(4 - totalSize, ByteReader.SEEK_CUR); | |
288 } | |
289 } | |
290 | |
291 switch (tag.format) { | |
292 case 1: // Byte | |
293 case 7: // Undefined | |
294 safeRead(1); | |
295 break; | |
296 | |
297 case 2: // String | |
298 safeRead(1); | |
299 if (tag.componentCount == 0) { | |
300 tag.value = ''; | |
301 } else if (tag.componentCount == 1) { | |
302 tag.value = String.fromCharCode(tag.value); | |
303 } else { | |
304 tag.value = String.fromCharCode.apply(null, tag.value); | |
305 } | |
306 break; | |
307 | |
308 case 3: // Short | |
309 safeRead(2); | |
310 break; | |
311 | |
312 case 4: // Long | |
313 safeRead(4); | |
314 break; | |
315 | |
316 case 9: // Signed Long | |
317 safeRead(4, null, true); | |
318 break; | |
319 | |
320 case 5: // Rational | |
321 safeRead(8, function() { | |
322 return [ br.readScalar(4), br.readScalar(4) ]; | |
323 }); | |
324 break; | |
325 | |
326 case 10: // Signed Rational | |
327 safeRead(8, function() { | |
328 return [ br.readScalar(4, true), br.readScalar(4, true) ]; | |
329 }); | |
330 break; | |
331 | |
332 default: // ??? | |
333 this.vlog('Unknown tag format 0x' + Number(tag.id).toString(16) + | |
334 ': ' + tag.format); | |
335 safeRead(4); | |
336 break; | |
337 } | |
338 | |
339 this.vlog('Read tag: 0x' + tag.id.toString(16) + '/' + tag.format + ': ' + | |
340 tag.value); | |
341 }; | |
342 | |
343 ExifParser.SCALEX = [1, -1, -1, 1, 1, 1, -1, -1]; | |
344 ExifParser.SCALEY = [1, 1, -1, -1, -1, 1, 1, -1]; | |
345 ExifParser.ROTATE90 = [0, 0, 0, 0, 1, 1, 1, 1]; | |
346 | |
347 /** | |
348 * Transform exif-encoded orientation into a set of parameters compatible with | |
349 * CSS and canvas transforms (scaleX, scaleY, rotation). | |
350 * | |
351 * @param {Object} ifd exif property dictionary (image or thumbnail) | |
352 */ | |
353 ExifParser.prototype.parseOrientation = function(ifd) { | |
354 if (ifd[EXIF_TAG_ORIENTATION]) { | |
355 var index = (ifd[EXIF_TAG_ORIENTATION].value || 1) - 1; | |
356 return { | |
357 scaleX: ExifParser.SCALEX[index], | |
358 scaleY: ExifParser.SCALEY[index], | |
359 rotate90: ExifParser.ROTATE90[index] | |
360 } | |
361 } | |
362 return null; | |
363 }; | |
364 | |
365 MetadataDispatcher.registerParserClass(ExifParser); | |
OLD | NEW |