<?php
/**
* @version	$Id: image_helper.php 15588 2012-10-18 15:30:36Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

	defined('FULL_PATH') or die('restricted access!');

	class ImageHelper extends kHelper {

		/**
		 * File helper reference
		 *
		 * @var FileHelper
		 */
		var $fileHelper = null;

		public function __construct()
		{
			parent::__construct();

			ini_set('gd.jpeg_ignore_warning', 1);
			$this->fileHelper = $this->Application->recallObject('FileHelper');
		}

		/**
		 * Parses format string into array
		 *
		 * @param string $format sample format: "resize:300x500;wm:inc/wm.png|c|-20"
		 * @return Array sample result: Array('max_width' => 300, 'max_height' => 500, 'wm_filename' => 'inc/wm.png', 'h_margin' => 'c', 'v_margin' => -20)
		 */
		function parseFormat($format)
		{
			$res = Array ();

			$format_parts = explode(';', $format);
			foreach ($format_parts as $format_part) {
				if (preg_match('/resize:(\d*)x(\d*)/', $format_part, $regs)) {
					$res['max_width'] = $regs[1];
					$res['max_height'] = $regs[2];
				}
				elseif (preg_match('/wm:([^\|]*)\|([^\|]*)\|([^\|]*)/', $format_part, $regs)) {
					$res['wm_filename'] = FULL_PATH.THEMES_PATH.'/'.$regs[1];
					$res['h_margin'] = strtolower($regs[2]);
					$res['v_margin'] = strtolower($regs[3]);
				}
				elseif (preg_match('/crop:([^\|]*)\|([^\|]*)/', $format_part, $regs)) {
					$res['crop_x'] = strtolower($regs[1]);
					$res['crop_y'] = strtolower($regs[2]);
				}
				elseif ($format_part == 'img_size' || $format_part == 'img_sizes') {
					$res['image_size'] = true;
				}
				elseif (preg_match('/fill:(.*)/', $format_part, $regs)) {
					$res['fill'] = $regs[1];
				} elseif (preg_match('/default:(.*)/', $format_part, $regs)) {
					$res['default'] = FULL_PATH.THEMES_PATH.'/'.$regs[1];
				}
			}

			return $res;
		}

		/**
		 * Resized given image to required dimensions & saves resized image to "resized" subfolder in source image folder
		 *
		 * @param string $src_image full path to image (on server)
		 * @param mixed $max_width maximal allowed resized image width or false if no limit
		 * @param mixed $max_height maximal allowed resized image height or false if no limit
		 * @return string direct url to resized image
		 */
		function ResizeImage($src_image, $max_width, $max_height = false)
		{
			$image_size = false;

			if (is_numeric($max_width)) {
				$params['max_width'] = $max_width;
				$params['max_height'] = $max_height;
			}
			else {
				$params = $this->parseFormat($max_width);

				if (array_key_exists('image_size', $params)) {
					// image_size param shouldn't affect resized file name (crc part)
					$image_size = $params['image_size'];
					unset($params['image_size']);
				}
			}

			if ((!$src_image || !file_exists($src_image)) && array_key_exists('default', $params) && !(defined('DBG_IMAGE_RECOVERY') && DBG_IMAGE_RECOVERY)) {
				$src_image = $params['default'];
			}

			if ($params['max_width'] > 0 || $params['max_height'] > 0) {
				list ($params['target_width'], $params['target_height'], $needs_resize) = $this->GetImageDimensions($src_image, $params['max_width'], $params['max_height'], $params);

				if (!is_numeric($params['max_width'])) {
					$params['max_width'] = $params['target_width'];
				}

				if (!is_numeric($params['max_height'])) {
					$params['max_height'] = $params['target_height'];
				}

				$src_path = dirname($src_image);
				$transform_keys = Array ('crop_x', 'crop_y', 'fill', 'wm_filename');

				if ($needs_resize || array_intersect(array_keys($params), $transform_keys)) {
					// resize required OR watermarking required -> change resulting image name !
					$src_path_escaped = preg_replace('/(\\\[\d]+)/', '\\\\\1', $src_path); // escape replacement patterns, like "\<number>"
					$dst_image = preg_replace('/^'.preg_quote($src_path, '/').'(.*)\.(.*)$/', $src_path_escaped . DIRECTORY_SEPARATOR . 'resized\\1_' . crc32(serialize($params)) . '.\\2', $src_image);

					$this->fileHelper->CheckFolder( dirname($dst_image) );

					if (!file_exists($dst_image) || filemtime($src_image) > filemtime($dst_image)) {
						// resized image not available OR should be recreated due source image change
						$params['dst_image'] = $dst_image;
						$image_resized = $this->ScaleImage($src_image, $params);
						if (!$image_resized) {
							// resize failed, because of server error
							$dst_image = $src_image;
						}
					}

					// resize/watermarking ok
					$src_image = $dst_image;
				}
			}

			if ($image_size) {
				// return only image size (resized or not)
				$image_info = $this->getImageInfo($src_image);
				return $image_info ? $image_info[3] : '';
			}

			return $this->fileHelper->pathToUrl($src_image);
		}

		/**
		 * Proportionally resizes given image to destination dimensions
		 *
		 * @param string $src_image full path to source image (already existing)
		 * @param Array $params
		 * @return bool
		 */
		function ScaleImage($src_image, $params)
		{
			$image_info = $this->getImageInfo($src_image);
			if (!$image_info) {
				return false;
			}

			/*list ($params['max_width'], $params['max_height'], $resized) = $this->GetImageDimensions($src_image, $params['max_width'], $params['max_height'], $params);
			if (!$resized) {
				// image dimensions are smaller or equals to required dimensions
				return false;
			}*/

			if (!$this->Application->ConfigValue('ForceImageMagickResize') && function_exists('imagecreatefromjpeg')) {
				// try to resize using GD
				$resize_map = Array (
					'image/jpeg' => 'imagecreatefromjpeg:imagejpeg:jpg',
					'image/gif' => 'imagecreatefromgif:imagegif:gif',
					'image/png' => 'imagecreatefrompng:imagepng:png',
					'image/bmp' => 'imagecreatefrombmp:imagejpeg:bmp',
					'image/x-ms-bmp' => 'imagecreatefrombmp:imagejpeg:bmp',
				);

				$mime_type = $image_info['mime'];
				if (!isset($resize_map[$mime_type])) {
					return false;
				}

				list ($read_function, $write_function, $file_extension) = explode(':', $resize_map[$mime_type]);

				// when source image has large dimensions (over 1MB filesize), then 16M is not enough
				kUtil::setResourceLimit();

				$src_image_rs = @$read_function($src_image);
				if ($src_image_rs) {
					$dst_image_rs = imagecreatetruecolor($params['target_width'], $params['target_height']); // resize target size

					$preserve_transparency = ($file_extension == 'gif') || ($file_extension == 'png');

					if ($preserve_transparency) {
						// preserve transparency of PNG and GIF images
						$dst_image_rs = $this->_preserveTransparency($src_image_rs, $dst_image_rs, $image_info[2]);
					}

					// 1. resize
					imagecopyresampled($dst_image_rs, $src_image_rs, 0, 0, 0, 0, $params['target_width'], $params['target_height'], $image_info[0], $image_info[1]);

					$watermark_size = 'target';

					if (array_key_exists('crop_x', $params) || array_key_exists('crop_y', $params)) {
						// 2.1. crop image to given size
						$dst_image_rs =& $this->_cropImage($dst_image_rs, $params, $preserve_transparency ? $image_info[2] : false);
						$watermark_size = 'max';
					} elseif (array_key_exists('fill', $params)) {
						// 2.2. fill image margins from resize with given color
						$dst_image_rs =& $this->_applyFill($dst_image_rs, $params, $preserve_transparency ? $image_info[2] : false);
						$watermark_size = 'max';
					}

					// 3. apply watermark
					$dst_image_rs =& $this->_applyWatermark($dst_image_rs, $params[$watermark_size . '_width'], $params[$watermark_size . '_height'], $params);

					if ($write_function == 'imagegif') {
						return @$write_function($dst_image_rs, $params['dst_image']);
					}

					return @$write_function($dst_image_rs, $params['dst_image'], $write_function == 'imagepng' ? 0 : 100);
				}
			}
			else {
				// try to resize using ImageMagick
				// TODO: implement crop and watermarking using imagemagick
				exec('/usr/bin/convert '.$src_image.' -resize '.$params['target_width'].'x'.$params['target_height'].' '.$params['dst_image'], $shell_output, $exec_status);
				return $exec_status == 0;
			}

			return false;
		}

		/**
		 * Preserve transparency for GIF and PNG images
		 *
		 * @param resource $src_image_rs
		 * @param resource $dst_image_rs
		 * @param int $image_type
		 * @return resource
		 */
		function _preserveTransparency($src_image_rs, $dst_image_rs, $image_type)
		{
			$transparent_index = imagecolortransparent($src_image_rs);

			// if we have a specific transparent color
			if ( $transparent_index >= 0 && $transparent_index < imagecolorstotal($src_image_rs) ) {
				// get the original image's transparent color's RGB values
				$transparent_color = imagecolorsforindex($src_image_rs, $transparent_index);

				// allocate the same color in the new image resource
				$transparent_index = imagecolorallocate($dst_image_rs, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']);

				// completely fill the background of the new image with allocated color
				imagefill($dst_image_rs, 0, 0, $transparent_index);

				// set the background color for new image to transparent
				imagecolortransparent($dst_image_rs, $transparent_index);

				return $dst_image_rs;
			}

			// always make a transparent background color for PNGs that don't have one allocated already
			if ( $image_type == IMAGETYPE_PNG ) {
				// turn off transparency blending (temporarily)
				imagealphablending($dst_image_rs, false);

				// create a new transparent color for image
				$transparent_color = imagecolorallocatealpha($dst_image_rs, 0, 0, 0, 127);

				// completely fill the background of the new image with allocated color
				imagefill($dst_image_rs, 0, 0, $transparent_color);

				// restore transparency blending
				imagesavealpha($dst_image_rs, true);
			}

			return $dst_image_rs;
		}

		/**
		 * Fills margins (if any) of resized are with given color
		 *
		 * @param resource $src_image_rs resized image resource
		 * @param Array $params crop parameters
		 * @param int|bool $image_type
		 * @return resource
		 */
		function &_applyFill(&$src_image_rs, $params, $image_type = false)
		{
			$x_position = round(($params['max_width'] - $params['target_width']) / 2); // center
			$y_position = round(($params['max_height'] - $params['target_height']) / 2); // center

			// crop resized image
			$fill_image_rs = imagecreatetruecolor($params['max_width'], $params['max_height']);

			if ($image_type !== false) {
				$fill_image_rs = $this->_preserveTransparency($src_image_rs, $fill_image_rs, $image_type);
			}

			$fill = $params['fill'];

			if (substr($fill, 0, 1) == '#') {
				// hexdecimal color
				$color = imagecolorallocate($fill_image_rs, hexdec( substr($fill, 1, 2) ), hexdec( substr($fill, 3, 2) ), hexdec( substr($fill, 5, 2) ));
			}
			else {
				// for now we don't support color names, but we will in future
				return $src_image_rs;
			}

			imagefill($fill_image_rs, 0, 0, $color);
			imagecopy($fill_image_rs, $src_image_rs, $x_position, $y_position, 0, 0, $params['target_width'], $params['target_height']);

			return $fill_image_rs;
		}

		/**
		 * Crop given image resource using given params and return resulting image resource
		 *
		 * @param resource $src_image_rs resized image resource
		 * @param Array $params crop parameters
		 * @param int|bool $image_type
		 * @return resource
		 */
		function &_cropImage(&$src_image_rs, $params, $image_type = false)
		{
			if ($params['crop_x'] == 'c') {
				$x_position = round(($params['max_width'] - $params['target_width']) / 2); // center
			}
			elseif ($params['crop_x'] >= 0) {
				$x_position = $params['crop_x']; // margin from left
			}
			else {
				$x_position = $params['target_width'] - ($params['max_width'] - $params['crop_x']); // margin from right
			}

			if ($params['crop_y'] == 'c') {
				$y_position = round(($params['max_height'] - $params['target_height']) / 2); // center
			}
			elseif ($params['crop_y'] >= 0) {
				$y_position = $params['crop_y']; // margin from top
			}
			else {
				$y_position = $params['target_height'] - ($params['max_height'] - $params['crop_y']); // margin from bottom
			}

			// crop resized image
			$crop_image_rs = imagecreatetruecolor($params['max_width'], $params['max_height']);

			if ($image_type !== false) {
				$crop_image_rs = $this->_preserveTransparency($src_image_rs, $crop_image_rs, $image_type);
			}

			if (array_key_exists('fill', $params)) {
				// fill image margins from resize with given color
				$crop_image_rs =& $this->_applyFill($crop_image_rs, $params, $image_type);
			}

			imagecopy($crop_image_rs, $src_image_rs, $x_position, $y_position, 0, 0, $params['target_width'], $params['target_height']);

			return $crop_image_rs;
		}

		/**
		 * Apply watermark (transparent PNG image) to given resized image resource
		 *
		 * @param resource $src_image_rs
		 * @param int $max_width
		 * @param int $max_height
		 * @param Array $params
		 * @return resource
		 */
		function &_applyWatermark(&$src_image_rs, $max_width, $max_height, $params)
		{
			$watermark_file = array_key_exists('wm_filename', $params) ? $params['wm_filename'] : false;

			if (!$watermark_file || !file_exists($watermark_file)) {
				// no watermark required, or provided watermark image is missing
				return $src_image_rs;
			}

			$watermark_img_rs = imagecreatefrompng($watermark_file);
			list ($watermark_width, $watermark_height) = $this->getImageInfo($watermark_file);

			imagealphablending($src_image_rs, true);

			if ($params['h_margin'] == 'c') {
				$x_position = round($max_width / 2 - $watermark_width / 2); // center
			}
			elseif ($params['h_margin'] >= 0) {
				$x_position = $params['h_margin']; // margin from left
			}
			else {
				$x_position = $max_width - ($watermark_width - $params['h_margin']); // margin from right
			}

			if ($params['v_margin'] == 'c') {
				$y_position = round($max_height / 2 - $watermark_height / 2); // center
			}
			elseif ($params['v_margin'] >= 0) {
				$y_position = $params['v_margin']; // margin from top
			}
			else {
				$y_position = $max_height - ($watermark_height - $params['v_margin']); // margin from bottom
			}

			imagecopy($src_image_rs, $watermark_img_rs, $x_position, $y_position, 0, 0, $watermark_width, $watermark_height);

			return $src_image_rs;
		}

		/**
		 * Returns destination image size without actual resizing (useful for <img .../> HTML tag)
		 *
		 * @param string $src_image full path to source image (already existing)
		 * @param int $dst_width destination image width (in pixels)
		 * @param int $dst_height destination image height (in pixels)
		 * @param Array $params
		 * @return Array resized image dimensions (0 - width, 1 - height)
		 */
		function GetImageDimensions($src_image, $dst_width, $dst_height, $params)
		{
			$image_info = $this->getImageInfo($src_image);
			if (!$image_info) {
				return false;
			}

			$orig_width = $image_info[0];
			$orig_height = $image_info[1];

			$too_large = is_numeric($dst_width) ? ($orig_width > $dst_width) : false;
			$too_large = $too_large || (is_numeric($dst_height) ? ($orig_height > $dst_height) : false);

			if ($too_large) {
				$width_ratio = $dst_width ? $dst_width / $orig_width : 1;
				$height_ratio = $dst_height ? $dst_height / $orig_height : 1;

				if (array_key_exists('crop_x', $params) || array_key_exists('crop_y', $params)) {
					// resize by smallest inverted radio
					$resize_by = $this->_getCropImageMinRatio($image_info, $dst_width, $dst_height);

					if ($resize_by === false) {
						return Array ($orig_width, $orig_height, false);
					}

					$ratio = $resize_by == 'width' ? $width_ratio : $height_ratio;
				}
				else {
					$ratio = min($width_ratio, $height_ratio);
				}

				$width = ceil($orig_width * $ratio);
				$height = ceil($orig_height * $ratio);
			}
			else {
				$width = $orig_width;
				$height = $orig_height;
			}

			return Array ($width, $height, $too_large);
		}

		/**
		 * Returns ratio type with smaller relation of original size to target size
		 *
		 * @param Array $image_info image information from "ImageHelper::getImageInfo"
		 * @param int $dst_width destination image width (in pixels)
		 * @param int $dst_height destination image height (in pixels)
		 * @return Array
		 */
		function _getCropImageMinRatio($image_info, $dst_width, $dst_height)
		{
			$width_ratio = $dst_width ? $image_info[0] / $dst_width : 1;
			$height_ratio = $dst_height ? $image_info[1] / $dst_height : 1;
			$minimal_ratio = min($width_ratio, $height_ratio);

			if ($minimal_ratio < 1) {
				// ratio is less then 1, image will be enlarged -> don't allow that
				return false;
			}

			return $width_ratio < $height_ratio ? 'width' : 'height';
		}

		/**
		 * Returns image dimensions + checks if given file is existing image
		 *
		 * @param string $src_image full path to source image (already existing)
		 * @return mixed
		 */
		function getImageInfo($src_image)
		{
			if (!file_exists($src_image)) {
				return false;
			}

			$image_info = @getimagesize($src_image);
			if (!$image_info) {
				trigger_error('Image <b>'.$src_image.'</b> <span class="debug_error">missing or invalid</span>', E_USER_WARNING);
				return false;
			}

			return $image_info;
		}

		/**
		 * Returns maximal image size (width & height) among fields specified
		 *
		 * @param kDBItem $object
		 * @param string $fields
		 * @param string $format any format, that returns full url (e.g. files_resized:WxH, resize:WxH, full_url, full_urls)
		 * @return string
		 */
		function MaxImageSize(&$object, $fields, $format = null)
		{
			static $cached_sizes = Array ();

			$cache_key = $object->getPrefixSpecial().'_'.$object->GetID();
			if (!isset($cached_sizes[$cache_key])) {
				$images = Array ();

				$fields = explode(',', $fields);
				foreach ($fields as $field) {
					$image_data = $object->GetField($field, $format);
					if (!$image_data) {
						continue;
					}

					$images = array_merge($images, explode('|', $image_data));
				}

				$max_width = 0;
				$max_height = 0;
				$base_url = rtrim($this->Application->BaseURL(), '/');

				foreach ($images as $image_url) {
					$image_path = preg_replace('/^'.preg_quote($base_url, '/').'(.*)/', FULL_PATH.'\\1', $image_url);
					$image_info = $this->getImageInfo($image_path);
					$max_width = max($max_width, $image_info[0]);
					$max_height = max($max_height, $image_info[1]);
				}

				$cached_sizes[$cache_key] = Array ($max_width, $max_height);
			}

			return $cached_sizes[$cache_key];
		}

		/**
		 * Puts existing item images (from sub-item) to virtual fields (in main item)
		 *
		 * @param kCatDBItem|kDBItem $object
		 */
		function LoadItemImages(&$object)
		{
			if (!$this->_canUseImages($object)) {
				return ;
			}

			$max_image_count = $this->Application->ConfigValue($object->Prefix.'_MaxImageCount');

			$sql = 'SELECT *
					FROM '.TABLE_PREFIX.'CatalogImages
					WHERE ResourceId = '.$object->GetDBField('ResourceId').'
					ORDER BY Priority DESC
					LIMIT 0, ' . (int)$max_image_count;
			$item_images = $this->Conn->Query($sql);

			$image_counter = 1;
			foreach ($item_images as $item_image) {
				$image_path = $item_image['ThumbPath'];
				if ($item_image['DefaultImg'] == 1 || $item_image['Name'] == 'main') {
					// process primary image separately
					if ( $object->isField('PrimaryImage') ) {
						$object->SetDBField('PrimaryImage', $image_path);
						$object->SetOriginalField('PrimaryImage', $image_path);
						$object->SetFieldOption('PrimaryImage', 'original_field', $item_image['Name']);

						$this->_loadCustomFields($object, $item_image, 0);
					}
					continue;
				}

				if (abs($item_image['Priority'])) {
					// use Priority as image counter, when specified
					$image_counter = abs($item_image['Priority']);
				}

				if ( $object->isField('Image'.$image_counter) ) {
					$object->SetDBField('Image'.$image_counter, $image_path);
					$object->SetOriginalField('Image'.$image_counter, $image_path);
					$object->SetFieldOption('Image'.$image_counter, 'original_field', $item_image['Name']);

					$this->_loadCustomFields($object, $item_image, $image_counter);
				}
				$image_counter++;
			}
		}

		/**
		 * Saves newly uploaded images to external image table
		 *
		 * @param kCatDBItem|kDBItem $object
		 */
		function SaveItemImages(&$object)
		{
			if (!$this->_canUseImages($object)) {
				return ;
			}

			$table_name = $this->Application->getUnitOption('img', 'TableName');
			$max_image_count = $this->Application->getUnitOption($object->Prefix, 'ImageCount'); // $this->Application->ConfigValue($object->Prefix.'_MaxImageCount');

			$i = 0;
			while ($i < $max_image_count) {
				$field = $i ? 'Image'.$i : 'PrimaryImage';
				$field_options = $object->GetFieldOptions($field);

				$image_src = $object->GetDBField($field);
				if ($image_src) {
					if (isset($field_options['original_field'])) {
						$key_clause = 'Name = '.$this->Conn->qstr($field_options['original_field']).' AND ResourceId = '.$object->GetDBField('ResourceId');

						if ($object->GetDBField('Delete'.$field)) {
							// if item was cloned, then new filename is in db (not in $image_src)
							$sql = 'SELECT ThumbPath
									FROM '.$table_name.'
									WHERE '.$key_clause;
							$image_src = $this->Conn->GetOne($sql);
							if (@unlink(FULL_PATH.$image_src)) {
								$sql = 'DELETE FROM '.$table_name.'
										WHERE '.$key_clause;
								$this->Conn->Query($sql);
							}
						}
						else {
							// image record found -> update
							$fields_hash = Array (
								'ThumbPath' => $image_src,
							);
							$this->_saveCustomFields($object, $fields_hash, $i);

							$this->Conn->doUpdate($fields_hash, $table_name, $key_clause);
						}
					}
					else {
						// image record not found -> create
						$fields_hash = Array (
							'ResourceId' => $object->GetDBField('ResourceId'),
							'Name' => $field,
							'AltName' => $field,
							'Enabled' => STATUS_ACTIVE,
							'DefaultImg' => $i ? 0 : 1, // first image is primary, others not primary
							'ThumbPath' => $image_src,
							'Priority' => ($i == 0)? 0 : $i * (-1),
						);
						$this->_saveCustomFields($object, $fields_hash, $i);

						$this->Conn->doInsert($fields_hash, $table_name);
						$field_options['original_field'] = $field;
						$object->SetFieldOptions($field, $field_options);
					}
				}
				$i++;
			}
		}

		/**
		 * Adds ability to load custom fields along with main image field
		 *
		 * @param kCatDBItem|kDBItem $object
		 * @param Array $fields_hash
		 * @param int $counter 0 - primary image, other number - additional image number
		 */
		function _loadCustomFields(&$object, $fields_hash, $counter)
		{
			$field_name = $counter ? 'Image' . $counter . 'Alt' : 'PrimaryImageAlt';

			$object->SetDBField($field_name, (string)$fields_hash['AltName']);
		}

		/**
		 * Adds ability to save custom field along with main image save
		 *
		 * @param kCatDBItem|kDBItem $object
		 * @param Array $fields_hash
		 * @param int $counter 0 - primary image, other number - additional image number
		 */
		function _saveCustomFields(&$object, &$fields_hash, $counter)
		{
			$field_name = $counter ? 'Image' . $counter . 'Alt' : 'PrimaryImageAlt';

			$fields_hash['AltName'] = (string)$object->GetDBField($field_name);
		}

		/**
		 * Checks, that item can use image upload capabilities
		 *
		 * @param kCatDBItem|kDBItem $object
		 * @return bool
		 */
		function _canUseImages(&$object)
		{
			$prefix = $object->Prefix == 'p' ? 'img' : $object->Prefix . '-img';

			return $this->Application->prefixRegistred($prefix);
		}
	}