@syncfusion/ej2-pdf
Version:
Feature-rich JavaScript PDF library with built-in support for loading and manipulating PDF document.
621 lines (620 loc) • 22.2 kB
JavaScript
import { PdfXmpMetadata } from './pdf-xmp-metadata';
import { PdfCustomSchema } from './pdf-custom-schema';
import { _bytesToString } from '../utils';
/**
* Internal XML reader for parsing XMP metadata from PDF streams.
* Reads RDF/XML format and reconstructs PdfXmpMetadata with all schemas.
*
* @private
*/
var _XmlReader = /** @class */ (function () {
function _XmlReader() {
}
/**
* Loads and parses XML data from a byte array or string.
*
* @private
* @param {Uint8Array | string} data The XML data to parse.
* @returns {void}
*/
_XmlReader.prototype._load = function (data) {
var pdfString;
if (data instanceof Uint8Array) {
pdfString = _bytesToString(data);
}
else {
pdfString = data;
}
var start = pdfString.lastIndexOf('<?xpacket begin');
if (start === -1) {
throw new Error('XMP metadata not found in PDF');
}
var end = pdfString.indexOf('<?xpacket end=', start);
if (end === -1) {
throw new Error('XMP metadata not found in PDF');
}
var xmlString = pdfString.substring(start, pdfString.indexOf('?>', end) + 2);
var parser = new DOMParser();
this._xmlDoc = parser.parseFromString(xmlString, 'application/xml');
this._validate(this._xmlDoc);
};
/**
* Validates the parsed XML document for errors.
*
* @private
* @param {Document} doc The XML document to validate.
* @returns {void}
*/
_XmlReader.prototype._validate = function (doc) {
var parserError = doc.querySelector('parsererror');
if (parserError) {
var errorText = parserError.textContent || 'Unknown parse error';
throw new Error('Invalid XMP XML: ' + errorText);
}
};
/**
* Parses the loaded XML document into a PdfXmpMetadata object.
*
* @private
* @returns {PdfXmpMetadata} The parsed metadata object.
*/
_XmlReader.prototype._parseXmp = function () {
this._xmp = new PdfXmpMetadata();
var rdfRoot = this._getRdfRoot();
var descriptions = rdfRoot.getElementsByTagNameNS('http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'Description');
for (var i = 0; i < descriptions.length; i++) {
this._parseSchemaNode(descriptions.item(i));
}
return this._xmp;
};
/**
* Locates the rdf:RDF root element in the XML document.
*
* @private
* @returns {Element} The rdf:RDF element.
*/
_XmlReader.prototype._getRdfRoot = function () {
if (!this._xmlDoc) {
throw new Error('XML document not loaded');
}
var rdfElements = this._xmlDoc.getElementsByTagNameNS('http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'RDF');
if (rdfElements.length === 0) {
throw new Error('rdf:RDF element not found in XMP metadata');
}
return rdfElements[0];
};
/**
* Parses a single rdf:Description node and routes to appropriate schema parser.
*
* @private
* @param {Element} node The rdf:Description element.
* @returns {void}
*/
_XmlReader.prototype._parseSchemaNode = function (node) {
var attributes = node.attributes;
for (var i = 0; i < attributes.length; i++) {
var attr = attributes.item(i);
if (attr.name.startsWith('xmlns:')) {
var prefix = attr.name.substring(6);
var namespaceUri = attr.value;
this._mapSchema(prefix, namespaceUri, node);
}
}
};
/**
* Routes schema parsing based on namespace URI.
*
* @private
* @param {string} prefix The namespace prefix.
* @param {string} ns The namespace URI.
* @param {Element} node The rdf:Description element.
* @returns {void}
*/
_XmlReader.prototype._mapSchema = function (prefix, ns, node) {
switch (ns) {
case 'http://purl.org/dc/elements/1.1/':
this._parseDublinCore(node);
break;
case 'http://ns.adobe.com/xap/1.0/':
this._parseBasic(node);
break;
case 'http://ns.adobe.com/pdf/1.3/':
this._parsePdf(node);
break;
case 'http://ns.adobe.com/xap/1.0/t/pg/':
this._parsePagedText(node);
break;
case 'http://ns.adobe.com/xap/1.0/rights/':
this._parseRights(node);
break;
case 'http://ns.adobe.com/xap/1.0/bj/':
this._parseJobTicket(node);
break;
case 'http://ns.adobe.com/pdfx/1.3/':
this._parseInternalCustom(prefix, ns, node);
break;
default:
this._parseCustom(prefix, ns, node);
break;
}
};
/**
* Parses Basic XMP schema properties.
*
* @private
* @param {Element} node The rdf:Description element.
* @returns {void}
*/
_XmlReader.prototype._parseBasic = function (node) {
var schema = this._xmp.basicSchema; //eslint-disable-line
var creatorTool = this._getValue(node, 'CreatorTool');
if (creatorTool) {
schema.creatorTool = creatorTool;
}
var label = this._getValue(node, 'Label');
if (label) {
schema.label = label;
}
var nickname = this._getValue(node, 'Nickname');
if (nickname) {
schema.nickname = nickname;
}
var baseUrl = this._getValue(node, 'BaseURL');
if (baseUrl) {
schema.baseUrl = baseUrl;
}
var createDate = this._getDate(node, 'CreateDate');
if (createDate) {
schema.createDate = createDate;
}
var modifyDate = this._getDate(node, 'ModifyDate');
if (modifyDate) {
schema.modifyDate = modifyDate;
}
var metadataDate = this._getDate(node, 'MetadataDate');
if (metadataDate) {
schema.metadataDate = metadataDate;
}
var advisory = this._getArray(node, 'Advisory');
if (advisory.length > 0) {
schema.advisory = advisory;
}
var identifier = this._getArray(node, 'Identifier');
if (identifier.length > 0) {
schema.identifier = identifier;
}
var thumbnails = this._getThumbnails(node, 'Thumbnails');
if (thumbnails.length > 0) {
schema.thumbnails = thumbnails;
}
var rating = this._getArray(node, 'Rating');
if (rating.length > 0) {
schema.rating = rating.map(function (r) { return parseFloat(r); });
}
};
/**
* Parses Dublin Core schema properties.
*
* @private
* @param {Element} node The rdf:Description element.
* @returns {void}
*/
_XmlReader.prototype._parseDublinCore = function (node) {
var schema = this._xmp.dublinCoreSchema;
var contributor = this._getArray(node, 'contributor');
if (contributor.length > 0) {
schema.contributor = contributor;
}
var creator = this._getArray(node, 'creator');
if (creator.length > 0) {
schema.creator = creator;
}
var date = this._getArray(node, 'date');
if (date.length > 0) {
schema.date = date;
}
var publisher = this._getArray(node, 'publisher');
if (publisher.length > 0) {
schema.publisher = publisher;
}
var relation = this._getArray(node, 'relation');
if (relation.length > 0) {
schema.relation = relation;
}
var subject = this._getArray(node, 'subject');
if (subject.length > 0) {
schema.subject = subject;
}
var type = this._getArray(node, 'type');
if (type.length > 0) {
schema.type = type;
}
var title = this._getLangArray(node, 'title');
if (Object.keys(title).length > 0) {
schema.title = title;
}
var description = this._getLangArray(node, 'description');
if (Object.keys(description).length > 0) {
schema.description = description;
}
var rights = this._getLangArray(node, 'rights');
if (Object.keys(rights).length > 0) {
schema.rights = rights;
}
var coverage = this._getValue(node, 'coverage');
if (coverage) {
schema.coverage = coverage;
}
var identifier = this._getValue(node, 'identifier');
if (identifier) {
schema.identifier = identifier;
}
var source = this._getValue(node, 'source');
if (source) {
schema.source = source;
}
var format = this._getValue(node, 'format');
if (format) {
schema.format = format;
}
};
/**
* Parses PDF schema properties.
*
* @private
* @param {Element} node The rdf:Description element.
* @returns {void}
*/
_XmlReader.prototype._parsePdf = function (node) {
var schema = this._xmp.pdfSchema;
var keywords = this._getValue(node, 'Keywords');
if (keywords) {
schema.keywords = keywords;
}
var producer = this._getValue(node, 'Producer');
if (producer) {
schema.producer = producer;
}
var pdfVersion = this._getValue(node, 'PDFVersion');
if (pdfVersion) {
schema.pdfVersion = pdfVersion;
}
};
/**
* Parses Paged-Text schema properties.
*
* @private
* @param {Element} node The rdf:Description element.
* @returns {void}
*/
_XmlReader.prototype._parsePagedText = function (node) {
var schema = this._xmp.pagedTextSchema;
var pageCount = this._getValue(node, 'NPages');
if (pageCount) {
schema.pageCount = parseInt(pageCount, 10);
}
var maxPageSizeElement = this._findChildElement(node, 'MaxPageSize');
if (maxPageSizeElement) {
var descriptionElement = this._findDirectChild(maxPageSizeElement, 'Description');
if (descriptionElement) {
var w = this._getValue(descriptionElement, 'w');
var h = this._getValue(descriptionElement, 'h');
var unit = this._getValue(descriptionElement, 'unit');
if (w && h) {
var dimensions = {
width: parseFloat(w),
height: parseFloat(h)
};
if (unit) {
dimensions.unit = unit;
}
schema.maxPageSize = dimensions;
}
}
}
var fonts = this._getArray(node, 'Fonts');
if (fonts.length > 0) {
schema.fonts = fonts;
}
var plateNames = this._getArray(node, 'PlateNames');
if (plateNames.length > 0) {
schema.plateNames = plateNames;
}
var colorants = this._getArray(node, 'Colorants');
if (colorants.length > 0) {
schema.colorants = colorants;
}
};
/**
* Parses Rights Management schema properties.
*
* @private
* @param {Element} node The rdf:Description element.
* @returns {void}
*/
_XmlReader.prototype._parseRights = function (node) {
var schema = this._xmp.rightsManagementSchema;
var certificateUrl = this._getValue(node, 'Certificate');
if (certificateUrl) {
schema.certificateUrl = certificateUrl;
}
var webStatement = this._getValue(node, 'WebStatement');
if (webStatement) {
schema.webStatement = webStatement;
}
var marked = this._getValue(node, 'Marked');
if (marked) {
schema.isMarked = marked === 'True';
}
var owners = this._getArray(node, 'Owner');
if (owners.length > 0) {
schema.owners = owners;
}
var usageTerms = this._getLangArray(node, 'UsageTerms');
if (Object.keys(usageTerms).length > 0) {
schema._setProperty('xmpRights:UsageTerms', usageTerms); //eslint-disable-line
}
};
/**
* Parses Basic Job Ticket schema properties.
*
* @private
* @param {Element} node The rdf:Description element.
* @returns {void}
*/
_XmlReader.prototype._parseJobTicket = function (node) {
var schema = this._xmp.basicJobTicketSchema;
var jobRef = this._getArray(node, 'JobRef');
if (jobRef.length > 0) {
schema.jobRef = jobRef;
}
};
/**
* Parses custom schema properties.
*
* @private
* @param {string} prefix The namespace prefix.
* @param {string} ns The namespace URI.
* @param {Element} node The rdf:Description element.
* @returns {void}
*/
_XmlReader.prototype._parseInternalCustom = function (prefix, ns, node) {
var customSchema = new PdfCustomSchema(this._xmp, prefix, ns);
var children = node.querySelectorAll('*');
for (var i = 0; i < children.length; i++) {
var child = children.item(i);
if (child.prefix === prefix || child.localName.startsWith(prefix + ':')) {
var key = child.localName.replace(prefix + ':', '');
var value = child.textContent || '';
customSchema.customData.set(key, value);
}
}
if (customSchema.customData.size > 0) {
this._xmp._customSchema = customSchema;
}
};
/**
* Parses custom schema properties.
*
* @private
* @param {string} prefix The namespace prefix.
* @param {string} ns The namespace URI.
* @param {Element} node The rdf:Description element.
* @returns {void}
*/
_XmlReader.prototype._parseCustom = function (prefix, ns, node) {
var customSchema = new PdfCustomSchema(this._xmp, prefix, ns);
var children = node.querySelectorAll('*');
for (var i = 0; i < children.length; i++) {
var child = children.item(i);
if (child.prefix === prefix || child.localName.startsWith(prefix + ':')) {
var key = child.localName.replace(prefix + ':', '');
var value = child.textContent || '';
customSchema.customData.set(key, value);
}
}
};
/**
* Gets a single text value from a child element.
*
* @private
* @param {Element} node The parent element.
* @param {string} tag The local name of the child element.
* @returns {string} The text content or undefined.
*/
_XmlReader.prototype._getValue = function (node, tag) {
var child = this._findChildElement(node, tag);
if (child) {
var textContent = child.textContent ? child.textContent.trim() : '';
if (textContent) {
return textContent;
}
}
var attr = node.getAttribute(this._getAttributeName(node, tag));
if (attr) {
return attr;
}
return undefined;
};
/**
* Gets the full attribute name for a tag, checking all possible namespace prefixes.
*
* @private
* @param {Element} node The element to check.
* @param {string} localName The local name to search for.
* @returns {string} The attribute name or null.
*/
_XmlReader.prototype._getAttributeName = function (node, localName) {
var attributes = node.attributes;
for (var i = 0; i < attributes.length; i++) {
var attr = attributes.item(i);
if (attr.localName === localName) {
return attr.name;
}
}
return null;
};
/**
* Gets a Date value from a child element.
*
* @private
* @param {Element} node The parent element.
* @param {string} tag The local name of the child element.
* @returns {Date} The parsed date or undefined.
*/
_XmlReader.prototype._getDate = function (node, tag) {
var value = this._getValue(node, tag);
if (value) {
var date = new Date(value);
if (!isNaN(date.getTime())) {
return date;
}
}
return undefined;
};
/**
* Gets an array of string values from an rdf:Bag or rdf:Seq container.
*
* @private
* @param {Element} node The parent element.
* @param {string} tag The local name of the property element.
* @returns {string[]} Array of string values.
*/
_XmlReader.prototype._getArray = function (node, tag) {
var result = [];
var child = this._findChildElement(node, tag);
if (child) {
var bag = this._findDirectChild(child, 'Bag') || this._findDirectChild(child, 'Seq');
if (bag) {
var items = this._findDirectChildren(bag, 'li');
for (var _i = 0, items_1 = items; _i < items_1.length; _i++) {
var item = items_1[_i];
var text = item.textContent;
if (text) {
result.push(text.trim());
}
}
}
}
return result;
};
/**
* Gets a multilingual array (language map) from an rdf:Alt container.
*
* @private
* @param {Element} node The parent element.
* @param {string} tag The local name of the property element.
* @returns {PdfXmpLangArray} Language map object.
*/
_XmlReader.prototype._getLangArray = function (node, tag) {
var result = {};
var child = this._findChildElement(node, tag);
if (child) {
var alt = this._findDirectChild(child, 'Alt');
if (alt) {
var items = this._findDirectChildren(alt, 'li');
for (var _i = 0, items_2 = items; _i < items_2.length; _i++) {
var item = items_2[_i];
var lang = item.getAttribute('xml:lang');
var text = item.textContent;
if (lang && text && lang !== '__proto__' && lang !== 'constructor' && lang !== 'prototype') {
result[String(lang)] = text.trim();
}
}
}
}
return result;
};
/**
* Gets an array of structured thumbnail objects from an rdf:Bag container.
*
* @private
* @param {Element} node The parent element.
* @param {string} tag The local name of the property element.
* @returns {PdfXmpThumbnail[]} Array of thumbnail structures.
*/
_XmlReader.prototype._getThumbnails = function (node, tag) {
var result = [];
var child = this._findChildElement(node, tag);
if (child) {
var bag = this._findDirectChild(child, 'Bag');
if (bag) {
var items = this._findDirectChildren(bag, 'li');
for (var _i = 0, items_3 = items; _i < items_3.length; _i++) {
var item = items_3[_i];
var width = this._getValue(item, 'width') || this._getValue(item, 'Width');
var height = this._getValue(item, 'height') || this._getValue(item, 'Height');
var format = this._getValue(item, 'format') || this._getValue(item, 'Format');
var image = this._getValue(item, 'image') || this._getValue(item, 'Image');
if (width && height && format && image) {
var thumbnail = {
width: parseInt(width, 10),
height: parseInt(height, 10),
format: format,
image: image
};
result.push(thumbnail);
}
}
}
}
return result;
};
/**
* Finds a child element by local name (case-insensitive for namespaces).
*
* @private
* @param {Element} node The parent element.
* @param {string} localName The local name to search for.
* @returns {Element} The found element or null.
*/
_XmlReader.prototype._findChildElement = function (node, localName) {
var children = node.children;
for (var i = 0; i < children.length; i++) {
var child = children.item(i);
if (child.localName === localName) {
return child;
}
}
return null;
};
/**
* Finds a direct child element by local name.
*
* @private
* @param {Element} node The parent element.
* @param {string} localName The local name to search for.
* @returns {Element} The found element or null.
*/
_XmlReader.prototype._findDirectChild = function (node, localName) {
var children = node.children;
for (var i = 0; i < children.length; i++) {
var child = children.item(i);
if (child.localName === localName) {
return child;
}
}
return null;
};
/**
* Finds all direct child elements with the specified local name.
*
* @private
* @param {Element} node The parent element.
* @param {string} localName The local name to search for.
* @returns {Element[]} Array of matching elements.
*/
_XmlReader.prototype._findDirectChildren = function (node, localName) {
var result = [];
var children = node.children;
for (var i = 0; i < children.length; i++) {
var child = children.item(i);
if (child.localName === localName) {
result.push(child);
}
}
return result;
};
return _XmlReader;
}());
export { _XmlReader };