Backport asynchronous thumbnail generation from Kabuki.
This commit is contained in:
parent
981b652e25
commit
1def1484cb
@ -76,11 +76,11 @@ class EditAsset extends HTMLController
|
|||||||
$image->removeAllThumbnails();
|
$image->removeAllThumbnails();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elseif (preg_match('~^thumb_(\d+)x(\d+)(_c[best]?)?$~', $_POST['replacement_target']))
|
elseif (preg_match('~^thumb_(\d+x\d+(?:_c[best]?)?)$~', $_POST['replacement_target'], $match))
|
||||||
{
|
{
|
||||||
$image = $asset->getImage();
|
$image = $asset->getImage();
|
||||||
if (($replace_result = $image->replaceThumbnail($_POST['replacement_target'], $_FILES['replacement']['tmp_name'])) !== 0)
|
if (($replace_result = $image->replaceThumbnail($match[1], $_FILES['replacement']['tmp_name'])) !== 0)
|
||||||
throw new Exception('Could not replace thumbnail \'' . $_POST['replacement_target'] . '\' with the uploaded file. Error code: ' . $replace_result);
|
throw new Exception('Could not replace thumbnail \'' . $match[1] . '\' with the uploaded file. Error code: ' . $replace_result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,12 +97,18 @@ class EditAsset extends HTMLController
|
|||||||
|
|
||||||
private function getThumbs(Asset $asset)
|
private function getThumbs(Asset $asset)
|
||||||
{
|
{
|
||||||
$path = $asset->getPath();
|
if (!$asset->isImage())
|
||||||
|
return [];
|
||||||
|
|
||||||
|
$image = $asset->getImage();
|
||||||
|
$subdir = $image->getSubdir();
|
||||||
|
$metadata = $image->getMeta();
|
||||||
|
$thumb_selectors = $image->getThumbnails();
|
||||||
|
|
||||||
$thumbs = [];
|
$thumbs = [];
|
||||||
$metadata = $asset->getMeta();
|
foreach ($thumb_selectors as $selector => $filename)
|
||||||
foreach ($metadata as $key => $meta)
|
|
||||||
{
|
{
|
||||||
if (!preg_match('~^thumb_(?<width>\d+)x(?<height>\d+)(?<suffix>_c(?<method>[best]?))?$~', $key, $thumb))
|
if (!preg_match('~^(?<width>\d+)x(?<height>\d+)(?<suffix>_c(?<method>[best]?))?$~', $selector, $thumb))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
$has_crop_boundary = isset($metadata['crop_' . $thumb['width'] . 'x' . $thumb['height']]);
|
$has_crop_boundary = isset($metadata['crop_' . $thumb['width'] . 'x' . $thumb['height']]);
|
||||||
@ -113,10 +119,10 @@ class EditAsset extends HTMLController
|
|||||||
'crop_method' => !$has_custom_image && !empty($thumb['method']) ? $thumb['method'] : (!empty($thumb['suffix']) ? 'c' : null),
|
'crop_method' => !$has_custom_image && !empty($thumb['method']) ? $thumb['method'] : (!empty($thumb['suffix']) ? 'c' : null),
|
||||||
'crop_region' => $has_crop_boundary ? $metadata['crop_' . $thumb['width'] . 'x' . $thumb['height']] : null,
|
'crop_region' => $has_crop_boundary ? $metadata['crop_' . $thumb['width'] . 'x' . $thumb['height']] : null,
|
||||||
'custom_image' => $has_custom_image,
|
'custom_image' => $has_custom_image,
|
||||||
'filename' => $meta,
|
'filename' => $filename,
|
||||||
'full_path' => THUMBSDIR . '/' . $path . '/' . $meta,
|
'full_path' => THUMBSDIR . '/' . $subdir . '/' . $filename,
|
||||||
'url' => THUMBSURL . '/' . $path . '/' . $meta,
|
'url' => THUMBSURL . '/' . $subdir . '/' . $filename,
|
||||||
'status' => file_exists(THUMBSDIR . '/' . $path . '/' . $meta),
|
'status' => file_exists(THUMBSDIR . '/' . $subdir . '/' . $filename),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
27
controllers/GenerateThumbnail.php
Normal file
27
controllers/GenerateThumbnail.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* GenerateThumbnail.php
|
||||||
|
* Contains the asynchronous thumbnail generation controller
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2017, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
class GenerateThumbnail extends HTMLController
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$asset = Asset::fromId($_GET['id']);
|
||||||
|
if (empty($asset) || !$asset->isImage())
|
||||||
|
throw new NotFoundException('Image not found');
|
||||||
|
|
||||||
|
$image = $asset->getImage();
|
||||||
|
$crop_mode = isset($_GET['mode']) ? $_GET['mode'] : false;
|
||||||
|
$url = $image->getThumbnailUrl($_GET['width'], $_GET['height'], $crop_mode, true, true);
|
||||||
|
|
||||||
|
if ($url)
|
||||||
|
{
|
||||||
|
header('Location: ' . $url);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
53
migrate_thumbs.php
Normal file
53
migrate_thumbs.php
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* migrate_thumbs.php
|
||||||
|
* Migrates old-style thumbnails (meta) to new table.
|
||||||
|
*
|
||||||
|
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
// Include the project's configuration.
|
||||||
|
require_once 'config.php';
|
||||||
|
|
||||||
|
// Set up the autoloader.
|
||||||
|
require_once 'vendor/autoload.php';
|
||||||
|
|
||||||
|
// Initialise the database.
|
||||||
|
$db = new Database(DB_SERVER, DB_USER, DB_PASS, DB_NAME);
|
||||||
|
Registry::set('db', $db);
|
||||||
|
|
||||||
|
// Do some authentication checks.
|
||||||
|
Session::start();
|
||||||
|
Registry::set('user', Authentication::isLoggedIn() ? Member::fromId($_SESSION['user_id']) : new Guest());
|
||||||
|
|
||||||
|
|
||||||
|
$res = $db->query('
|
||||||
|
SELECT id_asset, variable, value
|
||||||
|
FROM assets_meta
|
||||||
|
WHERE variable LIKE {string:thumbs}',
|
||||||
|
['thumbs' => 'thumb_%']);
|
||||||
|
|
||||||
|
while ($row = $db->fetch_assoc($res))
|
||||||
|
{
|
||||||
|
if (!preg_match('~^thumb_(?<width>\d+)x(?<height>\d+)(?:_(?<mode>c[best]?))?$~', $row['variable'], $match))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
echo 'Migrating ... ', $row['value'], '(#', $row['id_asset'], ")\r";
|
||||||
|
|
||||||
|
$db->insert('replace', 'assets_thumbs', [
|
||||||
|
'id_asset' => 'int',
|
||||||
|
'width' => 'int',
|
||||||
|
'height' => 'int',
|
||||||
|
'mode' => 'string-3',
|
||||||
|
'filename' => 'string-255',
|
||||||
|
], [
|
||||||
|
'id_asset' => $row['id_asset'],
|
||||||
|
'width' => $match['width'],
|
||||||
|
'height' => $match['height'],
|
||||||
|
'mode' => $match['mode'] ?? '',
|
||||||
|
'filename' => $row['value'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nDone\n";
|
||||||
|
|
@ -18,8 +18,10 @@ class Asset
|
|||||||
protected $image_height;
|
protected $image_height;
|
||||||
protected $date_captured;
|
protected $date_captured;
|
||||||
protected $priority;
|
protected $priority;
|
||||||
|
|
||||||
protected $meta;
|
protected $meta;
|
||||||
protected $tags;
|
protected $tags;
|
||||||
|
protected $thumbnails;
|
||||||
|
|
||||||
protected function __construct(array $data)
|
protected function __construct(array $data)
|
||||||
{
|
{
|
||||||
@ -58,8 +60,10 @@ class Asset
|
|||||||
|
|
||||||
public static function byRow(array $row, $return_format = 'object')
|
public static function byRow(array $row, $return_format = 'object')
|
||||||
{
|
{
|
||||||
|
$db = Registry::get('db');
|
||||||
|
|
||||||
// Supplement with metadata.
|
// Supplement with metadata.
|
||||||
$row['meta'] = Registry::get('db')->queryPair('
|
$row['meta'] = $db->queryPair('
|
||||||
SELECT variable, value
|
SELECT variable, value
|
||||||
FROM assets_meta
|
FROM assets_meta
|
||||||
WHERE id_asset = {int:id_asset}',
|
WHERE id_asset = {int:id_asset}',
|
||||||
@ -67,6 +71,24 @@ class Asset
|
|||||||
'id_asset' => $row['id_asset'],
|
'id_asset' => $row['id_asset'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// And thumbnails.
|
||||||
|
$row['thumbnails'] = $db->queryPair('
|
||||||
|
SELECT
|
||||||
|
CONCAT(
|
||||||
|
width,
|
||||||
|
{string:x},
|
||||||
|
height,
|
||||||
|
IF(mode != {string:empty}, CONCAT({string:_}, mode), {string:empty})
|
||||||
|
) AS selector, filename
|
||||||
|
FROM assets_thumbs
|
||||||
|
WHERE id_asset = {int:id_asset}',
|
||||||
|
[
|
||||||
|
'id_asset' => $row['id_asset'],
|
||||||
|
'empty' => '',
|
||||||
|
'x' => 'x',
|
||||||
|
'_' => '_',
|
||||||
|
]);
|
||||||
|
|
||||||
return $return_format == 'object' ? new Asset($row) : $row;
|
return $return_format == 'object' ? new Asset($row) : $row;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,6 +113,7 @@ class Asset
|
|||||||
{
|
{
|
||||||
$assets[$asset['id_asset']] = $asset;
|
$assets[$asset['id_asset']] = $asset;
|
||||||
$assets[$asset['id_asset']]['meta'] = [];
|
$assets[$asset['id_asset']]['meta'] = [];
|
||||||
|
$assets[$asset['id_asset']]['thumbnails'] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$metas = $db->queryRows('
|
$metas = $db->queryRows('
|
||||||
@ -105,6 +128,27 @@ class Asset
|
|||||||
foreach ($metas as $meta)
|
foreach ($metas as $meta)
|
||||||
$assets[$meta[0]]['meta'][$meta[1]] = $meta[2];
|
$assets[$meta[0]]['meta'][$meta[1]] = $meta[2];
|
||||||
|
|
||||||
|
$thumbnails = $db->queryRows('
|
||||||
|
SELECT id_asset,
|
||||||
|
CONCAT(
|
||||||
|
width,
|
||||||
|
{string:x},
|
||||||
|
height,
|
||||||
|
IF(mode != {string:empty}, CONCAT({string:_}, mode), {string:empty})
|
||||||
|
) AS selector, filename
|
||||||
|
FROM assets_thumbs
|
||||||
|
WHERE id_asset IN ({array_int:id_assets})
|
||||||
|
ORDER BY id_asset',
|
||||||
|
[
|
||||||
|
'id_assets' => $id_assets,
|
||||||
|
'empty' => '',
|
||||||
|
'x' => 'x',
|
||||||
|
'_' => '_',
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($thumbnails as $thumb)
|
||||||
|
$assets[$thumb[0]]['thumbnails'][$thumb[1]] = $thumb[2];
|
||||||
|
|
||||||
if ($return_format == 'array')
|
if ($return_format == 'array')
|
||||||
return $assets;
|
return $assets;
|
||||||
else
|
else
|
||||||
@ -270,7 +314,7 @@ class Asset
|
|||||||
return ASSETSDIR . '/' . $this->subdir . '/' . $this->filename;
|
return ASSETSDIR . '/' . $this->subdir . '/' . $this->filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPath()
|
public function getSubdir()
|
||||||
{
|
{
|
||||||
return $this->subdir;
|
return $this->subdir;
|
||||||
}
|
}
|
||||||
|
@ -11,12 +11,14 @@ class AssetIterator extends Asset
|
|||||||
private $return_format;
|
private $return_format;
|
||||||
private $res_assets;
|
private $res_assets;
|
||||||
private $res_meta;
|
private $res_meta;
|
||||||
|
private $res_thumbs;
|
||||||
|
|
||||||
protected function __construct($res_assets, $res_meta, $return_format)
|
protected function __construct($res_assets, $res_meta, $res_thumbs, $return_format)
|
||||||
{
|
{
|
||||||
$this->db = Registry::get('db');
|
$this->db = Registry::get('db');
|
||||||
$this->res_assets = $res_assets;
|
$this->res_assets = $res_assets;
|
||||||
$this->res_meta = $res_meta;
|
$this->res_meta = $res_meta;
|
||||||
|
$this->res_thumbs = $res_thumbs;
|
||||||
$this->return_format = $return_format;
|
$this->return_format = $return_format;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,6 +43,19 @@ class AssetIterator extends Asset
|
|||||||
// Reset internal pointer for next asset.
|
// Reset internal pointer for next asset.
|
||||||
$this->db->data_seek($this->res_meta, 0);
|
$this->db->data_seek($this->res_meta, 0);
|
||||||
|
|
||||||
|
// Looks up thumbnails.
|
||||||
|
$row['thumbnails'] = [];
|
||||||
|
while ($thumbs = $this->db->fetch_assoc($this->res_thumbs))
|
||||||
|
{
|
||||||
|
if ($thumbs['id_asset'] != $row['id_asset'])
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$row['thumbnails'][$thumbs['selector']] = $thumbs['filename'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset internal pointer for next asset.
|
||||||
|
$this->db->data_seek($this->res_thumbs, 0);
|
||||||
|
|
||||||
if ($this->return_format == 'object')
|
if ($this->return_format == 'object')
|
||||||
return new Asset($row);
|
return new Asset($row);
|
||||||
else
|
else
|
||||||
@ -51,6 +66,7 @@ class AssetIterator extends Asset
|
|||||||
{
|
{
|
||||||
$this->db->data_seek($this->res_assets, 0);
|
$this->db->data_seek($this->res_assets, 0);
|
||||||
$this->db->data_seek($this->res_meta, 0);
|
$this->db->data_seek($this->res_meta, 0);
|
||||||
|
$this->db->data_seek($this->res_thumbs, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function clean()
|
public function clean()
|
||||||
@ -135,7 +151,29 @@ class AssetIterator extends Asset
|
|||||||
ORDER BY id_asset',
|
ORDER BY id_asset',
|
||||||
$params);
|
$params);
|
||||||
|
|
||||||
$iterator = new self($res_assets, $res_meta, $return_format);
|
// Get a resource object for the asset thumbs.
|
||||||
|
$res_thumbs = $db->query('
|
||||||
|
SELECT id_asset, filename,
|
||||||
|
CONCAT(
|
||||||
|
width,
|
||||||
|
{string:x},
|
||||||
|
height,
|
||||||
|
IF(mode != {string:empty}, CONCAT({string:_}, mode), {string:empty})
|
||||||
|
) AS selector
|
||||||
|
FROM assets_thumbs
|
||||||
|
WHERE id_asset IN(
|
||||||
|
SELECT id_asset
|
||||||
|
FROM assets AS a
|
||||||
|
WHERE ' . $where . '
|
||||||
|
)
|
||||||
|
ORDER BY id_asset',
|
||||||
|
$params + [
|
||||||
|
'empty' => '',
|
||||||
|
'x' => 'x',
|
||||||
|
'_' => '_',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$iterator = new self($res_assets, $res_meta, $res_thumbs, $return_format);
|
||||||
|
|
||||||
// Returning total count, too?
|
// Returning total count, too?
|
||||||
if ($return_count)
|
if ($return_count)
|
||||||
|
@ -44,6 +44,12 @@ class Dispatcher
|
|||||||
{
|
{
|
||||||
return new ViewPhotoAlbum();
|
return new ViewPhotoAlbum();
|
||||||
}
|
}
|
||||||
|
// Asynchronously generating thumbnails?
|
||||||
|
elseif (preg_match('~^/thumbnail/(?<id>\d+)/(?<width>\d+)x(?<height>\d+)(?:_(?<mode>c(t|b|s|)))?/?~', $_SERVER['PATH_INFO'], $path))
|
||||||
|
{
|
||||||
|
$_GET = array_merge($_GET, $path);
|
||||||
|
return new GenerateThumbnail();
|
||||||
|
}
|
||||||
// Look for particular actions...
|
// Look for particular actions...
|
||||||
elseif (preg_match('~^/(?<action>[a-z]+)(?:/page/(?<page>\d+))?/?~', $_SERVER['PATH_INFO'], $path) && isset($possibleActions[$path['action']]))
|
elseif (preg_match('~^/(?<action>[a-z]+)(?:/page/(?<page>\d+))?/?~', $_SERVER['PATH_INFO'], $path) && isset($possibleActions[$path['action']]))
|
||||||
{
|
{
|
||||||
|
230
models/Image.php
230
models/Image.php
@ -82,191 +82,12 @@ class Image extends Asset
|
|||||||
* @param height: height 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 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].
|
* @param fit: whether to fit the image to given boundaries [true], or use them merely as an estimation [false].
|
||||||
|
* @param generate: whether or not to generate a thumbnail if no existing file was found.
|
||||||
*/
|
*/
|
||||||
public function getThumbnailUrl($width, $height, $crop = true, $fit = true)
|
public function getThumbnailUrl($width, $height, $crop = true, $fit = true, $generate = false)
|
||||||
{
|
{
|
||||||
// First, assert the image's dimensions are properly known in the database.
|
$thumbnail = new Thumbnail($this);
|
||||||
if (!isset($this->image_height, $this->image_width))
|
return $thumbnail->getUrl($width, $height, $crop, $fit, $generate);
|
||||||
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';
|
|
||||||
$thumb->setImageCompressionQuality(60);
|
|
||||||
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()
|
public function bestColor()
|
||||||
@ -299,6 +120,11 @@ class Image extends Asset
|
|||||||
return $this->meta['best_color_label'];
|
return $this->meta['best_color_label'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getId()
|
||||||
|
{
|
||||||
|
return $this->id_asset;
|
||||||
|
}
|
||||||
|
|
||||||
public function width()
|
public function width()
|
||||||
{
|
{
|
||||||
return $this->image_width;
|
return $this->image_width;
|
||||||
@ -309,37 +135,45 @@ class Image extends Asset
|
|||||||
return $this->image_height;
|
return $this->image_height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function ratio()
|
||||||
|
{
|
||||||
|
return $this->image_width / $this->image_height;
|
||||||
|
}
|
||||||
|
|
||||||
public function isPanorama()
|
public function isPanorama()
|
||||||
{
|
{
|
||||||
return $this->image_width / $this->image_height > 2;
|
return $this->ratio() >= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isPortrait()
|
public function isPortrait()
|
||||||
{
|
{
|
||||||
return $this->image_width / $this->image_height < 1;
|
return $this->ratio() < 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isLandscape()
|
public function isLandscape()
|
||||||
{
|
{
|
||||||
$ratio = $this->image_width / $this->image_height;
|
$ratio = $this->ratio();
|
||||||
return $ratio >= 1 && $ratio <= 2;
|
return $ratio >= 1 && $ratio <= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getThumbnails()
|
||||||
|
{
|
||||||
|
return $this->thumbnails;
|
||||||
|
}
|
||||||
|
|
||||||
public function removeAllThumbnails()
|
public function removeAllThumbnails()
|
||||||
{
|
{
|
||||||
foreach ($this->meta as $key => $value)
|
foreach ($this->thumbnails as $key => $value)
|
||||||
{
|
{
|
||||||
if (substr($key, 0, 6) !== 'thumb_')
|
|
||||||
continue;
|
|
||||||
|
|
||||||
$thumb_path = THUMBSDIR . '/' . $this->subdir . '/' . $value;
|
$thumb_path = THUMBSDIR . '/' . $this->subdir . '/' . $value;
|
||||||
if (is_file($thumb_path))
|
if (is_file($thumb_path))
|
||||||
unlink($thumb_path);
|
unlink($thumb_path);
|
||||||
|
|
||||||
unset($this->meta[$key]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->saveMetaData();
|
return Registry::get('db')->query('
|
||||||
|
DELETE FROM assets_thumbs
|
||||||
|
WHERE id_asset = {int:id_asset}',
|
||||||
|
['id_asset' => $this->id_asset]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function replaceThumbnail($descriptor, $tmp_file)
|
public function replaceThumbnail($descriptor, $tmp_file)
|
||||||
@ -347,7 +181,7 @@ class Image extends Asset
|
|||||||
if (!is_file($tmp_file))
|
if (!is_file($tmp_file))
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
if (!isset($this->meta[$descriptor]))
|
if (!isset($this->thumbnails[$descriptor]))
|
||||||
return -2;
|
return -2;
|
||||||
|
|
||||||
$image = new Imagick($tmp_file);
|
$image = new Imagick($tmp_file);
|
||||||
@ -355,12 +189,12 @@ class Image extends Asset
|
|||||||
unset($image);
|
unset($image);
|
||||||
|
|
||||||
// Check whether dimensions match.
|
// Check whether dimensions match.
|
||||||
$test_descriptor = 'thumb_' . $d['width'] . 'x' . $d['height'];
|
$test_descriptor = $d['width'] . 'x' . $d['height'];
|
||||||
if ($descriptor !== $test_descriptor && strpos($descriptor, $test_descriptor . '_') === false)
|
if ($descriptor !== $test_descriptor && strpos($descriptor, $test_descriptor . '_') === false)
|
||||||
return -3;
|
return -3;
|
||||||
|
|
||||||
// Save the custom thumbnail in the assets directory.
|
// Save the custom thumbnail in the assets directory.
|
||||||
$destination = ASSETSDIR . '/' . $this->subdir . '/' . $this->meta[$descriptor];
|
$destination = ASSETSDIR . '/' . $this->subdir . '/' . $this->thumbnails[$descriptor];
|
||||||
if (file_exists($destination) && !is_writable($destination))
|
if (file_exists($destination) && !is_writable($destination))
|
||||||
return -4;
|
return -4;
|
||||||
|
|
||||||
@ -368,7 +202,7 @@ class Image extends Asset
|
|||||||
return -5;
|
return -5;
|
||||||
|
|
||||||
// Copy it to the thumbnail directory, overwriting the automatically generated one, too.
|
// Copy it to the thumbnail directory, overwriting the automatically generated one, too.
|
||||||
$destination = THUMBSDIR . '/' . $this->subdir . '/' . $this->meta[$descriptor];
|
$destination = THUMBSDIR . '/' . $this->subdir . '/' . $this->thumbnails[$descriptor];
|
||||||
if (file_exists($destination) && !is_writable($destination))
|
if (file_exists($destination) && !is_writable($destination))
|
||||||
return -6;
|
return -6;
|
||||||
|
|
||||||
@ -376,7 +210,7 @@ class Image extends Asset
|
|||||||
return -7;
|
return -7;
|
||||||
|
|
||||||
// A little bookkeeping
|
// A little bookkeeping
|
||||||
$this->meta['custom_' . $d['width'] . 'x' . $d['height']] = $this->meta[$descriptor];
|
$this->meta['custom_' . $d['width'] . 'x' . $d['height']] = $this->thumbnails[$descriptor];
|
||||||
$this->saveMetaData();
|
$this->saveMetaData();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
344
models/Thumbnail.php
Normal file
344
models/Thumbnail.php
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
<?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';
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -279,7 +279,7 @@ class EditAssetForm extends SubTemplate
|
|||||||
echo ' crop';
|
echo ' crop';
|
||||||
}
|
}
|
||||||
elseif ($thumb['custom_image'])
|
elseif ($thumb['custom_image'])
|
||||||
echo ' (custom)';
|
echo ', custom';
|
||||||
|
|
||||||
echo ')
|
echo ')
|
||||||
</option>';
|
</option>';
|
||||||
|
Loading…
Reference in New Issue
Block a user