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) ) { $default_image = FULL_PATH . THEMES_PATH . '/' . $regs[1]; if ( strpos($default_image, '../') !== false ) { $default_image = realpath($default_image); } $res['default'] = $default_image; } } 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 * @throws RuntimeException When image doesn't exist. */ 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 ( !strlen($src_image) || !file_exists($src_image) ) { throw new RuntimeException(sprintf('Image "%s" doesn\'t exist', $src_image)); } if ( !$this->isSVG($src_image) && ($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']; } // Optimize, because when cropping from center without resize we'll get same image back. if ( !$needs_resize && isset($params['crop_x']) && $params['crop_x'] == 'c' && $params['crop_y'] == 'c' ) { unset($params['crop_x'], $params['crop_y'], $params['fill']); } $src_path = dirname($src_image); $transform_keys = Array ('crop_x', 'crop_y', 'fill', 'wm_filename'); // Resize required OR watermarking required -> change resulting image name ! if ( $needs_resize || array_intersect(array_keys($params), $transform_keys) ) { // Escape replacement patterns, like "\". $src_path_escaped = preg_replace('/(\\\[\d]+)/', '\\\\\1', $src_path); $params_hash = kUtil::crc32(serialize($this->fileHelper->makeRelative($params))); $dst_image = preg_replace( '/^' . preg_quote($src_path, '/') . '(.*)\.(.*)$/', $src_path_escaped . '\\1_' . $params_hash . '.\\2', $src_image ); // Keep resized version of all images under "/system/thumbs/" folder. $dst_image = preg_replace('/^' . preg_quote(FULL_PATH, '/') . '/', '', $dst_image, 1); $dst_image = FULL_PATH . THUMBS_PATH . $dst_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 ) { if ( $this->isSVG($src_image) ) { return 'width="' . $params['max_width'] . '" height="' . $params['max_height'] . '"'; } // 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 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) { // The SVG file is in vector format and can scale to any size. if ( $this->isSVG($src_image) ) { return array($dst_width, $dst_height, false); } $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 = (int)round($orig_width * $ratio); $height = (int)round($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) || $this->isSVG($src_image) ) { return false; } $image_info = @getimagesize($src_image); if (!$image_info) { trigger_error('Image '.$src_image.' missing or invalid', E_USER_WARNING); return false; } return $image_info; } /** * Checks if image is an SVG file. * * @param string $src_image Full path to source image (already existing). * * @return boolean */ protected function isSVG($src_image) { return pathinfo($src_image, PATHINFO_EXTENSION) == 'svg'; } /** * 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); } }