1095 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			1095 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| 
 | |
| /**
 | |
|  * @author          Tassos Marinos <info@tassos.gr>
 | |
|  * @link            https://www.tassos.gr
 | |
|  * @copyright       Copyright © 2024 Tassos All Rights Reserved
 | |
|  * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 | |
|  */
 | |
| 
 | |
| namespace NRFramework\Widgets;
 | |
| 
 | |
| defined('_JEXEC') or die;
 | |
| 
 | |
| use Joomla\CMS\Language\Text;
 | |
| use \NRFramework\Helpers\Widgets\Gallery as GalleryHelper;
 | |
| use NRFramework\Mimes;
 | |
| use NRFramework\File;
 | |
| use NRFramework\Image;
 | |
| 
 | |
| /**
 | |
|  *  Gallery
 | |
|  */
 | |
| class Gallery extends Widget
 | |
| {
 | |
| 	/**
 | |
| 	 * Widget default options
 | |
| 	 *
 | |
| 	 * @var array
 | |
| 	 */
 | |
| 	protected $widget_options = [
 | |
| 		/**
 | |
| 		 * The gallery items source.
 | |
| 		 * 
 | |
| 		 * This can be one or combination of the following:
 | |
| 		 * 
 | |
| 		 * - Path to a relative folder (String)
 | |
| 		 * 		/path/to/folder
 | |
| 		 * - Path to a relative image (String)
 | |
| 		 * 		/path/to/folder/image.png
 | |
| 		 * - URL of an image (String)
 | |
| 		 * 		https://example.com/path/to/image.png
 | |
| 		 * - Array of images (Array)
 | |
| 		 * 		[
 | |
| 		 * 			'url' => 'https://example.com/path/to/image.png',
 | |
| 		 * 			'thumbnail_url' => 'https://example.com/path/to/image_thumb.png',
 | |
| 		 * 			'caption' => 'This is a caption',
 | |
| 		 * 			'thumbnail_size' => [
 | |
| 		 * 				'width' => '200',
 | |
| 		 * 				'height' => '200'
 | |
| 		 * 			],
 | |
| 		 * 			'module' => 'position-2'
 | |
| 		 * 		]
 | |
| 		 * 
 | |
| 		 * 		- The `url` property is required.
 | |
| 		 * 		- All other properties are optional.
 | |
| 		 */
 | |
| 		'items' => [],
 | |
| 
 | |
| 		/**
 | |
| 		 * Set the ordering.
 | |
| 		 * 
 | |
| 		 * Available values:
 | |
| 		 * - default
 | |
| 		 * - alphabetical
 | |
| 		 * - reverse_alphabetical
 | |
| 		 * - random
 | |
| 		 */
 | |
| 		'ordering' => 'default',
 | |
| 
 | |
| 		// Set the module key to display whenever we are viewing a single item's lightbox, appearing after the image
 | |
| 		'module' => '',
 | |
| 
 | |
| 		/**
 | |
| 		 * Set the style of the gallery:
 | |
| 		 * 
 | |
| 		 * - masonry
 | |
| 		 * - grid
 | |
| 		 * - justified
 | |
| 		 */
 | |
| 		'style' => 'masonry',
 | |
| 
 | |
| 		// Each item height (in pixels) in Justified layout
 | |
| 		'justified_item_height' => null,
 | |
| 
 | |
| 		/**
 | |
| 		 * Define the columns per supported device.
 | |
| 		 * 
 | |
| 		 * Example value:
 | |
| 		 * - An integer representing the columns for all devices: 3
 | |
| 		 * - A value for each device:
 | |
| 		 * [
 | |
| 		 * 	'desktop' => 3,
 | |
| 		 * 	'tablet' => 2,
 | |
| 		 * 	'mobile' => 1
 | |
| 		 * ]
 | |
| 		 */
 | |
| 		'columns' => 4,
 | |
| 
 | |
| 		/**
 | |
| 		 * Define the gap per gallery item per supported device.
 | |
| 		 * 
 | |
| 		 * Example value:
 | |
| 		 * - An integer representing the gap for all devices: 30
 | |
| 		 * - A value for each device:
 | |
| 		 * [
 | |
| 		 * 	'desktop' => 30,
 | |
| 		 * 	'tablet' => 20,
 | |
| 		 * 	'mobile' => 10
 | |
| 		 * ]
 | |
| 		 */
 | |
| 		'gap' => 15,
 | |
| 
 | |
| 		/**
 | |
| 		 * Set the allowed file types.
 | |
| 		 * 
 | |
| 		 * This is used to validate the files loaded via a directory or a fixed path to an image.
 | |
| 		 * 
 | |
| 		 * Given URLs are not validated by this setting.
 | |
| 		 */
 | |
| 		'allowed_file_types' => '.jpg, .jpeg, .png',
 | |
| 
 | |
| 		// Gallery Items wrapper CSS classes
 | |
| 		'gallery_items_css' => '',
 | |
| 
 | |
| 		// Set whether to display a lightbox
 | |
| 		'lightbox' => false,
 | |
| 
 | |
| 		/**
 | |
| 		 * Source Image
 | |
| 		 */
 | |
| 		/**
 | |
| 		 * Should the source image be resized?
 | |
| 		 * 
 | |
| 		 * If `original_image_resize` is false, then the source image will appear
 | |
| 		 * in the lightbox (also if `thumbnails` is false, the source image will also appear as the thumbnail)
 | |
| 		 * 
 | |
| 		 * Issue: if this image is a raw photo, there are chances it will increase the page load in order for the browser to display the image.
 | |
| 		 * 
 | |
| 		 * By enabling this, we resize the source image to our desired dimensions and reduce the page load in the above scenario.
 | |
| 		 * 
 | |
| 		 * Note: Always ensure the source image is backed up to a safe place.
 | |
| 		 * Note 2: We require thumbnails or original image resize to be enabled for this to work.
 | |
| 		 * 		 Reason: The above options if enabled generate the gallery_info.txt file in the /cache folder which helps us
 | |
| 		 * 				 generate the source images only if necessary(image has been edited), otherwise, the source image would
 | |
| 		 * 				 be generated on each page refresh.
 | |
| 		 */
 | |
| 		'source_image_resize' => false,
 | |
| 
 | |
| 		// Source image resize width
 | |
| 		'source_image_resize_width' => 1920,
 | |
| 		
 | |
| 		// Source image resize height
 | |
| 		'source_image_resize_height' => null,
 | |
| 
 | |
| 		// Source image resize method (crop, stretch, fit)
 | |
| 		'source_image_resize_method' => 'crop',
 | |
| 
 | |
| 		// Source image resize quality
 | |
| 		'source_image_resize_image_quality' => 80,
 | |
| 
 | |
| 		/**
 | |
| 		 * Original Image
 | |
| 		 */
 | |
| 		// Should the original uploaded image be resized?
 | |
| 		'original_image_resize' => false,
 | |
| 
 | |
| 		// Resize method (crop, stretch, fit)
 | |
| 		'original_image_resize_method' => 'crop',
 | |
| 
 | |
| 		/**
 | |
| 		 * Original Image Resize Width.
 | |
| 		 * 
 | |
| 		 * If `original_image_resize_height` is null, resizes via the width to keep the aspect ratio.
 | |
| 		 */
 | |
| 		'original_image_resize_width' => 1920,
 | |
| 
 | |
| 		// Original Image Resize Height
 | |
| 		'original_image_resize_height' => null,
 | |
| 
 | |
| 		// Original Image Resize Quality
 | |
| 		'original_image_resize_image_quality' => 80,
 | |
| 
 | |
| 		/**
 | |
| 		 * Thumbnails
 | |
| 		 */
 | |
| 		// Set whether to generate thumbnails on-the-fly
 | |
| 		'thumbnails' => false,
 | |
| 
 | |
| 		// Resize method (crop, stretch, fit)
 | |
| 		'thumb_resize_method' => 'crop',
 | |
| 
 | |
| 		// Thumbnails width
 | |
| 		'thumb_width' => 300,
 | |
| 
 | |
| 		// Thumbnails height
 | |
| 		'thumb_height' => null,
 | |
| 
 | |
| 		// The CSS class of the thumbnail
 | |
| 		'thumb_class' => '',
 | |
| 
 | |
| 		/**
 | |
| 		 * Set whether to resize the images whenever their source file changes.
 | |
| 		 * 
 | |
| 		 * i.e. If we edit the source image and also need to recreate the resized original image or thumbnail.
 | |
| 		 * This is rather useful otherwise we would have to delete the resized image or thumbnail in order for it to be recreated.
 | |
| 		 */
 | |
| 		'force_resizing' => false,
 | |
| 
 | |
| 		// Destination folder
 | |
| 		'destination_folder' => 'cache/tassos/gallery',
 | |
| 
 | |
| 		// Attributes set to the wrapper
 | |
| 		'atts' => '',
 | |
| 
 | |
| 		// The unique hash of this gallery based on its options
 | |
| 		'hash' => null,
 | |
| 
 | |
| 		// Set whether to show warnings when an image that has been set to appear does not exist.
 | |
| 		'show_warnings' => true,
 | |
| 
 | |
| 		/**
 | |
| 		 * This is a list that's populated
 | |
| 		 * automatically by looking for tags
 | |
| 		 * in each gallery item.
 | |
| 		 */
 | |
| 		'tags' => [],
 | |
| 
 | |
| 		/**
 | |
| 		 * Set the tags position.
 | |
| 		 * 
 | |
| 		 * Available values:
 | |
| 		 * - disabled (No tags will appear in the gallery)
 | |
| 		 * - above
 | |
| 		 * - below
 | |
| 		 */
 | |
| 		'tags_position' => 'disabled',
 | |
| 
 | |
| 		/**
 | |
| 		 * Set the tags ordering.
 | |
| 		 * 
 | |
| 		 * Available values:
 | |
| 		 * - default
 | |
| 		 * - alphabetical
 | |
| 		 * - reverse_alphabetical
 | |
| 		 * - random
 | |
| 		 */
 | |
| 		'tags_ordering' => 'default',
 | |
| 
 | |
| 		// Set the label of the "All Tags" option
 | |
| 		'all_tags_item_label' => 'All',
 | |
| 
 | |
| 		/**
 | |
| 		 * Set whether to show the tags filter on mobile devices,
 | |
| 		 * show them as a dropdown or disable them.
 | |
| 		 * 
 | |
| 		 * Available values:
 | |
| 		 * - show
 | |
| 		 * - dropdown
 | |
| 		 * - disabled
 | |
| 		 */
 | |
| 		'tags_mobile' => 'show',
 | |
| 
 | |
| 		'tags_text_color' => '#555',
 | |
| 		
 | |
| 		'tags_text_color_hover' => '#fff',
 | |
| 
 | |
| 		'tags_bg_color_hover' => '#1E3148',
 | |
| 
 | |
| 		// Widget Custom CSS
 | |
| 		'custom_css' => ''
 | |
| 	];
 | |
| 
 | |
| 	public function __construct($options = [])
 | |
| 	{
 | |
| 		parent::__construct($options);
 | |
| 
 | |
| 		$this->prepare();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Prepares the Gallery.
 | |
| 	 * 
 | |
| 	 * @return  void
 | |
| 	 */
 | |
| 	private function prepare()
 | |
| 	{
 | |
| 		$this->options['hash'] = $this->getHash();
 | |
| 
 | |
| 		$this->options['destination_folder'] = JPATH_ROOT . DIRECTORY_SEPARATOR . $this->options['destination_folder'] . DIRECTORY_SEPARATOR . $this->options['hash'] . DIRECTORY_SEPARATOR;
 | |
| 
 | |
| 		$this->parseGalleryItems();
 | |
| 
 | |
| 		$this->cleanDestinationFolder();
 | |
| 		
 | |
| 		$this->resizeSourceImages();
 | |
| 		$this->resizeOriginalImages();
 | |
| 		$this->createThumbnails();
 | |
| 		
 | |
| 		// Set style on the gallery items container.
 | |
| 		$this->options['gallery_items_css'] .= ' ' . $this->getStyle();
 | |
| 
 | |
| 		// Set class to trigger lightbox.
 | |
| 		if ($this->options['lightbox'])
 | |
| 		{
 | |
| 			$this->options['css_class'] .= ' lightbox';
 | |
| 		}
 | |
| 
 | |
| 		$this->setAtts();
 | |
| 
 | |
| 		$this->prepareItems();
 | |
| 
 | |
| 		$this->setOrdering();
 | |
| 
 | |
| 		if ($this->options['load_css_vars'])
 | |
| 		{
 | |
| 			$this->options['custom_css'] = $this->getWidgetCSS();
 | |
| 		}
 | |
| 
 | |
| 		$this->prepareTags();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Sets the data attributes.
 | |
| 	 * 
 | |
| 	 * @return  void
 | |
| 	 */
 | |
| 	private function setAtts()
 | |
| 	{
 | |
| 		$atts = [];
 | |
| 
 | |
| 		$atts[] = 'data-id="' . $this->options['id'] . '"';
 | |
| 
 | |
| 		if ($this->options['style'] === 'justified' && $this->options['justified_item_height'])
 | |
| 		{
 | |
| 			$atts[] = 'data-item-height="' . $this->options['justified_item_height'] . '"';
 | |
| 		}
 | |
| 
 | |
| 		$this->options['atts'] = implode(' ', $atts);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Sets the ordering of the gallery.
 | |
| 	 * 
 | |
| 	 * @return  void
 | |
| 	 */
 | |
| 	private function setOrdering()
 | |
| 	{
 | |
| 		switch ($this->options['ordering']) {
 | |
| 			case 'random':
 | |
| 				shuffle($this->options['items']);
 | |
| 				break;
 | |
| 			case 'alphabetical':
 | |
| 				usort($this->options['items'], [$this, 'compareByThumbnailASC']);
 | |
| 				break;
 | |
| 			case 'reverse_alphabetical':
 | |
| 				usort($this->options['items'], [$this, 'compareByThumbnailDESC']);
 | |
| 				break;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Compares tag names in ASC order
 | |
| 	 * 
 | |
| 	 * @param   array  $a
 | |
| 	 * @param   array  $b
 | |
| 	 * 
 | |
| 	 * @return  bool
 | |
| 	 */
 | |
| 	public function compareByTagNameASC($a, $b)
 | |
| 	{
 | |
| 		return strcmp($a, $b);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Compares tag names in DESC order
 | |
| 	 * 
 | |
| 	 * @param   array  $a
 | |
| 	 * @param   array  $b
 | |
| 	 * 
 | |
| 	 * @return  bool
 | |
| 	 */
 | |
| 	public function compareByTagNameDESC($a, $b)
 | |
| 	{
 | |
| 		return strcmp($b, $a);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Compares thumbnail file names in ASC order
 | |
| 	 * 
 | |
| 	 * @param   array  $a
 | |
| 	 * @param   array  $b
 | |
| 	 * 
 | |
| 	 * @return  bool
 | |
| 	 */
 | |
| 	public function compareByThumbnailASC($a, $b)
 | |
| 	{
 | |
| 		return strcmp(basename($a['thumbnail']), basename($b['thumbnail']));
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Compares thumbnail file names in DESC order
 | |
| 	 * 
 | |
| 	 * @param   array  $a
 | |
| 	 * @param   array  $b
 | |
| 	 * 
 | |
| 	 * @return  bool
 | |
| 	 */
 | |
| 	public function compareByThumbnailDESC($a, $b)
 | |
| 	{
 | |
| 		return strcmp(basename($b['thumbnail']), basename($a['thumbnail']));
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get the hash of this gallery.
 | |
| 	 * 
 | |
| 	 * Generate the hash with only the essential options of the Gallery widget.
 | |
| 	 * i.e. with the data that are related to the images.
 | |
| 	 * 
 | |
| 	 * @return  string
 | |
| 	 */
 | |
| 	private function getHash()
 | |
| 	{
 | |
| 		$opts = [
 | |
| 			'items',
 | |
| 			'style',
 | |
| 			'allowed_file_types',
 | |
| 			'source_image_resize',
 | |
| 			'source_image_resize_width',
 | |
| 			'source_image_resize_height',
 | |
| 			'source_image_resize_method',
 | |
| 			'source_image_resize_image_quality',
 | |
| 			'original_image_resize',
 | |
| 			'original_image_resize_method',
 | |
| 			'original_image_resize_width',
 | |
| 			'original_image_resize_height',
 | |
| 			'original_image_resize_image_quality',
 | |
| 			'thumbnails',
 | |
| 			'thumb_resize_method',
 | |
| 			'thumb_width',
 | |
| 			'thumb_height',
 | |
| 			'force_resizing',
 | |
| 			'destination_folder'
 | |
| 		];
 | |
| 
 | |
| 		$payload = [];
 | |
| 		
 | |
| 		foreach ($opts as $opt)
 | |
| 		{
 | |
| 			$payload[$opt] = $this->options[$opt];
 | |
| 		}
 | |
| 		
 | |
| 		return md5(serialize($payload));
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Cleans the source folder.
 | |
| 	 * 
 | |
| 	 * If an image from the source folder is removed, we also remove the
 | |
| 	 * original image/thumbnail from the destination folder as well as
 | |
| 	 * from the gallery info file.
 | |
| 	 * 
 | |
| 	 * @return  void
 | |
| 	 */
 | |
| 	private function cleanDestinationFolder()
 | |
| 	{
 | |
| 		if (!$this->options['original_image_resize'] && !$this->options['thumbnails'])
 | |
| 		{
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		// Find all folders that we need to search
 | |
| 		$dirs_to_search = [];
 | |
| 
 | |
| 		// Store all source files
 | |
| 		$source_files = [];
 | |
| 
 | |
| 		foreach ($this->options['items'] as $key => $item)
 | |
| 		{
 | |
| 			if (!isset($item['path']))
 | |
| 			{
 | |
| 				continue;
 | |
| 			}
 | |
| 
 | |
| 			$source_files[] = pathinfo($item['path'], PATHINFO_BASENAME);
 | |
| 
 | |
| 			$directory = is_dir($item['path']) ? $item['path'] : dirname($item['path']);
 | |
| 
 | |
| 			if (in_array($directory, $dirs_to_search))
 | |
| 			{
 | |
| 				continue;
 | |
| 			}
 | |
| 			
 | |
| 			$dirs_to_search[] = $directory;
 | |
| 		}
 | |
| 
 | |
| 		if (empty($dirs_to_search))
 | |
| 		{
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		// Loop each directory found and check which files we need to delete
 | |
| 		foreach ($dirs_to_search as $dir)
 | |
| 		{
 | |
| 			$source_folder_info_file = GalleryHelper::getGalleryInfoFileData($dir);
 | |
| 
 | |
| 			// Find all soon to be deleted files
 | |
| 			$to_be_deleted = array_diff(array_keys($source_folder_info_file), $source_files);
 | |
| 
 | |
| 			if (!count($to_be_deleted))
 | |
| 			{
 | |
| 				continue;
 | |
| 			}
 | |
| 			
 | |
| 			foreach ($to_be_deleted as $source)
 | |
| 			{
 | |
| 				// Original image delete
 | |
| 				if (isset($source_folder_info_file[$source]))
 | |
| 				{
 | |
| 					$file = $this->options['destination_folder'] . $source_folder_info_file[$source]['filename'];
 | |
| 					if (file_exists($file))
 | |
| 					{
 | |
| 						unlink($file);
 | |
| 					}
 | |
| 				}
 | |
| 				
 | |
| 				// Thumbnail delete
 | |
| 				$parts = pathinfo($file);
 | |
| 				$thumbnail = $this->options['destination_folder'] . $parts['filename'] . '_thumb.' . $parts['extension'];
 | |
| 				if (file_exists($thumbnail))
 | |
| 				{
 | |
| 					unlink($thumbnail);
 | |
| 				}
 | |
| 
 | |
| 				// Also remove the image from the gallery info file.
 | |
| 				GalleryHelper::removeImageFromGalleryInfoFile(rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $source);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Returns the gallery style.
 | |
| 	 * 
 | |
| 	 * @return  string
 | |
| 	 */
 | |
| 	private function getStyle()
 | |
| 	{
 | |
| 		$style = $this->options['style'];
 | |
| 
 | |
| 		if ($style === 'justified')
 | |
| 		{
 | |
| 			return $style;
 | |
| 		}
 | |
| 
 | |
| 		// Get aspect ratio for source image, original image resized and thumbnail
 | |
| 		$thumb_height = intval($this->options['thumb_height']);
 | |
| 		$thumb_aspect_ratio = $thumb_height ? intval($this->options['thumb_width']) / $thumb_height : 0;
 | |
| 		
 | |
| 		$source_image_height = intval($this->options['source_image_resize_height']);
 | |
| 		$source_image_aspect_ratio = $source_image_height ? intval($this->options['source_image_resize_width']) / $source_image_height : 0;
 | |
| 		
 | |
| 		$original_image_height = intval($this->options['original_image_resize_height']);
 | |
| 		$original_image_aspect_ratio = $original_image_height ? intval($this->options['original_image_resize_width']) / $original_image_height : 0;
 | |
| 
 | |
| 		// Check whether the aspect ratio for thumb and lightbox image are the same and use `masonry` style
 | |
| 		$checking_aspect_ratio = $this->options['original_image_resize'] ? $original_image_aspect_ratio : $source_image_aspect_ratio;
 | |
| 		if ($thumb_aspect_ratio && $checking_aspect_ratio && $thumb_aspect_ratio === $checking_aspect_ratio)
 | |
| 		{
 | |
| 			return 'masonry';
 | |
| 		}
 | |
| 
 | |
| 		/**
 | |
| 		 * If both thumbnail width & height are equal we use the `grid` style.
 | |
| 		 */
 | |
| 		if ($this->options['thumb_width'] === $this->options['thumb_height'])
 | |
| 		{
 | |
| 			$style = 'grid';
 | |
| 		}
 | |
| 		
 | |
| 		/**
 | |
| 		 * If the style is grid and we do not have a null or 0 thumb_height set the fade lightbox CSS Class.
 | |
| 		 * 
 | |
| 		 * This CSS Class tells PhotoSwipe to use the fade transition.
 | |
| 		 */
 | |
| 		if ($style === 'grid' && (!is_null($this->options['thumb_height']) && $this->options['thumb_height'] !== '0'))
 | |
| 		{
 | |
| 			$this->options['css_class'] .= ' lightbox-fade';
 | |
| 		}
 | |
| 		
 | |
| 		return $style;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Prepare the tags.
 | |
| 	 * 
 | |
| 	 * @return  void
 | |
| 	 */
 | |
| 	private function prepareTags()
 | |
| 	{
 | |
| 		if ($this->options['tags_position'] === 'disabled')
 | |
| 		{
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		if (!is_array($this->options['items']))
 | |
| 		{
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		if ($this->options['all_tags_item_label'])
 | |
| 		{
 | |
| 			$this->options['all_tags_item_label'] = Text::_($this->options['all_tags_item_label']);
 | |
| 		}
 | |
| 
 | |
| 		$tags = $this->options['tags'];
 | |
| 
 | |
| 		if (count($tags) === 0)
 | |
| 		{
 | |
| 			foreach ($this->options['items'] as $key => &$item)
 | |
| 			{
 | |
| 				if (!isset($item['tags']))
 | |
| 				{
 | |
| 					continue;
 | |
| 				}
 | |
| 	
 | |
| 				if (!is_array($item['tags']))
 | |
| 				{
 | |
| 					continue;
 | |
| 				}
 | |
| 	
 | |
| 				$tags = array_merge($tags, $item['tags']);
 | |
| 			}
 | |
| 	
 | |
| 			$tags = array_unique($tags);
 | |
| 		}
 | |
| 
 | |
| 		// Sort tags
 | |
| 		switch ($this->options['tags_ordering'])
 | |
| 		{
 | |
| 			case 'random':
 | |
| 				shuffle($tags);
 | |
| 				break;
 | |
| 			case 'alphabetical':
 | |
| 				usort($tags, [$this, 'compareByTagNameASC']);
 | |
| 				break;
 | |
| 			case 'reverse_alphabetical':
 | |
| 				usort($tags, [$this, 'compareByTagNameDESC']);
 | |
| 				break;
 | |
| 		}
 | |
| 
 | |
| 		$this->options['tags'] = $tags;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Parses the gallery items by finding all iamges to display from all
 | |
| 	 * different sources.
 | |
| 	 * 
 | |
| 	 * @return  void
 | |
| 	 */
 | |
| 	private function parseGalleryItems()
 | |
| 	{
 | |
| 		// If it's a string, we assume its a path to a folder and we convert it to an array.
 | |
| 		$this->options['items'] = (array) $this->options['items'];
 | |
| 
 | |
| 		$items = [];
 | |
| 
 | |
| 		foreach ($this->options['items'] as $key => $value)
 | |
| 		{
 | |
| 			if (!$data = GalleryHelper::parseGalleryItems($value, $this->getAllowedFileTypes()))
 | |
| 			{
 | |
| 				continue;
 | |
| 			}
 | |
| 
 | |
| 			$items = array_merge($items, $data);
 | |
| 		}
 | |
| 
 | |
| 		// Ensure only unique image paths are used
 | |
| 		$items = array_unique($items, SORT_REGULAR);
 | |
| 
 | |
| 		$this->options['items'] = $items;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Returns the allowed file types in an array format.
 | |
| 	 * 
 | |
| 	 * @return  array
 | |
| 	 */
 | |
| 	public function getAllowedFileTypes()
 | |
| 	{
 | |
| 		$types = explode(',', $this->options['allowed_file_types']);
 | |
| 		$types = array_filter(array_map('trim', array_map('strtolower', $types)));
 | |
| 		return $types;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Resizes the source images.
 | |
| 	 * 
 | |
| 	 * @return  mixed
 | |
| 	 */
 | |
| 	private function resizeSourceImages()
 | |
| 	{
 | |
| 		if (!$this->options['source_image_resize'])
 | |
| 		{
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		// We require either original image resize or thumbnails to be enabled
 | |
| 		if (!$this->options['original_image_resize'] && !$this->options['thumbnails'])
 | |
| 		{
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		foreach ($this->options['items'] as $key => &$item)
 | |
| 		{
 | |
| 			if (!isset($item['path']))
 | |
| 			{
 | |
| 				continue;
 | |
| 			}
 | |
| 
 | |
| 			// Skip if source does not exist
 | |
| 			if (!is_file($item['path']))
 | |
| 			{
 | |
| 				continue;
 | |
| 			}
 | |
| 
 | |
| 			$source = $item['path'];
 | |
| 
 | |
| 			// Find source image in the destination folder
 | |
| 			if ($image_data = GalleryHelper::findSourceImageDetails($source, $this->options['destination_folder']))
 | |
| 			{
 | |
| 				// If force resizing is disabled, continue
 | |
| 				if (!$this->options['force_resizing'])
 | |
| 				{
 | |
| 					continue;
 | |
| 				}
 | |
| 				else
 | |
| 				{
 | |
| 					// If the destination image has not been edited and exists, abort
 | |
| 					if (!$image_data['edited'] && file_exists($image_data['path']))
 | |
| 					{
 | |
| 						continue;
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			if (is_null($this->options['source_image_resize_height']))
 | |
| 			{
 | |
| 				Image::resizeAndKeepAspectRatio(
 | |
| 					$source,
 | |
| 					$this->options['source_image_resize_width'],
 | |
| 					$this->options['source_image_resize_image_quality']
 | |
| 				);
 | |
| 			}
 | |
| 			else
 | |
| 			{
 | |
| 				Image::resize(
 | |
| 					$source,
 | |
| 					$this->options['source_image_resize_width'],
 | |
| 					$this->options['source_image_resize_height'],
 | |
| 					$this->options['source_image_resize_image_quality'],
 | |
| 					$this->options['source_image_resize_method']
 | |
| 				);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Resizes the original images.
 | |
| 	 * 
 | |
| 	 * @return  mixed
 | |
| 	 */
 | |
| 	private function resizeOriginalImages()
 | |
| 	{
 | |
| 		if (!$this->options['original_image_resize'])
 | |
| 		{
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		// Create destination folder if missing
 | |
| 		File::createDirs($this->options['destination_folder']);
 | |
| 		
 | |
| 		foreach ($this->options['items'] as $key => &$item)
 | |
| 		{
 | |
| 			if (!isset($item['path']))
 | |
| 			{
 | |
| 				continue;
 | |
| 			}
 | |
| 
 | |
| 			// Skip if source does not exist
 | |
| 			if (!is_file($item['path']))
 | |
| 			{
 | |
| 				continue;
 | |
| 			}
 | |
| 
 | |
| 			$source = $item['path'];
 | |
| 
 | |
| 			$unique = true;
 | |
| 
 | |
| 			// Path to resized image in destination folder
 | |
| 			$destination = $this->options['destination_folder'] . basename($source);
 | |
| 			
 | |
| 			// Find source image in the destination folder
 | |
| 			if ($image_data = GalleryHelper::findSourceImageDetails($source, $this->options['destination_folder']))
 | |
| 			{
 | |
| 				// If force resizing is disabled and the original image exists, set the URL of the destination image
 | |
| 				if (!$this->options['force_resizing'] && file_exists($image_data['path']))
 | |
| 				{
 | |
| 					$item['url'] = GalleryHelper::directoryImageToURL($image_data['path']);
 | |
| 					continue;
 | |
| 				}
 | |
| 				else
 | |
| 				{
 | |
| 					// If the destination image has not been edited and exists, abort
 | |
| 					if (!$image_data['edited'] && file_exists($image_data['path']))
 | |
| 					{
 | |
| 						$item['url'] = GalleryHelper::directoryImageToURL($image_data['path']);
 | |
| 						continue;
 | |
| 					}
 | |
| 					
 | |
| 					// Since we are forcing resizing, overwrite the existing image, do not create a new unique image
 | |
| 					$unique = false;
 | |
| 
 | |
| 					// The destination path is the same resized image
 | |
| 					$destination = $image_data['path'];
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			$original_image_file = is_null($this->options['original_image_resize_height'])
 | |
| 				?
 | |
| 				Image::resizeAndKeepAspectRatio(
 | |
| 					$source,
 | |
| 					$this->options['original_image_resize_width'],
 | |
| 					$this->options['original_image_resize_image_quality'],
 | |
| 					$destination,
 | |
| 					$unique
 | |
| 				)
 | |
| 				:
 | |
| 				Image::resize(
 | |
| 					$source,
 | |
| 					$this->options['original_image_resize_width'],
 | |
| 					$this->options['original_image_resize_height'],
 | |
| 					$this->options['original_image_resize_image_quality'],
 | |
| 					$this->options['original_image_resize_method'],
 | |
| 					$destination,
 | |
| 					$unique
 | |
| 				);
 | |
| 
 | |
| 			if (!$original_image_file)
 | |
| 			{
 | |
| 				continue;
 | |
| 			}
 | |
| 
 | |
| 			// Set image URL
 | |
| 			$item = array_merge($item, [
 | |
| 				'url' => GalleryHelper::directoryImageToURL($original_image_file)
 | |
| 			]);
 | |
| 
 | |
| 			// Update image data in Gallery Info File
 | |
| 			GalleryHelper::updateImageDataInGalleryInfoFile($source, $item);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Creates thumbnails.
 | |
| 	 * 
 | |
| 	 * If `force_resizing` is enabled, it will re-generate thumbnails under the following cases:
 | |
| 	 * 
 | |
| 	 * - If a thumbnail does not exist.
 | |
| 	 * - If the original image has been edited.
 | |
| 	 * 
 | |
| 	 * @return  mixed
 | |
| 	 */
 | |
| 	private function createThumbnails()
 | |
| 	{
 | |
| 		if (!$this->options['thumbnails'])
 | |
| 		{
 | |
| 			return false;
 | |
| 		}
 | |
| 		
 | |
| 		// Create destination folder if missing
 | |
| 		File::createDirs($this->options['destination_folder']);
 | |
| 
 | |
| 		foreach ($this->options['items'] as $key => &$item)
 | |
| 		{
 | |
| 			// Skip items that do not have a path set
 | |
| 			if (!isset($item['path']))
 | |
| 			{
 | |
| 				continue;
 | |
| 			}
 | |
| 
 | |
| 			// Skip if source does not exist
 | |
| 			if (!is_file($item['path']))
 | |
| 			{
 | |
| 				continue;
 | |
| 			}
 | |
| 
 | |
| 			$source = $item['path'];
 | |
| 
 | |
| 			$unique = true;
 | |
| 
 | |
| 			$parts = pathinfo($source);
 | |
| 			$destination = $this->options['destination_folder'] . $parts['filename'] . '_thumb.' . $parts['extension'];
 | |
| 			
 | |
| 			// Find source image in the destination folder
 | |
| 			if ($image_data = GalleryHelper::findSourceImageDetails($source, $this->options['destination_folder']))
 | |
| 			{
 | |
| 				/**
 | |
| 				 * Use the found original image path to produce the thumb file path.
 | |
| 				 * 
 | |
| 				 * This is used as we have multiple files with the same which produce file names of _copy_X
 | |
| 				 * and thus the above $destination will not be valid. Instead, we use the original file name
 | |
| 				 * to find the thumbnail file.
 | |
| 				 */
 | |
| 				if ($this->options['original_image_resize'])
 | |
| 				{
 | |
| 					$parts = pathinfo($image_data['path']);
 | |
| 					$destination = $this->options['destination_folder'] . $parts['filename'] . '_thumb.' . $parts['extension'];
 | |
| 				}
 | |
| 				
 | |
| 				// If force resizing is disabled and the thumbnail exists, set the URL of the destination image
 | |
| 				if (!$this->options['force_resizing'] && file_exists($destination))
 | |
| 				{
 | |
| 					$item['thumbnail_url'] = GalleryHelper::directoryImageToURL($destination);
 | |
| 					continue;
 | |
| 				}
 | |
| 				else
 | |
| 				{
 | |
| 					// If the destination image has not been edited and exists, abort
 | |
| 					if (!$image_data['edited'] && file_exists($destination))
 | |
| 					{
 | |
| 						$item['thumbnail_url'] = GalleryHelper::directoryImageToURL($destination);
 | |
| 						continue;
 | |
| 					}
 | |
| 					
 | |
| 					// Since we are forcing resizing, overwrite the existing image, do not create a new unique image
 | |
| 					$unique = false;
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			// Generate thumbnails
 | |
| 			$thumb_file = is_null($this->options['thumb_height'])
 | |
| 				?
 | |
| 				Image::resizeAndKeepAspectRatio(
 | |
| 					$source,
 | |
| 					$this->options['thumb_width'],
 | |
| 					100,
 | |
| 					$destination,
 | |
| 					$unique,
 | |
| 					true,
 | |
| 					'resize'
 | |
| 				)
 | |
| 				:
 | |
| 				Image::resize(
 | |
| 					$source,
 | |
| 					$this->options['thumb_width'],
 | |
| 					$this->options['thumb_height'],
 | |
| 					100,
 | |
| 					$this->options['thumb_resize_method'],
 | |
| 					$destination,
 | |
| 					$unique,
 | |
| 					true,
 | |
| 					'resize'
 | |
| 				);
 | |
| 
 | |
| 			if (!$thumb_file)
 | |
| 			{
 | |
| 				continue;
 | |
| 			}
 | |
| 
 | |
| 			// Set image thumbnail URL
 | |
| 			$item = array_merge($item, [
 | |
| 				'thumbnail_url' => GalleryHelper::directoryImageToURL($thumb_file)
 | |
| 			]);
 | |
| 
 | |
| 			// Update image data in Gallery Info File
 | |
| 			GalleryHelper::updateImageDataInGalleryInfoFile($source, $item);
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	/**
 | |
| 	 * Prepares the items.
 | |
| 	 * 
 | |
| 	 * - Sets the thumbnails image dimensions.
 | |
| 	 * - Assures caption property exist.
 | |
| 	 * 
 | |
| 	 * @return  mixed
 | |
| 	 */
 | |
| 	private function prepareItems()
 | |
| 	{
 | |
| 		if (!is_array($this->options['items']) || !count($this->options['items']))
 | |
| 		{
 | |
| 			return;
 | |
| 		}
 | |
| 		
 | |
| 		$smartTagsInstance = \NRFramework\SmartTags::getInstance();
 | |
| 
 | |
| 		foreach ($this->options['items'] as $key => &$item)
 | |
| 		{
 | |
| 			// Initialize image atts
 | |
| 			$item['img_atts'] = '';
 | |
| 
 | |
| 			// Initializes caption if none given
 | |
| 			if (!isset($item['caption']))
 | |
| 			{
 | |
| 				$item['caption'] = '';
 | |
| 			}
 | |
| 
 | |
| 			if (!isset($item['alt']) || empty($item['alt']))
 | |
| 			{
 | |
| 				$item['alt'] = !empty($item['caption']) ? mb_substr($item['caption'], 0, 100) : pathinfo($item['url'], PATHINFO_FILENAME);
 | |
| 			}
 | |
| 
 | |
| 			// Replace Smart Tags in alt
 | |
| 			$item['alt'] = $smartTagsInstance->replace($item['alt']);
 | |
| 
 | |
| 			if ($item['caption'])
 | |
| 			{
 | |
| 				$item['caption'] = $smartTagsInstance->replace($item['caption']);
 | |
| 			}
 | |
| 			
 | |
| 			// Ensure a thumbnail is given
 | |
| 			if (!isset($item['thumbnail_url']))
 | |
| 			{
 | |
| 				// If no thumbnail is given, set it to the full image
 | |
| 				$item['thumbnail_url'] = $item['url'];
 | |
| 				continue;
 | |
| 			}
 | |
| 
 | |
| 			// If the thumbnail size for this item is given, set the image attributes
 | |
| 			if (isset($item['thumbnail_size']))
 | |
| 			{
 | |
| 				$item['img_atts'] = 'width="' . $item['thumbnail_size']['width'] . '" height="' . $item['thumbnail_size']['height'] . '"';
 | |
| 				continue;
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 
 | |
| 	/**
 | |
| 	 * Returns the CSS for the widget.
 | |
| 	 * 
 | |
| 	 * @param   array  $exclude_breakpoints   Define breakpoints to exclude their CSS
 | |
| 	 * 
 | |
| 	 * @return  string
 | |
| 	 */
 | |
| 	public function getWidgetCSS($exclude_breakpoints = [])
 | |
| 	{
 | |
| 		$controls = [
 | |
|             [
 | |
|                 'property' => '--gap',
 | |
|                 'value' => $this->options['gap'],
 | |
| 				'unit' => 'px'
 | |
| 			],
 | |
| 			[
 | |
| 				'property' => '--tags-text-color',
 | |
| 				'value' => $this->options['tags_text_color']
 | |
| 			],
 | |
| 			[
 | |
| 				'property' => '--tags-text-color-hover',
 | |
| 				'value' => $this->options['tags_text_color_hover']
 | |
| 			],
 | |
| 			[
 | |
| 				'property' => '--tags-bg-color-hover',
 | |
| 				'value' => $this->options['tags_bg_color_hover']
 | |
| 			]
 | |
| 		];
 | |
| 
 | |
| 		if ($this->options['style'] !== 'justified')
 | |
| 		{
 | |
| 			$controls[] = [
 | |
|                 'property' => [
 | |
|                     '--columns' => '%value_raw%',
 | |
| 					'--display-items' => 'grid',
 | |
| 					'--image-width' => '100%'
 | |
|                 ],
 | |
| 				'fallback_value' => [
 | |
| 					'--display-items' => 'flex',
 | |
| 					'--display-items-flex-wrap' => 'wrap',
 | |
| 					'--image-width' => 'auto'
 | |
| 				],
 | |
| 				'value' => $this->options['columns'],
 | |
| 			];
 | |
| 		}
 | |
| 
 | |
| 		$selector = '.nrf-widget.' . $this->options['id'];
 | |
| 		
 | |
| 		$controlsInstance = new \NRFramework\Controls\Controls(null, $selector, $exclude_breakpoints);
 | |
|         
 | |
| 		if (!$controlsCSS = $controlsInstance->generateCSS($controls))
 | |
| 		{
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		return $controlsCSS;
 | |
| 	}
 | |
| } |