exif-metadata
Version:
metadata.js is a JavaScript API for parsing & copying JPEG metadata (EXIF, IPTC, XMP, Makernotes) using ArrayBuffers.
415 lines (370 loc) • 12.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _jdataview = require("jdataview");
var _jdataview2 = _interopRequireDefault(_jdataview);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var metadata = function () {
var IFD0 = {
0x8825: "GPSInfoIFDPointer",
0x0112: "Orientation",
0x010F: "Make",
0x0110: "Model",
0x013B: "Artist",
0x8298: "Copyright"
};
var GPSInfo = {
0x0001: "GPSLatitudeRef",
0x0002: "GPSLatitude",
0x0003: "GPSLongitudeRef",
0x0004: "GPSLongitude"
};
function gpsToDegree(rational, sign) {
try {
if (!rational || !sign) return null;
var rationals = rational.split(' ');
if (rationals.length !== 3) return null;
var degrees = rationalToFloat(rationals[0]);
var minutes = rationalToFloat(rationals[1]);
var seconds = rationalToFloat(rationals[2]);
var degree = roundFloat(degrees + minutes / 60.0 + seconds / 3600.0) * sign;
if (isNaN(degree)) return null;
return degree;
} catch (e) {
return null;
}
}
function rationalToFloat(rational) {
var parsedRational = rational.split('/');
if (parsedRational.length === 2) {
var nominator = parseFloat(parsedRational[0]);
var denominator = parseFloat(parsedRational[1]);
return nominator / denominator;
}
}
function roundFloat(number) {
var multiple = Math.pow(10, 6);
var roundedNumber = Math.round(number * multiple) / multiple;
return roundedNumber;
}
function refToSign(ref) {
if (!ref) {
return null;
}
switch (ref.toUpperCase()) {
case "N":
case "E":
return 1.0;
case "S":
case "W":
return -1.0;
default:
return null;
}
}
function getExifMarker(sourceArrayBuffer, marker) {
if (marker.type === 0xFFE1) {
var view = new _jdataview2.default(slice(sourceArrayBuffer, marker.position + 4, marker.position + 4 + marker.size));
if (view.getString(4) === 'Exif') {
return view;
}
}
return;
}
function readExifTags(ifds, start, position, view, littleEndian) {
var entries = view.getUint16(position, littleEndian);
var tags = [];
for (var i = 0; i < entries; i++) {
var tag = view.getUint16(position + 2, littleEndian);
var tagName = ifds[tag];
if (tagName) {
var type = view.getUint16(position + 4, littleEndian);
var length = view.getUint32(position + 6, littleEndian);
var offset = view.getUint32(position + 10, littleEndian);
tags[tagName] = getTagValue(view, type, length, position, offset + start, littleEndian);
}
position += 12;
}
return tags;
}
function getTagValue(view, type, length, position, offset, littleEndian) {
var pos = length > 4 ? offset : position + 10;
switch (type) {
case 2:
//string
return view.getString(length - 1, pos);
case 3:
//short
return view.getUint16(pos, littleEndian);
case 4:
//long
return view.getUint32(pos, littleEndian);
case 5:
// rational (should always be 3 for what we want)
var numerator, denominator;
if (length === 3) {
var arr = [];
for (var i = 0; i < length; i++) {
numerator = view.getUint32(offset + i * 8, littleEndian);
denominator = view.getUint32(offset + 4 + i * 8, littleEndian);
arr.push(numerator + '/' + denominator);
}
return arr.join(' ');
}
break;
default:
return null;
}
}
function slice(sourceArray, start, end) {
if (sourceArray.slice) {
try {
return sourceArray.slice(start, end);
} catch (e) {}
}
//IE 10 support
var size = end - start;
var source = new Uint8Array(sourceArray);
var buffer = new ArrayBuffer(size);
var result = new Uint8Array(buffer);
for (var i = 0; i < size; i++) {
result[i] = source[i + start];
}
return result;
}
function isAPPMarker(type) {
return type >= 0xFFE1 && type <= 0xFFEF || type === 0xFFFE;
}
function createBlob(markers) {
if (window.Blob) {
try {
return new Blob(markers, { type: 'image/jpeg' });
} catch (e) {}
}
//pre blob support
var BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder;
var bb = new BlobBuilder();
for (var i = 0; i < markers.length; i++) {
bb.append(markers[i]);
}
return bb.getBlob('image/jpeg');
}
function _parse(sourceArrayBuffer) {
var jpegMarkers = { valid: false, sosPosition: 0, appMarkers: [], markers: [] };
var type,
size,
position = 0;
var view = new _jdataview2.default(sourceArrayBuffer);
//valid jpeg?
if (view.getUint16() !== 0xFFD8) {
return jpegMarkers;
}
//if for some reason we don't find the SOS marker
//this should stop us from getting out of control.
var maxLength = Math.min(sourceArrayBuffer.byteLength, 2097152);
while (view.tell() < maxLength) {
type = view.getUint16();
switch (type) {
case 0xFFE0: //APP0
case 0xFFE1: //APP1
case 0xFFE2: //APP2
case 0xFFE3: //APP3
case 0xFFE4: //APP4
case 0xFFE5: //APP5
case 0xFFE6: //APP6
case 0xFFE7: //APP7
case 0xFFE8: //APP8
case 0xFFE9: //APP9
case 0xFFEA: //APP10
case 0xFFEB: //APP11
case 0xFFEC: //APP12
case 0xFFED: //APP13
case 0xFFEE: //APP14
case 0xFFEF: //APP15
case 0xFFC0: //SOF0
case 0xFFC1: //SOF1
case 0xFFC2: //SOF2
case 0xFFC3: //SOF3
case 0xFFDB: //DQT
case 0xFFDC: //DNL
case 0xFFDD: //DRI
case 0xFFDE: //DHP
case 0xFFDF: //EXP
case 0xFFC4: //DHT
case 0xFFC5: //SOF5
case 0xFFC6: //SOF6
case 0xFFC7: //SOF7
case 0xFFC8: //JPG
case 0xFFC9: //SOF9
case 0xFFCA: //SOF10
case 0xFFCB: //SOF11
case 0xFFCC: //DAC
case 0xFFCD: //SOF13
case 0xFFCE: //SOF14
case 0xFFCF: //SOF15
case 0xFFFE: //COM
case 0xFFD0: //RST0
case 0xFFD1: //RST1
case 0xFFD2: //RST2
case 0xFFD3: //RST3
case 0xFFD4: //RST4
case 0xFFD5: //RST5
case 0xFFD6: //RST6
case 0xFFD7: //RST7
case 0xFFF0: //JPG0
case 0xFFF1: //JPG1
case 0xFFF2: //JPG2
case 0xFFF3: //JPG3
case 0xFFF4: //JPG4
case 0xFFF5: //JPG5
case 0xFFF6: //JPG6
case 0xFFF7: //JPG7
case 0xFFF8: //JPG8
case 0xFFF9: //JPG9
case 0xFFFA: //JPG10
case 0xFFFB: //JPG11
case 0xFFFC: //JPG12
case 0xFFFD: //JPG13
case 0xFF01:
//TEM
size = view.getUint16();
position = view.tell() - 4;
var marker = { type: type, position: position, size: size };
if (isAPPMarker(type)) {
jpegMarkers.appMarkers.push(marker);
} else {
jpegMarkers.markers.push(marker);
}
view.seek(view.tell() + size - 2);
break;
default:
}
//SOS marker means we're done
if (type === 0xFFDA) {
jpegMarkers.valid = true;
jpegMarkers.sosPosition = view.tell() - 2;
break;
}
}
return jpegMarkers;
}
function _copy(sourceArrayBuffer, destArrayBuffer, sourceMarkers) {
var buffers = [],
marker;
//do we already have it parsed?
if (!sourceMarkers) {
sourceMarkers = _parse(sourceArrayBuffer);
}
//if there is nothing worth copying or something failed, bail out
var destMarkers = _parse(destArrayBuffer);
if (!sourceMarkers.valid || sourceMarkers.appMarkers.length === 0 || !destMarkers.valid) {
return createBlob([destArrayBuffer]);
}
//no guarentee APP0 marker will be there, but if it is, lets write it first
var jfifMarker;
for (var i = 0; i < destMarkers.markers.length; i++) {
if (destMarkers.markers[i].type === 0xFFE0) {
jfifMarker = destMarkers.markers[i];
destMarkers.markers.splice(i, 1);
break;
}
}
//write SOI
buffers.push(slice(destArrayBuffer, 0, 2));
//write the JFIF marker
if (jfifMarker) {
buffers.push(slice(destArrayBuffer, jfifMarker.position, jfifMarker.position + (jfifMarker.size + 2)));
}
//copy all the app markers from the source
for (var _i = 0; _i < sourceMarkers.appMarkers.length; _i++) {
marker = sourceMarkers.appMarkers[_i];
buffers.push(slice(sourceArrayBuffer, marker.position, marker.position + (marker.size + 2)));
}
//copy all the other markers from the dest
for (var _i2 = 0; _i2 < destMarkers.markers.length; _i2++) {
marker = destMarkers.markers[_i2];
buffers.push(slice(destArrayBuffer, marker.position, marker.position + (marker.size + 2)));
}
//copy everything else at the SOS marker
buffers.push(slice(destArrayBuffer, destMarkers.sosPosition, destArrayBuffer.byteLength));
return createBlob(buffers);
}
function _getExif(sourceArrayBuffer, sourceMarkers) {
var exif = { hasExif: false, copyright: null, artist: null, make: null, model: null, orientation: 0, hasGPSLocation: false, longitude: 0, latitude: 0 };
if (!sourceMarkers) {
sourceMarkers = _parse(sourceArrayBuffer);
}
//do we have anything worth parsing?
if (!sourceMarkers.valid || sourceMarkers.appMarkers.length === 0) {
return exif;
}
var view;
for (var i = 0; i < sourceMarkers.appMarkers.length; i++) {
view = getExifMarker(sourceArrayBuffer, sourceMarkers.appMarkers[i]);
if (view) {
break;
}
}
//did we find anything?
if (!view) {
return exif;
}
//set the start position
var start = view.tell() + 2; //should always be 6
//skip the 2 null bytes + set endian
var littleEndian = view.getUint16(view.tell() + 2) === 0x4949;
//valid tiff header?
if (view.getUint16(view.tell(), littleEndian) !== 0x002A || view.getUint32(view.tell(), littleEndian) !== 0x00000008) {
return exif;
}
//read IFD0 tags first
var ifdTags = readExifTags(IFD0, start, view.tell(), view, littleEndian);
//set IFD0 values
if (ifdTags) {
exif.hasExif = true;
exif.copyright = ifdTags.Copyright ? ifdTags.Copyright : null;
exif.make = ifdTags.Make ? ifdTags.Make : null;
exif.model = ifdTags.Model ? ifdTags.Model : null;
exif.artist = ifdTags.Artist ? ifdTags.Artist : null;
exif.orientation = ifdTags.Orientation ? ifdTags.Orientation : 0;
//do we have a GPS info pointer?
if (ifdTags.GPSInfoIFDPointer) {
var gpsTags = readExifTags(GPSInfo, start, ifdTags.GPSInfoIFDPointer + 6, view, littleEndian);
var latRef = refToSign(gpsTags.GPSLatitudeRef);
var longRef = refToSign(gpsTags.GPSLongitudeRef);
if (latRef !== null && longRef !== null) {
var latitude = gpsToDegree(gpsTags.GPSLatitude, latRef);
var longitude = gpsToDegree(gpsTags.GPSLongitude, longRef);
if (latitude !== null && longitude !== null) {
exif.hasGPSLocation = true;
exif.latitude = latitude;
exif.longitude = longitude;
}
}
}
}
return exif;
}
return {
parse: function parse(sourceArrayBuffer) {
return _parse(sourceArrayBuffer);
},
copy: function copy(sourceArrayBuffer, destArrayBuffer, sourceMetadata) {
return _copy(sourceArrayBuffer, destArrayBuffer, sourceMetadata);
},
getExif: function getExif(sourceArrayBuffer, sourceMetadata) {
return _getExif(sourceArrayBuffer, sourceMetadata);
}
};
}(); /*
* metadata.js
* https://github.com/rfrench/metadata.js
*
* Copyright 2014, Ryan French
*
* Licence: Do What The Fuck You Want To Public License
* http://www.wtfpl.net/
*/
exports.default = metadata;
//# sourceMappingURL=metadata.js.map