UNPKG

@sitemark/exifr

Version:

📑 The fastest and most versatile JavaScript EXIF reading library.

474 lines (380 loc) • 12.3 kB
'use strict'; var domParser = require('./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$1 = new Parser(); const doc = domParser$1.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']; } module.exports = XMPParser;