<?php
|
|
|
|
// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
|
|
//
|
|
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
|
|
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
|
|
// $Id$
|
|
|
|
//this script may only be included - so its better to die if called directly.
|
|
if (strpos($_SERVER['SCRIPT_NAME'], basename(__FILE__)) !== false) {
|
|
header('location: index.php');
|
|
exit;
|
|
}
|
|
/**
|
|
* Reconciles metadata included within a file according to the Metadata Working Group guidelines
|
|
* See http://www.metadataworkinggroup.org/pdf/mwg_guidance.pdf
|
|
* Metadata in images particularly is not standardized and there are 3 main formats (IPTC, EXIF and XMP)
|
|
* that overlap. The guidelines set forth how these data should be reconciled
|
|
*/
|
|
class ReconcileExifIptcXmp
|
|
{
|
|
/**
|
|
* Array of all the types of metadata handled by this class
|
|
*
|
|
* @var array
|
|
*/
|
|
public $alltypes = ['exif' => '', 'iptc' => '', 'xmp' => ''];
|
|
|
|
/**
|
|
* Maps IPTC field labels to EXIF field labels
|
|
*
|
|
* @var array
|
|
*/
|
|
public $iptcToExif = [
|
|
'2#055' => 'DateTimeOriginal', //date
|
|
'2#060' => 'DateTimeOriginalTime',//fake EXIF field to match IPTC time which is separated into a different field
|
|
'2#062' => 'DateTimeDigitized', //date
|
|
'2#063' => 'DateTimeDigitizedTime', //fake EXIF field to match IPTC time which is separated into a different field
|
|
'2#080' => 'Artist',
|
|
'2#116' => 'Copyright',
|
|
'2#120' => 'ImageDescription',
|
|
];
|
|
|
|
/**
|
|
* Maps IPTC field labels to XMP field labels
|
|
* XMP categories are indicated in comments
|
|
*
|
|
* @var array
|
|
*/
|
|
public $iptcToXmp = [
|
|
'2#004' => 'IntellectualGenre', //Iptc4xmpCore
|
|
'2#005' => 'title', //dc
|
|
'2#010' => 'Urgency', //photoshop
|
|
'2#012' => 'SubjectCode', //Iptc4xmpCore
|
|
'2#015' => 'Category', //photoshop
|
|
'2#020' => 'SupplementalCategories',//photoshop
|
|
'2#025' => 'subject', //dc
|
|
'2#040' => 'Instructions', //photoshop
|
|
'2#055' => 'DateCreated', //photoshop
|
|
'2#060' => 'DateCreatedTime', //fake XMP field to match IPTC time which is separated into a different field
|
|
'2#062' => 'CreateDate', //photoshop
|
|
'2#063' => 'CreateDateTime', //fake XMP field to match IPTC time which is separated into a different field
|
|
'2#080' => 'creator', //dc
|
|
'2#085' => 'AuthorsPosition', //photoshop
|
|
'2#090' => 'City', //photoshop
|
|
'2#092' => 'Location', //Iptc4xmpCore
|
|
'2#095' => 'State', //photoshop
|
|
'2#100' => 'CountryCode', //Iptc4xmpCore
|
|
'2#101' => 'Country', //photoshop
|
|
'2#103' => 'TransmissionReference', //photoshop
|
|
'2#105' => 'Headline', //photoshop
|
|
'2#110' => 'Credit', //photoshop
|
|
'2#115' => 'Source', //photoshop
|
|
'2#116' => 'rights', //dc
|
|
'2#118' => 'ContactInfoDetails', //Iptc4xmpCore
|
|
'2#120' => 'description', //dc
|
|
'2#122' => 'CaptionWriter', //photoshop
|
|
'2#140' => 'Instructions', //photosho
|
|
];
|
|
|
|
//Mapping for those fields where the name isn't the same and EXIF is preferred
|
|
/**
|
|
* Maps XMP field labels to EXIF labels where the labels aren't the same and EXIF is the preferred value
|
|
* per the Metadata Working Group guidelines
|
|
*
|
|
* @var array
|
|
*/
|
|
public $xmpToExif = [
|
|
'description' => 'ImageDescription',
|
|
'rights' => 'Copyright',
|
|
'creator' => 'Artist',
|
|
'GPSVersionID' => 'GPSVersion',
|
|
'FlashpixVersion' => 'FlashPixVersion',
|
|
'PixelXDimension' => 'ExifImageWidth',
|
|
'PixelYDimension' => 'ExifImageLength',
|
|
'format' => 'MimeType',
|
|
'ModifyDate' => 'DateTime',
|
|
'DateCreated' => 'DateTimeOriginal',
|
|
'CreateDate' => 'DateTimeDigitized',
|
|
'LensInfo' => 'UndefinedTag:0xA432',
|
|
];
|
|
|
|
/**
|
|
* Maps EXIF field labals to XMP labels where the labels don't match and where XMP is preferred
|
|
* XMP is preferred for dates, if it matches EXIF, because it includes a time zone offset
|
|
*
|
|
* @var array
|
|
*/
|
|
public $xmpPreferred = [
|
|
'DateTime' => 'ModifyDate',
|
|
'DateTimeOriginal' => 'DateCreated',
|
|
'DateTimeDigitized' => 'CreateDate',
|
|
];
|
|
|
|
/**
|
|
* Specifications for summary key information to be placed first in the array of data
|
|
*
|
|
* @var array
|
|
*/
|
|
public $basicSummary = [
|
|
'User Data' => [
|
|
'Title' => [
|
|
'iptc' => '2#005',
|
|
'xmp' => 'title',
|
|
],
|
|
'Description' => [
|
|
'exif' => 'ImageDescription',
|
|
'iptc' => '2#120',
|
|
'xmp' => 'description',
|
|
],
|
|
'Keywords' => [
|
|
'iptc' => '2#025',
|
|
'xmp' => 'subject',
|
|
],
|
|
'Creator' => [
|
|
'exif' => 'Artist',
|
|
'iptc' => '2#080',
|
|
'xmp' => 'creator',
|
|
],
|
|
'Copyright' => [
|
|
'exif' => 'Copyright',
|
|
'iptc' => '2#116',
|
|
'xmp' => 'rights',
|
|
],
|
|
],
|
|
'Dates' => [
|
|
'Date of Original' => [
|
|
'exif' => 'DateTimeOriginal',
|
|
'iptc' => '2#055',
|
|
'xmp' => 'DateCreated'
|
|
],
|
|
'Date Digitized' => [
|
|
'exif' => 'DateTimeDigitized',
|
|
'iptc' => '2#062',
|
|
'xmp' => 'CreateDate',
|
|
],
|
|
'Date Modified' => [
|
|
'exif' => 'DateTime',
|
|
'xmp' => 'ModifyDate',
|
|
],
|
|
'Metadata Date' => [
|
|
'xmp' => 'MetadataDate',
|
|
],
|
|
],
|
|
'File Data' => [
|
|
'File Type' => [
|
|
'exif' => 'FileType',
|
|
'xmp' => 'format',
|
|
],
|
|
'File Size' => [
|
|
'exif' => 'FileSize',
|
|
],
|
|
'Width' => [
|
|
'exif' => 'Width',
|
|
'xmp' => 'PixelXDimension',
|
|
],
|
|
'Height' => [
|
|
'exif' => 'Height',
|
|
'xmp' => 'PixelYDimension',
|
|
],
|
|
'Resolution' => [
|
|
'exif' => 'XResolution',
|
|
],
|
|
'Resolution Unit' => [
|
|
'exif' => 'ResolutionUnit',
|
|
],
|
|
],
|
|
];
|
|
/**
|
|
* Labels for reconciliation stats
|
|
*
|
|
* @var array
|
|
*/
|
|
public $statspecs = [
|
|
'fields' => [
|
|
'label' => 'Total Fields Shown',
|
|
],
|
|
'dupes' => [
|
|
'label' => 'Duplicate Fields'
|
|
],
|
|
'mismatches' => [
|
|
'label' => 'Mismatches',
|
|
],
|
|
];
|
|
|
|
/**
|
|
* Array used to determine which data types to compare based on which iteration we're on
|
|
*
|
|
* @var array
|
|
*/
|
|
public $repeat = [
|
|
2 => ['exif' => '', 'iptc' => ''],
|
|
3 => ['iptc' => '', 'xmp' => ''],
|
|
4 => ['exif' => '', 'xmp' => ''],
|
|
5 => ['exif' => '', 'xmp' => ''],
|
|
];
|
|
|
|
/**
|
|
* Map between xmp (keys) and exif (values) for the FLash field
|
|
* xmp stores as different fields whereas exif stores as one number
|
|
*
|
|
* @var array
|
|
*/
|
|
public $flashmap = [
|
|
'Fired' => [
|
|
'False' => 0,
|
|
'True' => 1,
|
|
],
|
|
'Return' => [
|
|
'0' => 0, //No return detected
|
|
'2' => 4, //Return not detected
|
|
'3' => 6, //Return detected
|
|
],
|
|
'Mode' => [
|
|
'0' => 0, //Unknown
|
|
'1' => 8, //On
|
|
'2' => 16, //Off
|
|
'3' => 24, //Auto
|
|
],
|
|
'Function' => [
|
|
'False' => 0,
|
|
'True' => 32,
|
|
],
|
|
'RedEyeMode' => [
|
|
'False' => 0,
|
|
'True' => 64,
|
|
],
|
|
];
|
|
|
|
/**
|
|
* Fields requiring special handling
|
|
*
|
|
* @var array
|
|
*/
|
|
public $special = [
|
|
'ComponentsConfiguration' => '',
|
|
];
|
|
|
|
/**
|
|
* Reconcile EXIF, IPTC and XMP metadata and return a single reconciled array
|
|
*
|
|
* @param array $metadata Expects the following format
|
|
* $metadata[type-eg IPTC][group-eg GPS][field-eg height]
|
|
*
|
|
* @return array|bool $finalall Array of reconciled data included stats
|
|
*/
|
|
public function reconcileAllMeta($metadata)
|
|
{
|
|
$types = [];
|
|
|
|
//check which metadata types exist
|
|
foreach ($this->alltypes as $alltype => $val) {
|
|
if ($metadata[$alltype] !== false) {
|
|
$types[$alltype] = '';
|
|
}
|
|
}
|
|
//return false if no metadata
|
|
if (count($types) == 0) {
|
|
return false;
|
|
//no need to reconcile with 1 type, just add summary info
|
|
} elseif (count($types) == 1) {
|
|
$omni['all'][key($types)] = $this->flatten($metadata[key($types)]);
|
|
$basicsum = $this->makeSummaryInfo($omni);
|
|
$metarray = [key($types) => $metadata[key($types)]];
|
|
$metarray = $basicsum + $metarray;
|
|
return $metarray;
|
|
//more than one metadata type, so need to reconcile
|
|
} else {
|
|
//set main array with all data from all types
|
|
foreach ($types as $type => $val) {
|
|
$omni[$type]['flat'] = $this->flatten($metadata[$type]);
|
|
}
|
|
$omni = $this->addFakeFields($omni);
|
|
foreach ($omni as $type => $flat) {
|
|
$omni[$type]['left'] = $omni[$type]['flat'];
|
|
}
|
|
//send to reconciling function
|
|
//if all three types are present, will need to iterate 5 times in total
|
|
if (count($types) == count($this->alltypes)) {
|
|
$omni = $this->reconcile($types, $omni, false, 1);
|
|
//if exif and xmp are the types, then will need to iterate twice: once for matching field names and once for
|
|
//mapped field names
|
|
} elseif (array_key_exists('exif', $types) && array_key_exists('xmp', $types)) {
|
|
$omni = $this->reconcile($types, $omni, false, 4);
|
|
//other combinations of two data types are only iterated once
|
|
} else {
|
|
$omni = $this->reconcile($types, $omni, false, 5);
|
|
}
|
|
//combine duplicated fields with unduplicated for final array
|
|
$omni['stats']['fields']['newval'] = 0;
|
|
foreach ($types as $type => $val) {
|
|
if (isset($omni[$type]['left']) && count($omni[$type]['left']) > 0) {
|
|
if (! isset($omni['all'][$type])) {
|
|
$omni['all'][$type] = $omni[$type]['left'];
|
|
$omni['stats']['fields']['newval'] += count($omni['all'][$type]);
|
|
} else {
|
|
$omni['all'][$type] += $omni[$type]['left'];
|
|
$omni['stats']['fields']['newval'] += count($omni['all'][$type]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//Prepare stats
|
|
$stats = [];
|
|
$finalall = [];
|
|
if (isset($omni['stats'])) {
|
|
foreach ($this->statspecs as $key => $array) {
|
|
if (isset($omni['stats'][$key])) {
|
|
$stats[$key] = $omni['stats'][$key];
|
|
$stats[$key]['label'] = $this->statspecs[$key]['label'];
|
|
}
|
|
}
|
|
if (isset($stats['mismatches']['newval'])) {
|
|
$stats['mismatches']['suffix'] = '(fields that should match but do not - see data detail)';
|
|
}
|
|
if (isset($stats['dupes']['newval']) && $stats['dupes']['newval'] > 0) {
|
|
$stats['dupes']['suffix'] = '(this is normal - standard preferred field shown)';
|
|
}
|
|
}
|
|
|
|
//summarize basic information to display first
|
|
$basicsum = $this->makeSummaryInfo($omni);
|
|
|
|
//add stats
|
|
if (isset($stats)) {
|
|
$basicsum['Summary of Basic Information']['Metadata Reconciliation Stats'] = $stats;
|
|
}
|
|
|
|
//unflatten the file metadata arrays by restoring the group level
|
|
foreach ($types as $type => $val) {
|
|
if (isset($metadata[$type])) {
|
|
foreach ($metadata[$type] as $group => $fields) {
|
|
$finalall[$type][$group] = array_intersect_key($omni['all'][$type], $fields);
|
|
}
|
|
}
|
|
}
|
|
$finalall = $basicsum + $finalall;
|
|
return $finalall;
|
|
}
|
|
|
|
/**
|
|
* Remove second layer of multiple array. Used to ease array comparisons
|
|
*
|
|
* @param array $multiArray Minimum 3-level multiple array
|
|
*
|
|
* @return array $flat flatted array
|
|
*/
|
|
public function flatten($multiArray)
|
|
{
|
|
$flat = [];
|
|
foreach ($multiArray as $secondkeys) {
|
|
$flat = $flat + $secondkeys;
|
|
}
|
|
return $flat;
|
|
}
|
|
|
|
/**
|
|
* Add fake EXIF fields to reconcile with IPTC time which is separated into a different field
|
|
*
|
|
* @param array $omni Array where reconciliation results are collected
|
|
*
|
|
* @return array $omni Array with fake fields added
|
|
*/
|
|
private function addFakeFields($omni)
|
|
{
|
|
if (array_key_exists('2#060', $omni['iptc']['flat'])) {
|
|
if (array_key_exists('DateTimeOriginal', $omni['exif']['flat'])) {
|
|
$omni['exif']['flat']['DateTimeOriginalTime'] = $omni['exif']['flat']['DateTimeOriginal'];
|
|
}
|
|
if (array_key_exists('DateCreated', $omni['xmp']['flat'])) {
|
|
$omni['xmp']['flat']['DateCreatedTime'] = $omni['xmp']['flat']['DateCreated'];
|
|
}
|
|
}
|
|
if (array_key_exists('2#063', $omni['iptc']['flat'])) {
|
|
if (array_key_exists('DateTimeDigitized', $omni['exif']['flat'])) {
|
|
$omni['exif']['flat']['DateTimeDigitizedTime'] = $omni['exif']['flat']['DateTimeDigitized'];
|
|
}
|
|
if (array_key_exists('CreateDate', $omni['xmp']['flat'])) {
|
|
$omni['xmp']['flat']['CreateDateTime'] = $omni['xmp']['flat']['CreateDate'];
|
|
}
|
|
}
|
|
return $omni;
|
|
}
|
|
|
|
/**
|
|
* Create array of a summary of key metadata information. The structure and fields of the summary are set by
|
|
* $this->basicInfo
|
|
*
|
|
* @param $omni
|
|
*
|
|
* @return mixed
|
|
*/
|
|
private function makeSummaryInfo($omni)
|
|
{
|
|
$basicsum = [];
|
|
foreach ($this->basicSummary as $infogroup => $fields) {
|
|
foreach ($fields as $label => $infotypes) {
|
|
foreach ($infotypes as $infotype => $fieldame) {
|
|
if (isset($omni['all'][$infotype][$fieldame])) {
|
|
$basicsum['Summary of Basic Information'][$infogroup][$label]
|
|
= $omni['all'][$infotype][$fieldame];
|
|
$basicsum['Summary of Basic Information'][$infogroup][$label]['label']
|
|
= $label;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return $basicsum;
|
|
}
|
|
|
|
/**
|
|
* Performs actual reconciliation of two data types. Multiple iterations are needed in some cases
|
|
*
|
|
* @param array $types Array of types of metadata included in the information
|
|
* @param array $omni Array of metadata to be reconciled
|
|
* @param bool $samekey Indicates whether the field labels for the two datatypes to be
|
|
* compared are the same or not
|
|
* @param numeric $i Indicates which data types to compare
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function reconcile($types, $omni, $samekey, $i)
|
|
{
|
|
$match = [];
|
|
//identify the types and determine matches
|
|
//for files with all 3 metadata types, first pass checks to see if any fields are triplicated
|
|
if (count($types) == 3) {
|
|
$type1 = 'exif';
|
|
$type2 = 'iptc';
|
|
$type3 = 'xmp';
|
|
//extract actual EXIF fields that could be duplicated in IPTC
|
|
$exifmatch = array_flip(array_intersect_key(array_flip($this->iptcToExif), $omni['exif']['left']));
|
|
//extract actual XMP fields that could be duplicated in IPTC
|
|
$xmpmatch = array_flip(array_intersect_key(array_flip($this->iptcToXmp), $omni['xmp']['left']));
|
|
//now extract any triplicated fields (ie, fields in all three metadata types)
|
|
//resulting array will have IPTC => EXIF fieldname key => value pairs
|
|
$match = array_intersect_key($exifmatch, $xmpmatch, $omni['iptc']['left']);
|
|
//need an array with XMP fieldnames too
|
|
$matchx = array_flip(array_intersect_key($xmpmatch, $match));
|
|
//for files with 2 metadata types, or for subsequent iterations after checking for triplicates for files
|
|
//with all three metadata types
|
|
} elseif (count($types) == 2) {
|
|
if ($samekey === false) {
|
|
if (array_key_exists('exif', $types)) {
|
|
$type1 = 'exif';
|
|
$type2 = key(array_diff_key($types, ['exif' => '']));
|
|
} else {
|
|
$type1 = 'xmp';
|
|
$type2 = 'iptc';
|
|
}
|
|
$map = $type2 . 'To' . ucfirst($type1);
|
|
//compare actual fields in type2 to list of possible duplicates with type1
|
|
$two2one = array_intersect_key($this->$map, $omni[$type2]['left']);
|
|
$one2two = array_flip($two2one);
|
|
//compare possible duplicate list to actual type one fields to identify actual duplicates
|
|
$match = array_intersect_key($one2two, $omni[$type1]['left']);
|
|
//$samekey = true, which is for EXIF and XMP fields with the same field names, therefore no mapping needed
|
|
} else {
|
|
$type1 = 'exif';
|
|
$type2 = 'xmp';
|
|
$match = array_intersect_key($omni[$type1]['left'], $omni[$type2]['left']);
|
|
}
|
|
}
|
|
//start reconciling if there are duplicates
|
|
if (count($match) > 0) {
|
|
foreach ($match as $name => $value) {
|
|
//set type => fieldname pairs for all metadata types in the file
|
|
if (count($types) == 3) {
|
|
$fnames = [
|
|
$type1 => $exifmatch[$name],
|
|
$type2 => $name,
|
|
$type3 => $xmpmatch[$name],
|
|
];
|
|
} else {
|
|
$fnames = [
|
|
$type1 => $name,
|
|
$type2 => $samekey === false ? $one2two[$name] : $name,
|
|
];
|
|
}
|
|
//check to see if duplicate fields have equal values
|
|
//check exif vs iptc
|
|
if (array_key_exists('exif', $types) && array_key_exists('iptc', $types)) {
|
|
$check['exif-iptc'] = $this->compareIptcExifValues(
|
|
$fnames['exif'],
|
|
$fnames['iptc'],
|
|
$omni['iptc']['left'][$fnames['iptc']]['rawval'],
|
|
$omni['exif']['left'][$fnames['exif']]['rawval']
|
|
);
|
|
}
|
|
//check exif vs xmp
|
|
if (array_key_exists('exif', $types) && array_key_exists('xmp', $types)) {
|
|
$check['exif-xmp'] = $this->compareExifXmpValues(
|
|
$fnames['exif'],
|
|
$omni['xmp']['left'][$fnames['xmp']]['rawval'],
|
|
$omni['exif']['left'][$fnames['exif']]['rawval']
|
|
);
|
|
//per MWG guidelines, prefer XMP time fields to EXIF if they match since XMP has the time zone
|
|
//offset and EXIF doesn't
|
|
if (array_key_exists($fnames['exif'], $this->xmpPreferred) && $check['exif-xmp'] == true) {
|
|
$preferred = 'xmp';
|
|
} else {
|
|
$preferred = 'exif';
|
|
}
|
|
}
|
|
//check iptc vs xmp
|
|
if (array_key_exists('iptc', $types) && array_key_exists('xmp', $types)) {
|
|
$check['iptc-xmp'] = $this->compareIptcXmpValues(
|
|
$fnames['xmp'],
|
|
$fnames['iptc'],
|
|
$omni['iptc']['left'][$fnames['iptc']]['rawval'],
|
|
$omni['xmp']['left'][$fnames['xmp']]['rawval']
|
|
);
|
|
}
|
|
//now determine which of the duplicates will be displayed according to MWG guidelines
|
|
if (array_key_exists('iptc', $types)) {
|
|
//per MWG guidelines, if actual and stored IPTC hash are equal or stored is empty,
|
|
//prefer other values over IPTC
|
|
$hashmatch = $this->checkIptcHash($omni['iptc']['left']);
|
|
if ($hashmatch) {
|
|
if (count($types) == 3) {
|
|
$type = $preferred;
|
|
} else {
|
|
$type = $type1;
|
|
}
|
|
//per MWG guidelines, if actual and stored IPTC hash differ, prefer IPTC over EXIF,
|
|
//but prefer XMP if values match
|
|
} else {
|
|
if (array_key_exists('xmp', $types)) {
|
|
if ($check['iptc-xmp']) {
|
|
$type = 'xmp';
|
|
} else {
|
|
$type = 'iptc';
|
|
}
|
|
} else {
|
|
$type = 'iptc';
|
|
}
|
|
}
|
|
//$type2 is XMP and $type1 is EXIF
|
|
} else {
|
|
//prefer XMP for certain date fields because they include time zone offset info
|
|
$type = $preferred;
|
|
}
|
|
|
|
foreach ($fnames as $tname => $fname) {
|
|
//this is the field data that will be displayed
|
|
if ($type == $tname) {
|
|
if (isset($omni['all'][$type][$fname])) {
|
|
$omni['all'][$type][$fname] += $omni[$tname]['left'][$fname];
|
|
} else {
|
|
$omni['all'][$type][$fname] = $omni[$tname]['left'][$fname];
|
|
}
|
|
//this is the duplicate data that will be stored with the displayed data, but not displayed
|
|
} else {
|
|
$omni['all'][$type][$fnames[$type]][$tname][$fname] = $omni[$tname]['left'][$fname];
|
|
}
|
|
}
|
|
//collect stats on mismatches
|
|
foreach ($check as $typecheck => $result) {
|
|
$omni['all'][$type][$fnames[$type]]['check'][$typecheck] = $result;
|
|
if ($result === false) {
|
|
$omni['mismatches'][$type][$fnames[$type]][$typecheck] = $result;
|
|
if (! isset($omni['stats']['mismatches']['newval'])) {
|
|
$omni['stats']['mismatches']['newval'] = 1;
|
|
} else {
|
|
$omni['stats']['mismatches']['newval'] += 1;
|
|
}
|
|
$note = ' (' . strtoupper($typecheck) . ' ' . tra('duplicate fields do not match') . ')';
|
|
if (! isset($omni['all'][$type][$fnames[$type]]['suffix'])) {
|
|
$omni['all'][$type][$fnames[$type]]['suffix'] = $note;
|
|
} else {
|
|
$omni['all'][$type][$fnames[$type]]['suffix'] .= ' ' . $note;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
//collect stats on how many duplicates
|
|
$count = $i == 1 ? count($match) * 3 : count($match);
|
|
if (! isset($omni['stats']['dupes']['newval'])) {
|
|
$omni['stats']['dupes']['newval'] = $count;
|
|
} else {
|
|
$omni['stats']['dupes']['newval'] += $count;
|
|
}
|
|
|
|
//delete duplicates from complete field list to identify unduplicated fields that are left
|
|
if (count($types) == 3) {
|
|
$omni['exif']['left'] = array_diff_key($omni['exif']['left'], array_flip($match));
|
|
$omni['iptc']['left'] = array_diff_key($omni['iptc']['left'], $match);
|
|
$omni['xmp']['left'] = array_diff_key($omni['xmp']['left'], $matchx);
|
|
} else {
|
|
$omni[$type1]['left'] = array_diff_key($omni[$type1]['left'], $match);
|
|
$omni[$type2]['left'] = array_diff_key(
|
|
$omni[$type2]['left'],
|
|
isset($samekey) && $samekey ? $match : array_flip($match)
|
|
);
|
|
}
|
|
//see if the data needs another pass
|
|
//files with all three metadata types need 5 passes in total
|
|
//files with EXIF and XMP need 2 passes, one for fields with matching field names and one for mapped fields
|
|
if (isset($i) && $i < 5) {
|
|
$i++;
|
|
$newsamekey = $i == 4 ? true : false;
|
|
$newtypes = $this->repeat[$i];
|
|
$omni = $this->reconcile($newtypes, $omni, $newsamekey, $i);
|
|
}
|
|
} else {
|
|
if (isset($i) && $i < 5) {
|
|
$i++;
|
|
$newsamekey = $i == 4 ? true : false;
|
|
$newtypes = $this->repeat[$i];
|
|
$omni = $this->reconcile($newtypes, $omni, $newsamekey, $i);
|
|
}
|
|
}
|
|
return $omni;
|
|
}
|
|
|
|
/**
|
|
* Check stored IPTC checksum against calculated checksum. Per Metadata Working Group guidelines, if there
|
|
* is no stored checksum or if it is there and matches the calculated checksum, then prefer the non-IPTC value
|
|
*
|
|
* @param array $iptcflat Array of IPTC data including the calulated and stored checksum values
|
|
*
|
|
* @return bool Return true when non-IPTC value should be used
|
|
*/
|
|
private function checkIptcHash($iptcflat)
|
|
{
|
|
if (
|
|
! isset($iptcflat['iptchashstored']['newval']) || (strlen($iptcflat['iptchashstored']['newval']) > 0
|
|
&& $iptcflat['iptchashstored']['newval'] == $iptcflat['iptchashcurrent']['newval'])
|
|
) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compare IPTC and EXIF values for fields that should be the same, taking into account IPTC length limitations
|
|
*
|
|
* @param string $iptcval Value of the IPTC field
|
|
* @param string $exifval Value of the EXIF field
|
|
*
|
|
* @return bool Return true or false depending on whether the fields matched or not
|
|
*/
|
|
private function compareIptcExifValues($exifkey, $iptckey, $iptcval, $exifval)
|
|
{
|
|
//handle special cases first
|
|
if (array_key_exists($exifkey, ['DateTimeDigitized' => '', 'DateTimeOriginal' => '', 'DateTimeDigitizedTime' => '', 'DateTimeOriginalTime' => ''])) {
|
|
$exifdate = new DateTime($exifval);
|
|
$iptcdate = new DateTime($iptcval);
|
|
//time
|
|
if ($iptckey == '2#060' || $iptckey == '2#063') {
|
|
$exifcheckval = $exifdate->format('H:i:s');
|
|
$iptccheckval = $iptcdate->format('H:i:s');
|
|
//date
|
|
} else {
|
|
$exifcheckval = $exifdate->format('Y-m-d');
|
|
$iptccheckval = $iptcdate->format('Y-m-d');
|
|
}
|
|
} else {
|
|
//IPTC fields have length limits, so compare up to the length of the IPTC field to avoid false negatives
|
|
$len = strlen($iptcval);
|
|
$exifcheckval = substr($exifval, 0, $len);
|
|
$iptccheckval = $iptcval;
|
|
}
|
|
if ($exifcheckval == $iptccheckval) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compare IPTC and XMP fields that should have the same values, taking into account IPTC length limitations
|
|
*
|
|
* @param string $xmpkey XMP field name for special handling cases
|
|
* @param string $iptcval IPTC field value
|
|
* @param string $xmpval XMP field value
|
|
*
|
|
* @return bool Return true or false depending on whether the fields matched or not
|
|
*/
|
|
private function compareIptcXmpValues($xmpkey, $iptckey, $iptcval, $xmpval)
|
|
{
|
|
$iptccheckval = '';
|
|
$xmpcheckval = '';
|
|
//the subject field is an array in both IPTC and XMP, so concatenate to compare
|
|
if ($xmpkey == 'subject') {
|
|
foreach ($iptcval as $val) {
|
|
$iptccheckval .= $val;
|
|
}
|
|
foreach ($xmpval as $val) {
|
|
$xmpcheckval .= $val['rawval'];
|
|
}
|
|
} elseif (array_key_exists($xmpkey, ['DateCreated' => '', 'CreateDate' => '', 'DateCreatedTime' => '', 'CreateDateTime' => ''])) {
|
|
$xmpdate = new DateTime($xmpval);
|
|
$iptcdate = new DateTime($iptcval);
|
|
//time
|
|
if ($iptckey == '2#060' || $iptckey == '2#063') {
|
|
$xmpcheckval = $xmpdate->format('H:i:s');
|
|
$iptccheckval = $iptcdate->format('H:i:s');
|
|
//date
|
|
} else {
|
|
$xmpcheckval = $xmpdate->format('Y-m-d');
|
|
$iptccheckval = $iptcdate->format('Y-m-d');
|
|
}
|
|
} else {
|
|
//the ultimate raw value for XMP list fields (<li>) is one level deeper
|
|
if (is_array($xmpval)) {
|
|
$xmpcheckval = $xmpval['rawval'];
|
|
} else {
|
|
$xmpcheckval = $xmpval;
|
|
}
|
|
//IPTC fields have length limits, so compare up to the length of the IPTC field to avoid false negatives
|
|
$iptccheckval = $iptcval;
|
|
$xmpcheckval = substr($xmpcheckval, 0, strlen($iptcval));
|
|
}
|
|
//now check against each other
|
|
if ($iptccheckval == $xmpcheckval) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compare EXIF and XMP fields that should have the same values
|
|
*
|
|
* @param string $exifkey EXIF field name for fields that need special handling
|
|
* @param string $xmpval XMP field vale
|
|
* @param string $exifval EXIF field value
|
|
*
|
|
* @return bool Return true or false depending on whether the fields matched or not
|
|
*/
|
|
private function compareExifXmpValues($exifkey, $xmpval, $exifval)
|
|
{
|
|
if (isset($xmpval)) {
|
|
//handle special cases
|
|
//XMP has the timezone offset for these fields whereas EXIF does not, so compare times without the offset
|
|
if (
|
|
$exifkey == 'DateTimeOriginal' ||
|
|
$exifkey == 'DateTimeDigitized' ||
|
|
$exifkey == 'DateTime' ||
|
|
$exifkey == 'DateTimeOriginalTime' ||
|
|
$exifkey == 'DateTimeDigitizedTime'
|
|
) {
|
|
$exifcheckval = strtotime($exifval);
|
|
$xmpcheckval = strtotime(substr($xmpval, 0, strlen($exifval)));
|
|
//XMP GPS Version raw field is already in final format whereas EXIF field is not
|
|
} elseif ($exifkey == 'GPSVersion') {
|
|
$xmpcheckval = explode('.', $xmpval);
|
|
$xmpcheckval = '0' . implode('0', $xmpcheckval);
|
|
} elseif ($exifkey == 'ComponentsConfiguration') {
|
|
foreach ($xmpval as $val) {
|
|
$new = '0' . $val['rawval'];
|
|
$xmpcheckval = isset($xmpcheckval) ? $xmpcheckval . $new : $new;
|
|
}
|
|
} elseif ($exifkey == 'UndefinedTag:0xA432') {
|
|
$exifcheckval = implode(' ', $exifval);
|
|
$xmpcheckval = $xmpval;
|
|
}
|
|
//set EXIF value to check for all other cases
|
|
if (! isset($exifcheckval)) {
|
|
$exifcheckval = $exifval;
|
|
}
|
|
//when the XMP value is an array
|
|
if (is_array($xmpval) && ! array_key_exists($exifkey, $this->special)) {
|
|
//Flash is an array in XMP and a single number code in EXIF
|
|
if ($exifkey == 'Flash') {
|
|
$exifcheckval = $exifval;
|
|
$xmpcheckval = '';
|
|
foreach ($xmpval as $flash => $status) {
|
|
$xmpcheckval = $xmpcheckval + $this->flashmap[$flash][$status['rawval']];
|
|
}
|
|
//the ultimate raw value for XMP list fields (<li>) is one level deeper
|
|
} else {
|
|
$xmpcheckval = $xmpval['rawval'];
|
|
}
|
|
}
|
|
//set XMP value to check for all other cases
|
|
if (! isset($xmpcheckval)) {
|
|
$xmpcheckval = $xmpval;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
//now check against each other
|
|
if ($xmpcheckval == $exifcheckval) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}
|