271 lines
6.3 KiB
Plaintext
271 lines
6.3 KiB
Plaintext
|
|
/* eslint-disable no-bitwise */
|
|
/* eslint-disable consistent-return */
|
|
|
|
'use strict';
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// Helpers
|
|
//
|
|
function error(message, code) {
|
|
var err = new Error(message);
|
|
err.code = code;
|
|
return err;
|
|
}
|
|
|
|
|
|
function utf8_decode(str) {
|
|
try {
|
|
return decodeURIComponent(escape(str));
|
|
} catch (_) {
|
|
return str;
|
|
}
|
|
}
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// Exif parser
|
|
//
|
|
// Input:
|
|
// - jpeg_bin: Uint8Array - jpeg file
|
|
// - exif_start: Number - start of TIFF header (after Exif\0\0)
|
|
// - exif_end: Number - end of Exif segment
|
|
// - on_entry: Number - callback
|
|
//
|
|
function ExifParser(jpeg_bin, exif_start, exif_end) {
|
|
// Uint8Array, exif without signature (which isn't included in offsets)
|
|
this.input = jpeg_bin.subarray(exif_start, exif_end);
|
|
|
|
// offset correction for `on_entry` callback
|
|
this.start = exif_start;
|
|
|
|
// Check TIFF header (includes byte alignment and first IFD offset)
|
|
var sig = String.fromCharCode.apply(null, this.input.subarray(0, 4));
|
|
|
|
if (sig !== 'II\x2A\0' && sig !== 'MM\0\x2A') {
|
|
throw error('invalid TIFF signature', 'EBADDATA');
|
|
}
|
|
|
|
// true if motorola (big endian) byte alignment, false if intel
|
|
this.big_endian = sig[0] === 'M';
|
|
}
|
|
|
|
|
|
ExifParser.prototype.each = function (on_entry) {
|
|
// allow premature exit
|
|
this.aborted = false;
|
|
|
|
var offset = this.read_uint32(4);
|
|
|
|
this.ifds_to_read = [ {
|
|
id: 0,
|
|
offset: offset
|
|
} ];
|
|
|
|
while (this.ifds_to_read.length > 0 && !this.aborted) {
|
|
var i = this.ifds_to_read.shift();
|
|
if (!i.offset) continue;
|
|
this.scan_ifd(i.id, i.offset, on_entry);
|
|
}
|
|
};
|
|
|
|
|
|
ExifParser.prototype.read_uint16 = function (offset) {
|
|
var d = this.input;
|
|
if (offset + 2 > d.length) throw error('unexpected EOF', 'EBADDATA');
|
|
|
|
return this.big_endian ?
|
|
d[offset] * 0x100 + d[offset + 1] :
|
|
d[offset] + d[offset + 1] * 0x100;
|
|
};
|
|
|
|
|
|
ExifParser.prototype.read_uint32 = function (offset) {
|
|
var d = this.input;
|
|
if (offset + 4 > d.length) throw error('unexpected EOF', 'EBADDATA');
|
|
|
|
return this.big_endian ?
|
|
d[offset] * 0x1000000 + d[offset + 1] * 0x10000 + d[offset + 2] * 0x100 + d[offset + 3] :
|
|
d[offset] + d[offset + 1] * 0x100 + d[offset + 2] * 0x10000 + d[offset + 3] * 0x1000000;
|
|
};
|
|
|
|
|
|
ExifParser.prototype.is_subifd_link = function (ifd, tag) {
|
|
return (ifd === 0 && tag === 0x8769) || // SubIFD
|
|
(ifd === 0 && tag === 0x8825) || // GPS Info
|
|
(ifd === 0x8769 && tag === 0xA005); // Interop IFD
|
|
};
|
|
|
|
|
|
// Returns byte length of a single component of a given format
|
|
//
|
|
ExifParser.prototype.exif_format_length = function (format) {
|
|
switch (format) {
|
|
case 1: // byte
|
|
case 2: // ascii
|
|
case 6: // sbyte
|
|
case 7: // undefined
|
|
return 1;
|
|
|
|
case 3: // short
|
|
case 8: // sshort
|
|
return 2;
|
|
|
|
case 4: // long
|
|
case 9: // slong
|
|
case 11: // float
|
|
return 4;
|
|
|
|
case 5: // rational
|
|
case 10: // srational
|
|
case 12: // double
|
|
return 8;
|
|
|
|
default:
|
|
// unknown type
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
|
|
// Reads Exif data
|
|
//
|
|
ExifParser.prototype.exif_format_read = function (format, offset) {
|
|
var v;
|
|
|
|
switch (format) {
|
|
case 1: // byte
|
|
case 2: // ascii
|
|
v = this.input[offset];
|
|
return v;
|
|
|
|
case 6: // sbyte
|
|
v = this.input[offset];
|
|
return v | (v & 0x80) * 0x1fffffe;
|
|
|
|
case 3: // short
|
|
v = this.read_uint16(offset);
|
|
return v;
|
|
|
|
case 8: // sshort
|
|
v = this.read_uint16(offset);
|
|
return v | (v & 0x8000) * 0x1fffe;
|
|
|
|
case 4: // long
|
|
v = this.read_uint32(offset);
|
|
return v;
|
|
|
|
case 9: // slong
|
|
v = this.read_uint32(offset);
|
|
return v | 0;
|
|
|
|
case 5: // rational
|
|
case 10: // srational
|
|
case 11: // float
|
|
case 12: // double
|
|
return null; // not implemented
|
|
|
|
case 7: // undefined
|
|
return null; // blob
|
|
|
|
default:
|
|
// unknown type
|
|
return null;
|
|
}
|
|
};
|
|
|
|
|
|
ExifParser.prototype.scan_ifd = function (ifd_no, offset, on_entry) {
|
|
var entry_count = this.read_uint16(offset);
|
|
|
|
offset += 2;
|
|
|
|
for (var i = 0; i < entry_count; i++) {
|
|
var tag = this.read_uint16(offset);
|
|
var format = this.read_uint16(offset + 2);
|
|
var count = this.read_uint32(offset + 4);
|
|
|
|
var comp_length = this.exif_format_length(format);
|
|
var data_length = count * comp_length;
|
|
var data_offset = data_length <= 4 ? offset + 8 : this.read_uint32(offset + 8);
|
|
var is_subifd_link = false;
|
|
|
|
if (data_offset + data_length > this.input.length) {
|
|
throw error('unexpected EOF', 'EBADDATA');
|
|
}
|
|
|
|
var value = [];
|
|
var comp_offset = data_offset;
|
|
|
|
for (var j = 0; j < count; j++, comp_offset += comp_length) {
|
|
var item = this.exif_format_read(format, comp_offset);
|
|
if (item === null) {
|
|
value = null;
|
|
break;
|
|
}
|
|
value.push(item);
|
|
}
|
|
|
|
if (Array.isArray(value) && format === 2) {
|
|
value = utf8_decode(String.fromCharCode.apply(null, value));
|
|
if (value && value[value.length - 1] === '\0') value = value.slice(0, -1);
|
|
}
|
|
|
|
if (this.is_subifd_link(ifd_no, tag)) {
|
|
if (Array.isArray(value) && Number.isInteger(value[0]) && value[0] > 0) {
|
|
this.ifds_to_read.push({
|
|
id: tag,
|
|
offset: value[0]
|
|
});
|
|
is_subifd_link = true;
|
|
}
|
|
}
|
|
|
|
var entry = {
|
|
is_big_endian: this.big_endian,
|
|
ifd: ifd_no,
|
|
tag: tag,
|
|
format: format,
|
|
count: count,
|
|
entry_offset: offset + this.start,
|
|
data_length: data_length,
|
|
data_offset: data_offset + this.start,
|
|
value: value,
|
|
is_subifd_link: is_subifd_link
|
|
};
|
|
|
|
if (on_entry(entry) === false) {
|
|
this.aborted = true;
|
|
return;
|
|
}
|
|
|
|
offset += 12;
|
|
}
|
|
|
|
if (ifd_no === 0) {
|
|
this.ifds_to_read.push({
|
|
id: 1,
|
|
offset: this.read_uint32(offset)
|
|
});
|
|
}
|
|
};
|
|
|
|
|
|
module.exports.ExifParser = ExifParser;
|
|
|
|
// returns orientation stored in Exif (1-8), 0 if none was found, -1 if error
|
|
module.exports.get_orientation = function (data) {
|
|
var orientation = 0;
|
|
try {
|
|
new ExifParser(data, 0, data.length).each(function (entry) {
|
|
if (entry.ifd === 0 && entry.tag === 0x112 && Array.isArray(entry.value)) {
|
|
orientation = entry.value[0];
|
|
return false;
|
|
}
|
|
});
|
|
return orientation;
|
|
} catch (err) {
|
|
return -1;
|
|
}
|
|
};
|