Skip to content

Commit

Permalink
v1.1.1: Corrected bug decoding XMP data
Browse files Browse the repository at this point in the history
  • Loading branch information
diderich committed Jul 29, 2022
1 parent 59953b6 commit 14ce254
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 45 deletions.
4 changes: 4 additions & 0 deletions .gitignore
@@ -0,0 +1,4 @@
# Composer files
composer.lock


5 changes: 4 additions & 1 deletion CHANGELOG.md
@@ -1,10 +1,13 @@
# CHANGELOG.md

Current version: `v1.1.0`
Current version: `v1.1.1`

Notable changes to **Metadata** - A PHP class for reading and writing *Photo Metadata* from JPEG files in a transparent
way:

## v1.1.1 - 2022-07-29
Corrected bug decoding XMP data. Added display of image data (i.e., EXIF data) to the test example.

## v1.1.0 - 2022-07-15
Support for multi-lingual captions, that is, the CAPTION field, added. Not that using other languages than the default
`x-default`, although consistent with the IPTC standard, will not work with typical photo applications, like Photoshop
Expand Down
4 changes: 2 additions & 2 deletions README.md
Expand Up @@ -186,7 +186,7 @@ All class files are commended in a phpDocumenter (https://www.phpdoc.org/) compl
The metadata class can be installed using composer

```shell
composer require diderich/meta
composer require diderich/metadata
```

# Testing
Expand All @@ -205,7 +205,7 @@ of the JPEG file. Issus without accompanying JPEG data will be closed by the aut
The following limitations currently exist and are acknowledged as such:
* IPTC/APP13: Only Latin 1 and UTF-8 encoded data can be read. Other data formats will throw an exception.
* XMP/APP1: The classes Xmp and/or XmpDocument may nor recognize all poorly/incorrectly formatted XMP/APP1 data.
* XMP/APP1: If an data element TAG is updated in the namespace NS, then it will also be updated in the namespaces
* XMP/APP1: If a data element TAG is updated in the namespace NS, then it will also be updated in the namespaces
Iptc4xmpCore, dc, aux, xmp, photoshop, and photomechanic, if a data entry exists in those name spaces. Other name
spaces, for example, used by other applications, are not updated/synchronized. This may result in inconsistent data.
* EXIF/APP1: Although all data read is returned, only the data considered relevant is decoded. For example, thumbnails
Expand Down
4 changes: 2 additions & 2 deletions src/Metadata.php
Expand Up @@ -21,7 +21,7 @@

class Metadata {

const VERSION = '1.1.0';
const VERSION = '1.1.1';

/** Fielt types */
const TYPE_INVALID = 0;
Expand Down Expand Up @@ -1118,7 +1118,7 @@ protected function importExif(): void
$this->setRW(self::IMG_COLOR_SPACE_FMT, _('Wide Gamut RGB')); break;
case 0xfffe:
$this->setRW(self::IMG_COLOR_SPACE_FMT, _('ICC Profile')); break;
case 0xfffe:
case 0xffff:
$this->setRW(self::IMG_COLOR_SPACE_FMT, _('Uncalibrated')); break;

}
Expand Down
4 changes: 3 additions & 1 deletion src/Metadata/Xmp.php
Expand Up @@ -62,7 +62,9 @@ class Xmp {
const CAMERA_SERIAL = "aux:SerialNumber"; /** Text: Camera serial number */
const LENS_MODEL = "aux:Lens"; /** Text: Lens description */
const LENS_SERIAL = "aux:LensSerialNumber"; /** Text: Lens serial number */
const COLOR_SPACE = "photoshop:ICCProfile"; /** Text: Color space */
const COLOR_SPACE = "photoshop:ICCProfile"; /** Text: Color Profile */

/** Text: Color space */

// History specification
const EDIT_HISTORY = 'xmpMM:History'; /** Seq: ResourceEvebt */
Expand Down
49 changes: 33 additions & 16 deletions src/Metadata/XmpDocument.php
Expand Up @@ -140,7 +140,7 @@ public function isXmpBag(string $name): bool
* @return string|false First node value found, or false, if not found
* @throw \Holiday\Metadata\Exception
*/
public function getXmpText(string $name, string|false $lang = self::LANG_DEFAULT): string|false
public function getXmpText(string $name, string|false $lang = false): string|false
{
if(strpos($name, ':') === false)
throw new Exception(_('Node name without prefix found'), Exception::INVALID_FIELD_ID, $name);
Expand Down Expand Up @@ -168,12 +168,12 @@ public function getXmpText(string $name, string|false $lang = self::LANG_DEFAULT
/**
* Get rdf:Alt value
*
* @param string $name Node name, including prefix
* @param string $lang Language of entry
* @return array|false First node value found, or false, if not found
* @param string $name Node name, including prefix
* @param string|false $lang Language of entry
* @return array|false First node value found, or false, if not found
* @throw \Holiday\Metadata\Exception
*/
public function getXmpLangAlt(string $name, string $lang = self::LANG_DEFAULT): array|false
public function getXmpLangAlt(string $name, string|false $lang = false): array|false
{
if(strpos($name, ':') === false)
throw new Exception(_('Node name without prefix found'), Exception::INVALID_FIELD_ID, $name);
Expand Down Expand Up @@ -319,7 +319,7 @@ public function setXmpSeq(string $name, string|int|false $data): void
* @param string|false $lang Language of entry
* @throw \Holiday\Metadata\Exception
*/
public function setXmpAlt(string $name, string|int|false $data, string|false $lang = self::LANG_DEFAULT): void
public function setXmpAlt(string $name, string|int|false $data, string|false $lang = false): void
{
if(self::existXmpAttribute($this->dom, Xmp::DESCRIPTION, $name))
throw new Exception(_('Cannot set')." 'rdf:Alt' "._('node value if an attribute with the same name exists'),
Expand Down Expand Up @@ -478,7 +478,7 @@ public function updateHistory(string $software): void
* @return string|array|false Node value or array, if $lang === LANG_ALL
* @throw \Holiday\Metadata\Exception
*/
protected function getXmpTextNS(string $ns, string $name, string|false $lang = self::LANG_DEFAULT): string|array|false
protected function getXmpTextNS(string $ns, string $name, string|false $lang = false): string|array|false
{
// Search text as attribute of any rdf:Description node
$descs = self::getXmpAllNodeByName($this->dom, Xmp::DESCRIPTION, $ns);
Expand All @@ -504,8 +504,16 @@ protected function getXmpTextNS(string $ns, string $name, string|false $lang = s
$subchildren = self::getXmpAllNodeByName($child_alt, 'rdf:li');
if($subchildren === false) return false;
foreach($subchildren as $subchild) {
if($lang === false) return (string)$subchild->nodeValue;
if($lang === self::LANG_ALL) {
if($lang === false) {
if($subchild->hasAttribute('xml:lang')) {
if($subchild->getAttribute('xml:lang') === self::LANG_DEFAULT)
return (string)$subchild->nodeValue;
}
else {
return (string)$subchild->nodeValue;
}
}
elseif($lang === self::LANG_ALL) {
if($subchild->hasAttribute('xml:lang')) {
$lang_result[$subchild->getAttribute('xml:lang')] = (string)$subchild->nodeValue;
}
Expand All @@ -516,7 +524,13 @@ protected function getXmpTextNS(string $ns, string $name, string|false $lang = s
}
}
else {
if($subchild->getAttribute('xml:lang') === $lang) return (string)$subchild->nodeValue;
if($subchild->hasAttribute('xml:lang')) {
if($subchild->getAttribute('xml:lang') === $lang)
return (string)$subchild->nodeValue;
}
else {
return (string)$subchild->nodeValue;
}
}
}
}
Expand All @@ -527,13 +541,13 @@ protected function getXmpTextNS(string $ns, string $name, string|false $lang = s
* Get Attribute / Node / rdf:Seq / rdf:Alt value in specific name space
*
* @access protected
* @param string $ns Name space
* @param string $name Node name, without prefix
* @param string $lang Language to retrieve, or all
* @return array|false Array of nodes, indexed by language
* @param string $ns Name space
* @param string $name Node name, without prefix
* @param string|false $lang Language to retrieve, or all
* @return array|false Array of nodes, indexed by language
* @throw \Holiday\Metadata\Exception
*/
protected function getXmpLangAltNS(string $ns, string $name, string $lang): array|false
protected function getXmpLangAltNS(string $ns, string $name, string|false $lang): array|false
{
// Search text as attribute of any rdf:Description node
$descs = self::getXmpAllNodeByName($this->dom, Xmp::DESCRIPTION, $ns);
Expand Down Expand Up @@ -706,7 +720,7 @@ protected function setXmpTextNS(string $ns, string $name, string|int|false $data
* @throw \Holiday\Metadata\Exception
*/
protected function setXmpLiNS(string $tag, string $ns, string $name, array|string|int|false $data,
string|false $lang = self::LANG_DEFAULT, bool $update_only = false): void
string|false $lang = false, bool $update_only = false): void
{
// Find node
$name = "$ns:$name";
Expand Down Expand Up @@ -956,7 +970,10 @@ protected function validateXmpDocument(array $ns_ary): void
}

// Re-load to ensure namespaces are recognized
// -- Disable warning messages from loadXML only
$old_error_reporting = error_reporting(error_reporting() & ~E_WARNING);
$status = $this->dom->loadXML($this->dom->saveXML());
error_reporting($old_error_reporting);
if($status === false)
throw new Exception(_('Internal error during XML re-validation'), Exception::INTERNAL_ERROR);
}
Expand Down
59 changes: 36 additions & 23 deletions test/example.php
Expand Up @@ -25,16 +25,22 @@ function my__autoload(string $name): void
if(file_exists("../src/$name.php")) require_once("../src/$name.php");
}
spl_autoload_register('my__autoload');

use \Holiday\Metadata;

/*** EXAMPLE ***/
$testfiles_ary = array('img.example.jpg', 'img.mlexample.jpg');
$exif_data_ary = array(Metadata::IMG_CAMERA_MAKE => 'CAMERA MAKE', Metadata::IMG_CAMERA_MODEL => 'CAMERA MODEL',
Metadata::IMG_CAMERA_SERIAL => 'CAMERA SERIAL', Metadata::IMG_LENS_MODEL => 'LENS MODEL',
Metadata::IMG_COLOR_SPACE_FMT => 'COLOR SPACE', Metadata::IMG_ISO => 'ISO SETTING',
Metadata::IMG_APERTURE_FMT => 'APERTURE', Metadata::IMG_EXPOSURE_FMT => 'EXPOSURE',
Metadata::IMG_FOCAL_LENGTH_FMT => 'FOCAL LENGTH', Metadata::IMG_FLASH_FMT => 'FLASH USED',
Metadata::IMG_SIZE_FMT => 'IMAGE SIZE', Metadata::IMG_RESOLUTION_FMT => 'RESOLUTION',
Metadata::IMG_SOFTWARE => 'SOFTWARE');

/**
* Use of class Metadata
*/
$metadata = new \Holiday\Metadata();

$metadata = new Metadata();
foreach($testfiles_ary as $filename) {
echo "PROCESSING EXAMPLE IMAGE USING \\Holiday\\Metadata CLASS: $filename".PHP_EOL;
echo "---".PHP_EOL;
Expand All @@ -44,15 +50,15 @@ function my__autoload(string $name): void


// Read some of the metadata (assuming metadata is available)
$caption = $metadata->get(\Holiday\Metadata::CAPTION, lang: \Holiday\Metadata::LANG_DEFAULT);
$caption_ary = $metadata->get(\Holiday\Metadata::CAPTION, lang: \Holiday\Metadata::LANG_ALL);
$date_created = $metadata->get(\Holiday\Metadata::CREATED_DATETIME);
$credit = $metadata->get(\Holiday\Metadata::CREDIT);
$city = $metadata->get(\Holiday\Metadata::CITY);
$country = $metadata->get(\Holiday\Metadata::COUNTRY);
$keywords = $metadata->get(\Holiday\Metadata::KEYWORDS);
$people = $metadata->get(\Holiday\Metadata::PEOPLE);
$event = $metadata->get(\Holiday\Metadata::EVENT);
$caption = $metadata->get(Metadata::CAPTION, lang: Metadata::LANG_DEFAULT);
$caption_ary = $metadata->get(Metadata::CAPTION, lang: Metadata::LANG_ALL);
$date_created = $metadata->get(Metadata::CREATED_DATETIME);
$credit = $metadata->get(Metadata::CREDIT);
$city = $metadata->get(Metadata::CITY);
$country = $metadata->get(Metadata::COUNTRY);
$keywords = $metadata->get(Metadata::KEYWORDS);
$people = $metadata->get(Metadata::PEOPLE);
$event = $metadata->get(Metadata::EVENT);
if(!empty($caption_ary)) {
echo "CAPTION:".PHP_EOL;
foreach($caption_ary as $lang => $text) {
Expand All @@ -68,11 +74,18 @@ function my__autoload(string $name): void
if($people !== false) echo "PEOPLE : ".implode(', ', $people).PHP_EOL;
echo PHP_EOL;

echo "IMAGE DATA".PHP_EOL;
foreach($exif_data_ary as $field_id => $field_name) {
$data = $metadata->get($field_id);
if($data !== false) echo '- '.substr($field_name.' ', 0, 15).': '.$data.PHP_EOL;
}
echo PHP_EOL;

// Read metadata in a transparent way and extend tagged keywords to their respective fields
$metadata->read($filename, extend: true);
echo "EXTENDING KEYWORD TAGS:".PHP_EOL;
$keywords = $metadata->get(\Holiday\Metadata::KEYWORDS);
$people = $metadata->get(\Holiday\Metadata::PEOPLE);
$keywords = $metadata->get(Metadata::KEYWORDS);
$people = $metadata->get(Metadata::PEOPLE);
if($keywords !== false) echo "KEYWORDS : ".implode(', ', $keywords).PHP_EOL;
if($people !== false) echo "PEOPLE : ".implode(', ', $people).PHP_EOL;
echo PHP_EOL;
Expand All @@ -81,59 +94,59 @@ function my__autoload(string $name): void
if($caption !== false && $date_created !== false && $city !== false && $country !== false && $credit !== false) {
$caption = strtoupper($city).', '.strtoupper($country).' - '.strtoupper(date('F d', $date_created)).': '.
$caption.' (Photo by '.$credit.')';
$metadata->set(\Holiday\Metadata::CAPTION, $caption, lang: \Holiday\Metadata::LANG_DEFAULT);
$metadata->set(Metadata::CAPTION, $caption, lang: Metadata::LANG_DEFAULT);
}
else {
echo "NOT ALL INFORMATION AVAILABLE TO UPDATE CAPTION".PHP_EOL;
}
if($event !== false) {
$metadata->set(\Holiday\Metadata::EVENT, strtoupper($event));
$metadata->set(Metadata::EVENT, strtoupper($event));
}
else {
$metadata->set(\Holiday\Metadata::EVENT, 'Event was empty');
$metadata->set(Metadata::EVENT, 'Event was empty');
}

// Write metadata back to the image file
$metadata->write("new.$filename");

// Read-back the data and display modified caption
$metadata->read("new.$filename");
$caption_ary = $metadata->get(\Holiday\Metadata::CAPTION, \Holiday\Metadata::LANG_ALL);
$caption_ary = $metadata->get(Metadata::CAPTION, Metadata::LANG_ALL);
if(!empty($caption_ary)) {
echo "NEW CAPTION:".PHP_EOL;
foreach($caption_ary as $lang => $text) {
$lang = substr($lang.' ', 0, 9);
echo " $lang: $text".PHP_EOL;
}
}
echo "NEW EVENT : ".$metadata->get(\Holiday\Metadata::EVENT).PHP_EOL.PHP_EOL;
echo "NEW EVENT : ".$metadata->get(Metadata::EVENT).PHP_EOL.PHP_EOL;

// Paste original data to new file
$metadata->read("$filename");
$metadata->paste("new.$filename");

// Read-back the data and display original caption
$metadata->read("new.$filename");
$caption_ary = $metadata->get(\Holiday\Metadata::CAPTION, \Holiday\Metadata::LANG_ALL);
$caption_ary = $metadata->get(Metadata::CAPTION, Metadata::LANG_ALL);
if(!empty($caption_ary)) {
echo "PASTED CAPTION:".PHP_EOL;
foreach($caption_ary as $lang => $text) {
$lang = substr($lang.' ', 0, 9);
echo " $lang: $text".PHP_EOL;
}
}
echo "PASTED EVENT: ".$metadata->get(\Holiday\Metadata::EVENT).PHP_EOL.PHP_EOL;
echo "PASTED EVENT: ".$metadata->get(Metadata::EVENT).PHP_EOL.PHP_EOL;
}

/**
* Use of exception handling class \Holiday\Metadata\Exception
* Use of exception handling class Metadata\Exception
*/
echo PHP_EOL;
try {
$metadata->read('invalid.file.name.jpg');
echo "FILE WAS SUCCESSFULLY READ ALTHOUGH IT SHOULD NOT EXIST".PHP_EOL;
}
catch(\Holiday\Metadata\Exception $exception) {
catch(Metadata\Exception $exception) {
echo "EXCEPTION CATCHED".PHP_EOL;
echo "---".PHP_EOL;
echo "CODE: ".$exception->getCode().PHP_EOL;
Expand Down
30 changes: 30 additions & 0 deletions test/example.txt
Expand Up @@ -9,6 +9,21 @@ EVENT : Demo Event
KEYWORDS : Demo Keyword A, Demo Keyword B, Demo Keyword C, Peo: Additional Person
PEOPLE : Demo Person A, Demo Person B, Demo Person C

IMAGE DATA
- CAMERA MAKE : Canon
- CAMERA MODEL : Canon EOS-1D X Mark III
- CAMERA SERIAL : 043032000222
- LENS MODEL : EF24-70mm f/2.8L II USM
- COLOR SPACE : Adobe RGB (1998)
- ISO SETTING : 200
- APERTURE : f/9.0
- EXPOSURE : 1/640 second(s)
- FOCAL LENGTH : 38 mm
- FLASH USED : No flash
- IMAGE SIZE : 5211 x 3474 px - 66.18 x 44.12 cm (7 MB)
- RESOLUTION : 200 dpi
- SOFTWARE : Adobe Photoshop 23.3 (Windows)

EXTENDING KEYWORD TAGS:
KEYWORDS : Demo Keyword A, Demo Keyword B, Demo Keyword C
PEOPLE : Demo Person A, Demo Person B, Demo Person C, Additional Person
Expand All @@ -31,6 +46,21 @@ PLACE : Klöntal, Schweiz
CREATED : 12.07.2022
KEYWORDS : Spaziergang, Monument, Tal, Berg, Gessner, Stein

IMAGE DATA
- CAMERA MAKE : Canon
- CAMERA MODEL : Canon EOS-1D X Mark III
- CAMERA SERIAL : 043032000222
- LENS MODEL : EF28-300mm f/3.5-5.6L IS USM
- COLOR SPACE : Adobe RGB (1998)
- ISO SETTING : 200
- APERTURE : f/3.5
- EXPOSURE : 1/80 second(s)
- FOCAL LENGTH : 28 mm
- FLASH USED : No flash
- IMAGE SIZE : 5408 x 3606 px - 45.79 x 30.53 cm (6 MB)
- RESOLUTION : 300 dpi
- SOFTWARE : Adobe Photoshop 23.4 (Windows)

EXTENDING KEYWORD TAGS:
KEYWORDS : Spaziergang, Monument, Tal, Berg, Gessner, Stein

Expand Down

0 comments on commit 14ce254

Please sign in to comment.