@sitemark/exifr
Version:
📑 The fastest and most versatile JavaScript EXIF reading library.
472 lines (379 loc) • 12.2 kB
JavaScript
import DOMParser from './dom-parser.js';
/* Based on https://github.com/mattiasw/ExifReader/blob/master/src/xmp-tags.js
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
var XMPParser = {
read
};
const tagTypes = {
// DJI
'drone-dji:CamReverse': 'bool',
'drone-dji:GimbalReverse': 'bool',
// CRS
'crs:Version': 'string',
// Camera
'Camera:IsNormalized': 'bool',
};
function read(xmpString) {
try {
const doc = getDocument(xmpString.replace(/\0/g, ""));
const rdf = getRDF(doc);
const xmp = parseXMPObject(convertToObject(rdf, true));
Object.keys(xmp).forEach((key) => {
xmp[key] = parseValue(key, xmp[key]);
});
return xmp;
} catch (error) {
return {};
}
}
function parseValue(key, value) {
if (value instanceof Array) {
return value.map((val) => parseValue(key, val));
} else if (value instanceof Object && 'value' in value) {
return {
...value,
value: parseValue(key, value.value),
};
} else if (tagTypes[key]) {
if (tagTypes[key] === 'bool') {
if (['True', 'true', '1'].includes(value)) {
return true;
} else if (['False', 'false', '0'].includes(value)) {
return false;
}
throw new Error(`Unknown value ${value} for key ${key} to be parsed as bool.`)
} else if (tagTypes[key] === 'string') {
return value;
}
} else if (!isNaN(value) && value !== '') {
return Number(value);
} else if (['False', 'false'].includes(value)) {
return false;
} else if (['True', 'true'].includes(value)) {
return true;
}
return value
}
function getDocument(xmlSource) {
const Parser = DOMParser.get();
if (!Parser) {
console.warn('Warning: DOMParser is not available. It is needed to be able to parse XMP tags.'); // eslint-disable-line no-console
throw new Error();
}
const domParser = new Parser();
const doc = domParser.parseFromString(xmlSource, 'application/xml');
if (doc.documentElement.nodeName === 'parsererror') {
throw new Error(doc.documentElement.textContent);
}
return doc;
}
function getRDF(node) {
for (let i = 0; i < node.childNodes.length; i++) {
if (node.childNodes[i].tagName === 'x:xmpmeta') {
return getRDF(node.childNodes[i]);
}
if (node.childNodes[i].tagName === 'rdf:RDF') {
return node.childNodes[i];
}
}
throw new Error();
}
function convertToObject(node, isTopNode = false) {
const childNodes = getChildNodes(node);
if (hasTextOnlyContent(childNodes)) {
if (isTopNode) {
return {};
}
return getTextValue(childNodes[0]);
}
return getElementsFromNodes(childNodes);
}
function getChildNodes(node) {
const elements = [];
for (let i = 0; i < node.childNodes.length; i++) {
elements.push(node.childNodes[i]);
}
return elements;
}
function hasTextOnlyContent(nodes) {
return (nodes.length === 1) && (nodes[0].nodeName === '#text');
}
function getTextValue(node) {
return node.nodeValue;
}
function getElementsFromNodes(nodes) {
const elements = {};
nodes.forEach((node) => {
if (isElement(node)) {
const nodeElement = getElementFromNode(node);
if (elements[node.nodeName] !== undefined) {
if (!Array.isArray(elements[node.nodeName])) {
elements[node.nodeName] = [elements[node.nodeName]];
}
elements[node.nodeName].push(nodeElement);
} else {
elements[node.nodeName] = nodeElement;
}
}
});
return elements;
}
function isElement(node) {
return (node.nodeName) && (node.nodeName !== '#text');
}
function getElementFromNode(node) {
return {
attributes: getAttributes(node),
value: convertToObject(node)
};
}
function getAttributes(element) {
const attributes = {};
for (let i = 0; i < element.attributes.length; i++) {
attributes[element.attributes[i].nodeName] = decodeURIComponent(escape(element.attributes[i].value));
}
return attributes;
}
function parseXMPObject(xmpObject) {
const tags = {};
if (typeof xmpObject === 'string') {
return xmpObject;
}
for (const nodeName in xmpObject) {
let nodes = xmpObject[nodeName];
if (!Array.isArray(nodes)) {
nodes = [nodes];
}
nodes.forEach((node) => {
Object.assign(tags, parseNodeAttributesAsTags(node.attributes));
if (typeof node.value === 'object') {
Object.assign(tags, parseNodeChildrenAsTags(node.value));
}
});
}
// Assign value directly to key to filter out attributes and description
for (const key of Object.keys(tags)) {
tags[key] = tags[key].value;
}
return tags;
}
function parseNodeAttributesAsTags(attributes) {
const tags = {};
for (const name in attributes) {
if (isTagAttribute(name)) {
tags[getLocalName(name)] = {
value: attributes[name],
attributes: {},
description: getDescription(attributes[name], name)
};
}
}
return tags;
}
function isTagAttribute(name) {
return (name !== 'rdf:parseType') && (!isNamespaceDefinition(name));
}
function isNamespaceDefinition(name) {
return name.split(':')[0] === 'xmlns';
}
function getLocalName(name) {
// return name.split(':')[1];
// We want to keep full name to avoid overwriting other tags when merging
return name;
}
function getDescription(value, name = undefined) {
if (Array.isArray(value)) {
return getDescriptionOfArray(value);
}
if (typeof value === 'object') {
return getDescriptionOfObject(value);
}
try {
return decodeURIComponent(escape(value));
} catch (error) {
return value;
}
}
function getDescriptionOfArray(value) {
return value.map((item) => {
if (item.value !== undefined) {
return getDescription(item.value);
}
return getDescription(item);
}).join(', ');
}
function getDescriptionOfObject(value) {
const descriptions = [];
for (const key in value) {
descriptions.push(`${getClearTextKey(key)}: ${value[key].value}`);
}
return descriptions.join('; ');
}
function getClearTextKey(key) {
if (key === 'CiAdrCity') {
return 'CreatorCity';
}
if (key === 'CiAdrCtry') {
return 'CreatorCountry';
}
if (key === 'CiAdrExtadr') {
return 'CreatorAddress';
}
if (key === 'CiAdrPcode') {
return 'CreatorPostalCode';
}
if (key === 'CiAdrRegion') {
return 'CreatorRegion';
}
if (key === 'CiEmailWork') {
return 'CreatorWorkEmail';
}
if (key === 'CiTelWork') {
return 'CreatorWorkPhone';
}
if (key === 'CiUrlWork') {
return 'CreatorWorkUrl';
}
return key;
}
function parseNodeChildrenAsTags(children) {
const tags = {};
for (const name in children) {
if (!isNamespaceDefinition(name)) {
tags[getLocalName(name)] = parseNodeAsTag(children[name], name);
}
}
return tags;
}
function parseNodeAsTag(node, name) {
if (hasNestedSimpleRdfDescription(node)) {
return parseNodeAsSimpleRdfDescription(node, name);
} else if (hasNestedStructureRdfDescription(node)) {
return parseNodeAsStructureRdfDescription(node, name);
} else if (isCompactStructure(node)) {
return parseNodeAsCompactStructure(node, name);
} else if (isArray(node)) {
return parseNodeAsArray(node, name);
}
return parseNodeAsSimpleValue(node, name);
}
function hasNestedSimpleRdfDescription(node) {
return ((node.attributes['rdf:parseType'] === 'Resource') && (node.value['rdf:value'] !== undefined))
|| ((node.value['rdf:Description'] !== undefined) && (node.value['rdf:Description'].value['rdf:value'] !== undefined));
}
function parseNodeAsSimpleRdfDescription(node, name) {
const attributes = parseNodeAttributes(node);
if (node.value['rdf:Description'] !== undefined) {
node = node.value['rdf:Description'];
}
Object.assign(attributes, parseNodeAttributes(node), parseNodeChildrenAsAttributes(node));
const value = parseRdfValue(node);
return {
value,
attributes,
description: getDescription(value, name)
};
}
function parseNodeAttributes(node) {
const attributes = {};
for (const name in node.attributes) {
if ((name !== 'rdf:parseType') && (name !== 'rdf:resource') && (!isNamespaceDefinition(name))) {
attributes[getLocalName(name)] = node.attributes[name];
}
}
return attributes;
}
function parseNodeChildrenAsAttributes(node) {
const attributes = {};
for (const name in node.value) {
if ((name !== 'rdf:value') && (!isNamespaceDefinition(name))) {
attributes[getLocalName(name)] = node.value[name].value;
}
}
return attributes;
}
function parseRdfValue(node) {
return getURIValue(node.value['rdf:value']) || node.value['rdf:value'].value;
}
function hasNestedStructureRdfDescription(node) {
return (node.attributes['rdf:parseType'] === 'Resource')
|| ((node.value['rdf:Description'] !== undefined) && (node.value['rdf:Description'].value['rdf:value'] === undefined));
}
function parseNodeAsStructureRdfDescription(node, name) {
const tag = {
value: {},
attributes: {}
};
if (node.value['rdf:Description'] !== undefined) {
Object.assign(tag.value, parseNodeAttributesAsTags(node.value['rdf:Description'].attributes));
Object.assign(tag.attributes, parseNodeAttributes(node));
node = node.value['rdf:Description'];
}
Object.assign(tag.value, parseNodeChildrenAsTags(node.value));
tag.description = getDescription(tag.value, name);
return tag;
}
function isCompactStructure(node) {
return (Object.keys(node.value).length === 0)
&& (node.attributes['rdf:resource'] === undefined);
}
function parseNodeAsCompactStructure(node, name) {
const value = parseNodeAttributesAsTags(node.attributes);
return {
value,
attributes: {},
description: getDescription(value, name)
};
}
function isArray(node) {
return getArrayChild(node.value) !== undefined;
}
function getArrayChild(value) {
return value['rdf:Bag'] || value['rdf:Seq'] || value['rdf:Alt'];
}
function parseNodeAsArray(node, name) {
let items = getArrayChild(node.value).value['rdf:li'];
const attributes = parseNodeAttributes(node);
const value = [];
if (!Array.isArray(items)) {
items = [items];
}
items.forEach((item) => {
value.push(parseArrayValue(item));
});
return {
value,
attributes,
description: getDescription(value, name)
};
}
function parseArrayValue(item) {
if (hasNestedSimpleRdfDescription(item)) {
return parseNodeAsSimpleRdfDescription(item);
}
if (hasNestedArrayValue(item)) {
return parseNodeChildrenAsTags(item.value);
}
return {
value: item.value,
attributes: parseNodeAttributes(item),
description: getDescription(item.value)
};
}
function hasNestedArrayValue(node) {
return node.attributes['rdf:parseType'] === 'Resource';
}
function parseNodeAsSimpleValue(node, name) {
const value = getURIValue(node) || parseXMPObject(node.value);
return {
value,
attributes: parseNodeAttributes(node),
description: getDescription(value, name)
};
}
function getURIValue(node) {
return node.attributes && node.attributes['rdf:resource'];
}
export { XMPParser as default };