Browse Source
Rewrite Picture to Image and add transparent images support, default pictures are compressed to webp
pull/1040/head
Rewrite Picture to Image and add transparent images support, default pictures are compressed to webp
pull/1040/head
28 changed files with 396 additions and 414 deletions
-
1CHANGELOG.md
-
9app/Contact.php
-
7app/Message.php
-
6app/Presence.php
-
2app/helpers/StringHelper.php
-
8app/helpers/UtilsHelper.php
-
2app/views/contact.tpl
-
16app/widgets/Avatar/Avatar.php
-
11app/widgets/Chat/Chat.php
-
4app/widgets/Chat/_chat_header.tpl
-
13app/widgets/CommunityConfig/CommunityConfig.php
-
2app/widgets/ContactActions/_contactactions_drawer.tpl
-
3app/widgets/ContactData/ContactData.php
-
9app/widgets/ContactHeader/ContactHeader.php
-
2app/widgets/ContactHeader/contactheader.tpl
-
19app/widgets/Picture/Picture.php
-
3app/widgets/Rooms/_rooms_room.tpl
-
14app/widgets/RoomsUtils/RoomsUtils.php
-
2app/widgets/RoomsUtils/_rooms_drawer.tpl
-
2app/widgets/Search/_search_roster.tpl
-
8app/widgets/Stickers/Stickers.php
-
7lib/moxl/src/Xec/Action/BOB/Request.php
-
9lib/moxl/src/Xec/Action/Pubsub/GetItem.php
-
8lib/moxl/src/Xec/Action/Pubsub/GetItems.php
-
7lib/moxl/src/Xec/Payload/Avatar.php
-
3src/Movim/Bootstrap.php
-
291src/Movim/Image.php
-
342src/Movim/Picture.php
@ -1,4 +1,4 @@ |
|||
<header id="contact_header"> |
|||
<header id="{$jid|cleanupId}_contact_header"> |
|||
{autoescape="off"} |
|||
{$c->prepareHeader($jid)} |
|||
{/autoescape} |
|||
|
@ -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; |
|||
} |
|||
} |
@ -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()); |
|||
} |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue