pics/models/Image.php
Aaron van Geffen c0d69f7205 Do not delete thumbnail queue when replacing an asset
Thumbnails are normally created on demand, e.g. when processing the format codes in a post's body text.
Normally, the temporary URL is only used once to generate thumbnails ad-hoc. However, when cache is
enabled, a reference to the asset may be used in a cached version of a formatted body text, skipping
the normal thumbnail generation routine.

When an asset is replaced, currently, all thumbnails are removed and references to them are removed
from the database. In case the asset is still referenced in a cached formatted body text, this could lead
to an error when requesting the thumbnail, as the thumbnail request is no longer present in the system.

As we do not know what posts use particular assets at this point in the code, it is best to work around this
issue by unsetting the thumbnail filenames rather than deleting the entries outright. This effectively
generates them again on the next request.

In the future, we should aim to keep track of what posts make use of assets, so cache may be invalidated
in a more targeted way.
2022-07-07 14:22:22 +02:00

230 lines
5.6 KiB
PHP

<?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 getImageUrls($width = null, $height = null)
{
$image_urls = [];
if (isset($width) || isset($height))
{
$thumbnail = new Thumbnail($this);
$image_urls[1] = $this->getThumbnailUrl($width, $height, false);
// Can we afford to generate double-density thumbnails as well?
if ((!isset($width) || $this->image_width >= $width * 2) &&
(!isset($height) || $this->image_height >= $height * 2))
$image_urls[2] = $this->getThumbnailUrl($width * 2, $height * 2, false);
}
else
$image_urls[1] = $this->getUrl();
return $image_urls;
}
public function getInlineImage($width = null, $height = null, $className = 'inline-image')
{
$image_urls = $this->getImageUrls($width, $height);
return '<img class="' . $className . '" src="' . $image_urls[1] . '" alt=""' .
(isset($image_urls[2]) ? ' srcset="' . $image_urls[2] . ' 2x"' : '') . '>';
}
/**
* @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].
* @param generate: whether or not to generate a thumbnail if no existing file was found.
*/
public function getThumbnailUrl($width, $height, $crop = true, $fit = true, $generate = false)
{
$thumbnail = new Thumbnail($this);
return $thumbnail->getUrl($width, $height, $crop, $fit, $generate);
}
public function getId()
{
return $this->id_asset;
}
public function width()
{
return $this->image_width;
}
public function height()
{
return $this->image_height;
}
public function ratio()
{
return $this->image_width / $this->image_height;
}
public function isPanorama()
{
return $this->ratio() >= 2;
}
public function isPortrait()
{
return $this->ratio() < 1;
}
public function isLandscape()
{
$ratio = $this->ratio();
return $ratio >= 1 && $ratio <= 2;
}
public function getThumbnails()
{
return $this->thumbnails;
}
public function removeAllThumbnails()
{
foreach ($this->thumbnails as $key => $filename)
{
$thumb_path = THUMBSDIR . '/' . $this->subdir . '/' . $filename;
if (is_file($thumb_path))
unlink($thumb_path);
}
return Registry::get('db')->query('
UPDATE assets_thumbs
SET filename = NULL
WHERE id_asset = {int:id_asset}',
['id_asset' => $this->id_asset]);
}
public function removeThumbnailsOfSize($width, $height)
{
foreach ($this->thumbnails as $key => $filename)
{
if (strpos($key, $width . 'x' . $height) !== 0)
continue;
$thumb_path = THUMBSDIR . '/' . $this->subdir . '/' . $filename;
if (is_file($thumb_path))
unlink($thumb_path);
}
return Registry::get('db')->query('
DELETE FROM assets_thumbs
WHERE id_asset = {int:id_asset} AND
width = {int:width} AND
height = {int:height}',
[
'height' => $height,
'id_asset' => $this->id_asset,
'width' => $width,
]);
}
public function replaceThumbnail($descriptor, $tmp_file)
{
if (!is_file($tmp_file))
return -1;
if (!isset($this->thumbnails[$descriptor]))
return -2;
$image = new Imagick($tmp_file);
$d = $image->getImageGeometry();
unset($image);
// Check whether dimensions match.
$test_descriptor = $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->thumbnails[$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->thumbnails[$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->thumbnails[$descriptor];
$this->saveMetaData();
return 0;
}
}