]> git.agnieray.net Git - galette.git/blobdiff - galette/lib/Galette/Core/Picture.php
Fix redirection when dynamic file does not exists
[galette.git] / galette / lib / Galette / Core / Picture.php
index 484955d67b64ab938d359cff92ccf1fdc9c81fb3..8390a21c481fa709f64f50e6e787170031d85748 100644 (file)
@@ -7,7 +7,7 @@
  *
  * 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).
  *
  *
  * @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
  */
 
 namespace Galette\Core;
 
+use ArrayObject;
+use Slim\Psr7\Response;
+use Throwable;
 use Analog\Analog;
 use Galette\Entity\Adherent;
 use Galette\Repository\Members;
@@ -50,7 +53,7 @@ use Galette\IO\FileTrait;
  * @package   Galette
  * @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
  */
@@ -69,6 +72,7 @@ class Picture implements FileInterface
     protected $tbl_prefix = '';
 
     protected $id;
+    protected $db_id;
     protected $height;
     protected $width;
     protected $optimal_height;
@@ -85,18 +89,19 @@ class Picture implements FileInterface
     /**
      * 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'
             )
         );
 
@@ -107,7 +112,7 @@ class Picture implements FileInterface
                 $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;
@@ -138,7 +143,7 @@ class Picture implements FileInterface
         //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 {
@@ -179,6 +184,11 @@ class Picture implements FileInterface
             $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;
     }
@@ -217,13 +227,17 @@ class Picture implements FileInterface
                     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;
     }
 
     /**
@@ -289,7 +303,7 @@ class Picture implements FileInterface
     }
 
     /**
-     * Get image file contents
+     * Get image file content
      *
      * @return mixed
      */
@@ -301,14 +315,23 @@ class Picture implements FileInterface
     /**
      * 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();
-        $this->getContents();
+        $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));
     }
 
     /**
@@ -329,9 +352,7 @@ class Picture implements FileInterface
             }
 
             $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) {
@@ -364,6 +385,10 @@ class Picture implements FileInterface
                 //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) {
@@ -384,7 +409,7 @@ class Picture implements FileInterface
                 $this->has_picture = false;
                 return true;
             }
-        } catch (\Exception $e) {
+        } catch (Throwable $e) {
             if ($transaction === true) {
                 $zdb->connection->rollBack();
             }
@@ -400,12 +425,13 @@ class Picture implements FileInterface
     /**
      * 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;
@@ -483,6 +509,22 @@ class Picture implements FileInterface
             );
         }
 
+        // 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 .
@@ -497,7 +539,7 @@ class Picture implements FileInterface
         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);
@@ -511,7 +553,7 @@ class Picture implements FileInterface
      * @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)
     {
@@ -531,7 +573,7 @@ class Picture implements FileInterface
                 $insert = $zdb->insert($this->tbl_prefix . $class::TABLE);
                 $insert->values(
                     array(
-                        $class::PK  => ':id',
+                        $class::PK  => ':' . $class::PK,
                         'picture'   => ':picture',
                         'format'    => ':format'
                     )
@@ -539,18 +581,10 @@ class Picture implements FileInterface
                 $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;
             }
@@ -564,7 +598,7 @@ class Picture implements FileInterface
             );
             $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: ' .
@@ -642,6 +676,7 @@ class Picture implements FileInterface
             );
 
             foreach ($valids as $valid) {
+                /** @var ArrayObject $valid */
                 $file = $existing_disk[$valid->id_adh];
                 $this->storeInDb(
                     $zdb,
@@ -660,105 +695,230 @@ class Picture implements FileInterface
     }
 
     /**
-     * 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;
+        }
+
+        $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;
+        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
+            // 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 = $w / $ratio;
+                $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;
     }
 
     /**