UNPKG

13.4 kBJavaScriptView Raw
1/*
2 * JavaScript Load Image Exif Parser
3 * https://github.com/blueimp/JavaScript-Load-Image
4 *
5 * Copyright 2013, Sebastian Tschan
6 * https://blueimp.net
7 *
8 * Licensed under the MIT license:
9 * https://opensource.org/licenses/MIT
10 */
11
12/* global define, module, require, DataView */
13
14/* eslint-disable no-console */
15
16;(function (factory) {
17 'use strict'
18 if (typeof define === 'function' && define.amd) {
19 // Register as an anonymous AMD module:
20 define(['./load-image', './load-image-meta'], factory)
21 } else if (typeof module === 'object' && module.exports) {
22 factory(require('./load-image'), require('./load-image-meta'))
23 } else {
24 // Browser globals:
25 factory(window.loadImage)
26 }
27})(function (loadImage) {
28 'use strict'
29
30 /**
31 * Exif tag map
32 *
33 * @name ExifMap
34 * @class
35 * @param {number|string} tagCode IFD tag code
36 */
37 function ExifMap(tagCode) {
38 if (tagCode) {
39 Object.defineProperty(this, 'map', {
40 value: this.ifds[tagCode].map
41 })
42 Object.defineProperty(this, 'tags', {
43 value: (this.tags && this.tags[tagCode]) || {}
44 })
45 }
46 }
47
48 ExifMap.prototype.map = {
49 Orientation: 0x0112,
50 Thumbnail: 'ifd1',
51 Blob: 0x0201, // Alias for JPEGInterchangeFormat
52 Exif: 0x8769,
53 GPSInfo: 0x8825,
54 Interoperability: 0xa005
55 }
56
57 ExifMap.prototype.ifds = {
58 ifd1: { name: 'Thumbnail', map: ExifMap.prototype.map },
59 0x8769: { name: 'Exif', map: {} },
60 0x8825: { name: 'GPSInfo', map: {} },
61 0xa005: { name: 'Interoperability', map: {} }
62 }
63
64 /**
65 * Retrieves exif tag value
66 *
67 * @param {number|string} id Exif tag code or name
68 * @returns {object} Exif tag value
69 */
70 ExifMap.prototype.get = function (id) {
71 return this[id] || this[this.map[id]]
72 }
73
74 /**
75 * Returns the Exif Thumbnail data as Blob.
76 *
77 * @param {DataView} dataView Data view interface
78 * @param {number} offset Thumbnail data offset
79 * @param {number} length Thumbnail data length
80 * @returns {undefined|Blob} Returns the Thumbnail Blob or undefined
81 */
82 function getExifThumbnail(dataView, offset, length) {
83 if (!length) return
84 if (offset + length > dataView.byteLength) {
85 console.log('Invalid Exif data: Invalid thumbnail data.')
86 return
87 }
88 return new Blob(
89 [loadImage.bufferSlice.call(dataView.buffer, offset, offset + length)],
90 {
91 type: 'image/jpeg'
92 }
93 )
94 }
95
96 var ExifTagTypes = {
97 // byte, 8-bit unsigned int:
98 1: {
99 getValue: function (dataView, dataOffset) {
100 return dataView.getUint8(dataOffset)
101 },
102 size: 1
103 },
104 // ascii, 8-bit byte:
105 2: {
106 getValue: function (dataView, dataOffset) {
107 return String.fromCharCode(dataView.getUint8(dataOffset))
108 },
109 size: 1,
110 ascii: true
111 },
112 // short, 16 bit int:
113 3: {
114 getValue: function (dataView, dataOffset, littleEndian) {
115 return dataView.getUint16(dataOffset, littleEndian)
116 },
117 size: 2
118 },
119 // long, 32 bit int:
120 4: {
121 getValue: function (dataView, dataOffset, littleEndian) {
122 return dataView.getUint32(dataOffset, littleEndian)
123 },
124 size: 4
125 },
126 // rational = two long values, first is numerator, second is denominator:
127 5: {
128 getValue: function (dataView, dataOffset, littleEndian) {
129 return (
130 dataView.getUint32(dataOffset, littleEndian) /
131 dataView.getUint32(dataOffset + 4, littleEndian)
132 )
133 },
134 size: 8
135 },
136 // slong, 32 bit signed int:
137 9: {
138 getValue: function (dataView, dataOffset, littleEndian) {
139 return dataView.getInt32(dataOffset, littleEndian)
140 },
141 size: 4
142 },
143 // srational, two slongs, first is numerator, second is denominator:
144 10: {
145 getValue: function (dataView, dataOffset, littleEndian) {
146 return (
147 dataView.getInt32(dataOffset, littleEndian) /
148 dataView.getInt32(dataOffset + 4, littleEndian)
149 )
150 },
151 size: 8
152 }
153 }
154 // undefined, 8-bit byte, value depending on field:
155 ExifTagTypes[7] = ExifTagTypes[1]
156
157 /**
158 * Returns Exif tag value.
159 *
160 * @param {DataView} dataView Data view interface
161 * @param {number} tiffOffset TIFF offset
162 * @param {number} offset Tag offset
163 * @param {number} type Tag type
164 * @param {number} length Tag length
165 * @param {boolean} littleEndian Little endian encoding
166 * @returns {object} Tag value
167 */
168 function getExifValue(
169 dataView,
170 tiffOffset,
171 offset,
172 type,
173 length,
174 littleEndian
175 ) {
176 var tagType = ExifTagTypes[type]
177 var tagSize
178 var dataOffset
179 var values
180 var i
181 var str
182 var c
183 if (!tagType) {
184 console.log('Invalid Exif data: Invalid tag type.')
185 return
186 }
187 tagSize = tagType.size * length
188 // Determine if the value is contained in the dataOffset bytes,
189 // or if the value at the dataOffset is a pointer to the actual data:
190 dataOffset =
191 tagSize > 4
192 ? tiffOffset + dataView.getUint32(offset + 8, littleEndian)
193 : offset + 8
194 if (dataOffset + tagSize > dataView.byteLength) {
195 console.log('Invalid Exif data: Invalid data offset.')
196 return
197 }
198 if (length === 1) {
199 return tagType.getValue(dataView, dataOffset, littleEndian)
200 }
201 values = []
202 for (i = 0; i < length; i += 1) {
203 values[i] = tagType.getValue(
204 dataView,
205 dataOffset + i * tagType.size,
206 littleEndian
207 )
208 }
209 if (tagType.ascii) {
210 str = ''
211 // Concatenate the chars:
212 for (i = 0; i < values.length; i += 1) {
213 c = values[i]
214 // Ignore the terminating NULL byte(s):
215 if (c === '\u0000') {
216 break
217 }
218 str += c
219 }
220 return str
221 }
222 return values
223 }
224
225 /**
226 * Determines if the given tag should be included.
227 *
228 * @param {object} includeTags Map of tags to include
229 * @param {object} excludeTags Map of tags to exclude
230 * @param {number|string} tagCode Tag code to check
231 * @returns {boolean} True if the tag should be included
232 */
233 function shouldIncludeTag(includeTags, excludeTags, tagCode) {
234 return (
235 (!includeTags || includeTags[tagCode]) &&
236 (!excludeTags || excludeTags[tagCode] !== true)
237 )
238 }
239
240 /**
241 * Parses Exif tags.
242 *
243 * @param {DataView} dataView Data view interface
244 * @param {number} tiffOffset TIFF offset
245 * @param {number} dirOffset Directory offset
246 * @param {boolean} littleEndian Little endian encoding
247 * @param {ExifMap} tags Map to store parsed exif tags
248 * @param {ExifMap} tagOffsets Map to store parsed exif tag offsets
249 * @param {object} includeTags Map of tags to include
250 * @param {object} excludeTags Map of tags to exclude
251 * @returns {number} Next directory offset
252 */
253 function parseExifTags(
254 dataView,
255 tiffOffset,
256 dirOffset,
257 littleEndian,
258 tags,
259 tagOffsets,
260 includeTags,
261 excludeTags
262 ) {
263 var tagsNumber, dirEndOffset, i, tagOffset, tagNumber, tagValue
264 if (dirOffset + 6 > dataView.byteLength) {
265 console.log('Invalid Exif data: Invalid directory offset.')
266 return
267 }
268 tagsNumber = dataView.getUint16(dirOffset, littleEndian)
269 dirEndOffset = dirOffset + 2 + 12 * tagsNumber
270 if (dirEndOffset + 4 > dataView.byteLength) {
271 console.log('Invalid Exif data: Invalid directory size.')
272 return
273 }
274 for (i = 0; i < tagsNumber; i += 1) {
275 tagOffset = dirOffset + 2 + 12 * i
276 tagNumber = dataView.getUint16(tagOffset, littleEndian)
277 if (!shouldIncludeTag(includeTags, excludeTags, tagNumber)) continue
278 tagValue = getExifValue(
279 dataView,
280 tiffOffset,
281 tagOffset,
282 dataView.getUint16(tagOffset + 2, littleEndian), // tag type
283 dataView.getUint32(tagOffset + 4, littleEndian), // tag length
284 littleEndian
285 )
286 tags[tagNumber] = tagValue
287 if (tagOffsets) {
288 tagOffsets[tagNumber] = tagOffset
289 }
290 }
291 // Return the offset to the next directory:
292 return dataView.getUint32(dirEndOffset, littleEndian)
293 }
294
295 /**
296 * Parses tags in a given IFD (Image File Directory).
297 *
298 * @param {object} data Data object to store exif tags and offsets
299 * @param {number|string} tagCode IFD tag code
300 * @param {DataView} dataView Data view interface
301 * @param {number} tiffOffset TIFF offset
302 * @param {boolean} littleEndian Little endian encoding
303 * @param {object} includeTags Map of tags to include
304 * @param {object} excludeTags Map of tags to exclude
305 */
306 function parseExifIFD(
307 data,
308 tagCode,
309 dataView,
310 tiffOffset,
311 littleEndian,
312 includeTags,
313 excludeTags
314 ) {
315 var dirOffset = data.exif[tagCode]
316 if (dirOffset) {
317 data.exif[tagCode] = new ExifMap(tagCode)
318 if (data.exifOffsets) {
319 data.exifOffsets[tagCode] = new ExifMap(tagCode)
320 }
321 parseExifTags(
322 dataView,
323 tiffOffset,
324 tiffOffset + dirOffset,
325 littleEndian,
326 data.exif[tagCode],
327 data.exifOffsets && data.exifOffsets[tagCode],
328 includeTags && includeTags[tagCode],
329 excludeTags && excludeTags[tagCode]
330 )
331 }
332 }
333
334 loadImage.parseExifData = function (dataView, offset, length, data, options) {
335 if (options.disableExif) {
336 return
337 }
338 var includeTags = options.includeExifTags
339 var excludeTags = options.excludeExifTags || {
340 0x8769: {
341 // ExifIFDPointer
342 0x927c: true // MakerNote
343 }
344 }
345 var tiffOffset = offset + 10
346 var littleEndian
347 var dirOffset
348 var thumbnailIFD
349 // Check for the ASCII code for "Exif" (0x45786966):
350 if (dataView.getUint32(offset + 4) !== 0x45786966) {
351 // No Exif data, might be XMP data instead
352 return
353 }
354 if (tiffOffset + 8 > dataView.byteLength) {
355 console.log('Invalid Exif data: Invalid segment size.')
356 return
357 }
358 // Check for the two null bytes:
359 if (dataView.getUint16(offset + 8) !== 0x0000) {
360 console.log('Invalid Exif data: Missing byte alignment offset.')
361 return
362 }
363 // Check the byte alignment:
364 switch (dataView.getUint16(tiffOffset)) {
365 case 0x4949:
366 littleEndian = true
367 break
368 case 0x4d4d:
369 littleEndian = false
370 break
371 default:
372 console.log('Invalid Exif data: Invalid byte alignment marker.')
373 return
374 }
375 // Check for the TIFF tag marker (0x002A):
376 if (dataView.getUint16(tiffOffset + 2, littleEndian) !== 0x002a) {
377 console.log('Invalid Exif data: Missing TIFF marker.')
378 return
379 }
380 // Retrieve the directory offset bytes, usually 0x00000008 or 8 decimal:
381 dirOffset = dataView.getUint32(tiffOffset + 4, littleEndian)
382 // Create the exif object to store the tags:
383 data.exif = new ExifMap()
384 if (!options.disableExifOffsets) {
385 data.exifOffsets = new ExifMap()
386 data.exifTiffOffset = tiffOffset
387 data.exifLittleEndian = littleEndian
388 }
389 // Parse the tags of the main image directory (IFD0) and retrieve the
390 // offset to the next directory (IFD1), usually the thumbnail directory:
391 dirOffset = parseExifTags(
392 dataView,
393 tiffOffset,
394 tiffOffset + dirOffset,
395 littleEndian,
396 data.exif,
397 data.exifOffsets,
398 includeTags,
399 excludeTags
400 )
401 if (dirOffset && shouldIncludeTag(includeTags, excludeTags, 'ifd1')) {
402 data.exif.ifd1 = dirOffset
403 if (data.exifOffsets) {
404 data.exifOffsets.ifd1 = tiffOffset + dirOffset
405 }
406 }
407 Object.keys(data.exif.ifds).forEach(function (tagCode) {
408 parseExifIFD(
409 data,
410 tagCode,
411 dataView,
412 tiffOffset,
413 littleEndian,
414 includeTags,
415 excludeTags
416 )
417 })
418 thumbnailIFD = data.exif.ifd1
419 // Check for JPEG Thumbnail offset and data length:
420 if (thumbnailIFD && thumbnailIFD[0x0201]) {
421 thumbnailIFD[0x0201] = getExifThumbnail(
422 dataView,
423 tiffOffset + thumbnailIFD[0x0201],
424 thumbnailIFD[0x0202] // Thumbnail data length
425 )
426 }
427 }
428
429 // Registers the Exif parser for the APP1 JPEG metadata segment:
430 loadImage.metaDataParsers.jpeg[0xffe1].push(loadImage.parseExifData)
431
432 loadImage.exifWriters = {
433 // Orientation writer:
434 0x0112: function (buffer, data, value) {
435 var orientationOffset = data.exifOffsets[0x0112]
436 if (!orientationOffset) return buffer
437 var view = new DataView(buffer, orientationOffset + 8, 2)
438 view.setUint16(0, value, data.exifLittleEndian)
439 return buffer
440 }
441 }
442
443 loadImage.writeExifData = function (buffer, data, id, value) {
444 loadImage.exifWriters[data.exif.map[id]](buffer, data, value)
445 }
446
447 loadImage.ExifMap = ExifMap
448
449 // Adds the following properties to the parseMetaData callback data:
450 // - exif: The parsed Exif tags
451 // - exifOffsets: The parsed Exif tag offsets
452 // - exifTiffOffset: TIFF header offset (used for offset pointers)
453 // - exifLittleEndian: little endian order if true, big endian if false
454
455 // Adds the following options to the parseMetaData method:
456 // - disableExif: Disables Exif parsing when true.
457 // - disableExifOffsets: Disables storing Exif tag offsets when true.
458 // - includeExifTags: A map of Exif tags to include for parsing.
459 // - excludeExifTags: A map of Exif tags to exclude from parsing.
460})