exif-reader
Version:
A small EXIF image metadata reader
246 lines (198 loc) • 6.78 kB
JavaScript
var tags = require('./tags');
module.exports = function (buffer) {
var startingOffset = 0;
if (buffer.toString('ascii', 0, 3) !== 'MM\0' && buffer.toString('ascii', 0, 3) !== 'II\x2a') {
startingOffset = 6;
if (buffer.toString('ascii', 0, 5) !== 'Exif\0')
throw new Error('Invalid EXIF data: buffer should start with "Exif", "MM" or "II".');
}
var bigEndian = null;
if (buffer[startingOffset] === 0x49 && buffer[startingOffset + 1] === 0x49)
bigEndian = false;
else if (buffer[startingOffset] === 0x4d && buffer[startingOffset + 1] === 0x4d)
bigEndian = true;
else
throw new Error('Invalid EXIF data: expected byte order marker.');
if (buffer.length < startingOffset + 4 || readUInt16(buffer, startingOffset + 2, bigEndian) !== 0x002A)
throw new Error('Invalid EXIF data: expected 0x002A.');
if (buffer.length <= startingOffset + 8) {
throw new Error('Invalid EXIF data: Ends before ifdOffset');
}
var ifdOffset = readUInt32(buffer, startingOffset + 4, bigEndian) + startingOffset;
if (ifdOffset < 8)
throw new Error('Invalid EXIF data: ifdOffset < 8');
var result = { bigEndian };
result.Image = readTags(buffer, ifdOffset, bigEndian, tags.Image, startingOffset);
if (buffer.length >= ifdOffset + 2) {
var numEntries = readUInt16(buffer, ifdOffset, bigEndian);
if (buffer.length >= ifdOffset + 2 + numEntries * 12 + 4) {
ifdOffset = readUInt32(buffer, ifdOffset + 2 + numEntries * 12, bigEndian);
if (ifdOffset !== 0)
result.Thumbnail = readTags(buffer, ifdOffset + startingOffset, bigEndian, tags.Image, startingOffset);
}
}
if (result.Image) {
if (isPositiveInteger(result.Image.ExifTag))
result.Photo = readTags(buffer, result.Image.ExifTag + startingOffset, bigEndian, tags.Photo, startingOffset);
if (isPositiveInteger(result.Image.GPSTag))
result.GPSInfo = readTags(buffer, result.Image.GPSTag + startingOffset, bigEndian, tags.GPSInfo, startingOffset);
}
if (result.Photo && isPositiveInteger(result.Photo.InteroperabilityTag)) {
result.Iop = readTags(buffer, result.Photo.InteroperabilityTag + startingOffset, bigEndian, tags.Iop, startingOffset);
}
return result;
};
var DATE_KEYS = {
DateTimeOriginal: true,
DateTimeDigitized: true,
DateTime: true
};
function readTags(buffer, offset, bigEndian, tags, startingOffset) {
if (buffer.length < offset + 2) {
return null;
}
var numEntries = readUInt16(buffer, offset, bigEndian);
offset += 2;
var res = {};
for (var i = 0; i < numEntries; i++) {
if (buffer.length >= offset + 2) {
var tag = readUInt16(buffer, offset, bigEndian);
} else {
return null;
}
offset += 2;
var key = tags[tag] || tag;
var val = readTag(buffer, offset, bigEndian, startingOffset);
if (key in DATE_KEYS)
val = parseDate(val);
res[key] = val;
offset += 10;
}
return res;
}
var SIZE_LOOKUP = [1, 1, 2, 4, 8, 1, 1, 2, 4, 8];
function readTag(buffer, offset, bigEndian, startingOffset) {
if (buffer.length < offset + 7) {
return null;
}
var type = readUInt16(buffer, offset, bigEndian);
// Exit early in case of unknown or bogus type
if (!type || type > SIZE_LOOKUP.length) return null;
var numValues = readUInt32(buffer, offset + 2, bigEndian);
var valueSize = SIZE_LOOKUP[type - 1];
var valueOffset;
if (valueSize * numValues <= 4) {
valueOffset = offset + 6;
} else {
if (buffer.length >= offset + 10) {
valueOffset = readUInt32(buffer, offset + 6, bigEndian) + startingOffset;
} else {
return null;
}
}
// Special case for ascii strings
if (type === 2) {
var asciiSlice = buffer.slice(valueOffset, valueOffset + numValues);
if (asciiSlice.some(x => x >> 7 > 0))
return asciiSlice;
var string = asciiSlice.toString('ascii');
if (string[string.length - 1] === '\0') // remove null terminator
string = string.slice(0, -1);
return string;
}
// Special case for buffers
if (type === 7)
return buffer.slice(valueOffset, valueOffset + numValues);
if (numValues === 1)
return readValue(buffer, valueOffset, bigEndian, type);
var res = [];
for (var i = 0; i < numValues && valueOffset < buffer.length; i++) {
res.push(readValue(buffer, valueOffset, bigEndian, type));
valueOffset += valueSize;
}
return res;
}
function readValue(buffer, offset, bigEndian, type) {
switch (type) {
case 1: // uint8
if (buffer.length < offset + 1) {
return null;
}
return buffer[offset];
case 3: // uint16
if (buffer.length < offset + 2) {
return null;
}
return readUInt16(buffer, offset, bigEndian);
case 4: // uint32
if (buffer.length < offset + 4) {
return null;
}
return readUInt32(buffer, offset, bigEndian);
case 5: // unsigned rational
if (buffer.length < offset + 8) {
return null;
}
return readUInt32(buffer, offset, bigEndian) / readUInt32(buffer, offset + 4, bigEndian);
case 6: // int8
if (buffer.length < offset + 1) {
return null;
}
return buffer.readInt8(offset);
case 8: // int16
if (buffer.length < offset + 2) {
return null;
}
return readInt16(buffer, offset, bigEndian);
case 9: // int32
if (buffer.length < offset + 4) {
return null;
}
return readInt32(buffer, offset, bigEndian);
case 10: // signed rational
if (buffer.length < offset + 8) {
return null;
}
return readInt32(buffer, offset, bigEndian) / readInt32(buffer, offset + 4, bigEndian);
}
}
function parseDate(string) {
if (typeof string !== 'string')
return null;
var match = string.match(/^(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
if (!match)
return null;
return new Date(Date.UTC(
match[1],
match[2] - 1,
match[3],
match[4],
match[5],
match[6],
0
));
}
function isPositiveInteger(value) {
return typeof value === 'number' && Math.floor(value) === value && value > 0;
}
// Buffer reading helpers to help switching between endianness
function readUInt16(buffer, offset, bigEndian) {
if (bigEndian)
return buffer.readUInt16BE(offset);
return buffer.readUInt16LE(offset);
}
function readUInt32(buffer, offset, bigEndian) {
if (bigEndian)
return buffer.readUInt32BE(offset);
return buffer.readUInt32LE(offset);
}
function readInt16(buffer, offset, bigEndian) {
if (bigEndian)
return buffer.readInt16BE(offset);
return buffer.readInt16LE(offset);
}
function readInt32(buffer, offset, bigEndian) {
if (bigEndian)
return buffer.readInt32BE(offset);
return buffer.readInt32LE(offset);
}