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] + ); } /**