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
8use Tiki\Lib\Image\Image;
9
10class Search_Action_FileGalleryImageOverlay implements Search_Action_Action
11{
12
13	protected $replaceKeys = [
14	  '%file_name%' => 'file.filename',
15	  '%file_id%' => 'file.fileId',
16	  '%parts_filename%' => 'parts.filename',
17	  '%parts_extension%' => 'parts.extension',
18	  '%gallery_name%' => 'gallery.name',
19	  '%gallery_id%' => 'gallery.galleryId',
20	  '%tracker_id%' => 'item.trackerId',
21	  '%item_id%' => 'item.itemId',
22	  '%field_id%' => 'field.fieldId',
23	  '%field_perm_name%' => 'field.permName',
24	  '%field_name%' => 'field.name',
25	  '%exif_date%' => 'exif.datetime',
26	  '%exif_gps%' => 'exif.gps',
27	  '%exif_gps_lat%' => 'exif.gps_lat',
28	  '%exif_gps_lon%' => 'exif.gps_lon',
29	  '%exif_gps_dms%' => 'exif.gps_dms',
30	  '%exif_gps_dms_lat%' => 'exif.gps_dms_lat',
31	  '%exif_gps_dms_lon%' => 'exif.gps_dms_lon',
32	];
33
34	function getValues()
35	{
36		return [
37		  'object_type' => true,
38		  'object_id' => true,
39		  'field' => true,
40		  'value' => true,
41		  'error_if_missing' => false,
42		];
43	}
44
45	function validate(JitFilter $data)
46	{
47
48		$object_type = $data->object_type->text();
49		$object_id = $data->object_id->int();
50		$field = $data->field->word();
51		$value = $data->value->text();
52
53		if ('tracker_field_' === substr($field, 0, 14)) {
54			$field = substr($field, 14);
55		}
56
57		if ($object_type != 'trackeritem') {
58			throw new Search_Action_Exception(tr('Cannot apply filegal_image_overlay action to an object type %0.', $object_type));
59		}
60
61		$trklib = TikiLib::lib('trk');
62		$info = $trklib->get_item_info($object_id);
63
64		if (! $info) {
65			throw new Search_Action_Exception(tr('Tracker item %0 not found.', $object_id));
66		}
67
68		$definition = Tracker_Definition::get($info['trackerId']);
69
70		$fieldDefinition = $definition->getFieldFromPermName($field);
71		if (! $fieldDefinition) {
72			throw new Search_Action_Exception(tr('Tracker field %0 not found for tracker %1.', $field, $info['trackerId']));
73		}
74
75		if ($fieldDefinition['type'] != 'FG') {
76			throw new Search_Action_Exception(tr('Tracker field %0 is not a Files field type.', $field));
77		}
78
79		if (empty($value)) {
80			throw new Search_Action_Exception(tr('filegal_image_overlay action missing value parameter.'));
81		}
82
83		return true;
84	}
85
86	function execute(JitFilter $data)
87	{
88
89		global $user, $prefs;
90
91		$object_type = $data->object_type->text();
92		$object_id = $data->object_id->int();
93		$field = $data->field->word();
94		$value = $data->value->text();
95		$error_if_missing = $data->error_if_missing->text();
96
97		if ('tracker_field_' === substr($field, 0, 14)) {
98			$field = substr($field, 14);
99		}
100
101		$trklib = TikiLib::lib('trk');
102		$info = $trklib->get_tracker_item($object_id);
103
104		/** @var Tracker_Definition $definition */
105		$definition = Tracker_Definition::get($info['trackerId']);
106		$fieldDefinition = $definition->getFieldFromPermName($field);
107
108		/** @var FileGalLib $fileGal */
109		$fileGal = TikiLib::lib('filegal');
110
111		$fileList = $info[$fieldDefinition['fieldId']];
112
113		if (empty($fileList)) {
114			return true;
115		}
116
117		if ($error_if_missing == 'y') {
118			$error_if_missing = true;
119		} else {
120			$error_if_missing = false;
121		}
122
123		$newFileList = [];
124		foreach (explode(',', $fileList) as $fileId) {
125			$file = $fileGal->get_file($fileId);
126			if (substr($file['filetype'], 0, 6) != 'image/') {
127				$newFileList[] = $fileId;
128				continue;
129			}
130			$galInfo = $fileGal->get_file_gallery_info($file['galleryId']);
131			$newUser = $user ?: $file['user'];
132			$overlayString = $this->generateString(
133				$value,
134				$file,
135				$galInfo,
136				$info,
137				$fieldDefinition,
138				$error_if_missing,
139				$missingKeys
140			);
141			if ($overlayString === false) {
142				throw new Search_Action_Exception(tr(
143					'filegal_image_overlay: Problem processing image "%0", the following values form the template are empty: %1',
144					$file['filename'],
145					implode(', ', $missingKeys)
146				));
147			}
148			$newImage = $this->addTextToImage($file['data'], $overlayString);
149			if ($newImage) {
150				$newFileList[] = $fileGal->update_single_file(
151					$galInfo,
152					$file['filename'],
153					$file['filesize'],
154					$file['filetype'],
155					$newImage,
156					$fileId,
157					$newUser
158				);
159			} else {
160				$newFileList[] = $fileId;
161			}
162		}
163
164		if ($prefs['fgal_keep_fileId'] == 'n') {
165			// new IDs are generated to for the last version we updated
166			$utilities = new Services_Tracker_Utilities;
167			$utilities->updateItem(
168				$definition,
169				[
170				'itemId' => $object_id,
171				'status' => $info['status'],
172				'fields' => [
173				  $field => implode(',', $newFileList),
174				],
175				]
176			);
177		}
178
179		return true;
180	}
181
182	function requiresInput(JitFilter $data)
183	{
184		return false;
185	}
186
187	/**
188	 * Generate a string based on the template provided
189	 *
190	 * @param string $template The template (see $replaceKeys)
191	 * @param array $fileData File Details
192	 * @param array $galleryData Gallery Details
193	 * @param array $itemData Item Details
194	 * @param array $fieldData Field Details
195	 * @param boolean $checkMissing If enable function will return false if some element of the template has a empty value
196	 * @param array $missingTemplateKeys The keys missing
197	 *
198	 * @return string|false
199	 */
200	protected function generateString(
201		$template,
202		$fileData,
203		$galleryData,
204		$itemData,
205		$fieldData,
206		$checkMissing = false,
207		&$missingTemplateKeys = []
208	) {
209		$dataValues = [
210		  'file' => $fileData,
211		  'gallery' => $galleryData,
212		  'item' => $itemData,
213		  'field' => $fieldData,
214		  'parts' => pathinfo($fileData['filename']),
215		  'exif' => $this->getExifArray($fileData),
216		];
217
218		$values = [];
219		foreach ($this->replaceKeys as $search => $dataKey) {
220			list($data, $key) = explode('.', $dataKey);
221			$values[$search] = (isset($dataValues[$data]) && isset($dataValues[$data][$key])) ? $dataValues[$data][$key] : '';
222		}
223
224		if ($checkMissing) {
225			foreach ($values as $key => $value) {
226				if (strpos($template, $key) !== false) {
227					if (empty($value)) {
228						$missingTemplateKeys[] = $key;
229					}
230				}
231			}
232			if (count($missingTemplateKeys)) {
233				return false;
234			}
235		}
236
237		return str_replace(array_keys($values), array_values($values), $template);
238	}
239
240	/**
241	 * Allow adding text as overlay to a image
242	 * @param $imageString
243	 * @param $text
244	 * @return string
245	 */
246	public function addTextToImage($imageString, $text)
247	{
248		$image = Image::create($imageString);
249
250		$image->addTextToImage($text);
251
252		return $image->display();
253	}
254
255	/**
256	 * Get some selected Exif information from a image
257	 * @param $fileData
258	 * @return array
259	 */
260	function getExifArray($fileData)
261	{
262		$exif = [];
263		if ($fileData['filetype'] != 'image/jpeg' || ! function_exists('exif_read_data')) {
264			return $exif;
265		}
266		$exifData = exif_read_data('data://image/jpeg;base64,' . base64_encode($fileData['data']));
267
268		$exif['datetime'] = isset($exifData['DateTimeOriginal']) ? $exifData['DateTimeOriginal'] : '';
269
270		if (isset($exifData['GPSLongitude']) && isset($exifData['GPSLatitude'])) {
271			$latitude = $this->gpsCoordinates($exifData["GPSLatitude"], $exifData['GPSLatitudeRef']);
272			$longitude = $this->gpsCoordinates($exifData["GPSLongitude"], $exifData['GPSLongitudeRef']);
273			$exif['gps'] = $latitude['dd'] . ', ' . $longitude['dd'];
274			$exif['gps_lat'] = $latitude['dd'];
275			$exif['gps_lon'] = $longitude['dd'];
276			$exif['gps_dms'] = $latitude['dms'] . ' ' . $longitude['dms'];
277			$exif['gps_dms_lat'] = $latitude['dms'];
278			$exif['gps_dms_lon'] = $longitude['dms'];
279		} else {
280			$exif['gps'] = '';
281			$exif['gps_lat'] = '';
282			$exif['gps_lon'] = '';
283			$exif['gps_dms'] = '';
284			$exif['gps_dms_lat'] = '';
285			$exif['gps_dms_lon'] = '';
286		}
287
288		return ($exif);
289	}
290
291	/**
292	 * Conver Exif coordinate information into DD and DMS GPS coordinates
293	 * @param $coordinate
294	 * @param $hemisphere
295	 * @return array
296	 */
297	protected function gpsCoordinates($coordinate, $hemisphere)
298	{
299		for ($i = 0; $i < 3; $i++) {
300			$part = explode('/', $coordinate[$i]);
301			if (count($part) == 1) {
302				$coordinate[$i] = $part[0];
303			} else {
304				if (count($part) == 2) {
305					$coordinate[$i] = (float)$part[0] / (float)$part[1];
306				} else {
307					$coordinate[$i] = 0;
308				}
309			}
310		}
311		list($degrees, $minutes, $seconds) = $coordinate;
312
313		$sign = ($hemisphere == 'W' || $hemisphere == 'S') ? -1 : 1;
314		$coordinateDD = sprintf("%.4f", $sign * ($degrees + $minutes / 60 + $seconds / 3600));
315
316		//normalize
317		$minutes += 60 * ($degrees - floor($degrees));
318		$degrees = floor($degrees);
319		$seconds += 60 * ($minutes - floor($minutes));
320		$minutes = floor($minutes);
321
322		//extra normalization, probably not necessary unless you get weird data
323		if ($seconds >= 60) {
324			$minutes += floor($seconds / 60.0);
325			$seconds -= 60 * floor($seconds / 60.0);
326		}
327		if ($minutes >= 60) {
328			$degrees += floor($minutes / 60.0);
329			$minutes -= 60 * floor($minutes / 60.0);
330		}
331
332		$coordinateDMS = sprintf("%d° %d' %.3f'' %s", $degrees, $minutes, $seconds, $hemisphere);
333
334		return [
335		  'dd' => $coordinateDD,
336		  'dms' => $coordinateDMS,
337		];
338	}
339}
340