Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(712)

Side by Side Diff: chrome/browser/resources/file_manager/js/exif_parser.js

Issue 8748004: More accurate EXIF parsing (Closed) Base URL: http://git.chromium.org/git/chromium.git@trunk
Patch Set: Created 9 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « chrome/browser/resources/file_manager/js/byte_reader.js ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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);
OLDNEW
« no previous file with comments | « chrome/browser/resources/file_manager/js/byte_reader.js ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698