[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:
Diogo Peralta Cordeiro 2021-05-01 22:48:44 +01:00 committed by Hugo Sales
parent 1c9f807595
commit 4fcccb1d1c
No known key found for this signature in database
GPG Key ID: 7D0C7EAFC9D835A0
3 changed files with 139 additions and 35 deletions

View File

@ -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 {

View File

@ -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'],

View File

@ -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'],
], ],
]; ];