1<?php 2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project 3// 4// All Rights Reserved. See copyright.txt for details and a complete list of authors. 5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details. 6// $Id$ 7 8//this script may only be included - so its better to die if called directly. 9if (strpos($_SERVER['SCRIPT_NAME'], basename(__FILE__)) !== false) { 10 header('location: index.php'); 11 exit; 12} 13/* 14 * Manipulates XMP metadata included within a file 15 */ 16class Xmp 17{ 18 /** 19 * Legend and label information for each field 20 * 21 * @var array 22 */ 23 var $specs = [ 24 'photoshop' => [ 25 'ColorMode' => [ 26 'options' => [ 27 '0' => 'Bitmap', 28 '1' => 'Gray scale', 29 '2' => 'Indexed color', 30 '3' => 'RGB color', 31 '4' => 'CMYK color', 32 '7' => 'Multi-channel', 33 '8' => 'Duotone', 34 '9' => 'LAB color', 35 ], 36 ], 37 ], 38 'dc' => [ 39 'rights' => [ 40 'label' => 'Rights', 41 ], 42 'description' => [ 43 'label' => 'Description', 44 ], 45 'title' => [ 46 'label' => 'Title', 47 ], 48 'subject' => [ 49 'label' => 'Subject', 50 ], 51 'format' => [ 52 'label' => 'Format', 53 ], 54 'creator' => [ 55 'label' => 'Creator', 56 ], 57 ], 58 ]; 59 60 /** 61 * Fields requiring special handling 62 * 63 * @var array 64 */ 65 var $special = [ 66 'ComponentsConfiguration' => '', 67 ]; 68 69 /** 70 * Process raw XMP string by converting to a DOM document, replacing legend codes with their meanings, 71 * fixing labels, etc. 72 * 73 * @param xml string$xmpstring 74 * 75 * @return array|bool 76 */ 77 function processRawData($xmpstring) 78 { 79 //need to do a little preparation before processing fields 80 if (! empty($xmpstring)) { 81 $xmp = new DOMDocument(); 82 //create a DOM Document from the XML string 83 $xmp->loadXML($xmpstring); 84 //convert Dom Document to an array 85 //TODO use Zend_Json::fromXml when zend fixes bug (see http://framework.zend.com/issues/browse/ZF-12224) 86 $xmparray = $this->xmpDomToArray($xmp); 87 //re-label Native Digest fields in tiff and exif sections to keep from overwriting when arrays are flattened 88 //in other functions 89 $sections = ['exif', 'tiff']; 90 foreach ($sections as $section) { 91 if (isset($xmparray[$section]['NativeDigest'])) { 92 $temp = explode(',', $xmparray[$section]['NativeDigest']['rawval']); 93 $xmparray[$section]['NativeDigest']['rawval'] = implode(', ', $temp); 94 $xmparray[$section][strtoupper($section) . 'NativeDigest'] = $xmparray[$section]['NativeDigest']; 95 unset($xmparray[$section]['NativeDigest']); 96 } 97 } 98 //now we can process fields 99 $filter = new Zend\Filter\Word\CamelCaseToSeparator(); 100 foreach ($xmparray as $group => $fields) { 101 foreach ($fields as $name => $field) { 102 if (isset($this->specs[$group][$name])) { 103 //shorten the variable 104 $specname = $this->specs[$group][$name]; 105 //convert coded fields into tags 106 if (isset($specname['options'])) { 107 $xmparray[$group][$name]['newval'] = $specname['options'][$xmparray[$group][$name]['rawval']]; 108 } else { 109 $xmparray[$group][$name]['newval'] = $xmparray[$group][$name]['rawval']; 110 } 111 //fix labels 112 if (isset($specname['label'])) { 113 $xmparray[$group][$name]['label'] = $specname['label']; 114 } else { 115 //create reading-friendly labels from camel case tags 116 $xmparray[$group][$name]['label'] = $filter->filter($name); 117 } 118 } else { 119 //those not covered in $specs 120 //create reading-friendly labels from camel case tags 121 $xmparray[$group][$name]['label'] = $filter->filter($name); 122 $xmparray[$group][$name]['newval'] = $xmparray[$group][$name]['rawval']; 123 } 124 //deal with arrays 125 if (is_array($field['rawval'])) { 126 if (array_key_exists($name, $this->special)) { 127 $xmparray[$group][$name]['newval'] = $this->specialHandling($name, $field['rawval']); 128 } elseif (isset($field['rawval']['rawval'])) { 129 $xmparray[$group][$name]['newval'] = $field['rawval']['rawval']; 130 } elseif (isset($field['rawval'][0])) { 131 $xmparray[$group][$name]['newval'] = ''; 132 foreach ($field['rawval'] as $val) { 133 $xmparray[$group][$name]['newval'] .= $val['rawval'] . '; '; 134 } 135 } else { 136 $xmparray[$group][$name]['newval'] = ''; 137 foreach ($field['rawval'] as $val) { 138 $xmparray[$group][$name]['newval'] .= $val['label'] . ': ' . $val['rawval'] . '; '; 139 } 140 } 141 } 142 //convert dates 143 if (array_key_exists( 144 $name, 145 [ 146 'ModifyDate' => '', 147 'DateCreated' => '', 148 'CreateDate' => '', 149 'MetadataDate' => '' 150 ] 151 ) 152 ) { 153 $dateObj = new DateTime($xmparray[$group][$name]['newval']); 154 $date = $dateObj->format('Y-m-d H:i:s T'); 155 $xmparray[$group][$name]['newval'] = $date; 156 } 157 } 158 } 159 } else { 160 return false; 161 } 162 return $xmparray; 163 } 164 165 /** 166 * Returns xmp metadata from a file as a fully formed xml string 167 * 168 * @param string $filecontent The file as a string (eg, after applying file_get_contents) 169 * @param string $filetype File type 170 * 171 * @return xml string|false $xmp_text Returns fully formed xml string 172 */ 173 function getXmp($filecontent, $filetype) 174 { 175 if ($filetype == 'image/jpeg') { 176 $done = false; 177 $start = 0; 178 //TODO need to be able to handle multiple segments 179 while ($done === false) { 180 //search for hexadecimal marker for segment APP1 used for xmp data, and note position 181 $app1_hit = strpos($filecontent, "\xFF\xE1", $start); 182 if ($app1_hit !== false) { 183 //next two bytes after marker indicate the segment size 184 $size_raw = substr($filecontent, $app1_hit + 2, 2); 185 $size = unpack('nsize', $size_raw); 186 /*the segment APP1 marker is also used for other things (like EXIF data), 187 so check that the segment starts with the right info 188 allowing for 2 bytes for the marker and 2 bytes for the size before segment data starts*/ 189 $seg_data = substr($filecontent, $app1_hit + 4, $size['size']); 190 $xmp_hit = strpos($seg_data, 'http://ns.adobe.com/xap/1.0/'); 191 if ($xmp_hit === 0) { 192 $xmp_text_start = strpos($seg_data, '<rdf:RDF'); 193 $xmp_text_end = strpos($seg_data, '</rdf:RDF>'); 194 $endlen = strlen('</rdf:RDF>'); 195 $xmp_length = $xmp_text_end + $endlen - $xmp_text_start; 196 $xmp_text = substr($seg_data, $xmp_text_start, $xmp_length); 197 } 198 //start at the end of the segment just searched for the next search 199 $start = $app1_hit + 4 + $size['size']; 200 } else { 201 $done = true; 202 } 203 } 204 if (! isset($xmp_text)) { 205 $xmp_text = false; 206 } 207 } else { 208 $xmp_text = false; 209 } 210 return $xmp_text; 211 } 212 213 /** 214 * Convert an XML DomDocument from an image to an array 215 * 216 * @param DOM document $xmpObj XML document to process 217 * 218 * @return array|bool $xmparray Relevant portions of document converted to an array 219 */ 220 function xmpDomToArray($xmpObj) 221 { 222 if ($xmpObj !== false) { 223 //This section is for the first pass 224 if (get_class($xmpObj) == 'DOMDocument') { 225 //File metadata is in the Description elements 226 //There's one description element for each section of xmp data (exif,tiff, dc, etc.) 227 //$parent is a DOMNodeList 228 $topparents = $xmpObj->getElementsByTagName('Description'); 229 $toplen = $topparents->length; 230 //iterate through sections (like tiff, exif, xap, etc.) 231 for ($i = 0; $i < $toplen; $i++) { 232 //these sections (like exif, tiff, etc.) have child nodes, so no values captured at this level 233 //$children is a DOMNodeList 234 $children = $topparents->item($i)->childNodes; 235 $childrenlen = $children->length; 236 //iterate through fields in a section, e.g. Orientation, XResolution, etc. within tiff section 237 for ($j = 0; $j < $childrenlen; $j++) { 238 $child = $children->item($j); 239 //only pick up DOMElements to avoid empty DOMText fields 240 if ($child->nodeType == 1) { 241 //if $child has at least one child that is not a single DOMText field, then send back 242 //through to process children 243 if ($child->childNodes->length > 0 244 && ! ($child->childNodes->length == 1 && $child->firstChild->nodeType != 1) 245 ) { 246 $xmparray[$child->prefix][$child->localName]['rawval'] 247 = $this->xmpDomToArray($child->childNodes); 248 //this is where data from fields with single values (not multiple values) are picked up. 249 //Most fields go through here 250 } else { 251 $xmparray[$child->prefix][$child->localName]['rawval'] = $child->nodeValue; 252 } 253 $xmparray[$child->prefix][$child->localName]['key'] = $child->prefix; 254 $xmparray[$child->prefix][$child->localName]['label'] = ucfirst($child->localName); 255 $xmparray[$child->prefix][$child->localName]['locator'] = $child->getNodePath(); 256 } 257 } 258 } 259 //if sent from above code, then array is already at the field name, e.g.$xmparray['exif']['ISOSpeedRatings'] 260 } elseif (get_class($xmpObj) == 'DOMNodeList') { 261 $nodelist = $xmpObj; 262 $nodelistlen = $nodelist->length; 263 //iterate through node list 264 for ($i = 0; $i < $nodelistlen; $i++) { 265 $nodeitem = $nodelist->item($i); 266 //only pick up DOMElements to avoid empty DOMText fields 267 if ($nodeitem->nodeType == 1) { 268 //if the item itself has multiple items 269 if ($nodeitem->childNodes->length > 1) { 270 //Should capture tags like Seq, Alt, or Bag that have one or more list (li) items 271 if ($nodeitem->prefix == 'rdf' && $nodeitem->localName != 'li') { 272 $list = $nodeitem->childNodes; 273 $listlen = $nodeitem->childNodes->length; 274 //iterate through list items 275 for ($z = 0; $z < $listlen; $z++) { 276 $listitem = $list->item($z); 277 //only pick up DOMElements to avoid empty DOMText fields 278 if ($listitem->nodeType == 1) { 279 //3 items indicates there is really only one list item (li) with content since 280 //an empty text field precedes and succeeds every content field 281 if ($listlen == 3) { 282 $xmparray = [ 283 'key' => $listitem->prefix, 284 'label' => $listitem->localName, 285 'rawval' => $listitem->nodeValue, 286 'locator' => $listitem->getNodePath(), 287 ]; 288 //multiple list (li) items go in an array here 289 } else { 290 $xmparray[] = [ 291 'key' => $listitem->prefix, 292 'label' => $listitem->localName, 293 'rawval' => $listitem->nodeValue, 294 'locator' => $listitem->getNodePath(), 295 ]; 296 } 297 } 298 } 299 return $xmparray; 300 } else { 301 //in case a list item (li) has children - images tested so far don't seem to have this 302 //situation, so untested 303 $xmparray[$nodeitem->prefix][$nodeitem->localName] = 304 $this->xmpDomToArray($nodeitem->childNodes); 305 } 306 //fields like ['exif']['Flash'] go here, ie multiple items but not a list (li) inside of 307 //another element (like Seq, Bag or Alt) 308 } else { 309 $xmparray[$nodeitem->localName]['key'] = $nodeitem->prefix; 310 $xmparray[$nodeitem->localName]['label'] = $nodeitem->localName; 311 $xmparray[$nodeitem->localName]['rawval'] = $nodeitem->nodeValue; 312 $xmparray[$nodeitem->localName]['locator'] = $nodeitem->getNodePath(); 313 } 314 } 315 } 316 } 317 } else { 318 $xmparray = false; 319 } 320 return $xmparray; 321 } 322 323 function specialHandling($fieldname, $value) 324 { 325 $ret = ''; 326 if ($fieldname == 'ComponentsConfiguration' && isset($value)) { 327 include_once 'lib/metadata/datatypes/exif.php'; 328 $exif = new Exif; 329 foreach ($value as $singleval) { 330 $ret .= $exif->specs['EXIF'][$fieldname]['options']['0' . $singleval['rawval']] . ' '; 331 } 332 } 333 return trim($ret); 334 } 335} 336