[CORE][ImageEncoder] Add width and height back in attachment entity and allow for differently sized thumbs
The strategy adopted involves predicting the thumb size as we did in v2 before having vips resize
This commit is contained in:
parent
1c9f807595
commit
4fcccb1d1c
|
@ -56,12 +56,18 @@ class ImageEncoder extends Plugin
|
||||||
/**
|
/**
|
||||||
* Encodes the image to self::preferredType() format ensuring it's valid.
|
* Encodes the image to self::preferredType() format ensuring it's valid.
|
||||||
*
|
*
|
||||||
* @param SymfonyFile $sfile i/o
|
* @param \SplFileInfo $file
|
||||||
* @param null|string $mimetype out
|
* @param null|string $mimetype in/out
|
||||||
|
* @param null|string $title in/out
|
||||||
|
* @param null|int $width out
|
||||||
|
* @param null|int $height out
|
||||||
|
*
|
||||||
|
* @throws Vips\Exception
|
||||||
|
* @throws \App\Util\Exception\TemporaryFileException
|
||||||
*
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function onAttachmentValidation(\SplFileInfo &$file, ?string &$mimetype, ?string &$title): bool
|
public function onAttachmentValidation(\SplFileInfo &$file, ?string &$mimetype, ?string &$title, ?int &$width, ?int &$height): bool
|
||||||
{
|
{
|
||||||
$original_mimetype = $mimetype;
|
$original_mimetype = $mimetype;
|
||||||
if (GSFile::mimetypeMajor($original_mimetype) != 'image') {
|
if (GSFile::mimetypeMajor($original_mimetype) != 'image') {
|
||||||
|
@ -97,11 +103,6 @@ class ImageEncoder extends Plugin
|
||||||
return Event::stop;
|
return Event::stop;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onResizeImage(Attachment $attachment, AttachmentThumbnail $thumbnail, int $width, int $height, bool $smart_crop): bool
|
|
||||||
{
|
|
||||||
return $this->onResizeImagePath($attachment->getPath(), $thumbnail->getPath(), $width, $height, $smart_crop, $__mimetype);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resizes an image. It will encode the image in the
|
* Resizes an image. It will encode the image in the
|
||||||
* `self::preferredType()` format. This only applies henceforward,
|
* `self::preferredType()` format. This only applies henceforward,
|
||||||
|
@ -123,7 +124,7 @@ class ImageEncoder extends Plugin
|
||||||
* @return bool
|
* @return bool
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public function onResizeImagePath(string $source, string $destination, int $width, int $height, bool $smart_crop, ?string &$mimetype)
|
public function onResizeImagePath(string $source, string $destination, int &$width, int &$height, bool $smart_crop, ?string &$mimetype)
|
||||||
{
|
{
|
||||||
$old_limit = ini_set('memory_limit', Common::config('attachments', 'memory_limit'));
|
$old_limit = ini_set('memory_limit', Common::config('attachments', 'memory_limit'));
|
||||||
try {
|
try {
|
||||||
|
@ -145,6 +146,10 @@ class ImageEncoder extends Plugin
|
||||||
if ($smart_crop) {
|
if ($smart_crop) {
|
||||||
$image = $image->smartcrop($width, $height);
|
$image = $image->smartcrop($width, $height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$width = $image->width;
|
||||||
|
$height = $image->height;
|
||||||
|
|
||||||
$image->writeToFile($destination);
|
$image->writeToFile($destination);
|
||||||
unset($image);
|
unset($image);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -55,6 +55,8 @@ class Attachment extends Entity
|
||||||
private ?int $source;
|
private ?int $source;
|
||||||
private ?int $scope;
|
private ?int $scope;
|
||||||
private ?int $size;
|
private ?int $size;
|
||||||
|
private ?int $width;
|
||||||
|
private ?int $height;
|
||||||
private \DateTimeInterface $modified;
|
private \DateTimeInterface $modified;
|
||||||
|
|
||||||
public function setId(int $id): self
|
public function setId(int $id): self
|
||||||
|
@ -189,17 +191,40 @@ class Attachment extends Entity
|
||||||
return $this->size;
|
return $this->size;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setModified(DateTimeInterface $modified): self
|
public function setWidth(?int $width): self
|
||||||
|
{
|
||||||
|
$this->width = $width;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getWidth(): ?int
|
||||||
|
{
|
||||||
|
return $this->width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setHeight(?int $height): self
|
||||||
|
{
|
||||||
|
$this->height = $height;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHeight(): ?int
|
||||||
|
{
|
||||||
|
return $this->height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setModified(\DateTimeInterface $modified): self
|
||||||
{
|
{
|
||||||
$this->modified = $modified;
|
$this->modified = $modified;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getModified(): DateTimeInterface
|
public function getModified(): \DateTimeInterface
|
||||||
{
|
{
|
||||||
return $this->modified;
|
return $this->modified;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// }}} Autocode
|
// }}} Autocode
|
||||||
|
|
||||||
const URLHASH_ALGO = 'sha256';
|
const URLHASH_ALGO = 'sha256';
|
||||||
|
@ -272,6 +297,8 @@ class Attachment extends Entity
|
||||||
'source' => ['type' => 'int', 'default' => null, 'description' => 'Source of the Attachment (upload, TFN, embed)'],
|
'source' => ['type' => 'int', 'default' => null, 'description' => 'Source of the Attachment (upload, TFN, embed)'],
|
||||||
'scope' => ['type' => 'int', 'default' => null, 'description' => 'visibility scope for this attachment'],
|
'scope' => ['type' => 'int', 'default' => null, 'description' => 'visibility scope for this attachment'],
|
||||||
'size' => ['type' => 'int', 'description' => 'size of resource when available'],
|
'size' => ['type' => 'int', 'description' => 'size of resource when available'],
|
||||||
|
'width' => ['type' => 'int', 'description' => 'width in pixels, if it can be described as such and data is available'],
|
||||||
|
'height' => ['type' => 'int', 'description' => 'height in pixels, if it can be described as such and data is available'],
|
||||||
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
||||||
],
|
],
|
||||||
'primary key' => ['id'],
|
'primary key' => ['id'],
|
||||||
|
|
|
@ -27,10 +27,10 @@ use App\Core\Entity;
|
||||||
use App\Core\Event;
|
use App\Core\Event;
|
||||||
use App\Core\GSFile;
|
use App\Core\GSFile;
|
||||||
use App\Core\Log;
|
use App\Core\Log;
|
||||||
use App\Core\Router;
|
|
||||||
use App\Util\Common;
|
use App\Util\Common;
|
||||||
use App\Util\Exception\NotFoundException;
|
use App\Util\Exception\NotFoundException;
|
||||||
use App\Util\Exception\ServerException;
|
use App\Util\Exception\ServerException;
|
||||||
|
use App\Util\TemporaryFile;
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -53,6 +53,7 @@ class AttachmentThumbnail extends Entity
|
||||||
private int $attachment_id;
|
private int $attachment_id;
|
||||||
private int $width;
|
private int $width;
|
||||||
private int $height;
|
private int $height;
|
||||||
|
private string $filename;
|
||||||
private \DateTimeInterface $modified;
|
private \DateTimeInterface $modified;
|
||||||
|
|
||||||
public function setAttachmentId(int $attachment_id): self
|
public function setAttachmentId(int $attachment_id): self
|
||||||
|
@ -88,17 +89,29 @@ class AttachmentThumbnail extends Entity
|
||||||
return $this->height;
|
return $this->height;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setModified(DateTimeInterface $modified): self
|
public function setFilename(string $filename): self
|
||||||
|
{
|
||||||
|
$this->filename = $filename;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFilename(): string
|
||||||
|
{
|
||||||
|
return $this->filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setModified(\DateTimeInterface $modified): self
|
||||||
{
|
{
|
||||||
$this->modified = $modified;
|
$this->modified = $modified;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getModified(): DateTimeInterface
|
public function getModified(): \DateTimeInterface
|
||||||
{
|
{
|
||||||
return $this->modified;
|
return $this->modified;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// }}} Autocode
|
// }}} Autocode
|
||||||
|
|
||||||
private Attachment $attachment;
|
private Attachment $attachment;
|
||||||
|
@ -117,19 +130,27 @@ class AttachmentThumbnail extends Entity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getOrCreate(Attachment $attachment, ?int $width = null, ?int $height = null, ?bool $crop = null)
|
public static function getOrCreate(Attachment $attachment, int $width, int $height, bool $crop)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
return Cache::get('thumb-' . $attachment->getId() . "-{$width}x{$height}",
|
return Cache::get('thumb-' . $attachment->getId() . "-{$width}x{$height}",
|
||||||
function () use ($attachment, $width, $height) {
|
function () use ($crop, $attachment, $width, $height) {
|
||||||
return DB::findOneBy('attachment_thumbnail', ['attachment_id' => $attachment->getId(), 'width' => $width, 'height' => $height]);
|
[$predicted_width, $predicted_height] = self::predictScalingValues($attachment->getWidth(),$attachment->getHeight(), $width, $height, $crop);
|
||||||
});
|
return DB::findOneBy('attachment_thumbnail', ['attachment_id' => $attachment->getId(), 'width' => $predicted_width, 'height' => $predicted_height]);
|
||||||
|
});
|
||||||
} catch (NotFoundException $e) {
|
} catch (NotFoundException $e) {
|
||||||
$thumbnail = self::create(['attachment_id' => $attachment->getId(), 'width' => $width, 'height' => $height, 'attachment' => $attachment]);
|
$ext = image_type_to_extension(IMAGETYPE_WEBP, include_dot: true);
|
||||||
$event_map = ['image' => 'ResizeImage', 'video' => 'ResizeVideo'];
|
$temp = new TemporaryFile(prefix: null, suffix: $ext);
|
||||||
|
$thumbnail = self::create(['attachment_id' => $attachment->getId()]);
|
||||||
|
$event_map = ['image' => 'ResizeImagePath', 'video' => 'ResizeVideoPath'];
|
||||||
$major_mime = GSFile::mimetypeMajor($attachment->getMimetype());
|
$major_mime = GSFile::mimetypeMajor($attachment->getMimetype());
|
||||||
if (in_array($major_mime, array_keys($event_map))) {
|
if (in_array($major_mime, array_keys($event_map))) {
|
||||||
Event::handle($event_map[$major_mime], [$attachment, $thumbnail, $width, $height, $crop]);
|
Event::handle($event_map[$major_mime], [$attachment->getPath(), $temp->getRealPath(), &$width, &$height, $crop, &$mimetype]);
|
||||||
|
$thumbnail->setWidth($width);
|
||||||
|
$thumbnail->setHeight($height);
|
||||||
|
$filename = "{$width}x{$height}{$ext}-" . $attachment->getFileHash();
|
||||||
|
$temp->commit(Common::config('thumbnail', 'dir') . $filename);
|
||||||
|
$thumbnail->setFilename($filename);
|
||||||
DB::persist($thumbnail);
|
DB::persist($thumbnail);
|
||||||
DB::flush();
|
DB::flush();
|
||||||
return $thumbnail;
|
return $thumbnail;
|
||||||
|
@ -140,13 +161,6 @@ class AttachmentThumbnail extends Entity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFilename()
|
|
||||||
{
|
|
||||||
// TODO only for images
|
|
||||||
$ext = image_type_to_extension(IMAGETYPE_WEBP, include_dot: true);
|
|
||||||
return $this->getAttachment()->getFileHash() . "-{$this->width}x{$this->height}{$ext}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPath()
|
public function getPath()
|
||||||
{
|
{
|
||||||
return Common::config('thumbnail', 'dir') . $this->getFilename();
|
return Common::config('thumbnail', 'dir') . $this->getFilename();
|
||||||
|
@ -164,8 +178,8 @@ class AttachmentThumbnail extends Entity
|
||||||
{
|
{
|
||||||
$attrs = [
|
$attrs = [
|
||||||
'height' => $this->getHeight(),
|
'height' => $this->getHeight(),
|
||||||
'width' => $this->getWidth(),
|
'width' => $this->getWidth(),
|
||||||
'src' => $this->getUrl(),
|
'src' => $this->getUrl(),
|
||||||
];
|
];
|
||||||
return $overwrite ? array_merge($orig, $attrs) : array_merge($attrs, $orig);
|
return $overwrite ? array_merge($orig, $attrs) : array_merge($attrs, $orig);
|
||||||
}
|
}
|
||||||
|
@ -187,18 +201,76 @@ class AttachmentThumbnail extends Entity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets scaling values for images of various types. Cropping can be enabled.
|
||||||
|
*
|
||||||
|
* Values will scale _up_ to fit max values if cropping is enabled!
|
||||||
|
* With cropping disabled, the max value of each axis will be respected.
|
||||||
|
*
|
||||||
|
* @param $width int Original width
|
||||||
|
* @param $height int Original height
|
||||||
|
* @param $maxW int Resulting max width
|
||||||
|
* @param $maxH int Resulting max height
|
||||||
|
* @param $crop bool Crop to the size (not preserving aspect ratio)
|
||||||
|
*
|
||||||
|
* @return array [predicted width, predicted height]
|
||||||
|
*/
|
||||||
|
public static function predictScalingValues(
|
||||||
|
int $width,
|
||||||
|
int $height,
|
||||||
|
int $maxW,
|
||||||
|
int $maxH,
|
||||||
|
bool $crop
|
||||||
|
): array
|
||||||
|
{
|
||||||
|
// Cropping data (for original image size). Default values, 0 and null,
|
||||||
|
// imply no cropping and with preserved aspect ratio (per axis).
|
||||||
|
$cx = 0; // crop x
|
||||||
|
$cy = 0; // crop y
|
||||||
|
$cw = null; // crop area width
|
||||||
|
$ch = null; // crop area height
|
||||||
|
|
||||||
|
if ($crop) {
|
||||||
|
$s_ar = $width / $height;
|
||||||
|
$t_ar = $maxW / $maxH;
|
||||||
|
|
||||||
|
$rw = $maxW;
|
||||||
|
$rh = $maxH;
|
||||||
|
|
||||||
|
// Source aspect ratio differs from target, recalculate crop points!
|
||||||
|
if ($s_ar > $t_ar) {
|
||||||
|
$cx = floor($width / 2 - $height * $t_ar / 2);
|
||||||
|
$cw = ceil($height * $t_ar);
|
||||||
|
} elseif ($s_ar < $t_ar) {
|
||||||
|
$cy = floor($height / 2 - $width / $t_ar / 2);
|
||||||
|
$ch = ceil($width / $t_ar);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$rw = $maxW;
|
||||||
|
$rh = ceil($height * $rw / $width);
|
||||||
|
|
||||||
|
// Scaling caused too large height, decrease to max accepted value
|
||||||
|
if ($rh > $maxH) {
|
||||||
|
$rh = $maxH;
|
||||||
|
$rw = ceil($width * $rh / $height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [(int)$rw, (int)$rh];
|
||||||
|
}
|
||||||
|
|
||||||
public static function schemaDef(): array
|
public static function schemaDef(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => 'attachment_thumbnail',
|
'name' => 'attachment_thumbnail',
|
||||||
'fields' => [
|
'fields' => [
|
||||||
'attachment_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Attachment.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'thumbnail for what attachment'],
|
'attachment_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Attachment.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'thumbnail for what attachment'],
|
||||||
'width' => ['type' => 'int', 'not null' => true, 'description' => 'width of thumbnail'],
|
'width' => ['type' => 'int', 'not null' => true, 'description' => 'width of thumbnail'],
|
||||||
'height' => ['type' => 'int', 'not null' => true, 'description' => 'height of thumbnail'],
|
'height' => ['type' => 'int', 'not null' => true, 'description' => 'height of thumbnail'],
|
||||||
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
'filename' => ['type' => 'varchar', 'length' => 191, 'not null' => true, 'description' => 'thubmnail filename'],
|
||||||
|
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
||||||
],
|
],
|
||||||
'primary key' => ['attachment_id', 'width', 'height'],
|
'primary key' => ['attachment_id', 'width', 'height'],
|
||||||
'indexes' => [
|
'indexes' => [
|
||||||
'attachment_thumbnail_attachment_id_idx' => ['attachment_id'],
|
'attachment_thumbnail_attachment_id_idx' => ['attachment_id'],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
Loading…
Reference in New Issue
Block a user