*
* PHP version 5
*
- * Copyright © 2006-2014 The Galette Team
+ * Copyright © 2006-2023 The Galette Team
*
* This file is part of Galette (http://galette.tuxfamily.org).
*
* @category Core
* @package Galette
*
- * @author Frédéric Jaqcuot <unknown@unknow.com>
+ * @author Frédéric Jacquot <unknown@unknow.com>
* @author Johan Cwiklinski <johan@x-tnd.be>
- * @copyright 2006-2014 The Galette Team
+ * @copyright 2006-2023 The Galette Team
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
- * @version SVN: $Id$
* @link http://galette.tuxfamily.org
*/
namespace Galette\Core;
+use ArrayObject;
+use Slim\Psr7\Response;
+use Throwable;
use Analog\Analog;
use Galette\Entity\Adherent;
use Galette\Repository\Members;
* @name Picture
* @category Core
* @package Galette
- * @author Frédéric Jaqcuot <unknown@unknow.com>
+ * @author Frédéric Jacquot <unknown@unknow.com>
* @author Johan Cwiklinski <johan@x-tnd.be>
- * @copyright 2006-2014 The Galette Team
+ * @copyright 2006-2023 The Galette Team
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
* @link http://galette.tuxfamily.org
*/
use FileTrait;
//constants that will not be overrided
- const SQL_ERROR = -10;
- const SQL_BLOB_ERROR = -11;
+ public const SQL_ERROR = -10;
+ public const SQL_BLOB_ERROR = -11;
//constants that can be overrided
//(do not use self::CONSTANT, but get_class[$this]::CONSTANT)
- const TABLE = 'pictures';
- const PK = Adherent::PK;
+ public const TABLE = 'pictures';
+ public const PK = Adherent::PK;
protected $tbl_prefix = '';
protected $id;
+ protected $db_id;
protected $height;
protected $width;
protected $optimal_height;
/**
* Default constructor.
*
- * @param int $id_adh the id of the member
+ * @param mixed|null $id_adh the id of the member
*/
- public function __construct($id_adh = '')
+ public function __construct($id_adh = null)
{
$this->init(
null,
- array('jpeg', 'jpg', 'png', 'gif'),
+ array('jpeg', 'jpg', 'png', 'gif', 'webp'),
array(
'jpg' => 'image/jpeg',
'png' => 'image/png',
- 'gif' => 'image/gif'
+ 'gif' => 'image/gif',
+ 'webp' => 'image/webp'
)
);
$this->db_id = $id_adh;
}
- //if file does not exists on the FileSystem, check for it in the database
+ //if file does not exist on the FileSystem, check for it in the database
if (!$this->checkFileOnFS()) {
if ($this->checkFileInDB()) {
$this->has_picture = true;
//if file has been deleted since we store our object in the session,
//we try to retrieve it
if (!$this->checkFileOnFS()) {
- //if file does not exists on the FileSystem,
+ //if file does not exist on the FileSystem,
//check for it in the database
//$this->checkFileInDB();
} else {
$this->format = 'gif';
$this->mime = 'image/gif';
return true;
+ } elseif (file_exists($file_wo_ext . '.webp')) {
+ $this->file_path = realpath($file_wo_ext . '.webp');
+ $this->format = 'webp';
+ $this->mime = 'image/webp';
+ return true;
}
return false;
}
case 'gif':
$this->mime = 'image/gif';
break;
+ case 'webp':
+ $this->mime = 'image/webp';
+ break;
}
$this->file_path = realpath($file_wo_ext . '.' . $this->format);
return true;
}
- } catch (\Exception $e) {
+ } catch (Throwable $e) {
return false;
}
+ return false;
}
/**
}
}
+ /**
+ * Get image file content
+ *
+ * @return mixed
+ */
+ public function getContents()
+ {
+ readfile($this->file_path);
+ }
+
/**
* Set header and displays the picture.
*
+ * @param Response $response Response
+ *
* @return object the binary file
*/
- public function display()
+ public function display(Response $response)
{
- header('Content-type: '.$this->mime);
- ob_clean();
- flush();
- readfile($this->file_path);
+ $response = $response->withHeader('Content-Type', $this->mime)
+ ->withHeader('Content-Transfer-Encoding', 'binary')
+ ->withHeader('Expires', '0')
+ ->withHeader('Cache-Control', 'must-revalidate')
+ ->withHeader('Pragma', 'public');
+
+ $stream = fopen('php://memory', 'r+');
+ fwrite($stream, file_get_contents($this->file_path));
+ rewind($stream);
+
+ return $response->withBody(new \Slim\Psr7\Stream($stream));
}
/**
}
$delete = $zdb->delete($this->tbl_prefix . $class::TABLE);
- $delete->where(
- $class::PK . ' = ' . $this->db_id
- );
+ $delete->where([$class::PK => $this->db_id]);
$del = $zdb->execute($delete);
if (!$del->count() > 0) {
//return unlink($file_wo_ext . '.gif');
$_file = $file_wo_ext . '.gif';
$success = unlink($_file);
+ } elseif (file_exists($file_wo_ext . '.webp')) {
+ //return unlink($file_wo_ext . '.webp');
+ $_file = $file_wo_ext . '.webp';
+ $success = unlink($_file);
}
if ($_file !== null && $success !== true) {
$this->has_picture = false;
return true;
}
- } catch (\Exception $e) {
+ } catch (Throwable $e) {
if ($transaction === true) {
$zdb->connection->rollBack();
}
/**
* Stores an image on the disk and in the database
*
- * @param object $file the uploaded file
- * @param boolean $ajax If the image cames from an ajax call (dnd)
+ * @param object $file The uploaded file
+ * @param boolean $ajax If the image cames from an ajax call (dnd)
+ * @param array $cropping Cropping properties
*
- * @return true|false result of the storage process
+ * @return bool|int
*/
- public function store($file, $ajax = false)
+ public function store($file, $ajax = false, $cropping = null)
{
/** TODO: fix max size (by preferences ?) */
global $zdb;
$err_msg .= 'Invalid extension for file ' . $name . '.';
$ret = self::INVALID_EXTENSION;
} else {
- $err_msg = 'Invalid filename `' . $name . '` (Tip: ';
+ $err_msg = 'Invalid filename `' . $name . '` (Tip: ';
$err_msg .= preg_replace(
'|%s|',
htmlentities($this->getBadChars()),
}
//Second, let's check file size
- if ($file['size'] > ( $this->maxlenght * 1024 )) {
+ if ($file['size'] > ($this->maxlenght * 1024)) {
Analog::log(
- '[' . $class . '] File is too big (' . ( $file['size'] * 1024 ) .
- 'Ko for maximum authorized ' . ( $this->maxlenght * 1024 ) .
+ '[' . $class . '] File is too big (' . ($file['size'] * 1024) .
+ 'Ko for maximum authorized ' . ($this->maxlenght * 1024) .
'Ko',
Analog::ERROR
);
);
}
+ // Source image must have minimum dimensions to match the cropping process requirements
+ // and ensure the final picture will fit the maximum allowed resizing dimensions.
+ if (isset($cropping['ratio']) && isset($cropping['focus'])) {
+ if ($current[0] < $this->mincropsize || $current[1] < $this->mincropsize) {
+ $min_current = min($current[0], $current[1]);
+ Analog::log(
+ '[' . $class . '] Image is too small. The minimum image side size allowed is ' .
+ $this->mincropsize . 'px, but current is ' . $min_current . 'px.',
+ Analog::ERROR
+ );
+ return self::IMAGE_TOO_SMALL;
+ } else {
+ Analog::log('[' . $class . '] Image dimensions are OK, proceed', Analog::DEBUG);
+ }
+ }
+
$this->delete();
$new_file = $this->store_path .
if ($current[0] > $this->max_width || $current[1] > $this->max_height) {
/** FIXME: what if image cannot be resized?
Should'nt we want to stop the process here? */
- $this->resizeImage($new_file, $extension);
+ $this->resizeImage($new_file, $extension, null, $cropping);
}
return $this->storeInDb($zdb, $this->db_id, $new_file, $extension);
* @param string $file File path on disk
* @param string $ext File extension
*
- * @return boolean
+ * @return bool|int
*/
private function storeInDb(Db $zdb, $id, $file, $ext)
{
$f = fopen($file, 'r');
$picture = '';
- while ($r=fread($f, 8192)) {
+ while ($r = fread($f, 8192)) {
$picture .= $r;
}
fclose($f);
$insert = $zdb->insert($this->tbl_prefix . $class::TABLE);
$insert->values(
array(
- $class::PK => ':id',
+ $class::PK => ':' . $class::PK,
'picture' => ':picture',
'format' => ':format'
)
$stmt = $zdb->sql->prepareStatementForSqlObject($insert);
$container = $stmt->getParameterContainer();
$container->offsetSet(
- $class::PK,
- ':id'
- );
- $container->offsetSet(
- 'picture',
+ 'picture', //'picture',
':picture',
$container::TYPE_LOB
);
- $container->offsetSet(
- 'format',
- ':format'
- );
$stmt->setParameterContainer($container);
$this->insert_stmt = $stmt;
}
);
$zdb->connection->commit();
$this->has_picture = true;
- } catch (\Exception $e) {
+ } catch (Throwable $e) {
$zdb->connection->rollBack();
Analog::log(
'An error occurred storing picture in database: ' .
);
foreach ($valids as $valid) {
+ /** @var ArrayObject $valid */
$file = $existing_disk[$valid->id_adh];
$this->storeInDb(
$zdb,
}
/**
- * Resize the image if it exceed max allowed sizes
+ * Resize and eventually crop the image if it exceeds max allowed sizes
*
- * @param string $source the source image
- * @param string $ext file's extension
- * @param string $dest the destination image.
- * If null, we'll use the source image. Defaults to null
+ * @param string $source The source image
+ * @param string $ext File's extension
+ * @param string $dest The destination image.
+ * If null, we'll use the source image. Defaults to null
+ * @param array $cropping Cropping properties
*
- * @return void
+ * @return boolean
*/
- private function resizeImage($source, $ext, $dest = null)
+ private function resizeImage($source, $ext, $dest = null, $cropping = null)
{
$class = get_class($this);
- if (function_exists("gd_info")) {
- $gdinfo = gd_info();
- $h = $this->max_height;
- $w = $this->max_width;
- if ($dest == null) {
- $dest = $source;
- }
+ if (!function_exists("gd_info")) {
+ Analog::log(
+ '[' . $class . '] GD is not present - ' .
+ 'pictures could not be resized!',
+ Analog::ERROR
+ );
+ return false;
+ }
- switch (strtolower($ext)) {
- case 'jpg':
- if (!$gdinfo['JPEG Support']) {
- Analog::log(
- '[' . $class . '] GD has no JPEG Support - ' .
- 'pictures could not be resized!',
- Analog::ERROR
- );
- return false;
+ $gdinfo = gd_info();
+ $h = $this->max_height;
+ $w = $this->max_width;
+ if ($dest == null) {
+ $dest = $source;
+ }
+
+ switch (strtolower($ext)) {
+ case 'jpg':
+ if (!$gdinfo['JPEG Support']) {
+ Analog::log(
+ '[' . $class . '] GD has no JPEG Support - ' .
+ 'pictures could not be resized!',
+ Analog::ERROR
+ );
+ return false;
+ }
+ break;
+ case 'png':
+ if (!$gdinfo['PNG Support']) {
+ Analog::log(
+ '[' . $class . '] GD has no PNG Support - ' .
+ 'pictures could not be resized!',
+ Analog::ERROR
+ );
+ return false;
+ }
+ break;
+ case 'gif':
+ if (!$gdinfo['GIF Create Support']) {
+ Analog::log(
+ '[' . $class . '] GD has no GIF Support - ' .
+ 'pictures could not be resized!',
+ Analog::ERROR
+ );
+ return false;
+ }
+ break;
+ case 'webp':
+ if (!$gdinfo['WebP Support']) {
+ Analog::log(
+ '[' . $class . '] GD has no WebP Support - ' .
+ 'pictures could not be resized!',
+ Analog::ERROR
+ );
+ return false;
+ }
+ break;
+
+ default:
+ return false;
+ }
+
+ list($cur_width, $cur_height, $cur_type, $curattr)
+ = getimagesize($source);
+
+ $ratio = $cur_width / $cur_height;
+
+ // Define cropping variables if necessary.
+ $thumb_cropped = false;
+ // Cropping is based on the smallest side of the source in order to
+ // provide as less focusing options as possible if the source doesn't
+ // fit the final ratio (center, top, bottom, left, right).
+ $min_size = min($cur_width, $cur_height);
+ // Cropping dimensions.
+ $crop_width = $min_size;
+ $crop_height = $min_size;
+ // Cropping focus.
+ $crop_x = 0;
+ $crop_y = 0;
+ if (isset($cropping['ratio']) && isset($cropping['focus'])) {
+ // Calculate cropping dimensions
+ switch ($cropping['ratio']) {
+ case 'portrait_ratio':
+ // Calculate cropping dimensions
+ if ($ratio < 1) {
+ $crop_height = ceil($crop_width * 4 / 3);
+ } else {
+ $crop_width = ceil($crop_height * 3 / 4);
}
+ // Calculate resizing dimensions
+ $w = ceil($h * 3 / 4);
break;
- case 'png':
- if (!$gdinfo['PNG Support']) {
- Analog::log(
- '[' . $class . '] GD has no PNG Support - ' .
- 'pictures could not be resized!',
- Analog::ERROR
- );
- return false;
+ case 'landscape_ratio':
+ // Calculate cropping dimensions
+ if ($ratio > 1) {
+ $crop_width = ceil($crop_height * 4 / 3);
+ } else {
+ $crop_height = ceil($crop_width * 3 / 4);
}
+ // Calculate resizing dimensions
+ $h = ceil($w * 3 / 4);
break;
- case 'gif':
- if (!$gdinfo['GIF Create Support']) {
- Analog::log(
- '[' . $class . '] GD has no GIF Support - ' .
- 'pictures could not be resized!',
- Analog::ERROR
- );
- return false;
+ }
+ // Calculate focus coordinates
+ switch ($cropping['focus']) {
+ case 'center':
+ if ($ratio > 1) {
+ $crop_x = ceil(($cur_width - $crop_width) / 2);
+ } elseif ($ratio == 1) {
+ $crop_x = ceil(($cur_width - $crop_width) / 2);
+ $crop_y = ceil(($cur_height - $crop_height) / 2);
+ } else {
+ $crop_y = ceil(($cur_height - $crop_height) / 2);
}
break;
- default:
- return false;
+ case 'top':
+ $crop_x = ceil(($cur_width - $crop_width) / 2);
+ break;
+ case 'bottom':
+ $crop_y = $cur_height - $crop_height;
+ break;
+ case 'right':
+ $crop_x = $cur_width - $crop_width;
+ break;
}
-
- list($cur_width, $cur_height, $cur_type, $curattr)
- = getimagesize($source);
-
- $ratio = $cur_width / $cur_height;
-
- // calculate image size according to ratio
- if ($cur_width>$cur_height) {
- $h = $w/$ratio;
+ // Cropped image.
+ $thumb_cropped = imagecreatetruecolor($crop_width, $crop_height);
+ // Cropped ratio.
+ $ratio = $crop_width / $crop_height;
+ // Otherwise, calculate image size according to the source's ratio.
+ } else {
+ if ($cur_width > $cur_height) {
+ $h = round($w / $ratio);
} else {
- $w = $h*$ratio;
+ $w = round($h * $ratio);
}
+ }
- $thumb = imagecreatetruecolor($w, $h);
- switch ($ext) {
- case 'jpg':
- $image = ImageCreateFromJpeg($source);
+ // Resized image.
+ $thumb = imagecreatetruecolor($w, $h);
+
+ $image = false;
+ switch ($ext) {
+ case 'jpg':
+ $image = imagecreatefromjpeg($source);
+ // Crop
+ if ($thumb_cropped !== false) {
+ // First, crop.
+ imagecopyresampled($thumb_cropped, $image, 0, 0, $crop_x, $crop_y, $cur_width, $cur_height, $cur_width, $cur_height);
+ // Then, resize.
+ imagecopyresampled($thumb, $thumb_cropped, 0, 0, 0, 0, $w, $h, $crop_width, $crop_height);
+ // Resize
+ } else {
imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
- imagejpeg($thumb, $dest);
- break;
- case 'png':
- $image = ImageCreateFromPng($source);
- // Turn off alpha blending and set alpha flag. That prevent alpha
- // transparency to be saved as an arbitrary color (black in my tests)
- imagealphablending($thumb, false);
- imagealphablending($image, false);
- imagesavealpha($thumb, true);
- imagesavealpha($image, true);
+ }
+ imagejpeg($thumb, $dest);
+ break;
+ case 'png':
+ $image = imagecreatefrompng($source);
+ // Turn off alpha blending and set alpha flag. That prevent alpha
+ // transparency to be saved as an arbitrary color (black in my tests)
+ imagealphablending($image, false);
+ imagesavealpha($image, true);
+ imagealphablending($thumb, false);
+ imagesavealpha($thumb, true);
+ // Crop
+ if ($thumb_cropped !== false) {
+ imagealphablending($thumb_cropped, false);
+ imagesavealpha($thumb_cropped, true);
+ // First, crop.
+ imagecopyresampled($thumb_cropped, $image, 0, 0, $crop_x, $crop_y, $cur_width, $cur_height, $cur_width, $cur_height);
+ // Then, resize.
+ imagecopyresampled($thumb, $thumb_cropped, 0, 0, 0, 0, $w, $h, $crop_width, $crop_height);
+ // Resize
+ } else {
imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
- imagepng($thumb, $dest);
- break;
- case 'gif':
- $image = ImageCreateFromGif($source);
+ }
+ imagepng($thumb, $dest);
+ break;
+ case 'gif':
+ $image = imagecreatefromgif($source);
+ // Crop
+ if ($thumb_cropped !== false) {
+ // First, crop.
+ imagecopyresampled($thumb_cropped, $image, 0, 0, $crop_x, $crop_y, $cur_width, $cur_height, $cur_width, $cur_height);
+ // Then, resize.
+ imagecopyresampled($thumb, $thumb_cropped, 0, 0, 0, 0, $w, $h, $crop_width, $crop_height);
+ // Resize
+ } else {
imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
- imagegif($thumb, $dest);
- break;
- }
- } else {
- Analog::log(
- '[' . $class . '] GD is not present - ' .
- 'pictures could not be resized!',
- Analog::ERROR
- );
+ }
+ imagegif($thumb, $dest);
+ break;
+ case 'webp':
+ $image = imagecreatefromwebp($source);
+ // Crop
+ if ($thumb_cropped !== false) {
+ // First, crop.
+ imagecopyresampled($thumb_cropped, $image, 0, 0, $crop_x, $crop_y, $cur_width, $cur_height, $cur_width, $cur_height);
+ // Then, resize.
+ imagecopyresampled($thumb, $thumb_cropped, 0, 0, 0, 0, $w, $h, $crop_width, $crop_height);
+ // Resize
+ } else {
+ imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
+ }
+ imagewebp($thumb, $dest);
+ break;
}
+
+ return true;
}
/**