forked from Public/pics
Initial commit.
This is to be the new HashRU website based on the Aaronweb.net/Kabuki CMS.
This commit is contained in:
382
models/Image.php
Normal file
382
models/Image.php
Normal file
@@ -0,0 +1,382 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* Image.php
|
||||
* Contains key class Image.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class Image extends Asset
|
||||
{
|
||||
const TYPE_PANORAMA = 1;
|
||||
const TYPE_LANDSCAPE = 2;
|
||||
const TYPE_PORTRAIT = 4;
|
||||
|
||||
protected function __construct(array $data)
|
||||
{
|
||||
foreach ($data as $attribute => $value)
|
||||
$this->$attribute = $value;
|
||||
}
|
||||
|
||||
public static function fromId($id_asset, $return_format = 'object')
|
||||
{
|
||||
$asset = parent::fromId($id_asset, 'array');
|
||||
if ($asset)
|
||||
return $return_format == 'object' ? new Image($asset) : $asset;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function fromIds(array $id_assets, $return_format = 'object')
|
||||
{
|
||||
if (empty($id_assets))
|
||||
return [];
|
||||
|
||||
$assets = parent::fromIds($id_assets, 'array');
|
||||
|
||||
if ($return_format == 'array')
|
||||
return $assets;
|
||||
else
|
||||
{
|
||||
$objects = [];
|
||||
foreach ($assets as $id => $asset)
|
||||
$objects[$id] = new Image($asset);
|
||||
return $objects;
|
||||
}
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$data = [];
|
||||
foreach ($this->meta as $key => $value)
|
||||
$data[] = [
|
||||
'id_asset' => $this->id_asset,
|
||||
'variable' => $key,
|
||||
'value' => $value,
|
||||
];
|
||||
|
||||
return Registry::get('db')->insert('replace', 'assets_meta', [
|
||||
'id_asset' => 'int',
|
||||
'variable' => 'string',
|
||||
'value' => 'string',
|
||||
], $data);
|
||||
}
|
||||
|
||||
public function getExif()
|
||||
{
|
||||
return EXIF::fromFile($this->getPath());
|
||||
}
|
||||
|
||||
public function getPath()
|
||||
{
|
||||
return ASSETSDIR . '/' . $this->subdir . '/' . $this->filename;
|
||||
}
|
||||
|
||||
public function getUrl()
|
||||
{
|
||||
return ASSETSURL . '/' . $this->subdir . '/' . $this->filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 estimation [false].
|
||||
*/
|
||||
public function getThumbnailUrl($width, $height, $crop = true, $fit = true)
|
||||
{
|
||||
// First, assert the image's dimensions are properly known in the database.
|
||||
if (!isset($this->image_height, $this->image_width))
|
||||
throw new UnexpectedValueException('Image width or height is undefined -- inconsistent database?');
|
||||
|
||||
// Inferring width or height?
|
||||
if (!$height)
|
||||
$height = ceil($width / $this->image_width * $this->image_height);
|
||||
elseif (!$width)
|
||||
$width = ceil($height / $this->image_height * $this->image_width);
|
||||
|
||||
// Inferring the height from the original image's ratio?
|
||||
if (!$fit)
|
||||
$height = floor($width / ($this->image_width / $this->image_height));
|
||||
|
||||
// Assert we have both, now...
|
||||
if (empty($width) || empty($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)
|
||||
{
|
||||
// If the original image's aspect ratio is much wider, take a slice instead.
|
||||
if ($this->image_width / $this->image_height > $width / $height)
|
||||
$crop = 'slice';
|
||||
|
||||
// We won't be cropping if the thumbnail is proportional to its original.
|
||||
if (abs($width / $height - $this->image_width / $this->image_height) <= 0.05)
|
||||
$crop = false;
|
||||
}
|
||||
|
||||
// Do we have an exact crop boundary for these dimensions?
|
||||
$crop_selector = "crop_{$width}x{$height}";
|
||||
if (isset($this->meta[$crop_selector]))
|
||||
$crop = 'exact';
|
||||
|
||||
// Now, do we need to suffix the filename?
|
||||
if ($crop)
|
||||
$suffix = '_c' . (is_string($crop) && $crop !== 'center' ? substr($crop, 0, 1) : '');
|
||||
else
|
||||
$suffix = '';
|
||||
|
||||
// Check whether we already resized this earlier.
|
||||
$thumb_selector = "thumb_{$width}x{$height}{$suffix}";
|
||||
if (isset($this->meta[$thumb_selector]) && file_exists(THUMBSDIR . '/' . $this->subdir . '/' . $this->meta[$thumb_selector]))
|
||||
return THUMBSURL . '/' . $this->subdir . '/' . $this->meta[$thumb_selector];
|
||||
|
||||
// Do we have a custom thumbnail on file?
|
||||
$custom_selector = "custom_{$width}x{$height}";
|
||||
if (isset($this->meta[$custom_selector]))
|
||||
{
|
||||
if (file_exists(ASSETSDIR . '/' . $this->subdir . '/' . $this->meta[$custom_selector]))
|
||||
{
|
||||
// Copy the custom thumbail to the general thumbnail directory.
|
||||
copy(ASSETSDIR . '/' . $this->subdir . '/' . $this->meta[$custom_selector],
|
||||
THUMBSDIR . '/' . $this->subdir . '/' . $this->meta[$custom_selector]);
|
||||
|
||||
// Let's remember this for future reference.
|
||||
$this->meta[$thumb_selector] = $this->meta[$custom_selector];
|
||||
$this->save();
|
||||
|
||||
return THUMBSURL . '/' . $this->subdir . '/' . $this->meta[$custom_selector];
|
||||
}
|
||||
else
|
||||
throw new UnexpectedValueException('Custom thumbnail expected, but missing in file system!');
|
||||
}
|
||||
|
||||
// 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->subdir . '/' . $this->filename);
|
||||
|
||||
// The image might have some orientation set through EXIF. Let's apply this first.
|
||||
self::applyRotation($thumb);
|
||||
|
||||
// Just resizing? Easy peasy.
|
||||
if (!$crop)
|
||||
$thumb->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
|
||||
// Cropping in the center?
|
||||
elseif ($crop === true || $crop === 'center')
|
||||
$thumb->cropThumbnailImage($width, $height);
|
||||
// Exact cropping? We can do that.
|
||||
elseif ($crop === 'exact')
|
||||
{
|
||||
list($crop_width, $crop_height, $crop_x_pos, $crop_y_pos) = explode(',', $this->meta[$crop_selector]);
|
||||
$thumb->cropImage($crop_width, $crop_height, $crop_x_pos, $crop_y_pos);
|
||||
$thumb->resizeImage($width, $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 ($crop === 'top' || $crop === 'bottom')
|
||||
{
|
||||
$crop_width = $size['width'];
|
||||
$crop_height = floor($size['width'] / $width * $height);
|
||||
$target_x = 0;
|
||||
$target_y = $crop === 'top' ? 0 : $size['height'] - $crop_height;
|
||||
}
|
||||
// Otherwise, we're taking a vertical slice from the centre.
|
||||
else
|
||||
{
|
||||
$crop_width = floor($size['height'] / $height * $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($width, $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?
|
||||
$thumbfilename = substr($this->filename, 0, strrpos($this->filename, '.')) . "_{$width}x{$height}{$suffix}.$ext";
|
||||
|
||||
// Ensure the thumbnail subdirectory exists.
|
||||
if (!is_dir(THUMBSDIR . '/' . $this->subdir))
|
||||
mkdir(THUMBSDIR . '/' . $this->subdir, 0755, true);
|
||||
|
||||
// Save it in a public spot.
|
||||
$thumb->writeImage(THUMBSDIR . '/' . $this->subdir . '/' . $thumbfilename);
|
||||
$thumb->clear();
|
||||
$thumb->destroy();
|
||||
}
|
||||
// Blast! Curse your sudden but inevitable betrayal!
|
||||
catch (ImagickException $e)
|
||||
{
|
||||
throw new Exception('ImageMagick error occurred while generating thumbnail. Output: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Let's remember this for future reference.
|
||||
$this->meta[$thumb_selector] = $thumbfilename;
|
||||
$this->save();
|
||||
|
||||
// Ah yes, you wanted a URL, didn't you...
|
||||
return THUMBSURL . '/' . $this->subdir . '/' . $this->meta[$thumb_selector];
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public function bestColor()
|
||||
{
|
||||
// Save some computations if we can.
|
||||
if (isset($this->meta['best_color']))
|
||||
return $this->meta['best_color'];
|
||||
|
||||
// Find out what colour is most prominent.
|
||||
$color = new BestColor($this);
|
||||
$this->meta['best_color'] = $color->hex();
|
||||
$this->save();
|
||||
|
||||
// There's your colour.
|
||||
return $this->meta['best_color'];
|
||||
}
|
||||
|
||||
public function bestLabelColor()
|
||||
{
|
||||
// Save some computations if we can.
|
||||
if (isset($this->meta['best_color_label']))
|
||||
return $this->meta['best_color_label'];
|
||||
|
||||
// Find out what colour is most prominent.
|
||||
$color = new BestColor($this);
|
||||
$this->meta['best_color_label'] = $color->rgba();
|
||||
$this->save();
|
||||
|
||||
// There's your colour.
|
||||
return $this->meta['best_color_label'];
|
||||
}
|
||||
|
||||
public function width()
|
||||
{
|
||||
return $this->image_width;
|
||||
}
|
||||
|
||||
public function height()
|
||||
{
|
||||
return $this->image_height;
|
||||
}
|
||||
|
||||
public function isPanorama()
|
||||
{
|
||||
return $this->image_width / $this->image_height > 2;
|
||||
}
|
||||
|
||||
public function isPortrait()
|
||||
{
|
||||
return $this->image_width / $this->image_height < 1;
|
||||
}
|
||||
|
||||
public function isLandscape()
|
||||
{
|
||||
$ratio = $this->image_width / $this->image_height;
|
||||
return $ratio >= 1 && $ratio <= 2;
|
||||
}
|
||||
|
||||
public function removeAllThumbnails()
|
||||
{
|
||||
foreach ($this->meta as $key => $value)
|
||||
{
|
||||
if (substr($key, 0, 6) !== 'thumb_')
|
||||
continue;
|
||||
|
||||
$thumb_path = THUMBSDIR . '/' . $this->subdir . '/' . $value;
|
||||
if (is_file($thumb_path))
|
||||
unlink($thumb_path);
|
||||
|
||||
unset($this->meta[$key]);
|
||||
}
|
||||
|
||||
$this->saveMetaData();
|
||||
}
|
||||
|
||||
public function replaceThumbnail($descriptor, $tmp_file)
|
||||
{
|
||||
if (!is_file($tmp_file))
|
||||
return -1;
|
||||
|
||||
if (!isset($this->meta[$descriptor]))
|
||||
return -2;
|
||||
|
||||
$image = new Imagick($tmp_file);
|
||||
$d = $image->getImageGeometry();
|
||||
unset($image);
|
||||
|
||||
// Check whether dimensions match.
|
||||
$test_descriptor = 'thumb_' . $d['width'] . 'x' . $d['height'];
|
||||
if ($descriptor !== $test_descriptor && strpos($descriptor, $test_descriptor . '_') === false)
|
||||
return -3;
|
||||
|
||||
// Save the custom thumbnail in the assets directory.
|
||||
$destination = ASSETSDIR . '/' . $this->subdir . '/' . $this->meta[$descriptor];
|
||||
if (file_exists($destination) && !is_writable($destination))
|
||||
return -4;
|
||||
|
||||
if (!copy($tmp_file, $destination))
|
||||
return -5;
|
||||
|
||||
// Copy it to the thumbnail directory, overwriting the automatically generated one, too.
|
||||
$destination = THUMBSDIR . '/' . $this->subdir . '/' . $this->meta[$descriptor];
|
||||
if (file_exists($destination) && !is_writable($destination))
|
||||
return -6;
|
||||
|
||||
if (!copy($tmp_file, $destination))
|
||||
return -7;
|
||||
|
||||
// A little bookkeeping
|
||||
$this->meta['custom_' . $d['width'] . 'x' . $d['height']] = $this->meta[$descriptor];
|
||||
$this->saveMetaData();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user