Browse Source

Rewrite Picture to Image and add transparent images support, default pictures are compressed to webp

pull/1040/head
Timothée Jaussoin 4 years ago
parent
commit
3e5fa57efc
  1. 1
      CHANGELOG.md
  2. 9
      app/Contact.php
  3. 7
      app/Message.php
  4. 6
      app/Presence.php
  5. 2
      app/helpers/StringHelper.php
  6. 8
      app/helpers/UtilsHelper.php
  7. 2
      app/views/contact.tpl
  8. 16
      app/widgets/Avatar/Avatar.php
  9. 11
      app/widgets/Chat/Chat.php
  10. 4
      app/widgets/Chat/_chat_header.tpl
  11. 13
      app/widgets/CommunityConfig/CommunityConfig.php
  12. 2
      app/widgets/ContactActions/_contactactions_drawer.tpl
  13. 3
      app/widgets/ContactData/ContactData.php
  14. 9
      app/widgets/ContactHeader/ContactHeader.php
  15. 2
      app/widgets/ContactHeader/contactheader.tpl
  16. 19
      app/widgets/Picture/Picture.php
  17. 3
      app/widgets/Rooms/_rooms_room.tpl
  18. 14
      app/widgets/RoomsUtils/RoomsUtils.php
  19. 2
      app/widgets/RoomsUtils/_rooms_drawer.tpl
  20. 2
      app/widgets/Search/_search_roster.tpl
  21. 8
      app/widgets/Stickers/Stickers.php
  22. 7
      lib/moxl/src/Xec/Action/BOB/Request.php
  23. 9
      lib/moxl/src/Xec/Action/Pubsub/GetItem.php
  24. 8
      lib/moxl/src/Xec/Action/Pubsub/GetItems.php
  25. 7
      lib/moxl/src/Xec/Payload/Avatar.php
  26. 3
      src/Movim/Bootstrap.php
  27. 291
      src/Movim/Image.php
  28. 342
      src/Movim/Picture.php

1
CHANGELOG.md

@ -53,6 +53,7 @@ v0.20 (trunk)
* Introducing Material Chips in the design, use them for the tags
* Prepare PHP 8.1 support
* Movim can now be installed as a Progressive Web App
* Rewrite Picture to Image and add transparent images support, default pictures are compressed to webp
v0.19
---------------------------

9
app/Contact.php

@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Capsule\Manager as DB;
use Respect\Validation\Validator;
use Movim\Picture;
use Movim\Image;
class Contact extends Model
{
@ -114,16 +114,17 @@ class Contact extends Model
return;
}
$p = new Picture;
$p = new Image;
$p->setKey($this->id);
$p->fromBase($this->photobin);
$p->set($this->id);
$p->save();
unset($this->photobin);
}
public function getPhoto($size = 'm')
{
return getPhoto($this->id, $size);
return !empty($this->id) ? getPhoto($this->id, $size) : null;
}
public function setLocation($stanza)

7
app/Message.php

@ -3,7 +3,7 @@
namespace App;
use Movim\Model;
use Movim\Picture;
use Movim\Image;
use Movim\Route;
use Illuminate\Database\QueryException;
@ -277,10 +277,11 @@ class Message extends Model
if (substr((string)$results[0], 0, 10) == 'data:image') {
$str = explode('base64,', $results[0]);
if (isset($str[1])) {
$p = new Picture;
$p = new Image;
$p->fromBase(urldecode($str[1]));
$key = sha1(urldecode($str[1]));
$p->set($key, 'png');
$p->setKey($key);
$p->save(false, false, 'png');
$this->sticker = $key;
}

6
app/Presence.php

@ -3,11 +3,9 @@
namespace App;
use Movim\Model;
use Movim\Picture;
use Movim\Image;
use Movim\Session;
use App\PresenceBuffer;
class Presence extends Model
{
protected $primaryKey = ['session_id', 'jid', 'resource'];
@ -70,7 +68,7 @@ class Presence extends Model
public function getConferencePictureAttribute()
{
return (new Picture)->get($this->mucjid, 120);
return Image::getOrCreate($this->mucjid, 120);
}
public function getConferenceColorAttribute()

2
app/helpers/StringHelper.php

@ -446,7 +446,7 @@ function firstLetterCapitalize($string, bool $firstOnly = false): string
/**
* Return a clean string that can be used for HTML ids
*/
function cleanupId($string = '', bool $withHash = false): string
function cleanupId(string $string = '', bool $withHash = false): string
{
$id = 'id-' . strtolower(preg_replace('/([^a-z0-9]+)/i', '-', $string));
return $withHash ? $id . '-' . substr(hash('sha256', $string), 0, 6) : $id;

8
app/helpers/UtilsHelper.php

@ -3,7 +3,7 @@
use Monolog\Logger;
use Monolog\Handler\SyslogHandler;
use Monolog\Handler\StreamHandler;
use Movim\Picture;
use Movim\Image;
class Utils
{
@ -122,7 +122,7 @@ function resolveInfos($postCollection)
/**
* Return a picture with a specific size
*/
function getPhoto($id, $size = 'm')
function getPhoto(string $key, string $size = 'm')
{
$sizes = [
'xxl' => [1280, 300],
@ -133,7 +133,7 @@ function getPhoto($id, $size = 'm')
'o' => [false, false]
];
return (new Picture)->get($id, $sizes[$size][0], $sizes[$size][1]);
return Image::getOrCreate($key, $sizes[$size][0], $sizes[$size][1]);
}
/**
@ -757,7 +757,7 @@ function compileStickers(): int
if ($key != 'icon') {
$count++;
copy($path, PUBLIC_CACHE_PATH.hash(Picture::$hash, $key).'.png');
copy($path, PUBLIC_CACHE_PATH.hash(Image::$hash, $key).'.png');
}
}

2
app/views/contact.tpl

@ -24,6 +24,6 @@
</aside>
<div>
<?php $this->widget('ContactHeader'); ?>
<?php $this->widget('CommunityPosts'); ?>
<?php //$this->widget('CommunityPosts'); ?>
</div>
</main>

16
app/widgets/Avatar/Avatar.php

@ -1,10 +1,9 @@
<?php
use Movim\Image;
use Moxl\Xec\Action\Avatar\Get;
use Moxl\Xec\Action\Avatar\Set;
use Movim\Picture;
class Avatar extends \Movim\Widget\Base
{
public function load()
@ -65,15 +64,18 @@ class Avatar extends \Movim\Widget\Base
public function ajaxSubmit($avatar)
{
$p = new Picture;
$p->fromBase($avatar->photobin->value);
$tempKey = \generateKey(6);
$p->set('temp', 'jpeg', 60);
$p = new Image;
$p->fromBase($avatar->photobin->value);
$p->setKey($tempKey);
$p->save(false, false, 'jpeg', 60);
$p = new Picture;
$p->get('temp');
// Reload
$p->load();
$r = new Set;
$r->setData($p->toBase())->request();
$p->remove();
}
}

11
app/widgets/Chat/Chat.php

@ -19,10 +19,10 @@ use Respect\Validation\Validator;
use Illuminate\Database\Capsule\Manager as DB;
use Movim\Picture;
use Movim\ChatStates;
use Movim\ChatOwnState;
use Movim\EmbedLight;
use Movim\Image;
class Chat extends \Movim\Widget\Base
{
@ -1106,9 +1106,7 @@ class Chat extends \Movim\Widget\Base
// Sticker message
if (isset($message->sticker)) {
$p = new Picture;
$sticker = $p->get($message->sticker, false, false, 'png');
$stickerSize = $p->getSize();
$sticker = Image::getOrCreate($message->sticker, false, false, 'png');
if ($sticker == false
&& $message->jidfrom != $message->session) {
@ -1118,6 +1116,11 @@ class Chat extends \Movim\Widget\Base
->setCid($message->sticker)
->request();
} else {
$p = new Image;
$p->setKey($message->sticker);
$p->load();
$stickerSize = $p->getGeometry();
$message->sticker = [
'url' => $sticker,
'width' => $stickerSize['width'],

4
app/widgets/Chat/_chat_header.tpl

@ -13,7 +13,7 @@
</span>
{if="$curl"}
<span class="primary icon bubble color active {$conference->name|stringToColor}
<span class="primary icon bubble active
{if="!$conference->connected"}disabled{/if}"
style="background-image: url({$curl});"
onclick="RoomsUtils_ajaxShowSubject('{$jid|echapJS}')">
@ -191,7 +191,7 @@
{$url = $contact->getPhoto()}
{if="$url"}
<span class="primary icon bubble active color
<span class="primary icon bubble active
{if="$roster && $roster->presence"}status {$roster->presence->presencekey}{/if}"
onclick="ChatActions_ajaxGetContact('{$contact->jid|echapJS}')">
<img src="{$url}">

13
app/widgets/CommunityConfig/CommunityConfig.php

@ -1,5 +1,6 @@
<?php
use Movim\Image;
use Movim\Widget\Base;
use Moxl\Xec\Action\Pubsub\GetConfig;
@ -8,8 +9,6 @@ use Moxl\Xec\Action\Avatar\Set as AvatarSet;
use Respect\Validation\Validator;
use Movim\Picture;
class CommunityConfig extends Base
{
public function load()
@ -80,14 +79,18 @@ class CommunityConfig extends Base
$key = $origin.$node.'avatar';
$p = new Picture;
$p = new Image;
$p->fromBase($form->photobin->value);
$p->set($key, 'jpeg', 60);
$p->setKey($key);
$p->save(false, false, 'jpeg', 60);
// Reload the freshly compressed picture
$p->load();
$r = new AvatarSet;
$r->setTo($origin)
->setNode($node)
->setUrl($p->getOriginal($key))
->setUrl(Image::getOrCreate($key, false, false, 'jpeg', true))
->setData($p->toBase())->request();
}

2
app/widgets/ContactActions/_contactactions_drawer.tpl

@ -9,7 +9,7 @@
<li>
{if="$url"}
<span onclick="MovimUtils.reload('{$c->route('contact', $jid)}')"
class="primary icon bubble color active
class="primary icon bubble active
{if="$roster && $roster->presence"}status {$roster->presence->presencekey}{/if}
">
<img src="{$url}">

3
app/widgets/ContactData/ContactData.php

@ -66,6 +66,9 @@ class ContactData extends Base
$r = new Moxl\Xec\Action\Vcard4\Get;
$r->setTo(echapJid($jid))->request();
} else if ($contact) {
$this->rpc('MovimTpl.fill', '#'.cleanupId($jid) . '_contact_data', $this->prepareData($jid));
$this->rpc('Notification_ajaxGet');
}
}

9
app/widgets/ContactHeader/ContactHeader.php

@ -15,11 +15,18 @@ class ContactHeader extends Base
$this->registerEvent('roster_additem_handle', 'onUpdate');
$this->registerEvent('roster_updateitem_handle', 'onUpdate', 'contact');
$this->registerEvent('roster_removeitem_handle', 'onUpdate', 'contact');
$this->registerEvent('vcard_get_handle', 'onVcardReceived', 'contact');
$this->registerEvent('vcard4_get_handle', 'onVcardReceived', 'contact');
}
public function onUpdate($packet)
{
$this->rpc('MovimTpl.fill', '#contact_header', $this->prepareHeader($packet->content));
$this->rpc('MovimTpl.fill', '#'.cleanupId($packet->content) . '_contact_header', $this->prepareHeader($packet->content));
}
public function onVcardReceived($packet)
{
$this->rpc('MovimTpl.fill', '#'.cleanupId($packet->content->id) . '_contact_header', $this->prepareHeader($packet->content->id));
}
public function ajaxEditContact($jid)

2
app/widgets/ContactHeader/contactheader.tpl

@ -1,4 +1,4 @@
<header id="contact_header">
<header id="{$jid|cleanupId}_contact_header">
{autoescape="off"}
{$c->prepareHeader($jid)}
{/autoescape}

19
app/widgets/Picture/Picture.php

@ -1,7 +1,7 @@
<?php
use Movim\Widget\Base;
use Movim\Picture as MovimPicture;
use Movim\Image;
class Picture extends Base
{
@ -51,13 +51,16 @@ class Picture extends Base
$headers = preg_split('/[\r\n]+/', substr($response, 0, $header_size));
$body = substr($response, $header_size);
$p = null;
if ($compress && $body) {
$picture = new MovimPicture;
$picture->fromBin($body);
$body = $picture->set(false, 'webp', 85);
$p = new Image;
$p->fromBin($body);
$p->inMemory();
$p->save(false, false, DEFAULT_PICTURE_FORMAT, 85);
if ($body->getImageWidth() > $this->sizeLimit || $body->getImageHeight() > $this->sizeLimit) {
$body->adaptiveResizeImage($this->sizeLimit, $this->sizeLimit, true, false);
if ($p->getImage()->getImageWidth() > $this->sizeLimit || $p->getImage()->getImageHeight() > $this->sizeLimit) {
$p->getImage()->adaptiveResizeImage($this->sizeLimit, $this->sizeLimit, true, false);
}
header_remove('Content-Type');
@ -74,7 +77,9 @@ class Picture extends Base
header('Content-Disposition: attachment; filename="'.basename($parsedUrl['path']).'"');
}
header('Cache-Control: max-age=' . 3600*24);
print $body;
print $p ? $p->getImage() : $body;
return;
}

3
app/widgets/Rooms/_rooms_room.tpl

@ -9,8 +9,7 @@
">
{$url = $conference->getPhoto()}
{if="$url"}
<span class="primary icon bubble color small
{$conference->name|stringToColor}"
<span class="primary icon bubble small"
id="{$conference->conference|cleanupId}-rooms-primary"
style="background-image: url({$url});">
{autoescape="off"}

14
app/widgets/RoomsUtils/RoomsUtils.php

@ -13,7 +13,7 @@ use Moxl\Xec\Action\Bookmark\Synchronize;
use Moxl\Xec\Payload\Packet;
use Movim\Widget\Base;
use Movim\Picture;
use Movim\Image;
use App\Conference;
use App\Info;
@ -124,13 +124,15 @@ class RoomsUtils extends Base
return;
}
$p = new Picture;
$p->fromBase($form->photobin->value);
$tempKey = \generateKey(6);
$p->set('temp', 'jpeg', 60);
$p = new Image;
$p->fromBase($form->photobin->value);
$p->setKey($tempKey);
$p->save(false, false, 'jpeg', 60);
$p = new Picture;
$p->get('temp');
// Reload
$p->load();
$vcard = new stdClass;
$vcard->photobin = new stdClass;

2
app/widgets/RoomsUtils/_rooms_drawer.tpl

@ -11,7 +11,7 @@
<ul class="list thick">
<li>
{if="$curl"}
<span class="primary icon bubble color active {$conference->name|stringToColor}"
<span class="primary icon bubble active"
style="background-image: url({$curl});">
</span>
{else}

2
app/widgets/Search/_search_roster.tpl

@ -8,7 +8,7 @@
<li
id="{$value->jid|cleanupId}"
title="{$value->jid}"
name="{$value->jid|cleanupId}-{$value->truename|cleanupId}-{$value->group|cleanupId}{if="$value->presence"}-{$value->presence->presencetext|cleanupId}{/if}"
name="{$value->jid|cleanupId}-{if="$value->truename"}{$value->truename|cleanupId}{/if}-{if="$value->group"}{$value->group|cleanupId}{/if}{if="$value->presence"}-{$value->presence->presencetext|cleanupId}{/if}"
class="{if="$value->presence && $value->presence->value > 4"}faded{/if}"
>
{$url = $value->getPhoto('m')}

8
app/widgets/Stickers/Stickers.php

@ -8,7 +8,7 @@ use Psr\Http\Message\ResponseInterface;
use App\Configuration;
use App\MessageFile;
use Movim\Picture;
use Movim\Image;
class Stickers extends \Movim\Widget\Base
{
@ -32,7 +32,7 @@ class Stickers extends \Movim\Widget\Base
list($c, $ext) = explode('@', $cid);
list($sh, $key) = explode('+', $c);
$base64 = base64_encode(file_get_contents(PUBLIC_CACHE_PATH.hash(Picture::$hash, $key).'.png'));
$base64 = base64_encode(file_get_contents(PUBLIC_CACHE_PATH.hash(Image::$hash, $key).'.png'));
$a = new Answer;
$a->setTo($to)
@ -58,8 +58,8 @@ class Stickers extends \Movim\Widget\Base
}
// Caching the picture
if (!file_exists(PUBLIC_CACHE_PATH.hash(Picture::$hash, $key).'.png')) {
copy($filepath, PUBLIC_CACHE_PATH.hash(Picture::$hash, $key).'.png');
if (!file_exists(PUBLIC_CACHE_PATH.hash(Image::$hash, $key).'.png')) {
copy($filepath, PUBLIC_CACHE_PATH.hash(Image::$hash, $key).'.png');
}
// Creating a message

7
lib/moxl/src/Xec/Action/BOB/Request.php

@ -4,7 +4,7 @@ namespace Moxl\Xec\Action\BOB;
use Moxl\Xec\Action;
use Moxl\Stanza\BOB;
use Movim\Picture;
use Movim\Image;
class Request extends Action
{
@ -22,9 +22,10 @@ class Request extends Action
{
$data = (string)$stanza->data;
$p = new Picture;
$p = new Image;
$p->fromBase($data);
$p->set($this->_cid, 'png');
$p->setKey($this->_cid);
$p->save(false, false, 'png');
$this->pack(['to' => $this->_to, 'cid' => $this->_cid]);
$this->deliver();

9
lib/moxl/src/Xec/Action/Pubsub/GetItem.php

@ -4,7 +4,8 @@ namespace Moxl\Xec\Action\Pubsub;
use Moxl\Stanza\Pubsub;
use Moxl\Xec\Action\Pubsub\Errors;
use Movim\Picture;
use Movim\Image;
class GetItem extends Errors
{
@ -72,10 +73,10 @@ class GetItem extends Errors
->first();
if ($i && $i->avatarhash !== (string)$item->metadata->info->attributes()->id) {
$p = new Picture;
$p = new Image;
$p->fromURL((string)$item->metadata->info->attributes()->url);
$p->set((string)$item->metadata->info->attributes()->id);
$p->setKey((string)$item->metadata->info->attributes()->id);
$p->save();
$i->avatarhash = (string)$item->metadata->info->attributes()->id;
$i->save();

8
lib/moxl/src/Xec/Action/Pubsub/GetItems.php

@ -4,7 +4,8 @@ namespace Moxl\Xec\Action\Pubsub;
use Moxl\Stanza\Pubsub;
use Moxl\Xec\Action\Pubsub\Errors;
use Movim\Picture;
use Movim\Image;
class GetItems extends Errors
{
@ -79,9 +80,10 @@ class GetItems extends Errors
->first();
if ($i && $i->avatarhash !== (string)$item->metadata->info->attributes()->id) {
$p = new Picture;
$p = new Image;
$p->fromURL((string)$item->metadata->info->attributes()->url);
$p->set((string)$item->metadata->info->attributes()->id);
$p->setKey((string)$item->metadata->info->attributes()->id);
$p->save();
$i->avatarhash = (string)$item->metadata->info->attributes()->id;
$i->save();

7
lib/moxl/src/Xec/Payload/Avatar.php

@ -2,7 +2,7 @@
namespace Moxl\Xec\Payload;
use Movim\Picture;
use Movim\Image;
class Avatar extends Payload
{
@ -10,9 +10,10 @@ class Avatar extends Payload
{
$jid = current(explode('/', (string)$parent->attributes()->from));
$p = new Picture;
$p = new Image;
$p->fromBase((string)$stanza->items->item->data);
$p->set($jid);
$p->setKey($jid);
$p->save();
$this->event('vcard', \App\Contact::firstOrNew(['id' => $jid]));
}

3
src/Movim/Bootstrap.php

@ -109,6 +109,9 @@ class Bootstrap
define('WIDGETS_PATH', DOCUMENT_ROOT . '/app/widgets/');
define('MOVIM_SQL_DATE', 'Y-m-d H:i:s');
define('DEFAULT_PICTURE_FORMAT', 'webp');
define('DEFAULT_PICTURE_QUALITY', 95);
define('API_SOCKET', CACHE_PATH . 'socketapi.sock');
}

291
src/Movim/Image.php

@ -0,0 +1,291 @@
<?php
namespace Movim;
class Image
{
private $_key;
private $_im;
private $_inMemory = false;
public static $folder = 'cache/';
public static $formats = ['jpeg' => '.jpg', 'png' => '.png', 'webp' => '.webp', 'gif' => '.gif'];
public static $hash = 'sha256'; // Cache need to be cleared in a migration if changed
private static $originalType = '_o';
public function __construct()
{
$this->_im = new \Imagick;
$this->_im->setBackgroundColor(new \ImagickPixel('transparent'));
}
/**
* Set the picture $key
*/
public function setKey(string $key): void
{
$this->_key = $key;
}
/**
* Allow save without writing the file to disk
*/
public function inMemory()
{
$this->_inMemory = true;
}
/**
* @desc Load a bin picture from a path
*/
public function load(string $format = DEFAULT_PICTURE_FORMAT): bool
{
if (!empty($this->_key)) {
return $this->fromPath(
PUBLIC_CACHE_PATH .
hash(Image::$hash, $this->_key) .
self::$originalType .
self::$formats[$format]
);
}
return false;
}
/**
* @desc Get the current picture geometry
*/
public function getGeometry(): ?array
{
if ($this->_im) {
return $this->_im->getImageGeometry();
}
}
/**
* @desc Load a bin picture from an URL
*/
public function fromURL(string $url):bool
{
$bin = requestURL($url);
if ($bin) {
return $this->fromBin($bin);
}
return false;
}
/**
* @desc Load a bin picture from a base64
*/
public function fromBase(?string $base = null): bool
{
if ($base) {
return $this->fromBin((string)base64_decode((string)$base));
}
return false;
}
/**
* @desc Load a bin picture from a binary
*/
public function fromBin(?string $bin = null): bool
{
if ($bin) {
try {
$this->_im->readImageBlob((string)$bin);
return true;
} catch (\ImagickException $e) {
error_log($e->getMessage());
}
}
return false;
}
/**
* @desc Convert to a base64
*/
public function toBase(): string
{
if ($this->_im) {
return base64_encode($this->_im->getImageBlob());
}
}
/**
* @desc Return the picture URL or create it if possible
*/
public static function getOrCreate(string $key, $width = false, $height = false, $format = DEFAULT_PICTURE_FORMAT, bool $noTime = false): ?string
{
if (!in_array($format, array_keys(self::$formats))) {
$format = DEFAULT_PICTURE_FORMAT;
}
$type = $width ? '_' . $width
: self::$originalType;
/**
* The file is in the cache and we can directly return it
*/
if (file_exists(
PUBLIC_CACHE_PATH . hash(Image::$hash, $key) .
$type . self::$formats[$format]
)) {
return urilize(
self::$folder . hash(Image::$hash, $key) . $type . self::$formats[$format],
$noTime
);
}
/**
* The file is not in the cache but we do have the original to build the requested size
*/
elseif (
$width
&& file_exists(
PUBLIC_CACHE_PATH . hash(Image::$hash, $key) .
self::$originalType . self::$formats[$format]
)
) {
$im = new Image;
$im->setKey($key);
if (!$im->load($format)) {
\Utils::error('Cannot load ' . $key . ' original file');
}
$im->save($width, $height, $format);
return urilize(
self::$folder . hash(Image::$hash, $key) . $type . self::$formats[$format],
$noTime
);
}
return null;
}
public function save($width = false, $height = false, $format = DEFAULT_PICTURE_FORMAT, $quality = DEFAULT_PICTURE_QUALITY)
{
if (!$this->_key && !$this->_inMemory) return;
$type = $width ? '_' . $width
: self::$originalType;
if (!$this->_inMemory) {
// Cleanup the existing files
$path = PUBLIC_CACHE_PATH . hash(Image::$hash, $this->_key) . $type . self::$formats[$format];
// If the file exists we replace it
if (file_exists($path)) {
@unlink($path);
// And destroy all the thumbnails if it's the original
if ($width == false) {
foreach (glob(
PUBLIC_CACHE_PATH .
hash(Image::$hash, $this->_key) .
'*' . self::$formats[$format],
GLOB_NOSORT
) as $pathThumb) {
@unlink($pathThumb);
}
}
}
}
// Save the file
try {
$this->_im = $this->_im->coalesceImages();
$this->_im->setImageFormat($format);
if ($format == 'jpeg') {
$this->_im->setImageCompression(\Imagick::COMPRESSION_JPEG);
$this->_im->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
// Put 11 as a value for now, see http://php.net/manual/en/imagick.flattenimages.php#116956
$this->_im->setImageAlphaChannel(Imagick::ALPHACHANNEL_REMOVE);
//$this->_im->setImageAlphaChannel(11);
$this->_im->setImageBackgroundColor('#ffffff');
}
if ($format == 'webp') {
$this->_im->setImageAlphaChannel(\Imagick::ALPHACHANNEL_ACTIVATE);
$this->_im->setBackgroundColor(new \ImagickPixel('transparent'));
}
if ($format == 'jpeg' || $format == 'webp') {
$this->_im = $this->_im->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN);
$this->_im->setImageCompressionQuality($quality);
}
if (empty($this->_im->getImageProperties('png:gAMA'))) {
$this->_im->setOption('png:exclude-chunk', 'gAMA');
}
// Resize
if (!$height) {
$height = $width;
}
if ($width && $height) {
$geo = $this->_im->getImageGeometry();
$this->_im->cropThumbnailImage($width, $height);
if ($width > $geo['width']) {
$factor = floor($width / $geo['width']);
$this->_im->blurImage($factor, 10);
}
}
if (!$this->_inMemory && $path) {
$this->_im = $this->_im->deconstructImages();
$this->_im->writeImages($path, true);
$this->_im->clear();
}
} catch (\ImagickException $e) {
\Utils::error($e->getMessage());
}
}
/**
* Get the Imagick image
*/
public function getImage(): \Imagick
{
return $this->_im;
}
/**
* Remove the original
*/
public function remove(string $format = DEFAULT_PICTURE_FORMAT)
{
$path = PUBLIC_CACHE_PATH . hash(Image::$hash, $this->_key) . self::$originalType . self::$formats[$format];
if (file_exists($path)) {
@unlink($path);
}
}
/**
* @desc Load a bin picture from a path
*/
private function fromPath(string $path): bool
{
if (file_exists($path)) {
$size = filesize($path);
if ($size > 0) {
$handle = fopen($path, "r");
$bin = fread($handle, $size);
fclose($handle);
if ($bin) {
return $this->fromBin($bin);
}
}
}
return false;
}
}

342
src/Movim/Picture.php

@ -1,342 +0,0 @@
<?php
namespace Movim;
define('DEFAULT_PICTURE_FORMAT', 'jpeg');
define('DEFAULT_PICTURE_QUALITY', 95);
define('DEFAULT_PICTURE_EXPIRATION_HOURS', 12);
class Picture
{
private $_path = PUBLIC_CACHE_PATH;
private $_folder = 'cache/';
private $_key;
private $_bin = false;
private $_formats = ['jpeg' => '.jpg', 'png' => '.png', 'webp' => '.webp', 'gif' => '.gif'];
public static $hash = 'sha256'; // Cache need to be cleared in a migration if changed
/**
* @desc Load a bin picture from an URL
*/
public function fromURL($url)
{
$bin = requestURL($url);
if ($bin) {
$this->_bin = $bin;
}
}
/**
* @desc Load a bin picture from a path
*/
public function fromKey($key)
{
return $this->fromPath(
$this->_path.
hash(Picture::$hash, $key).
$this->_formats[DEFAULT_PICTURE_FORMAT]
);
}
/**
* @desc Load a bin picture from a path
*/
public function fromPath($path)
{
if (file_exists($path)) {
$size = filesize($path);
if ($size > 0) {
$handle = fopen($path, "r");
$this->_bin = fread($handle, $size);
fclose($handle);
}
}
}
/**
* @desc Load a bin picture from a binary
*/
public function fromBin($bin = false)
{
if ($bin) {
$this->_bin = (string)$bin;
}
}
/**
* @desc Return the binary
*/
public function toBin()
{
if ($this->_bin) {
return $this->_bin;
}
return false;
}
/**
* @desc Load a bin picture from a base64
*/
public function fromBase($base = false)
{
if ($base) {
$this->_bin = (string)base64_decode((string)$base);
}
}
/**
* @desc Convert to a base64
*/
public function toBase()
{
if ($this->_bin) {
return base64_encode($this->_bin);
}
return false;
}
/**
* @desc check if a picture is old
*/
public function isOld($key, $format = DEFAULT_PICTURE_FORMAT)
{
$original = $this->_path.hash(Picture::$hash, $key).$this->_formats[$format];
return (!file_exists($original) || (file_exists($original)
&& filemtime($original) < time() - 3600 * DEFAULT_PICTURE_EXPIRATION_HOURS));
}
/**
* @desc Get the original picture URL, without timestamp
* @param $key The key of the picture
*/
public function getOriginal($key)
{
return $this->get($key, false, false, DEFAULT_PICTURE_FORMAT, true);
}
/**
* @desc Get a picture of the current size
* @param $key The key of the picture
* @param $width The width requested
* @param $height The height requested
* @return The url of the picture
*/
public function get($key, $width = false, $height = false, $format = DEFAULT_PICTURE_FORMAT, bool $noTime = false)
{
if (!in_array($format, array_keys($this->_formats))) {
$format = DEFAULT_PICTURE_FORMAT;
}
$this->_key = $key;
$original = $this->_path.hash(Picture::$hash, $this->_key).$this->_formats[$format];
// We request the original picture
if ($width == false) {
if (file_exists($original)) {
$this->fromPath($original);
return urilize(
$this->_folder . hash(Picture::$hash, $this->_key) . $this->_formats[$format],
$noTime
);
}
}
// We request a specific size
if (file_exists(
$this->_path.hash(Picture::$hash, $this->_key) .
'_' . $width.$this->_formats[$format]
)
) {
$this->fromPath(
$this->_path.hash(Picture::$hash, $this->_key) .
'_' . $width.$this->_formats[$format]
);
return urilize(
$this->_folder.hash(Picture::$hash, $this->_key) .
'_' . $width.$this->_formats[$format],
$noTime
);
}
if (file_exists($original)) {
$this->fromPath($original);
$this->_createThumbnail($width, $height);
return urilize(
$this->_folder.hash(Picture::$hash, $this->_key) .
'_' . $width . $this->_formats[$format],
$noTime
);
}
}
/**
* @desc Get the current picture size
* @param $key The picture key
*/
public function getSize()
{
if ($this->_bin) {
$im = new \Imagick;
try {
$im->readImageBlob($this->_bin);
if ($im != false) {
return $im->getImageGeometry();
}
} catch (\ImagickException $e) {
error_log($e->getMessage());
}
}
}
/**
* @desc Save a picture (original size)
* @param $key The key of the picture, if = false return the compressed binary
*/
public function set($key = false, $format = DEFAULT_PICTURE_FORMAT, $quality = DEFAULT_PICTURE_QUALITY)
{
if (!in_array($format, array_keys($this->_formats))) {
$format = DEFAULT_PICTURE_FORMAT;
}
if ($key) {
$this->_key = $key;
$path = $this->_path.hash(Picture::$hash, $this->_key).$this->_formats[$format];
// If the file exist we replace it
if (file_exists($path) && $this->_bin) {
@unlink($path);
// And destroy all the thumbnails
foreach (
glob(
$this->_path.
hash(Picture::$hash, $key).
'*'.$this->_formats[$format],
GLOB_NOSORT
) as $pathThumb) {
@unlink($pathThumb);
}
}
} else {
$path = false;
}
if ($this->_bin) {
$im = new \Imagick;
try {
$finfo = new \finfo(FILEINFO_MIME_TYPE);
// Convert the picture to PNG with GD if Imagick doesn't handle WEBP
if ($finfo->buffer($this->_bin) == 'image/webp'
&& empty(\Imagick::queryFormats('WEBP'))
&& array_key_exists('WebP Support', \gd_info())
&& $path
) {
$temp = tmpfile();
fwrite($temp, $this->_bin);
$resource = \imagecreatefromwebp(stream_get_meta_data($temp)['uri']);
fclose($temp);
\imagepng($resource, $path.'.temp', 0);
$this->fromPath($path.'.temp');
}
$im->readImageBlob($this->_bin);
if ($im != false) {
$im = $im->coalesceImages();
$im->setImageFormat($format);
if ($format == 'jpeg') {
$im->setImageCompression(\Imagick::COMPRESSION_JPEG);
$im->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
// Put 11 as a value for now, see http://php.net/manual/en/imagick.flattenimages.php#116956
//$im->setImageAlphaChannel(Imagick::ALPHACHANNEL_REMOVE);
$im->setImageAlphaChannel(11);
$im->setImageBackgroundColor('#ffffff');
$im = $im->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN);
}
if ($format == 'webp') {
$im->setImageAlphaChannel(\Imagick::ALPHACHANNEL_ACTIVATE);
$im->setBackgroundColor(new \ImagickPixel('transparent'));
}
if ($format == 'jpeg' || $format == 'webp') {
$im->setImageCompressionQuality($quality);
}
if (empty($im->getImageProperties('png:gAMA'))) {
$im->setOption('png:exclude-chunk', 'gAMA');
}
if ($path) {
$im = $im->deconstructImages();
$im->writeImages($path, true);
$im->clear();
return true;
}
return $im;
}
return false;
} catch (\ImagickException $e) {
error_log($e->getMessage());
}
}
}
/**
* @desc Create a thumbnail of the picture and save it
* @param $size The size requested
*/
private function _createThumbnail($width, $height = false, $format = DEFAULT_PICTURE_FORMAT)
{
if (!in_array($format, array_keys($this->_formats))) {
$format = DEFAULT_PICTURE_FORMAT;
}
if (!$height) {
$height = $width;
}
$path = $this->_path.hash(Picture::$hash, $this->_key).'_'.$width.$this->_formats[$format];
$im = new \Imagick;
try {
$im->readImageBlob($this->_bin);
$im->setImageFormat($format);
if ($format == 'jpeg') {
$im->setImageCompression(\Imagick::COMPRESSION_JPEG);
$im->setImageAlphaChannel(11);
// Put 11 as a value for now, see http://php.net/manual/en/imagick.flattenimages.php#116956
//$im->setImageAlphaChannel(Imagick::ALPHACHANNEL_REMOVE);
$im->setImageBackgroundColor('#ffffff');
$im = $im->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN);
}
$geo = $im->getImageGeometry();
$im->cropThumbnailImage($width, $height);
if ($width > $geo['width']) {
$factor = floor($width/$geo['width']);
$im->blurImage($factor, 10);
}
$im->setImageCompressionQuality(85);
$im->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
$im->writeImage($path);
$im->clear();
} catch (\ImagickException $e) {
error_log($e->getMessage());
}
}
}
Loading…
Cancel
Save