Skip to content

Commit

Permalink
Update use for namespaces in XmpDocument
Browse files Browse the repository at this point in the history
  • Loading branch information
diderich committed Jul 11, 2022
1 parent 597f342 commit 7f3e625
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 61 deletions.
9 changes: 4 additions & 5 deletions src/Metadata/Xmp.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ class Xmp {
const CREATED_DATETIME = "xmp:CreatedDate"; /** Read only: Date and time YYYY-MM-DD HH:MM:SS+HH:MM */
const CREDIT = "photoshop:Credit"; /** Text: Credit Line */
const GENRE = "Iptc4xmpCore:IntellectualGenre"; /** Text: Genre */
const HEADLINE = "dc:title"; /** Text: Headline */
const INSTRUCTIONS = "photoshop:Instructions"; /** Text: Instructions */
const KEYWORDS = "dc:subject"; /** Bag: Keywords */
const LOCATION = "Iptc4xmpCore:Location"; /** Text: Location */
Expand Down Expand Up @@ -90,7 +89,10 @@ class Xmp {
*/
public static function decode(string $segment): XmpDocument|false
{
if(empty($segment)) return new XmpDocument(false);
// Encode XMP data as XML / DOMDocument
$dom = new \DOMDocument('1.0', 'UTF-8');

if(empty($segment)) return new XmpDocument($dom);

// Extract data from XMP block
$xmp_block = substr($segment, Xmp::XMP_HEADER_LEN);
Expand All @@ -100,9 +102,6 @@ public static function decode(string $segment): XmpDocument|false
if($xmp_end === false) throw new Exception(_('XMP metadata end tag not found'), Exception::DATA_FORMAT_ERROR);
$xmp_data =substr($xmp_block, $xmp_start, $xmp_end - $xmp_start + 12);

// Encode XMP data as XML / DOMDocument
$dom = new \DOMDocument('1.0', 'UTF-8');

// -- Disable warning messages from loadXML only
$old_error_reporting = error_reporting(error_reporting() & ~E_WARNING);
$xml_status = $dom->loadXML($xmp_data);
Expand Down
131 changes: 75 additions & 56 deletions src/Metadata/XmpDocument.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,13 @@ class XmpDocument {
/**
* Constructor
*
* @param string $version XML version
* @param string $encoding Data encoding, typically UTF-8
* @param DOMDocument DOM of XMP data
*/
public function __construct(\DOMDocument|false $dom)
public function __construct(\DOMDocument $dom)
{
$this->nsPriorityAry = array(self::NS_IPTC4XMPCORE, self::NS_DC, self::NS_AUX, self::NS_XMP, self::NS_PHOTOSHOP,
self::NS_PHOTOMECHANIC);
// All namespaces supported by default, others may be added before use using 'setXmpNamespace'
$all_ns = array('Iptc4xmpCore' => 'http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/',
'aux' => 'http://ns.adobe.com/exif/1.0/aux/',
'dc' => 'http://purl.org/dc/elements/1.1/',
Expand All @@ -56,8 +55,8 @@ public function __construct(\DOMDocument|false $dom)
'stRef' => 'http://ns.adobe.com/xap/1.0/sType/ResourceRef#',
'xmpMM' => 'http://ns.adobe.com/xap/1.0/mm/',
'xmpRights' => 'http://ns.adobe.com/xap/1.0/rights/');
$this->dom = self::validateXmpDocument($dom, $all_ns);
$this->dom = $dom;
$this->validateXmpDocument($all_ns);
}

/**
Expand Down Expand Up @@ -190,6 +189,39 @@ public function getXmpBag(string $name): array|false
/**
* SET XMP METADATA
*/

/**
* Define a new / ensure the existence of a given namespace
*
* @param string $ns Namespace identifier
* @param string $uri Namespace URI
*/
public function setXmpNamespace(string $ns, string $uri): void
{
$descs = self::getXmpAllNodeByName($this->dom, Xmp::DESCRIPTION);

// Search for an rdf:Description element with the specified namespace definition as attribute
$found = false;
if($descs !== false) {
foreach($descs as $desc) {
if($desc->hasAttribute("xmlns:$ns")) {
$found = true;
if(!$desc->hasAttribute('rdf:about')) $desc->setAttribute('rdf:about', '');
}
}
}

// Create a new rdf:Description element under rdf:RDF including the namespace definition
if(!$found) {
$root = self::getXmpFirstNodeByName($this->dom, 'rdf:RDF');
if($root === false)
throw new Exception(_('Internal error finding')." 'rdf:RDF' "._('element'), Exception::INTERNAL_ERROR);
$elt = $this->dom->createElement(Xmp::DESCRIPTION);
$desc = $root->appendChild($elt);
$desc->setAttribute("xmlns:$ns", $uri);
$desc->setAttribute('rdf:about', '');
}
}

/**
* Set Attribute (removing identically named nodes) and updating values in other namespaces
Expand Down Expand Up @@ -302,19 +334,23 @@ public function setXmpBag(string $name, array|false $data): void
*/
public function updateHistory(string $software): void
{
$history = self::getXmpFirstNodeByName($this->dom, Xmp::EDIT_HISTORY);
// Serch for a rdf:Description element that referneces to the xmpMM namespace in which histories are saves
$desc = self::getXmpFirstNodeByName($this->dom, Xmp::DESCRIPTION);
if($desc === false)
throw new Exception(_('Internal error finding')." 'rdf:Description' "._('including')." 'xmpMM' ".
_('as namespaces'), Exception::INTERNAL_ERROR);
$history = self::getXmpFirstNodeByName($desc, Xmp::EDIT_HISTORY);

// Create new history entry, if none exists
if($history === false) {
$desc = self::getXmpFirstNodeByName($this->dom, Xmp::DESCRIPTION);
$new_child = $this->dom->createElement('xmpMM:History');
$history = $desc->appendChild($new_child);
}

// Create new sequency, if none exists
$seq = self::getXmpFirstNodeByName($history, 'rdf:Seq');
If($seq === false) {
$new_child = $this->dom->createElement(Xmp::EDIT_HISTORY);
$new_child = $this->dom->createElement('rdf:Seq');
$seq = $history->appendChild($new_child);
}

Expand Down Expand Up @@ -640,52 +676,35 @@ protected static function getXmpAllNodeByName(\DOMDocument|\DOMElement|\DOMNode
* the referenced namespaces. If no DOM document exists, a new one is created from scratch
*
* @access protected
* @param \DOMDocument|false $dom Read DOM document representing XMP data
* @return \DOMDocument Valid XMP DOM document
* @param array $ns_ary Array of all namespaces that must exist
*/
protected static function validateXmpDocument(\DOMDocument|false $dom, array $ns_priority_ary): \DOMDocument
{
// Create new DOM, if non exists
if($dom === false) {
$dom = new \DOMDocument('1.0', 'UTF-8');
$elt = $dom->createElement('x:xmpmeta');
$root = $dom->appendChild($elt);
$root->setAttribute('xmlns:x', 'adobe:ns:meta/');
$root->setAttribute('x:xmptk', 'XMP Core 5.6.0');
}

// Check that DOM has a rdf:RDF node
$root = self::getXmpFirstNodeByName($dom, 'rdf:RDF');
if($root === false) {
$elt = $dom->createElement('rdf:RDF');
$root = $dom->appendChild($elt);
}

// Ensure that root DOM defines its namespace
if(!$root->hasAttribute('xmlns:rdf'))
$root->setAttribute('xmlns:rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');

// For each namespace not found, add a rdf:Document section xmlns:$ns="$uri"
$descs = self::getXmpAllNodeByName($root, Xmp::DESCRIPTION);
foreach($ns_priority_ary as $ns => $uri) {
$found = false;
if($descs !== false) {
foreach($descs as $desc) {
if($desc->hasAttribute("xmlns:$ns")) {
$found = true;
if(!$desc->hasAttribute('rdf:about')) $desc->setAttribute('rdf:about', '');
}
}
}
if(!$found) {
$elt = $dom->createElement(Xmp::DESCRIPTION);
$desc = $root->appendChild($elt);
$desc->setAttribute("xmlns:$ns", $uri);
$desc->setAttribute('rdf:about', '');
}
}

// Return DOM
return $dom;
}
protected function validateXmpDocument(array $ns_ary): void
{
// Find xmpmeta root node
$mroot = self::getXmpFirstNodeByName($this->dom, 'x:xmpmeta');
if($mroot === false) {
$elt = $this->dom->createElement('x:xmpmeta');
$mroot = $this->dom->appendChild($elt);
$mroot->setAttribute('xmlns:x', 'adobe:ns:meta/');
$mroot->setAttribute('x:xmptk', 'XMP Core 5.6.0');
}

// Check that DOM has a rdf:RDF node
$root = self::getXmpFirstNodeByName($this->dom, 'rdf:RDF');
if($root === false) {
$elt = $this->dom->createElementNS('http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'rdf:RDF');
$root = $mroot->appendChild($elt);
}

// For each namespace not found, add a rdf:Document section xmlns:$ns="$uri"
foreach($ns_ary as $ns => $uri) {
$this->setXmpNamespace($ns, $uri);
}

// Re-load to ensure namespaces are recognized
$status = $this->dom->loadXML($this->dom->saveXML());
if($status === false)
throw new Exception(_('Internal error during XML re-validation'), Exception::INTERNAL_ERROR);
echo str_replace("><", ">\n<", $this->dom->saveXML());
}
}

0 comments on commit 7f3e625

Please sign in to comment.