forked from GNUsocial/gnu-social
[ENTITY][AttachmentThumbnail] Now thumbnails are always only available in three sizes: small, medium, big
Commit jointly produced with eli (Eliseu Amaro) Breaking change: Entity changed to only store the tinyint referring to the size stored With this, the logic was simplified and now it's not possible to make an instance produce unnecessary thumbs. The aspect ratio is preserved and thus the thumbs will always look nice. New configuration was added to maintain flexibility.
This commit is contained in:
parent
7beb5c2995
commit
05f16a3084
@ -106,101 +106,6 @@ class Embed extends Plugin
|
|||||||
return $this->smart_crop ?? Common::config('thumbnail', 'smart_crop');
|
return $this->smart_crop ?? Common::config('thumbnail', 'smart_crop');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add common favicon, embed sizes and social media image sizes
|
|
||||||
* (Commented out ancient sizes)
|
|
||||||
* TODO: This is a temporary "solution" until we handle different sizes properly, discuss with Eliseu
|
|
||||||
*
|
|
||||||
* @param array $sizes
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function onGetAllowedThumbnailSizes(array &$sizes): bool
|
|
||||||
{
|
|
||||||
$sizes[] = ['width' => 16, 'height' => 16]; // Standard favicon size for browsers
|
|
||||||
$sizes[] = ['width' => 24, 'height' => 24]; // IE9 pinned site size for user interface
|
|
||||||
$sizes[] = ['width' => 32, 'height' => 32]; // Standard for most desktop browsers, facebook small photo thumbnail
|
|
||||||
$sizes[] = ['width' => 36, 'height' => 36]; // Facebook big photo thumbnail
|
|
||||||
// $sizes[] = ['width' => 48, 'height' => 48]; // Windows site (mid-size standard favicon)
|
|
||||||
$sizes[] = ['width' => 55, 'height' => 55]; // Pinterest small thumb
|
|
||||||
// $sizes[] = ['width' => 57, 'height' => 57]; // Standard iOS home screen (iPod Touch, iPhone first generation to 3G)
|
|
||||||
// $sizes[] = ['width' => 60, 'height' => 60]; // iPhone touch up to iOS 7
|
|
||||||
// $sizes[] = ['width' => 64, 'height' => 64]; // Windows site, Safari Reader List sidebar in HiDPI/Retina
|
|
||||||
// $sizes[] = ['width' => 70, 'height' => 70]; // Win 8.1 Metro tile
|
|
||||||
// $sizes[] = ['width' => 72, 'height' => 72]; // iPad touch up to iOS 6
|
|
||||||
// $sizes[] = ['width' => 76, 'height' => 76]; // iPad home screen icon up to iOS 7
|
|
||||||
// $sizes[] = ['width' => 96, 'height' => 96]; // GoogleTV icon
|
|
||||||
$sizes[] = ['width' => 110, 'height' => 110]; // Instagram profile picture
|
|
||||||
// $sizes[] = ['width' => 114, 'height' => 114]; // iPhone retina touch up to iOS6
|
|
||||||
$sizes[] = ['width' => 116, 'height' => 116]; // Facebook page small square shared link
|
|
||||||
// $sizes[] = ['width' => 120, 'height' => 120]; // iPhone retina touch up to iOS6
|
|
||||||
$sizes[] = ['width' => 128, 'height' => 128]; // Chrome Web Store icon and Small Windows 8 Star Screen Icon, Facebook profile picture smartphone
|
|
||||||
// $sizes[] = ['width' => 144, 'height' => 144]; // IE10 Metro tile for pinned site, Apple iPad retina iOS 6 to iOS 7
|
|
||||||
$sizes[] = ['width' => 150, 'height' => 150]; // Win 8.1 Metro tile (standard MS tile)
|
|
||||||
$sizes[] = ['width' => 154, 'height' => 154]; // Facebook feed small square shared link
|
|
||||||
$sizes[] = ['width' => 152, 'height' => 152]; // Apple iPad iOS 10 retina touch icon
|
|
||||||
$sizes[] = ['width' => 161, 'height' => 161]; // Instagram thumbnails
|
|
||||||
$sizes[] = ['width' => 165, 'height' => 165]; // Pinterest profile picture
|
|
||||||
$sizes[] = ['width' => 167, 'height' => 167]; // Apple iPad Retina touch icon
|
|
||||||
$sizes[] = ['width' => 170, 'height' => 170]; // Facebook Profile Picture desktop
|
|
||||||
$sizes[] = ['width' => 180, 'height' => 180]; // Apple iPhone Retina touch icon, Facebook Profile Picture
|
|
||||||
$sizes[] = ['width' => 192, 'height' => 192]; // Google Developer Web App Manifest Recommendation
|
|
||||||
// $sizes[] = ['width' => 195, 'height' => 195]; // Opera Speed Dial icon
|
|
||||||
$sizes[] = ['width' => 196, 'height' => 196]; // Chrome for Android home screen icon
|
|
||||||
$sizes[] = ['width' => 200, 'height' => 200]; // GitHub profile photo
|
|
||||||
$sizes[] = ['width' => 222, 'height' => 150]; // Pinterest large thumb
|
|
||||||
// $sizes[] = ['width' => 228, 'height' => 228]; // Opera Coast icon
|
|
||||||
$sizes[] = ['width' => 250, 'height' => 250]; // Google My Business minimum
|
|
||||||
// $sizes[] = ['width' => 260, 'height' => 260];
|
|
||||||
$sizes[] = ['width' => 300, 'height' => 300]; // Instagram personal profile image
|
|
||||||
// $sizes[] = ['width' => 310, 'height' => 150]; // Win 8.1 wide Metro tile
|
|
||||||
// $sizes[] = ['width' => 310, 'height' => 310]; // Win 8.1 big Metro tile
|
|
||||||
$sizes[] = ['width' => 360, 'height' => 360];
|
|
||||||
$sizes[] = ['width' => 400, 'height' => 150]; // Facebook small cover photo, facebook small fundraiser image
|
|
||||||
// $sizes[] = ['width' => 400, 'height' => 300];
|
|
||||||
$sizes[] = ['width' => 400, 'height' => 400]; // Twitter profile photo
|
|
||||||
$sizes[] = ['width' => 470, 'height' => 174]; // Facebook event small image
|
|
||||||
$sizes[] = ['width' => 470, 'height' => 246]; // Facebook feed small rectangular link
|
|
||||||
$sizes[] = ['width' => 480, 'height' => 360]; // YouTube video thumbnail
|
|
||||||
$sizes[] = ['width' => 484, 'height' => 252]; // Facebook page small rectangular link
|
|
||||||
$sizes[] = ['width' => 510, 'height' => 510]; // Instagram feed image
|
|
||||||
$sizes[] = ['width' => 512, 'height' => 512]; // Android Chrome big
|
|
||||||
// $sizes[] = ['width' => 560, 'height' => 315];
|
|
||||||
// $sizes[] = ['width' => 560, 'height' => 340];
|
|
||||||
$sizes[] = ['width' => 600, 'height' => 900]; // Pinterest expanded pin
|
|
||||||
$sizes[] = ['width' => 612, 'height' => 612]; // Instagram small photo
|
|
||||||
// $sizes[] = ['width' => 640, 'height' => 385];
|
|
||||||
$sizes[] = ['width' => 700, 'height' => 800]; // Twitter two image post
|
|
||||||
$sizes[] = ['width' => 720, 'height' => 720]; // Google My Business
|
|
||||||
$sizes[] = ['width' => 800, 'height' => 300]; // Facebook fundraiser image
|
|
||||||
$sizes[] = ['width' => 800, 'height' => 800]; // Youtube channel profile image
|
|
||||||
$sizes[] = ['width' => 820, 'height' => 312]; // Facebook cover photo
|
|
||||||
// $sizes[] = ['width' => 853, 'height' => 505];
|
|
||||||
$sizes[] = ['width' => 900, 'height' => 600]; // Instagram company photo
|
|
||||||
$sizes[] = ['width' => 943, 'height' => 943]; // Big safari pinned tab
|
|
||||||
$sizes[] = ['width' => 1024, 'height' => 768]; // Standard 4:3 ratio photo
|
|
||||||
$sizes[] = ['width' => 1080, 'height' => 720]; // Standard 3:2 ratio photo
|
|
||||||
$sizes[] = ['width' => 1080, 'height' => 1080]; // Standard 1:1 ratio photo, Instagram photo
|
|
||||||
$sizes[] = ['width' => 1080, 'height' => 1350]; // Instagram portrait photo
|
|
||||||
$sizes[] = ['width' => 1080, 'height' => 1920]; // Instagram story
|
|
||||||
$sizes[] = ['width' => 1128, 'height' => 191]; // Instagram company cover image
|
|
||||||
$sizes[] = ['width' => 1128, 'height' => 376]; // Instagram Main image
|
|
||||||
$sizes[] = ['width' => 1200, 'height' => 600]; // Twitter four images
|
|
||||||
$sizes[] = ['width' => 1200, 'height' => 627]; // Instagram shared link
|
|
||||||
$sizes[] = ['width' => 1200, 'height' => 628]; // Facebook and Twitter shared link
|
|
||||||
$sizes[] = ['width' => 1200, 'height' => 630]; // Facebook shared image
|
|
||||||
$sizes[] = ['width' => 1200, 'height' => 686]; // Twitter three images
|
|
||||||
$sizes[] = ['width' => 1200, 'height' => 675]; // Twitter shared image
|
|
||||||
$sizes[] = ['width' => 1280, 'height' => 720]; // Standard small 16:9 ratio photo, YouTube HD
|
|
||||||
$sizes[] = ['width' => 1500, 'height' => 1500]; // Twitter header photo
|
|
||||||
$sizes[] = ['width' => 1584, 'height' => 396]; // Instagram personal background image
|
|
||||||
$sizes[] = ['width' => 1920, 'height' => 1005]; // Facebook event image
|
|
||||||
$sizes[] = ['width' => 1920, 'height' => 1080]; // Standard big 16:9 ratio photo
|
|
||||||
$sizes[] = ['width' => 2048, 'height' => 1152]; // YouTube channel cover photo
|
|
||||||
|
|
||||||
return Event::next;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This code executes when GNU social creates the page routing, and we hook
|
* This code executes when GNU social creates the page routing, and we hook
|
||||||
* on this event to add our action handler for Embed.
|
* on this event to add our action handler for Embed.
|
||||||
@ -329,7 +234,7 @@ class Embed extends Plugin
|
|||||||
$temp_file = new TemporaryFile();
|
$temp_file = new TemporaryFile();
|
||||||
$temp_file->write($img_data);
|
$temp_file->write($img_data);
|
||||||
try {
|
try {
|
||||||
$attachment = GSFile::sanitizeAndStoreFileAsAttachment($temp_file);
|
$attachment = GSFile::storeFileAsAttachment($temp_file);
|
||||||
$embed_data['attachment_id'] = $attachment->getId();
|
$embed_data['attachment_id'] = $attachment->getId();
|
||||||
} catch (ClientException) {
|
} catch (ClientException) {
|
||||||
DB::persist($attachment = Attachment::create(['mimetype' => $link->getMimetype()]));
|
DB::persist($attachment = Attachment::create(['mimetype' => $link->getMimetype()]));
|
||||||
|
@ -157,7 +157,7 @@ class StoreRemoteMedia extends Plugin
|
|||||||
// Create an attachment for this
|
// Create an attachment for this
|
||||||
$temp_file = new TemporaryFile();
|
$temp_file = new TemporaryFile();
|
||||||
$temp_file->write($media);
|
$temp_file->write($media);
|
||||||
$attachment = GSFile::sanitizeAndStoreFileAsAttachment($temp_file);
|
$attachment = GSFile::storeFileAsAttachment($temp_file);
|
||||||
|
|
||||||
// Relate the link with the attachment
|
// Relate the link with the attachment
|
||||||
DB::persist(AttachmentToLink::create([
|
DB::persist(AttachmentToLink::create([
|
||||||
@ -177,8 +177,7 @@ class StoreRemoteMedia extends Plugin
|
|||||||
if (!$this->getStoreOriginal()) {
|
if (!$this->getStoreOriginal()) {
|
||||||
$thumbnail = AttachmentThumbnail::getOrCreate(
|
$thumbnail = AttachmentThumbnail::getOrCreate(
|
||||||
attachment: $attachment,
|
attachment: $attachment,
|
||||||
width: $this->getThumbnailWidth(),
|
size: 'small',
|
||||||
height: $this->getThumbnailHeight(),
|
|
||||||
crop: $this->getSmartCrop()
|
crop: $this->getSmartCrop()
|
||||||
);
|
);
|
||||||
$attachment->deleteStorage();
|
$attachment->deleteStorage();
|
||||||
|
20
social.yaml
20
social.yaml
@ -102,7 +102,7 @@ parameters:
|
|||||||
uploads: true
|
uploads: true
|
||||||
show_thumbs: true
|
show_thumbs: true
|
||||||
process_links: true
|
process_links: true
|
||||||
sanitize: true
|
sanitize: false
|
||||||
ext_blacklist: []
|
ext_blacklist: []
|
||||||
memory_limit: 1024M
|
memory_limit: 1024M
|
||||||
|
|
||||||
@ -111,17 +111,23 @@ parameters:
|
|||||||
ssl:
|
ssl:
|
||||||
dir: "%kernel.project_dir%/file/thumbnails/"
|
dir: "%kernel.project_dir%/file/thumbnails/"
|
||||||
smart_crop: false
|
smart_crop: false
|
||||||
max_size_px: 1000
|
maximum_pixels: 256000
|
||||||
width: 450
|
minimum_width: 16
|
||||||
height: 600
|
minimum_height: 16
|
||||||
|
small: 32
|
||||||
|
medium: 256
|
||||||
|
big: 496
|
||||||
|
default_size: medium
|
||||||
mimetype: 'image/webp'
|
mimetype: 'image/webp'
|
||||||
extension: '.webp'
|
extension: '.webp'
|
||||||
|
|
||||||
plugin_embed:
|
plugin_store_remote_media:
|
||||||
width: 128
|
max_file_size: 4000000
|
||||||
height: 128
|
|
||||||
smart_crop: false
|
smart_crop: false
|
||||||
|
|
||||||
|
plugin_embed:
|
||||||
|
max_px: 64000
|
||||||
|
smart_crop: false
|
||||||
|
|
||||||
theme:
|
theme:
|
||||||
server:
|
server:
|
||||||
|
@ -49,7 +49,7 @@ class Attachment extends Controller
|
|||||||
if ($id <= 0) { // This should never happen coming from the router, but let's bail if it does
|
if ($id <= 0) { // This should never happen coming from the router, but let's bail if it does
|
||||||
// @codeCoverageIgnoreStart
|
// @codeCoverageIgnoreStart
|
||||||
Log::critical("Attachment controller called with {$id}, which should not be possible");
|
Log::critical("Attachment controller called with {$id}, which should not be possible");
|
||||||
throw new ClientException(_m('No such attachment'), 404);
|
throw new ClientException(_m('No such attachment.'), 404);
|
||||||
// @codeCoverageIgnoreEnd
|
// @codeCoverageIgnoreEnd
|
||||||
} else {
|
} else {
|
||||||
$res = null;
|
$res = null;
|
||||||
@ -92,7 +92,7 @@ class Attachment extends Controller
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
} catch (NotFoundException) {
|
} catch (NotFoundException) {
|
||||||
throw new ClientException(_m('No such attachment'), 404);
|
throw new ClientException(_m('No such attachment.'), 404);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,29 +132,17 @@ class Attachment extends Controller
|
|||||||
*
|
*
|
||||||
* @return Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
public function attachment_thumbnail(Request $request, int $id): Response
|
public function attachment_thumbnail(Request $request, int $id, string $size = 'small'): Response
|
||||||
{
|
{
|
||||||
$attachment = DB::findOneBy('attachment', ['id' => $id]);
|
$attachment = DB::findOneBy('attachment', ['id' => $id]);
|
||||||
|
|
||||||
$default_width = Common::config('thumbnail', 'width');
|
$crop = Common::config('thumbnail', 'smart_crop');
|
||||||
$default_height = Common::config('thumbnail', 'height');
|
|
||||||
$default_crop = Common::config('thumbnail', 'smart_crop');
|
|
||||||
$width = $this->int('w') ?: $default_width;
|
|
||||||
$height = $this->int('h') ?: $default_height;
|
|
||||||
$crop = $this->bool('c') ?: $default_crop;
|
|
||||||
|
|
||||||
$sizes = null;
|
$thumbnail = AttachmentThumbnail::getOrCreate(attachment: $attachment, size: $size, crop: $crop);
|
||||||
Event::handle('GetAllowedThumbnailSizes', [&$sizes]);
|
if (is_null($thumbnail)) {
|
||||||
if (!in_array(['width' => $width, 'height' => $height], $sizes)) {
|
throw new ClientException(_m('Can not generate thumbnail for attachment with id={id}', ['id' => $attachment->getId()]));
|
||||||
throw new ClientException(_m('The requested thumbnail dimensions are not allowed'), 400); // 400 Bad Request
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_null($attachment->getWidth()) && !is_null($attachment->getHeight())) {
|
|
||||||
[$width, $height] = AttachmentThumbnail::predictScalingValues($attachment->getWidth(), $attachment->getHeight(), $width, $height, $crop);
|
|
||||||
}
|
|
||||||
|
|
||||||
$thumbnail = AttachmentThumbnail::getOrCreate(attachment: $attachment, width: $width, height: $height, crop: $crop);
|
|
||||||
|
|
||||||
$filename = $thumbnail->getFilename();
|
$filename = $thumbnail->getFilename();
|
||||||
$path = $thumbnail->getPath();
|
$path = $thumbnail->getPath();
|
||||||
$mimetype = $thumbnail->getMimetype();
|
$mimetype = $thumbnail->getMimetype();
|
||||||
|
@ -345,9 +345,9 @@ class Attachment extends Entity
|
|||||||
return Router::url('attachment_view', ['id' => $this->getId()]);
|
return Router::url('attachment_view', ['id' => $this->getId()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getThumbnailUrl()
|
public function getThumbnailUrl(?string $size = null)
|
||||||
{
|
{
|
||||||
return Router::url('attachment_thumbnail', ['id' => $this->getId(), 'w' => Common::config('thumbnail', 'width'), 'h' => Common::config('thumbnail', 'height')]);
|
return Router::url('attachment_thumbnail', ['id' => $this->getId(), 'size' => $size ?? Common::config('thumbnail', 'default_size')]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function schemaDef(): array
|
public static function schemaDef(): array
|
||||||
|
@ -26,7 +26,6 @@ use App\Core\DB\DB;
|
|||||||
use App\Core\Entity;
|
use App\Core\Entity;
|
||||||
use App\Core\Event;
|
use App\Core\Event;
|
||||||
use App\Core\GSFile;
|
use App\Core\GSFile;
|
||||||
use function App\Core\I18n\_m;
|
|
||||||
use App\Core\Log;
|
use App\Core\Log;
|
||||||
use App\Core\Router\Router;
|
use App\Core\Router\Router;
|
||||||
use App\Util\Common;
|
use App\Util\Common;
|
||||||
@ -50,17 +49,21 @@ use Symfony\Component\Mime\MimeTypes;
|
|||||||
* @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org
|
* @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org
|
||||||
* @author Hugo Sales <hugo@hsal.es>
|
* @author Hugo Sales <hugo@hsal.es>
|
||||||
* @author Diogo Peralta Cordeiro <mail@diogo.site>
|
* @author Diogo Peralta Cordeiro <mail@diogo.site>
|
||||||
|
* @author Eliseu Amaro <mail@eliseuama.ro>
|
||||||
* @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
|
* @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
|
||||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||||
*/
|
*/
|
||||||
class AttachmentThumbnail extends Entity
|
class AttachmentThumbnail extends Entity
|
||||||
{
|
{
|
||||||
|
public const SIZE_SMALL = 0;
|
||||||
|
public const SIZE_MEDIUM = 1;
|
||||||
|
public const SIZE_BIG = 2;
|
||||||
|
|
||||||
// {{{ Autocode
|
// {{{ Autocode
|
||||||
// @codeCoverageIgnoreStart
|
// @codeCoverageIgnoreStart
|
||||||
private int $attachment_id;
|
private int $attachment_id;
|
||||||
private ?string $mimetype;
|
private ?string $mimetype;
|
||||||
private int $width;
|
private int $size = self::SIZE_SMALL;
|
||||||
private int $height;
|
|
||||||
private string $filename;
|
private string $filename;
|
||||||
private \DateTimeInterface $modified;
|
private \DateTimeInterface $modified;
|
||||||
|
|
||||||
@ -97,15 +100,15 @@ class AttachmentThumbnail extends Entity
|
|||||||
return $this->width;
|
return $this->width;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setHeight(int $height): self
|
public function getSize(): int
|
||||||
{
|
{
|
||||||
$this->height = $height;
|
return $this->size;
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getHeight(): int
|
public function setSize(int $size): self
|
||||||
{
|
{
|
||||||
return $this->height;
|
$this->size = $size;
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setFilename(string $filename): self
|
public function setFilename(string $filename): self
|
||||||
@ -161,24 +164,24 @@ class AttachmentThumbnail extends Entity
|
|||||||
*
|
*
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public static function getOrCreate(Attachment $attachment, int $width, int $height, bool $crop)
|
public static function getOrCreate(Attachment $attachment, ?string $size = null, bool $crop = false)
|
||||||
{
|
{
|
||||||
// We need to keep these in mind for DB indexing
|
$size = $size ?? Common::config('thumbnail', 'default_size');
|
||||||
$predicted_width = null;
|
$size_int = match ($size) {
|
||||||
$predicted_height = null;
|
'medium' => self::SIZE_MEDIUM,
|
||||||
|
'big' => self::SIZE_BIG,
|
||||||
|
default => self::SIZE_SMALL,
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
if (is_null($attachment->getWidth()) || is_null($attachment->getHeight())) {
|
return Cache::get('thumb-' . $attachment->getId() . "-{$size}",
|
||||||
// @codeCoverageIgnoreStart
|
function () use ($crop, $attachment, $size_int) {
|
||||||
// TODO: check if we can generate from an existing thumbnail
|
return DB::findOneBy('attachment_thumbnail', ['attachment_id' => $attachment->getId(), 'size' => $size_int]);
|
||||||
throw new ClientException(_m('Invalid dimensions requested for thumbnail.'));
|
|
||||||
// @codeCoverageIgnoreEnd
|
|
||||||
}
|
|
||||||
return Cache::get('thumb-' . $attachment->getId() . "-{$width}x{$height}",
|
|
||||||
function () use ($crop, $attachment, $width, $height, &$predicted_width, &$predicted_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) {
|
||||||
|
if (is_null($attachment->getWidth()) || is_null($attachment->getHeight())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
[$predicted_width, $predicted_height] = self::predictScalingValues($attachment->getWidth(), $attachment->getHeight(), $size, $crop);
|
||||||
if (!file_exists($attachment->getPath())) {
|
if (!file_exists($attachment->getPath())) {
|
||||||
throw new NotStoredLocallyException();
|
throw new NotStoredLocallyException();
|
||||||
}
|
}
|
||||||
@ -194,10 +197,9 @@ class AttachmentThumbnail extends Entity
|
|||||||
foreach ($encoders as $encoder) {
|
foreach ($encoders as $encoder) {
|
||||||
/** @var ?TemporaryFile */
|
/** @var ?TemporaryFile */
|
||||||
$temp = null; // Let the EncoderPlugin create a temporary file for us
|
$temp = null; // Let the EncoderPlugin create a temporary file for us
|
||||||
if ($encoder($attachment->getPath(), $temp, $width, $height, $crop, $mimetype)) {
|
if ($encoder($attachment->getPath(), $temp, $predicted_width, $predicted_height, $crop, $mimetype)) {
|
||||||
$thumbnail->setAttachment($attachment);
|
$thumbnail->setAttachment($attachment);
|
||||||
$thumbnail->setWidth($predicted_width);
|
$thumbnail->setSize($size_int);
|
||||||
$thumbnail->setHeight($predicted_height);
|
|
||||||
$mimetype = $temp->getMimeType();
|
$mimetype = $temp->getMimeType();
|
||||||
$ext = '.' . MimeTypes::getDefault()->getExtensions($mimetype)[0];
|
$ext = '.' . MimeTypes::getDefault()->getExtensions($mimetype)[0];
|
||||||
$filename = "{$predicted_width}x{$predicted_height}{$ext}-" . $attachment->getFilehash();
|
$filename = "{$predicted_width}x{$predicted_height}{$ext}-" . $attachment->getFilehash();
|
||||||
@ -209,7 +211,7 @@ class AttachmentThumbnail extends Entity
|
|||||||
return $thumbnail;
|
return $thumbnail;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new ClientException(_m('Can not generate thumbnail for attachment with id={id}', ['id' => $attachment->getId()]));
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,20 +222,7 @@ class AttachmentThumbnail extends Entity
|
|||||||
|
|
||||||
public function getUrl()
|
public function getUrl()
|
||||||
{
|
{
|
||||||
return Router::url('attachment_thumbnail', ['id' => $this->getAttachmentId(), 'w' => $this->getWidth(), 'h' => $this->getHeight()]);
|
return Router::url('attachment_thumbnail', ['id' => $this->getAttachmentId(), 'size' => $this->getSize()]);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the HTML attributes for this thumbnail
|
|
||||||
*/
|
|
||||||
public function getHTMLAttributes(array $orig = [], bool $overwrite = true)
|
|
||||||
{
|
|
||||||
$attrs = [
|
|
||||||
'height' => $this->getHeight(),
|
|
||||||
'width' => $this->getWidth(),
|
|
||||||
'src' => $this->getUrl(),
|
|
||||||
];
|
|
||||||
return $overwrite ? array_merge($orig, $attrs) : array_merge($attrs, $orig);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -245,7 +234,7 @@ class AttachmentThumbnail extends Entity
|
|||||||
if (file_exists($filepath)) {
|
if (file_exists($filepath)) {
|
||||||
if (@unlink($filepath) === false) {
|
if (@unlink($filepath) === false) {
|
||||||
// @codeCoverageIgnoreStart
|
// @codeCoverageIgnoreStart
|
||||||
Log::warning("Failed deleting file for attachment thumbnail with id={$this->attachment_id}, width={$this->width}, height={$this->height} at {$filepath}");
|
Log::warning("Failed deleting file for attachment thumbnail with id={$this->getAttachmentId()}, size={$this->getSize()} at {$filepath}");
|
||||||
// @codeCoverageIgnoreEnd
|
// @codeCoverageIgnoreEnd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -263,33 +252,76 @@ class AttachmentThumbnail extends Entity
|
|||||||
*
|
*
|
||||||
* @param int $existing_width Original width
|
* @param int $existing_width Original width
|
||||||
* @param int $existing_height Original height
|
* @param int $existing_height Original height
|
||||||
* @param int $requested_width Resulting max width
|
* @param int $allowed_aspect_ratios
|
||||||
* @param int $requested_height Resulting max height
|
* @param int $requested_size
|
||||||
* @param bool $crop Crop to the size (not preserving aspect ratio)
|
* @param bool $crop
|
||||||
*
|
*
|
||||||
* @return array [predicted width, predicted height]
|
* @return array [predicted width, predicted height]
|
||||||
*/
|
*/
|
||||||
public static function predictScalingValues(
|
public static function predictScalingValues(
|
||||||
int $existing_width,
|
int $existing_width,
|
||||||
int $existing_height,
|
int $existing_height,
|
||||||
int $requested_width,
|
string $requested_size,
|
||||||
int $requested_height,
|
|
||||||
bool $crop
|
bool $crop
|
||||||
): array {
|
): array {
|
||||||
if ($crop) {
|
/**
|
||||||
$rw = min($existing_width, $requested_width);
|
* 1:1 => Square
|
||||||
$rh = min($existing_height, $requested_height);
|
* 4:3 => SD
|
||||||
} else {
|
* 11:8 => Academy Ratio
|
||||||
if ($existing_width > $existing_height) {
|
* 3:2 => Classic 35mm
|
||||||
$rw = min($existing_width, $requested_width);
|
* 16:10 => Golden Ratio
|
||||||
$rh = ceil($existing_height * $rw / $existing_width);
|
* 16:9 => Widescreen
|
||||||
} else {
|
* 2.2:1 => Standard 70mm film
|
||||||
$rh = min($existing_height, $requested_height);
|
*/
|
||||||
$rw = ceil($existing_width * $rh / $existing_height);
|
$allowed_aspect_ratios = [1, 1.3, 1.376, 1.5, 1.6, 1.7, 2.2]; // Ascending array
|
||||||
}
|
$sizes = [
|
||||||
|
'small' => Common::config('thumbnail', 'small'),
|
||||||
|
'medium' => Common::config('thumbnail', 'medium'),
|
||||||
|
'big' => Common::config('thumbnail', 'big'),
|
||||||
|
];
|
||||||
|
|
||||||
|
// We only scale if the image is larger than the minimum width and height for a thumbnail
|
||||||
|
if ($existing_width < Common::config('thumbnail', 'minimum_width') && $existing_height < Common::config('thumbnail', 'minimum_height')) {
|
||||||
|
return [$existing_width, $existing_height];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [(int) $rw, (int) $rh];
|
// We only scale if the total of pixels is greater than the maximum allowed for a thumbnail
|
||||||
|
$total_of_pixels = $existing_width * $existing_height;
|
||||||
|
if ($total_of_pixels < Common::config('thumbnail', 'maximum_pixels')) {
|
||||||
|
return [$existing_width, $existing_height];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this a portrait image?
|
||||||
|
$flip = $existing_height > $existing_width;
|
||||||
|
|
||||||
|
// Find the aspect ratio of the given image
|
||||||
|
$existing_aspect_ratio = !$flip ? $existing_width / $existing_height : $existing_height / $existing_width;
|
||||||
|
|
||||||
|
// Binary search the closer allowed aspect ratio
|
||||||
|
$left = 0;
|
||||||
|
$right = count($allowed_aspect_ratios) - 1;
|
||||||
|
while ($left < $right) {
|
||||||
|
$mid = floor($left + ($right - $left) / 2);
|
||||||
|
|
||||||
|
// Comparing absolute distances with middle value and right value
|
||||||
|
if (abs($existing_aspect_ratio - $allowed_aspect_ratios[$mid]) < abs($existing_aspect_ratio - $allowed_aspect_ratios[$right])) {
|
||||||
|
// search the left side of the array
|
||||||
|
$right = $mid;
|
||||||
|
} else {
|
||||||
|
// search the right side of the array
|
||||||
|
$left = $mid + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$closest_aspect_ratio = $allowed_aspect_ratios[$left];
|
||||||
|
unset($mid, $left, $right);
|
||||||
|
|
||||||
|
// TODO: For crop, we should test a threshold and understand if the image would better be cropped
|
||||||
|
|
||||||
|
// Resulting width and height
|
||||||
|
$rw = (int) ($sizes[$requested_size]);
|
||||||
|
$rh = (int) ($rw / $closest_aspect_ratio);
|
||||||
|
|
||||||
|
return !$flip ? [$rw, $rh] : [$rh, $rw];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function schemaDef(): array
|
public static function schemaDef(): array
|
||||||
@ -299,12 +331,11 @@ class AttachmentThumbnail extends Entity
|
|||||||
'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'],
|
||||||
'mimetype' => ['type' => 'varchar', 'length' => 129, 'description' => 'resource mime type 64+1+64, images hardly will show up with long mimetypes, this is probably safe considering rfc6838#section-4.2'],
|
'mimetype' => ['type' => 'varchar', 'length' => 129, 'description' => 'resource mime type 64+1+64, images hardly will show up with long mimetypes, this is probably safe considering rfc6838#section-4.2'],
|
||||||
'width' => ['type' => 'int', 'not null' => true, 'description' => 'width of thumbnail'],
|
'size' => ['type' => 'int', 'not null' => true, 'default' => 0, 'description' => '0 = small; 1 = medium; 2 = big'],
|
||||||
'height' => ['type' => 'int', 'not null' => true, 'description' => 'height of thumbnail'],
|
|
||||||
'filename' => ['type' => 'varchar', 'length' => 191, 'not null' => true, 'description' => 'thumbnail filename'],
|
'filename' => ['type' => 'varchar', 'length' => 191, 'not null' => true, 'description' => 'thumbnail filename'],
|
||||||
'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' => ['attachment_id', 'width', 'height'],
|
'primary key' => ['attachment_id', 'size'],
|
||||||
'indexes' => [
|
'indexes' => [
|
||||||
'attachment_thumbnail_attachment_id_idx' => ['attachment_id'],
|
'attachment_thumbnail_attachment_id_idx' => ['attachment_id'],
|
||||||
],
|
],
|
||||||
|
@ -45,7 +45,6 @@ abstract class Attachments
|
|||||||
$r->connect('attachment_show', '/attachment/{id<\d+>}', [C\Attachment::class, 'attachment_show']);
|
$r->connect('attachment_show', '/attachment/{id<\d+>}', [C\Attachment::class, 'attachment_show']);
|
||||||
$r->connect('attachment_view', '/attachment/{id<\d+>}/view', [C\Attachment::class, 'attachment_view']);
|
$r->connect('attachment_view', '/attachment/{id<\d+>}/view', [C\Attachment::class, 'attachment_view']);
|
||||||
$r->connect('attachment_download', '/attachment/{id<\d+>}/download', [C\Attachment::class, 'attachment_download']);
|
$r->connect('attachment_download', '/attachment/{id<\d+>}/download', [C\Attachment::class, 'attachment_download']);
|
||||||
$r->connect('attachment_thumbnail', '/attachment/{id<\d+>}/thumbnail', [C\Attachment::class, 'attachment_thumbnail']);
|
$r->connect('attachment_thumbnail', '/attachment/{id<\d+>}/thumbnail/{size<big|medium|small>}', [C\Attachment::class, 'attachment_thumbnail']);
|
||||||
$r->connect('thumbnail', '/thumbnail/{id<\d+>}', [C\Attachment::class, 'attachment_thumbnail']); // Backwards-compatibility
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user