diff --git a/src/Metadata/Xmp/ImageRegion.php b/src/Metadata/Xmp/ImageRegion.php
index bf5ae93..04c9768 100644
--- a/src/Metadata/Xmp/ImageRegion.php
+++ b/src/Metadata/Xmp/ImageRegion.php
@@ -109,54 +109,60 @@ public function __construct($xpath = null, $node = null)
return;
}
- $this->id = self::getNodeValue($xpath, 'Iptc4xmpExt:rId', $node);
+ $this->id = self::getChildValueOrAttr($xpath, 'Iptc4xmpExt:rId', $node);
+
+ $xpathNames = "Iptc4xmpExt:Name/rdf:Alt/rdf:li";
$this->names = self::getNodeValues(
$xpath,
- 'Iptc4xmpExt:Name/rdf:Alt/rdf:li',
+ // Matches only children and grand-children, not deeper:
+ "./$xpathNames|./*/$xpathNames",
$node
);
+
$this->types = self::getEntityOrConceptValues(
$xpath,
- 'Iptc4xmpExt:rCtype',
+ './/Iptc4xmpExt:rCtype', // matches any descendant with this name
$node
);
$this->roles = self::getEntityOrConceptValues(
$xpath,
- 'Iptc4xmpExt:rRole',
+ './/Iptc4xmpExt:rRole', // matches any descendant with this name
$node
);
- $this->regionDefinitionId = self::getNodeValue(
+ $this->regionDefinitionId = self::getChildValueOrAttr(
$xpath,
'FramerightIdc:RegionDefinitionId',
$node
);
- $this->regionName = self::getNodeValue(
+ $this->regionName = self::getChildValueOrAttr(
$xpath,
'FramerightIdc:RegionName',
$node
);
- $xpathToRb = 'Iptc4xmpExt:RegionBoundary';
-
- foreach ([
- 'rbShape',
- 'rbUnit',
- 'rbH',
- 'rbW',
- 'rbRx',
- ] as $property) {
- $this->$property = self::getNodeValue(
- $xpath,
- "$xpathToRb/Iptc4xmpExt:$property",
- $node
- );
+ $xpathToRb = './/Iptc4xmpExt:RegionBoundary'; // matches any descendant with this name
+ $rbNode = $xpath->query($xpathToRb, $node)->item(0);
+ if ($rbNode) {
+ foreach ([
+ 'rbShape',
+ 'rbUnit',
+ 'rbH',
+ 'rbW',
+ 'rbRx',
+ ] as $property) {
+ $this->$property = self::getChildValueOrAttr(
+ $xpath,
+ "Iptc4xmpExt:$property",
+ $rbNode
+ );
+ }
}
$this->rbXY = new Point(
- self::getNodeValue($xpath, "$xpathToRb/Iptc4xmpExt:rbX", $node),
- self::getNodeValue($xpath, "$xpathToRb/Iptc4xmpExt:rbY", $node)
+ self::getChildValueOrAttr($xpath, "Iptc4xmpExt:rbX", $rbNode),
+ self::getChildValueOrAttr($xpath, "Iptc4xmpExt:rbY", $rbNode)
);
$verticesNodes = $xpath->query(
@@ -167,8 +173,8 @@ public function __construct($xpath = null, $node = null)
$this->rbVertices = [];
foreach ($verticesNodes as $verticesNode) {
$point = new Point(
- self::getNodeValue($xpath, "Iptc4xmpExt:rbX", $verticesNode),
- self::getNodeValue($xpath, "Iptc4xmpExt:rbY", $verticesNode)
+ self::getChildValueOrAttr($xpath, "Iptc4xmpExt:rbX", $verticesNode),
+ self::getChildValueOrAttr($xpath, "Iptc4xmpExt:rbY", $verticesNode)
);
array_push($this->rbVertices, $point);
}
@@ -203,8 +209,10 @@ public function matchesRoleFilter($filter) {
}
/**
+ * Get the value of the first node matching the given XPath expression.
+ *
* @param \DOMXPath $xpath
- * @param string $expression XPath expression leading to one single node.
+ * @param string $expression XPath expression.
* @param \DOMNode $contextNode
*
* @return string|null
@@ -214,6 +222,47 @@ private static function getNodeValue($xpath, $expression, $contextNode) {
return $node ? $node->nodeValue : null;
}
+ /**
+ * Attempts to find a value in the following order:
+ * 1. First direct child node having the given name.
+ * 2. Context node's attribute with this name.
+ * 3. First direct child node's attribute with this name.
+ *
+ * @param \DOMXPath $xpath
+ * @param string $nodeOrAttrName
+ * @param \DOMNode $contextNode
+ *
+ * @return string|null
+ */
+ private static function getChildValueOrAttr($xpath, $nodeOrAttrName, $contextNode) {
+ $childValue = self::getNodeValue($xpath, $nodeOrAttrName, $contextNode);
+ if ($childValue) {
+ return $childValue;
+ }
+
+ $attrNameWithoutNamespace = preg_replace('/^.*:/', '', $nodeOrAttrName);
+ $contextNodeAttrs = $contextNode->attributes;
+ if ($contextNodeAttrs) {
+ $contextNodeAttr = $contextNodeAttrs->getNamedItem($attrNameWithoutNamespace);
+ if ($contextNodeAttr) {
+ return $contextNodeAttr->nodeValue;
+ }
+ }
+
+ $childNodeWithAttr = $xpath->query("*[@$nodeOrAttrName]", $contextNode)->item(0);
+ if (!$childNodeWithAttr) {
+ return null;
+ }
+
+ $childNodeAttrs = $childNodeWithAttr->attributes;
+ if (!$childNodeAttrs) {
+ return null;
+ }
+
+ $childNodeAttr = $childNodeAttrs->getNamedItem($attrNameWithoutNamespace);
+ return $childNodeAttr->nodeValue;
+ }
+
/**
* @param \DOMXPath $xpath
* @param string $expression XPath expression leading to several nodes.
diff --git a/tests/Fixtures/README.md b/tests/Fixtures/README.md
index e14078e..39cf6d4 100644
--- a/tests/Fixtures/README.md
+++ b/tests/Fixtures/README.md
@@ -2,5 +2,7 @@
## Image sources
-* [`IPTC-PhotometadataRef-Std2021.1.jpg`](https://iptc.org/std/photometadata/examples/IPTC-PhotometadataRef-Std2021.1.jpg),
+- [`IPTC-PhotometadataRef-Std2021.1.jpg`](https://iptc.org/std/photometadata/examples/IPTC-PhotometadataRef-Std2021.1.jpg),
open standard
+- [`skaterphotoshop.jpg`](skaterphotoshop.jpg), authored by
+ [Marina Ekroos](http://marinaekroos.com/)
diff --git a/tests/Fixtures/skaterphotoshop.jpg b/tests/Fixtures/skaterphotoshop.jpg
new file mode 100644
index 0000000..ad26b81
Binary files /dev/null and b/tests/Fixtures/skaterphotoshop.jpg differ
diff --git a/tests/Metadata/XmpTest.php b/tests/Metadata/XmpTest.php
index a992436..58ac971 100644
--- a/tests/Metadata/XmpTest.php
+++ b/tests/Metadata/XmpTest.php
@@ -378,7 +378,6 @@ public function testGetImageRegionFromFramerightImage()
$jpeg = JPEG::fromFile(
__DIR__ . '/../Fixtures/frameright.jpg');
-
$xmp = $jpeg->getXmp();
$expectedRegion = new ImageRegion();
@@ -400,7 +399,59 @@ public function testGetImageRegionFromFramerightImage()
$this->assertEquals([
$expectedRegion,
], $xmp->getImageRegions());
+ }
+ /**
+ * @covers ::getImageRegions
+ *
+ * When opening an image with regions in Photoshop and saving it again, the
+ * RDF/XML structure might change e.g. from
+ *
+ *
+ * rectangle
+ * relative
+ * 0.6663040522164808
+ * 1
+ * 0.25564318738101716
+ * 0
+ *
+ *
+ * to
+ *
+ *
+ *
+ * This test validates that we can parse what's generated by Photoshop.
+ */
+ public function testGetImageRegionFromImageResavedWithPhotoshop()
+ {
+ $jpeg = JPEG::fromFile(
+ __DIR__ . '/../Fixtures/skaterphotoshop.jpg');
+
+ $xmp = $jpeg->getXmp();
+
+ $expectedFirstRegion = new ImageRegion();
+ $expectedFirstRegion->regionDefinitionId = '6a3d4ec1-7c0c-58db-9645-6185a80efbcd';
+ $expectedFirstRegion->regionName = '1:1 Square';
+ $expectedFirstRegion->id = 'crop-cb6bbb93-539d-401b-bb57-341f461ab2ab';
+ $expectedFirstRegion->names = null;
+ $expectedFirstRegion->types = null;
+ $expectedFirstRegion->roles = [
+ 'http://cv.iptc.org/newscodes/imageregionrole/cropping',
+ ];
+ $expectedFirstRegion->rbShape = 'rectangle';
+ $expectedFirstRegion->rbUnit = 'relative';
+ $expectedFirstRegion->rbXY = new Point(0.25564318738101716, 0);
+ $expectedFirstRegion->rbRx = null;
+ $expectedFirstRegion->rbH = '1';
+ $expectedFirstRegion->rbW = '0.6663040522164808';
+
+ $this->assertEquals(
+ $expectedFirstRegion,
+ $xmp->getImageRegions()[0]
+ );
}
/**