OLD | NEW |
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 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 const EXIF_MARK_SOI = 0xffd8; // Start of image data. | 5 const EXIF_MARK_SOI = 0xffd8; // Start of image data. |
6 const EXIF_MARK_SOS = 0xffda; // Start of "stream" (the actual image data). | 6 const EXIF_MARK_SOS = 0xffda; // Start of "stream" (the actual image data). |
7 const EXIF_MARK_SOF = 0xffc0; // Start of "frame" | 7 const EXIF_MARK_SOF = 0xffc0; // Start of "frame" |
8 const EXIF_MARK_EXIF = 0xffe1; // Start of exif block. | 8 const EXIF_MARK_EXIF = 0xffe1; // Start of exif block. |
9 | 9 |
10 const EXIF_ALIGN_LITTLE = 0x4949; // Indicates little endian exif data. | 10 const EXIF_ALIGN_LITTLE = 0x4949; // Indicates little endian exif data. |
(...skipping 11 matching lines...) Expand all Loading... |
22 const EXIF_TAG_X_DIMENSION = 0xA002; | 22 const EXIF_TAG_X_DIMENSION = 0xA002; |
23 const EXIF_TAG_Y_DIMENSION = 0xA003; | 23 const EXIF_TAG_Y_DIMENSION = 0xA003; |
24 | 24 |
25 function ExifParser(parent) { | 25 function ExifParser(parent) { |
26 ImageParser.call(this, parent, 'jpeg', /\.jpe?g$/i); | 26 ImageParser.call(this, parent, 'jpeg', /\.jpe?g$/i); |
27 } | 27 } |
28 | 28 |
29 ExifParser.prototype = {__proto__: ImageParser.prototype}; | 29 ExifParser.prototype = {__proto__: ImageParser.prototype}; |
30 | 30 |
31 ExifParser.prototype.parse = function(file, callback, errorCallback) { | 31 ExifParser.prototype.parse = function(file, callback, errorCallback) { |
| 32 var metadata = this.createDefaultMetadata(); |
| 33 this.requestSlice(file, callback, errorCallback, metadata, 0); |
| 34 }; |
| 35 |
| 36 ExifParser.prototype.requestSlice = function ( |
| 37 file, callback, errorCallback, metadata, filePos, opt_length) { |
| 38 // Read at least 1Kb so that we do not issue too many read requests. |
| 39 opt_length = Math.max(1024, opt_length || 0); |
| 40 |
32 var self = this; | 41 var self = this; |
33 var currentStep = -1; | 42 var reader = new FileReader(); |
| 43 reader.onerror = errorCallback; |
| 44 reader.onload = function() { self.parseSlice( |
| 45 file, callback, errorCallback, metadata, filePos, reader.result); |
| 46 }; |
| 47 reader.readAsArrayBuffer(file.webkitSlice(filePos, filePos + opt_length)); |
| 48 }; |
34 | 49 |
35 function nextStep(var_args) { | 50 ExifParser.prototype.parseSlice = function( |
36 self.vlog('exif nextStep: ' + steps[currentStep + 1].name); | 51 file, callback, errorCallback, metadata, filePos, buf) { |
37 try { | 52 try { |
38 steps[++currentStep].apply(null, arguments); | 53 var br = new ByteReader(buf); |
39 } catch(e) { | 54 |
40 onError(e.stack || e.toString()); | 55 if (!br.canRead(4)) { |
| 56 // We never ask for less than 4 bytes. This can only mean we reached EOF. |
| 57 throw new Error('Unexpected EOF @' + (filePos + buf.byteLength)); |
41 } | 58 } |
| 59 |
| 60 if (filePos == 0) { |
| 61 // First slice, check for the SOI mark. |
| 62 var firstMark = this.readMark(br); |
| 63 if (firstMark != EXIF_MARK_SOI) |
| 64 throw new Error('Invalid file header: ' + firstMark.toString(16)); |
| 65 } |
| 66 |
| 67 var self = this; |
| 68 function reread(opt_offset, opt_bytes) { |
| 69 self.requestSlice(file, callback, errorCallback, metadata, |
| 70 filePos + br.tell() + (opt_offset || 0), opt_bytes); |
| 71 } |
| 72 |
| 73 while (true) { |
| 74 if (!br.canRead(4)) { |
| 75 // Cannot read the mark and the length, request a minimum-size slice. |
| 76 reread(); |
| 77 return; |
| 78 } |
| 79 |
| 80 var mark = this.readMark(br); |
| 81 if (mark == EXIF_MARK_SOS) |
| 82 throw new Error('SOS marker found before SOF'); |
| 83 |
| 84 var markLength = this.readMarkLength(br); |
| 85 |
| 86 var nextSectionStart = br.tell() + markLength; |
| 87 if (!br.canRead(markLength)) { |
| 88 // Get the entire section. |
| 89 reread(-4, markLength + 4); |
| 90 return; |
| 91 } |
| 92 |
| 93 if (mark == EXIF_MARK_EXIF) { |
| 94 this.parseExifSection(metadata, buf, br); |
| 95 } else if (mark == EXIF_MARK_SOF) { |
| 96 // The most reliable size information is encoded in the SOF section. |
| 97 br.seek(1, ByteReader.SEEK_CUR); // Skip the precision byte. |
| 98 var height = br.readScalar(2); |
| 99 var width = br.readScalar(2); |
| 100 ExifParser.setImageSize(metadata, width, height); |
| 101 callback(metadata); // We are done! |
| 102 return; |
| 103 } |
| 104 |
| 105 br.seek(nextSectionStart, ByteReader.SEEK_BEG); |
| 106 } |
| 107 } catch (e) { |
| 108 errorCallback(e.toString()); |
| 109 } |
| 110 }; |
| 111 |
| 112 ExifParser.prototype.parseExifSection = function(metadata, buf, br) { |
| 113 var magic = br.readString(6); |
| 114 if (magic != 'Exif\0\0') { |
| 115 // Some JPEG files may have sections marked with EXIF_MARK_EXIF |
| 116 // but containing something else (e.g. XML text). Ignore such sections. |
| 117 this.vlog('Invalid EXIF magic: ' + magic + br.readString(100)); |
| 118 return; |
42 } | 119 } |
43 | 120 |
44 function onError(err) { | 121 // Offsets inside the EXIF block are based after the magic string. |
45 errorCallback(err, steps[currentStep].name); | 122 // Create a new ByteReader based on the current position to make offset |
| 123 // calculations simpler. |
| 124 br = new ByteReader(buf, br.tell()); |
| 125 |
| 126 var order = br.readScalar(2); |
| 127 if (order == EXIF_ALIGN_LITTLE) { |
| 128 br.setByteOrder(ByteReader.LITTLE_ENDIAN); |
| 129 } else if (order != EXIF_ALIGN_BIG) { |
| 130 this.log('Invalid alignment value: ' + order.toString(16)); |
| 131 return; |
46 } | 132 } |
47 | 133 |
48 var steps = | 134 var tag = br.readScalar(2); |
49 [ // Step one, read the file header into a byte array. | 135 if (tag != EXIF_TAG_TIFF) { |
50 function readHeader(file) { | 136 this.log('Invalid TIFF tag: ' + tag.toString(16)); |
51 var reader = new FileReader(); | 137 return; |
52 reader.onerror = onError; | 138 } |
53 reader.onload = function(event) { nextStep(file, reader.result) }; | |
54 reader.readAsArrayBuffer(file.webkitSlice(0, 1024)); | |
55 }, | |
56 | 139 |
57 // Step two, find the exif marker and read all exif data. | 140 metadata.littleEndian = (order == EXIF_ALIGN_LITTLE); |
58 function findExif(file, buf) { | 141 metadata.ifd = { |
59 var br = new ByteReader(buf); | 142 image: {}, |
60 var mark = self.readMark(br); | 143 thumbnail: {} |
61 if (mark != EXIF_MARK_SOI) | 144 }; |
62 return onError('Invalid file header: ' + mark.toString(16)); | 145 var directoryOffset = br.readScalar(4); |
63 | 146 |
64 while (true) { | 147 // Image directory. |
65 if (mark == EXIF_MARK_SOS || br.eof()) { | 148 this.vlog('Read image directory.'); |
66 return onError('Unable to find EXIF or SOF marker'); | 149 br.seek(directoryOffset); |
67 } | 150 directoryOffset = this.readDirectory(br, metadata.ifd.image); |
| 151 metadata.imageTransform = this.parseOrientation(metadata.ifd.image); |
68 | 152 |
69 mark = self.readMark(br); | 153 // Thumbnail Directory chained from the end of the image directory. |
70 if (mark == EXIF_MARK_SOF) { | 154 if (directoryOffset) { |
71 // If we reached this section first then there is no EXIF data. | 155 this.vlog('Read thumbnail directory.'); |
72 // Extract image dimensions and return. | 156 br.seek(directoryOffset); |
| 157 this.readDirectory(br, metadata.ifd.thumbnail); |
| 158 // If no thumbnail orientation is encoded, assume same orientation as |
| 159 // the primary image. |
| 160 metadata.thumbnailTransform = |
| 161 this.parseOrientation(metadata.ifd.thumbnail) || |
| 162 metadata.imageTransform; |
| 163 } |
73 | 164 |
74 // TODO(kaznacheev) Here we are assuming that SOF section lies within | 165 // EXIF Directory may be specified as a tag in the image directory. |
75 // first 1024 bytes. This must be true for any normal JPEG file | 166 if (EXIF_TAG_EXIFDATA in metadata.ifd.image) { |
76 // with no EXIF data. Still might want to handle this more carefully. | 167 this.vlog('Read EXIF directory.'); |
77 if (br.tell() + 7 < buf.byteLength) { | 168 directoryOffset = metadata.ifd.image[EXIF_TAG_EXIFDATA].value; |
78 br.seek(3, ByteReader.SEEK_CUR); | 169 br.seek(directoryOffset); |
79 var metadata = self.createDefaultMetadata(); | 170 metadata.ifd.exif = {}; |
80 metadata.width = br.readScalar(2); | 171 this.readDirectory(br, metadata.ifd.exif); |
81 metadata.height = br.readScalar(2); | 172 } |
82 callback(metadata); | |
83 return; | |
84 } | |
85 } | |
86 if (mark == EXIF_MARK_EXIF) { | |
87 var length = self.readMarkLength(br); | |
88 | 173 |
89 // Offsets inside the EXIF block are based after this bit of | 174 // GPS Directory may also be linked from the image directory. |
90 // magic, so we verify and discard it here, before exif parsing, | 175 if (EXIF_TAG_GPSDATA in metadata.ifd.image) { |
91 // to make offset calculations simpler. | 176 this.vlog('Read GPS directory.'); |
92 var magic = br.readString(6); | 177 directoryOffset = metadata.ifd.image[EXIF_TAG_GPSDATA].value; |
93 if (magic != 'Exif\0\0') | 178 br.seek(directoryOffset); |
94 return onError('Invalid EXIF magic: ' + magic.toString(16)); | 179 metadata.ifd.gps = {}; |
| 180 this.readDirectory(br, metadata.ifd.gps); |
| 181 } |
95 | 182 |
96 var pos = br.tell(); | 183 // Thumbnail may be linked from the image directory. |
97 var reader = new FileReader(); | 184 if (EXIF_TAG_JPG_THUMB_OFFSET in metadata.ifd.thumbnail && |
98 reader.onerror = onError; | 185 EXIF_TAG_JPG_THUMB_LENGTH in metadata.ifd.thumbnail) { |
99 reader.onload = function(event) { nextStep(file, reader.result) }; | 186 this.vlog('Read thumbnail image.'); |
100 reader.readAsArrayBuffer(file.webkitSlice(pos, pos + length - 6)); | 187 br.seek(metadata.ifd.thumbnail[EXIF_TAG_JPG_THUMB_OFFSET].value); |
101 return; | 188 metadata.thumbnailURL = br.readImage( |
102 } | 189 metadata.ifd.thumbnail[EXIF_TAG_JPG_THUMB_LENGTH].value); |
| 190 } else { |
| 191 this.vlog('Image has EXIF data, but no JPG thumbnail.'); |
| 192 } |
| 193 }; |
103 | 194 |
104 self.skipMarkData(br); | 195 ExifParser.setImageSize = function(metadata, width, height) { |
105 } | 196 if (metadata.imageTransform && metadata.imageTransform.rotate90) { |
106 }, | 197 metadata.width = height; |
107 | 198 metadata.height = width; |
108 // Step three, parse the exif data. | 199 } else { |
109 function readDirectories(file, buf) { | 200 metadata.width = width; |
110 var br = new ByteReader(buf); | 201 metadata.height = height; |
111 var order = br.readScalar(2); | 202 } |
112 if (order == EXIF_ALIGN_LITTLE) { | |
113 br.setByteOrder(ByteReader.LITTLE_ENDIAN); | |
114 } else if (order != EXIF_ALIGN_BIG) { | |
115 return onError('Invalid alignment value: ' + order.toString(16)); | |
116 } | |
117 | |
118 var tag = br.readScalar(2); | |
119 if (tag != EXIF_TAG_TIFF) | |
120 return onError('Invalid TIFF tag: ' + tag.toString(16)); | |
121 | |
122 var metadata = self.createDefaultMetadata(); | |
123 metadata.littleEndian = (order == EXIF_ALIGN_LITTLE); | |
124 metadata.ifd = { | |
125 image: {}, | |
126 thumbnail: {} | |
127 }; | |
128 var directoryOffset = br.readScalar(4); | |
129 | |
130 // Image directory. | |
131 self.vlog('Read image directory.'); | |
132 br.seek(directoryOffset); | |
133 directoryOffset = self.readDirectory(br, metadata.ifd.image); | |
134 metadata.imageTransform = self.parseOrientation(metadata.ifd.image); | |
135 | |
136 // Thumbnail Directory chained from the end of the image directory. | |
137 if (directoryOffset) { | |
138 self.vlog('Read thumbnail directory.'); | |
139 br.seek(directoryOffset); | |
140 self.readDirectory(br, metadata.ifd.thumbnail); | |
141 // If no thumbnail orientation is encoded, assume same orientation as | |
142 // the primary image. | |
143 metadata.thumbnailTransform = | |
144 self.parseOrientation(metadata.ifd.thumbnail) || | |
145 metadata.imageTransform; | |
146 } | |
147 | |
148 // EXIF Directory may be specified as a tag in the image directory. | |
149 if (EXIF_TAG_EXIFDATA in metadata.ifd.image) { | |
150 self.vlog('Read EXIF directory.'); | |
151 directoryOffset = metadata.ifd.image[EXIF_TAG_EXIFDATA].value; | |
152 br.seek(directoryOffset); | |
153 metadata.ifd.exif = {}; | |
154 self.readDirectory(br, metadata.ifd.exif); | |
155 | |
156 if (EXIF_TAG_X_DIMENSION in metadata.ifd.exif && | |
157 EXIF_TAG_Y_DIMENSION in metadata.ifd.exif) { | |
158 if (metadata.imageTransform && metadata.imageTransform.rotate90) { | |
159 metadata.width = metadata.ifd.exif[EXIF_TAG_Y_DIMENSION].value; | |
160 metadata.height = metadata.ifd.exif[EXIF_TAG_X_DIMENSION].value; | |
161 } else { | |
162 metadata.width = metadata.ifd.exif[EXIF_TAG_X_DIMENSION].value; | |
163 metadata.height = metadata.ifd.exif[EXIF_TAG_Y_DIMENSION].value; | |
164 } | |
165 } | |
166 } | |
167 | |
168 // GPS Directory may also be linked from the image directory. | |
169 if (EXIF_TAG_GPSDATA in metadata.ifd.image) { | |
170 self.vlog('Read GPS directory.'); | |
171 directoryOffset = metadata.ifd.image[EXIF_TAG_GPSDATA].value; | |
172 br.seek(directoryOffset); | |
173 metadata.ifd.gps = {}; | |
174 self.readDirectory(br, metadata.ifd.gps); | |
175 } | |
176 | |
177 // Thumbnail may be linked from the image directory. | |
178 if (EXIF_TAG_JPG_THUMB_OFFSET in metadata.ifd.thumbnail && | |
179 EXIF_TAG_JPG_THUMB_LENGTH in metadata.ifd.thumbnail) { | |
180 self.vlog('Read thumbnail image.'); | |
181 br.seek(metadata.ifd.thumbnail[EXIF_TAG_JPG_THUMB_OFFSET].value); | |
182 metadata.thumbnailURL = br.readImage( | |
183 metadata.ifd.thumbnail[EXIF_TAG_JPG_THUMB_LENGTH].value); | |
184 } else { | |
185 self.vlog('Image has EXIF data, but no JPG thumbnail.'); | |
186 } | |
187 | |
188 nextStep(metadata); | |
189 }, | |
190 | |
191 // Step four, we're done. | |
192 callback | |
193 ]; | |
194 | |
195 nextStep(file); | |
196 }; | 203 }; |
197 | 204 |
198 ExifParser.prototype.readMark = function(br) { | 205 ExifParser.prototype.readMark = function(br) { |
199 return br.readScalar(2); | 206 return br.readScalar(2); |
200 }; | 207 }; |
201 | 208 |
202 ExifParser.prototype.readMarkLength = function(br) { | 209 ExifParser.prototype.readMarkLength = function(br) { |
203 // Length includes the 2 bytes used to store the length. | 210 // Length includes the 2 bytes used to store the length. |
204 return br.readScalar(2) - 2; | 211 return br.readScalar(2) - 2; |
205 }; | 212 }; |
206 | 213 |
207 ExifParser.prototype.readMarkData = function(br) { | |
208 var length = this.readMarkLength(br); | |
209 return br.readSlice(length); | |
210 }; | |
211 | |
212 ExifParser.prototype.skipMarkData = function(br) { | |
213 br.seek(this.readMarkLength(br), ByteReader.SEEK_CUR); | |
214 }; | |
215 | |
216 ExifParser.prototype.readDirectory = function(br, tags) { | 214 ExifParser.prototype.readDirectory = function(br, tags) { |
217 var entryCount = br.readScalar(2); | 215 var entryCount = br.readScalar(2); |
218 for (var i = 0; i < entryCount; i++) { | 216 for (var i = 0; i < entryCount; i++) { |
219 var tagId = br.readScalar(2); | 217 var tagId = br.readScalar(2); |
220 var tag = tags[tagId] = {id: tagId}; | 218 var tag = tags[tagId] = {id: tagId}; |
221 tag.format = br.readScalar(2); | 219 tag.format = br.readScalar(2); |
222 tag.componentCount = br.readScalar(4); | 220 tag.componentCount = br.readScalar(4); |
223 this.readTagValue(br, tag); | 221 this.readTagValue(br, tag); |
224 } | 222 } |
225 | 223 |
(...skipping 119 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
345 return { | 343 return { |
346 scaleX: ExifParser.SCALEX[index], | 344 scaleX: ExifParser.SCALEX[index], |
347 scaleY: ExifParser.SCALEY[index], | 345 scaleY: ExifParser.SCALEY[index], |
348 rotate90: ExifParser.ROTATE90[index] | 346 rotate90: ExifParser.ROTATE90[index] |
349 } | 347 } |
350 } | 348 } |
351 return null; | 349 return null; |
352 }; | 350 }; |
353 | 351 |
354 MetadataDispatcher.registerParserClass(ExifParser); | 352 MetadataDispatcher.registerParserClass(ExifParser); |
OLD | NEW |