forked from Public/pics
		
	This crop mode was intended to get the '_ce' suffix, but was inadvertently getting '_c' instead.
		
			
				
	
	
		
			347 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			347 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
/*****************************************************************************
 | 
						|
 * Thumbnail.php
 | 
						|
 * Contains key class Thumbnail.
 | 
						|
 *
 | 
						|
 * Kabuki CMS (C) 2013-2015, Aaron van Geffen
 | 
						|
 *****************************************************************************/
 | 
						|
 | 
						|
class Thumbnail
 | 
						|
{
 | 
						|
	private $image;
 | 
						|
	private $thumbnails;
 | 
						|
 | 
						|
	private $properly_initialised;
 | 
						|
	private $width;
 | 
						|
	private $height;
 | 
						|
	private $crop_mode;
 | 
						|
 | 
						|
	const CROP_MODE_NONE         = 0;
 | 
						|
	const CROP_MODE_BOUNDARY     = 1;
 | 
						|
	const CROP_MODE_CUSTOM_FILE  = 2;
 | 
						|
	const CROP_MODE_SLICE_TOP    = 3;
 | 
						|
	const CROP_MODE_SLICE_CENTRE = 4;
 | 
						|
	const CROP_MODE_SLICE_BOTTOM = 5;
 | 
						|
 | 
						|
	public function __construct($image)
 | 
						|
	{
 | 
						|
		$this->image      = $image;
 | 
						|
		$this->image_meta = $image->getMeta();
 | 
						|
		$this->thumbnails = $image->getThumbnails();
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param width: width of the thumbnail.
 | 
						|
	 * @param height: height of the thumbnail.
 | 
						|
	 * @param crop: whether and how to crop original image to fit. [false|true|'top'|'center'|'bottom']
 | 
						|
	 * @param fit: whether to fit the image to given boundaries [true], or use them merely as an estimate [false].
 | 
						|
	 * @param generate: whether or not to generate a thumbnail if no existing file was found.
 | 
						|
	 */
 | 
						|
	public function getUrl($width, $height, $crop = true, $fit = true, $generate = false)
 | 
						|
	{
 | 
						|
		$this->init($width, $height, $crop, $fit);
 | 
						|
 | 
						|
		// Check whether we've already resized this earlier.
 | 
						|
		$thumb_selector = $this->width . 'x' . $this->height . $this->filename_suffix;
 | 
						|
		if (!empty($this->thumbnails[$thumb_selector]))
 | 
						|
		{
 | 
						|
			if (file_exists(THUMBSDIR . '/' . $this->image->getSubdir() . '/' . $this->thumbnails[$thumb_selector]))
 | 
						|
				return THUMBSURL . '/' . $this->image->getSubdir() . '/' . $this->thumbnails[$thumb_selector];
 | 
						|
		}
 | 
						|
 | 
						|
		// Do we have a custom thumbnail on file?
 | 
						|
		$custom_selector = 'custom_' . $this->width . 'x' . $this->height;
 | 
						|
		if (isset($this->image_meta[$custom_selector]))
 | 
						|
		{
 | 
						|
			if (file_exists(ASSETSDIR . '/' . $this->image->getSubdir() . '/' . $this->image_meta[$custom_selector]))
 | 
						|
			{
 | 
						|
				// Copy the custom thumbail to the general thumbnail directory.
 | 
						|
				copy(ASSETSDIR . '/' . $this->image->getSubdir() . '/' . $this->image_meta[$custom_selector],
 | 
						|
					THUMBSDIR . '/' . $this->image->getSubdir() . '/' . $this->image_meta[$custom_selector]);
 | 
						|
 | 
						|
				// Let's remember this for future reference.
 | 
						|
				$this->markAsGenerated($this->image_meta[$custom_selector]);
 | 
						|
 | 
						|
				return THUMBSURL . '/' . $this->image->getSubdir() . '/' . $this->image_meta[$custom_selector];
 | 
						|
			}
 | 
						|
			else
 | 
						|
				throw new UnexpectedValueException('Custom thumbnail expected, but missing in file system!');
 | 
						|
		}
 | 
						|
 | 
						|
		// Is this the right moment to generate a thumbnail, then?
 | 
						|
		if ($generate && array_key_exists($thumb_selector, $this->thumbnails))
 | 
						|
		{
 | 
						|
			return $this->generate();
 | 
						|
		}
 | 
						|
 | 
						|
		// If not, queue it for generation at another time, and return a URL to generate it with.
 | 
						|
		elseif (!$generate)
 | 
						|
		{
 | 
						|
			$this->markAsQueued();
 | 
						|
			return BASEURL . '/thumbnail/' . $this->image->getId() . '/' . $this->width . 'x' . $this->height . $this->filename_suffix . '/';
 | 
						|
		}
 | 
						|
 | 
						|
		// Still here..? What are you up to? ..Sneaking?
 | 
						|
		else
 | 
						|
		{
 | 
						|
			throw new Exception("Trying to generate a thumbnail for values that were not previously initialised by the system?\n" . print_r(func_get_args(), true));
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param width: width of the thumbnail.
 | 
						|
	 * @param height: height of the thumbnail.
 | 
						|
	 * @param crop: whether and how to crop original image to fit. [false|true|'top'|'center'|'bottom']
 | 
						|
	 * @param fit: whether to fit the image to given boundaries [true], or use them merely as an estimate [false].
 | 
						|
	 */
 | 
						|
	private function init($width, $height, $crop = true, $fit = true)
 | 
						|
	{
 | 
						|
		$this->properly_initialised = false;
 | 
						|
 | 
						|
		// First, assert the image's dimensions are properly known in the database.
 | 
						|
		if ($this->image->width() === null || $this->image->height() === null)
 | 
						|
			throw new UnexpectedValueException('Image width or height is undefined -- inconsistent database?');
 | 
						|
 | 
						|
		$this->width = $width;
 | 
						|
		$this->height = $height;
 | 
						|
 | 
						|
		// Inferring width or height?
 | 
						|
		if (!$this->height)
 | 
						|
			$this->height = ceil($this->width / $this->image->ratio());
 | 
						|
		elseif (!$this->width)
 | 
						|
			$this->width = ceil($this->height * $this->image->ratio());
 | 
						|
 | 
						|
		// Inferring the height from the original image's ratio?
 | 
						|
		if (!$fit)
 | 
						|
			$this->height = floor($this->width / $this->image->ratio());
 | 
						|
 | 
						|
		// Assert we have both, now...
 | 
						|
		if (empty($this->width) || empty($this->height))
 | 
						|
			throw new InvalidArgumentException('Expecting at least either width or height as argument.');
 | 
						|
 | 
						|
		// If we're cropping, verify we're in the right mode.
 | 
						|
		if ($crop)
 | 
						|
		{
 | 
						|
			// Do we have an exact crop boundary set for these dimensions?
 | 
						|
			$crop_selector = 'crop_' . $this->width . 'x' . $this->height;
 | 
						|
			if (isset($this->image_meta[$crop_selector]))
 | 
						|
				$this->crop_mode = self::CROP_MODE_BOUNDARY;
 | 
						|
 | 
						|
			// If the original image's aspect ratio is much wider, take a slice instead.
 | 
						|
			if ($this->image->ratio() > $this->ratio())
 | 
						|
				$this->crop_mode = self::CROP_MODE_SLICE_CENTRE;
 | 
						|
 | 
						|
			// We won't be cropping if the thumbnail is proportional to its original.
 | 
						|
			elseif (abs($this->ratio() - $this->image->ratio()) <= 0.025)
 | 
						|
				$this->crop_mode = self::CROP_MODE_NONE;
 | 
						|
 | 
						|
			// Slice from the top?
 | 
						|
			elseif ($crop === 'top' || $crop === 'ct')
 | 
						|
				$this->crop_mode = self::CROP_MODE_SLICE_TOP;
 | 
						|
 | 
						|
			// Slice from the bottom?
 | 
						|
			elseif ($crop === 'bottom' || $crop === 'cb')
 | 
						|
				$this->crop_mode = self::CROP_MODE_SLICE_BOTTOM;
 | 
						|
 | 
						|
			// Slice from the centre?
 | 
						|
			elseif ($crop === 'centre' || $crop === 'center' || $crop === 'cs' || $crop === true)
 | 
						|
				$this->crop_mode = self::CROP_MODE_SLICE_CENTRE;
 | 
						|
 | 
						|
			// Unexpected value? Assume no crop.
 | 
						|
			else
 | 
						|
				$this->crop_mode = self::CROP_MODE_NONE;
 | 
						|
		}
 | 
						|
		else
 | 
						|
			$this->crop_mode = self::CROP_MODE_NONE;
 | 
						|
 | 
						|
		// Now, do we need to suffix the filename?
 | 
						|
		if ($this->crop_mode !== self::CROP_MODE_NONE)
 | 
						|
		{
 | 
						|
			$this->filename_suffix = '_c';
 | 
						|
			if ($this->crop_mode === self::CROP_MODE_SLICE_TOP)
 | 
						|
				$this->filename_suffix .= 't';
 | 
						|
			elseif ($this->crop_mode === self::CROP_MODE_SLICE_CENTRE)
 | 
						|
				$this->filename_suffix .= 's';
 | 
						|
			elseif ($this->crop_mode === self::CROP_MODE_SLICE_BOTTOM)
 | 
						|
				$this->filename_suffix .= 'b';
 | 
						|
			elseif ($this->crop_mode === self::CROP_MODE_BOUNDARY)
 | 
						|
				$this->filename_suffix .= 'e';
 | 
						|
		}
 | 
						|
		else
 | 
						|
			$this->filename_suffix = '';
 | 
						|
 | 
						|
		$this->properly_initialised = true;
 | 
						|
	}
 | 
						|
 | 
						|
	private function generate()
 | 
						|
	{
 | 
						|
		if (!$this->properly_initialised)
 | 
						|
			throw new UnexpectedValueException('The thumbnail factory was not intialised before use!');
 | 
						|
 | 
						|
		// Let's try some arcane stuff...
 | 
						|
		try
 | 
						|
		{
 | 
						|
			if (!class_exists('Imagick'))
 | 
						|
				throw new Exception("The PHP module 'imagick' appears to be disabled. Please enable it to use image resampling functions.");
 | 
						|
 | 
						|
			$thumb = new Imagick(ASSETSDIR . '/' . $this->image->getSubdir() . '/' . $this->image->getFilename());
 | 
						|
 | 
						|
			// The image might have some orientation set through EXIF. Let's apply this first.
 | 
						|
			self::applyRotation($thumb);
 | 
						|
 | 
						|
			// Just resizing? Easy peasy.
 | 
						|
			if ($this->crop_mode === self::CROP_MODE_NONE)
 | 
						|
				$thumb->resizeImage($this->width, $this->height, Imagick::FILTER_LANCZOS, 1);
 | 
						|
 | 
						|
			// // Cropping in the center?
 | 
						|
			elseif ($this->crop_mode === self::CROP_MODE_SLICE_CENTRE)
 | 
						|
				$thumb->cropThumbnailImage($this->width, $this->height);
 | 
						|
 | 
						|
			// Exact cropping? We can do that.
 | 
						|
			elseif ($this->crop_mode === self::CROP_MODE_BOUNDARY)
 | 
						|
			{
 | 
						|
				$crop_selector = 'crop_' . $this->width . 'x' . $this->height;
 | 
						|
				list($crop_width, $crop_height, $crop_x_pos, $crop_y_pos) = explode(',', $this->image_meta[$crop_selector]);
 | 
						|
				$thumb->cropImage($crop_width, $crop_height, $crop_x_pos, $crop_y_pos);
 | 
						|
				$thumb->resizeImage($this->width, $this->height, Imagick::FILTER_LANCZOS, 1);
 | 
						|
			}
 | 
						|
 | 
						|
			// Advanced cropping? Fun!
 | 
						|
			else
 | 
						|
			{
 | 
						|
				$size = $thumb->getImageGeometry();
 | 
						|
 | 
						|
				// Taking a horizontal slice from the top or bottom of the original image?
 | 
						|
				if ($this->crop_mode === self::CROP_MODE_SLICE_TOP || $this->crop_mode === self::CROP_MODE_SLICE_BOTTOM)
 | 
						|
				{
 | 
						|
					$crop_width = $size['width'];
 | 
						|
					$crop_height = floor($size['width'] / $this->width * $this->height);
 | 
						|
					$target_x = 0;
 | 
						|
					$target_y = $this->crop_mode === self::CROP_MODE_SLICE_TOP ? 0 : $size['height'] - $crop_height;
 | 
						|
				}
 | 
						|
				// Otherwise, we're taking a vertical slice from the centre.
 | 
						|
				else
 | 
						|
				{
 | 
						|
					$crop_width = floor($size['height'] / $this->height * $this->width);
 | 
						|
					$crop_height = $size['height'];
 | 
						|
					$target_x = floor(($size['width'] - $crop_width) / 2);
 | 
						|
					$target_y = 0;
 | 
						|
				}
 | 
						|
 | 
						|
				$thumb->cropImage($crop_width, $crop_height, $target_x, $target_y);
 | 
						|
				$thumb->resizeImage($this->width, $this->height, Imagick::FILTER_LANCZOS, 1);
 | 
						|
			}
 | 
						|
 | 
						|
			// What sort of image is this? Fall back to PNG if we must.
 | 
						|
			switch ($thumb->getImageFormat())
 | 
						|
			{
 | 
						|
				case 'JPEG':
 | 
						|
					$ext = 'jpg';
 | 
						|
					break;
 | 
						|
 | 
						|
				case 'GIF':
 | 
						|
					$ext = 'gif';
 | 
						|
					break;
 | 
						|
 | 
						|
				case 'PNG':
 | 
						|
				default:
 | 
						|
					$thumb->setFormat('PNG');
 | 
						|
					$ext = 'png';
 | 
						|
					break;
 | 
						|
			}
 | 
						|
 | 
						|
			// So, how do we name this?
 | 
						|
			$thumb_filename = substr($this->image->getFilename(), 0, strrpos($this->image->getFilename(), '.')) .
 | 
						|
				'_' . $this->width . 'x' . $this->height . $this->filename_suffix . '.' . $ext;
 | 
						|
 | 
						|
			// Ensure the thumbnail subdirectory exists.
 | 
						|
			if (!is_dir(THUMBSDIR . '/' . $this->image->getSubdir()))
 | 
						|
				mkdir(THUMBSDIR . '/' . $this->image->getSubdir(), 0755, true);
 | 
						|
 | 
						|
			// Save it in a public spot.
 | 
						|
			$thumb->writeImage(THUMBSDIR . '/' . $this->image->getSubdir() . '/' . $thumb_filename);
 | 
						|
 | 
						|
			// Let's remember this for future reference...
 | 
						|
			$this->markAsGenerated($thumb_filename);
 | 
						|
 | 
						|
			$thumb->clear();
 | 
						|
			$thumb->destroy();
 | 
						|
 | 
						|
			// Finally, return the URL for the generated thumbnail image.
 | 
						|
			return THUMBSURL . '/' . $this->image->getSubdir() . '/' . $thumb_filename;
 | 
						|
		}
 | 
						|
		// Blast! Curse your sudden but inevitable betrayal!
 | 
						|
		catch (ImagickException $e)
 | 
						|
		{
 | 
						|
			throw new Exception('ImageMagick error occurred while generating thumbnail. Output: ' . $e->getMessage());
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	private static function applyRotation(Imagick $image)
 | 
						|
	{
 | 
						|
		switch ($image->getImageOrientation())
 | 
						|
		{
 | 
						|
			// Clockwise rotation
 | 
						|
			case Imagick::ORIENTATION_RIGHTTOP:
 | 
						|
				$image->rotateImage("#000", 90);
 | 
						|
				break;
 | 
						|
 | 
						|
			// Counter-clockwise rotation
 | 
						|
			case Imagick::ORIENTATION_LEFTBOTTOM:
 | 
						|
				$image->rotateImage("#000", 270);
 | 
						|
				break;
 | 
						|
 | 
						|
			// Upside down?
 | 
						|
			case Imagick::ORIENTATION_BOTTOMRIGHT:
 | 
						|
				$image->rotateImage("#000", 180);
 | 
						|
		}
 | 
						|
 | 
						|
		// Having rotated the image, make sure the EXIF data is set properly.
 | 
						|
		$image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
 | 
						|
	}
 | 
						|
 | 
						|
	private function ratio()
 | 
						|
	{
 | 
						|
		return $this->width / $this->height;
 | 
						|
	}
 | 
						|
 | 
						|
	private function updateDb($filename)
 | 
						|
	{
 | 
						|
		if (!$this->properly_initialised)
 | 
						|
			throw new UnexpectedValueException('The thumbnail factory was not intialised before use!');
 | 
						|
 | 
						|
		$mode = !empty($this->filename_suffix) ? substr($this->filename_suffix, 1) : '';
 | 
						|
		$success = Registry::get('db')->insert('replace', 'assets_thumbs', [
 | 
						|
			'id_asset' => 'int',
 | 
						|
			'width' => 'int',
 | 
						|
			'height' => 'int',
 | 
						|
			'mode' => 'string-3',
 | 
						|
			'filename' => 'string-255',
 | 
						|
		], [
 | 
						|
			'id_asset' => $this->image->getId(),
 | 
						|
			'width' => $this->width,
 | 
						|
			'height' => $this->height,
 | 
						|
			'mode' => $mode,
 | 
						|
			'filename' => $filename,
 | 
						|
		]);
 | 
						|
 | 
						|
		if ($success)
 | 
						|
		{
 | 
						|
			$thumb_selector = $this->width . 'x' . $this->height . $this->filename_suffix;
 | 
						|
			$this->thumbnails[$thumb_selector] = $filename !== 'NULL' ? $filename : '';
 | 
						|
		}
 | 
						|
 | 
						|
		return $success;
 | 
						|
	}
 | 
						|
 | 
						|
	private function markAsQueued()
 | 
						|
	{
 | 
						|
		$this->updateDb('NULL');
 | 
						|
	}
 | 
						|
 | 
						|
	private function markAsGenerated($filename)
 | 
						|
	{
 | 
						|
		$this->updateDb($filename);
 | 
						|
	}
 | 
						|
}
 |