2017-12-20 14:51:23 +01:00
< ? php
/*****************************************************************************
* Thumbnail . php
* Contains key class Thumbnail .
*
2020-12-30 13:02:47 +01:00
* Kabuki CMS ( C ) 2013 - 2020 , Aaron van Geffen
2017-12-20 14:51:23 +01:00
*****************************************************************************/
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 ]))
{
2020-12-30 13:02:47 +01:00
$thumb_path = '/' . $this -> image -> getSubdir () . '/' . $this -> thumbnails [ $thumb_selector ];
if ( file_exists ( THUMBSDIR . $thumb_path ))
return THUMBSURL . $thumb_path ;
2017-12-20 14:51:23 +01:00
}
// Do we have a custom thumbnail on file?
$custom_selector = 'custom_' . $this -> width . 'x' . $this -> height ;
if ( isset ( $this -> image_meta [ $custom_selector ]))
{
2020-12-30 13:02:47 +01:00
$custom_thumb_path = '/' . $this -> image -> getSubdir () . '/' . $this -> image_meta [ $custom_selector ];
if ( file_exists ( ASSETSDIR . $custom_thumb_path ))
2017-12-20 14:51:23 +01:00
{
2020-12-30 13:02:47 +01:00
// Ensure destination thumbnail directory exists.
if ( ! file_exists ( $this -> image -> getSubdir ()))
@ mkdir ( THUMBSDIR . '/' . $this -> image -> getSubdir (), 0755 , true );
2017-12-20 14:51:23 +01:00
// Copy the custom thumbail to the general thumbnail directory.
2020-12-30 13:02:47 +01:00
copy ( ASSETSDIR . $custom_thumb_path , THUMBSDIR . $custom_thumb_path );
2017-12-20 14:51:23 +01:00
// Let's remember this for future reference.
$this -> markAsGenerated ( $this -> image_meta [ $custom_selector ]);
2020-12-30 13:02:47 +01:00
return THUMBSURL . $custom_thumb_path ;
2017-12-20 14:51:23 +01:00
}
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
{
2020-12-30 13:02:47 +01:00
throw new Exception ( " Trying to generate a thumbnail for selector " . $thumb_selector . " , which does not appear to have been requested by the system. \n " . print_r ( func_get_args (), true ));
2017-12-20 14:51:23 +01:00
}
}
/**
* @ 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 )
2018-02-03 20:14:40 +01:00
$this -> width = ceil ( $this -> height * $this -> image -> ratio ());
2017-12-20 14:51:23 +01:00
// 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 ;
// 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 ;
2020-12-30 13:02:47 +01:00
// If the original image's aspect ratio is much wider, take a slice instead.
elseif ( $this -> image -> ratio () > $this -> ratio ())
$this -> crop_mode = self :: CROP_MODE_SLICE_CENTRE ;
2017-12-20 14:51:23 +01:00
// 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' ;
2020-11-29 16:18:41 +01:00
elseif ( $this -> crop_mode === self :: CROP_MODE_BOUNDARY )
$this -> filename_suffix .= 'e' ;
2017-12-20 14:51:23 +01:00
}
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 );
2020-03-22 22:56:53 +01:00
// No need to preserve every detail.
$thumb -> setImageCompressionQuality ( 80 );
2017-12-20 14:51:23 +01:00
// 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 );
}
}