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