Skip to content

Commit

Permalink
fix: Support parsing Photoshop-saved images
Browse files Browse the repository at this point in the history
  • Loading branch information
lourot committed Feb 1, 2024
1 parent a87b531 commit 8ac64a7
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 27 deletions.
99 changes: 74 additions & 25 deletions src/Metadata/Xmp/ImageRegion.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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);
}
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion tests/Fixtures/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
Binary file added tests/Fixtures/skaterphotoshop.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 52 additions & 1 deletion tests/Metadata/XmpTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,6 @@ public function testGetImageRegionFromFramerightImage()
$jpeg = JPEG::fromFile(
__DIR__ . '/../Fixtures/frameright.jpg');


$xmp = $jpeg->getXmp();

$expectedRegion = new ImageRegion();
Expand All @@ -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
*
* <Iptc4xmpExt:RegionBoundary rdf:parseType="Resource">
* <Iptc4xmpExt:rbShape>rectangle</Iptc4xmpExt:rbShape>
* <Iptc4xmpExt:rbUnit>relative</Iptc4xmpExt:rbUnit>
* <Iptc4xmpExt:rbW>0.6663040522164808</Iptc4xmpExt:rbW>
* <Iptc4xmpExt:rbH>1</Iptc4xmpExt:rbH>
* <Iptc4xmpExt:rbX>0.25564318738101716</Iptc4xmpExt:rbX>
* <Iptc4xmpExt:rbY>0</Iptc4xmpExt:rbY>
* </Iptc4xmpExt:RegionBoundary>
*
* to
*
* <Iptc4xmpExt:RegionBoundary Iptc4xmpExt:rbShape="rectangle" Iptc4xmpExt:rbUnit="relative"
* Iptc4xmpExt:rbW="0.6663040522164808" Iptc4xmpExt:rbH="1"
* Iptc4xmpExt:rbX="0.25564318738101716"
* Iptc4xmpExt:rbY="0" />
*
* 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]
);
}

/**
Expand Down

0 comments on commit 8ac64a7

Please sign in to comment.