]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Core/Picture.php
e19bf67064ff2a577c22a96297f19b378e5fad90
[galette.git] / galette / lib / Galette / Core / Picture.php
1 <?php
2
3 /**
4 * Copyright © 2003-2024 The Galette Team
5 *
6 * This file is part of Galette (https://galette.eu).
7 *
8 * Galette is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * Galette is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
20 */
21
22 namespace Galette\Core;
23
24 use ArrayObject;
25 use Laminas\Db\Adapter\Driver\StatementInterface;
26 use Laminas\Db\Sql\Select;
27 use Slim\Psr7\Response;
28 use Throwable;
29 use Analog\Analog;
30 use Galette\Entity\Adherent;
31 use Galette\Repository\Members;
32 use Galette\IO\FileInterface;
33 use Galette\IO\FileTrait;
34
35 /**
36 * Picture handling
37 *
38 * @author Frédéric Jacquot <gna@logeek.com>
39 * @author Johan Cwiklinski <johan@x-tnd.be>
40 */
41 class Picture implements FileInterface
42 {
43 use FileTrait {
44 writeOnDisk as protected trait_writeOnDisk;
45 store as protected trait_store;
46 getMimeType as protected trait_getMimeType;
47 }
48
49 //constants that will not be overridden
50 public const SQL_ERROR = -10;
51 public const SQL_BLOB_ERROR = -11;
52 //constants that can be overridden
53 //(do not use self::CONSTANT, but get_class[$this]::CONSTANT)
54 public const TABLE = 'pictures';
55 public const PK = Adherent::PK;
56
57 protected string $tbl_prefix = '';
58
59 protected string|int $id;
60 protected int $db_id;
61 protected int $height;
62 protected int $width;
63 protected int $optimal_height;
64 protected int $optimal_width;
65 protected string $file_path;
66 protected string $format;
67 protected string $mime;
68 protected bool $has_picture = false;
69 protected string $store_path = GALETTE_PHOTOS_PATH;
70 protected int $max_width = 200;
71 protected int $max_height = 200;
72 private StatementInterface $insert_stmt;
73 /** @var ?array<string, mixed> */
74 private ?array $cropping;
75
76 /**
77 * Default constructor.
78 *
79 * @param mixed|null $id_adh the id of the member
80 */
81 public function __construct($id_adh = null)
82 {
83 $this->init(
84 null,
85 array('jpeg', 'jpg', 'png', 'gif', 'webp'),
86 array(
87 'jpg' => 'image/jpeg',
88 'png' => 'image/png',
89 'gif' => 'image/gif',
90 'webp' => 'image/webp'
91 )
92 );
93
94 // '!==' needed, otherwise ''==0
95 if ($id_adh !== '' && $id_adh !== null) {
96 $this->id = $id_adh;
97 if (!isset($this->db_id)) {
98 $this->db_id = $id_adh;
99 }
100
101 //if file does not exist on the FileSystem, check for it in the database
102 if (!$this->checkFileOnFS()) {
103 if ($this->checkFileInDB()) {
104 $this->has_picture = true;
105 }
106 } else {
107 $this->has_picture = true;
108 }
109 }
110
111 // if we still have no picture, take the default one
112 if (empty($this->file_path)) {
113 $this->getDefaultPicture();
114 }
115
116 //we should not have an empty file_path, but...
117 if (!empty($this->file_path)) {
118 $this->setSizes();
119 }
120 }
121
122 /**
123 * "Magic" function called on unserialize
124 *
125 * @return void
126 */
127 public function __wakeup(): void
128 {
129 //if file has been deleted since we store our object in the session,
130 //we try to retrieve it
131 if (isset($this->id) && !$this->checkFileOnFS()) {
132 //if file does not exist on the FileSystem,
133 //check for it in the database
134 //$this->checkFileInDB();
135 } else {
136 $this->has_picture = false;
137 }
138
139 // if we still have no picture, take the default one
140 if (empty($this->file_path)) {
141 $this->getDefaultPicture();
142 }
143
144 //we should not have an empty file_path, but...
145 if (!empty($this->file_path)) {
146 $this->setSizes();
147 }
148 }
149
150 /**
151 * Check if current file is present on the File System
152 *
153 * @return boolean true if file is present on FS, false otherwise
154 */
155 private function checkFileOnFS(): bool
156 {
157 $file_wo_ext = $this->store_path . $this->id;
158 if (file_exists($file_wo_ext . '.jpg')) {
159 $this->file_path = realpath($file_wo_ext . '.jpg');
160 $this->format = 'jpg';
161 $this->mime = 'image/jpeg';
162 return true;
163 } elseif (file_exists($file_wo_ext . '.png')) {
164 $this->file_path = realpath($file_wo_ext . '.png');
165 $this->format = 'png';
166 $this->mime = 'image/png';
167 return true;
168 } elseif (file_exists($file_wo_ext . '.gif')) {
169 $this->file_path = realpath($file_wo_ext . '.gif');
170 $this->format = 'gif';
171 $this->mime = 'image/gif';
172 return true;
173 } elseif (file_exists($file_wo_ext . '.webp')) {
174 $this->file_path = realpath($file_wo_ext . '.webp');
175 $this->format = 'webp';
176 $this->mime = 'image/webp';
177 return true;
178 }
179 return false;
180 }
181
182 /**
183 * Check if current file is present in the database,
184 * and copy it to the File System
185 *
186 * @return boolean true if file is present in the DB, false otherwise
187 */
188 private function checkFileInDB(): bool
189 {
190 global $zdb;
191
192 try {
193 $select = $this->getCheckFileQuery();
194 $results = $zdb->execute($select);
195 $pic = $results->current();
196
197 if ($pic) {
198 // we must regenerate the picture file
199 $file_wo_ext = $this->store_path . $this->id;
200 file_put_contents(
201 $file_wo_ext . '.' . $pic->format,
202 $pic->picture
203 );
204
205 $this->format = $pic->format;
206 switch ($this->format) {
207 case 'jpg':
208 $this->mime = 'image/jpeg';
209 break;
210 case 'png':
211 $this->mime = 'image/png';
212 break;
213 case 'gif':
214 $this->mime = 'image/gif';
215 break;
216 case 'webp':
217 $this->mime = 'image/webp';
218 break;
219 }
220 $this->file_path = realpath($file_wo_ext . '.' . $this->format);
221 return true;
222 }
223 } catch (Throwable $e) {
224 return false;
225 }
226 return false;
227 }
228
229 /**
230 * Returns the relevant query to check if picture exists in database.
231 *
232 * @return Select SELECT query
233 */
234 protected function getCheckFileQuery(): Select
235 {
236 global $zdb;
237 $class = get_class($this);
238
239 $select = $zdb->select($this->tbl_prefix . $class::TABLE);
240 $select->columns(
241 array(
242 'picture',
243 'format'
244 )
245 );
246 $select->where(array($class::PK => $this->db_id));
247 return $select;
248 }
249
250 /**
251 * Gets the default picture to show, anyway
252 *
253 * @return void
254 */
255 protected function getDefaultPicture(): void
256 {
257 $this->file_path = realpath(_CURRENT_THEME_PATH . 'images/default.png');
258 $this->format = 'png';
259 $this->mime = 'image/png';
260 $this->has_picture = false;
261 }
262
263 /**
264 * Set picture sizes
265 *
266 * @return void
267 */
268 private function setSizes(): void
269 {
270 list($width, $height) = getimagesize($this->file_path);
271 $this->height = $height;
272 $this->width = $width;
273 $this->optimal_height = $height;
274 $this->optimal_width = $width;
275
276 if ($this->height > $this->width) {
277 if ($this->height > $this->max_height) {
278 $ratio = $this->max_height / $this->height;
279 $this->optimal_height = $this->max_height;
280 $this->optimal_width = (int)($this->width * $ratio);
281 }
282 } else {
283 if ($this->width > $this->max_width) {
284 $ratio = $this->max_width / $this->width;
285 $this->optimal_width = $this->max_width;
286 $this->optimal_height = (int)($this->height * $ratio);
287 }
288 }
289 }
290
291 /**
292 * Get image file content
293 *
294 * @return mixed
295 */
296 public function getContents()
297 {
298 readfile($this->file_path);
299 }
300
301 /**
302 * Set header and displays the picture.
303 *
304 * @param Response $response Response
305 *
306 * @return Response the binary file
307 */
308 public function display(Response $response): Response
309 {
310 $response = $response->withHeader('Content-Type', $this->mime)
311 ->withHeader('Content-Transfer-Encoding', 'binary')
312 ->withHeader('Expires', '0')
313 ->withHeader('Cache-Control', 'must-revalidate')
314 ->withHeader('Pragma', 'public');
315
316 $stream = fopen('php://memory', 'r+');
317 fwrite($stream, file_get_contents($this->file_path));
318 rewind($stream);
319
320 return $response->withBody(new \Slim\Psr7\Stream($stream));
321 }
322
323 /**
324 * Deletes a picture, from both database and filesystem
325 *
326 * @param boolean $transaction Whether to use a transaction here or not
327 *
328 * @return boolean true if image was successfully deleted, false otherwise
329 */
330 public function delete(bool $transaction = true): bool
331 {
332 global $zdb;
333 $class = get_class($this);
334
335 try {
336 if ($transaction === true) {
337 $zdb->connection->beginTransaction();
338 }
339
340 $delete = $zdb->delete($this->tbl_prefix . $class::TABLE);
341 $delete->where([$class::PK => $this->db_id]);
342 $del = $zdb->execute($delete);
343
344 if (!$del->count() > 0) {
345 Analog::log(
346 'Unable to remove picture database entry for ' . $this->db_id,
347 Analog::ERROR
348 );
349 //it may be possible image is missing in the database.
350 //let's try to remove file anyway.
351 }
352
353 $file_wo_ext = $this->store_path . $this->id;
354
355 // take back default picture
356 $this->getDefaultPicture();
357 // fix sizes
358 $this->setSizes();
359
360 $success = false;
361 $_file = null;
362 if (file_exists($file_wo_ext . '.jpg')) {
363 //return unlink($file_wo_ext . '.jpg');
364 $_file = $file_wo_ext . '.jpg';
365 $success = unlink($_file);
366 } elseif (file_exists($file_wo_ext . '.png')) {
367 //return unlink($file_wo_ext . '.png');
368 $_file = $file_wo_ext . '.png';
369 $success = unlink($_file);
370 } elseif (file_exists($file_wo_ext . '.gif')) {
371 //return unlink($file_wo_ext . '.gif');
372 $_file = $file_wo_ext . '.gif';
373 $success = unlink($_file);
374 } elseif (file_exists($file_wo_ext . '.webp')) {
375 //return unlink($file_wo_ext . '.webp');
376 $_file = $file_wo_ext . '.webp';
377 $success = unlink($_file);
378 }
379
380 if ($_file !== null && $success !== true) {
381 //unable to remove file that exists!
382 if ($transaction === true) {
383 $zdb->connection->rollBack();
384 }
385 Analog::log(
386 'The file ' . $_file .
387 ' was found on the disk but cannot be removed.',
388 Analog::ERROR
389 );
390 return false;
391 } else {
392 if ($transaction === true) {
393 $zdb->connection->commit();
394 }
395 $this->has_picture = false;
396 return true;
397 }
398 } catch (Throwable $e) {
399 if ($transaction === true) {
400 $zdb->connection->rollBack();
401 }
402 Analog::log(
403 'An error occurred attempting to delete picture ' . $this->db_id .
404 'from database | ' . $e->getMessage(),
405 Analog::ERROR
406 );
407 return false;
408 }
409 }
410
411 /**
412 * Stores an image on the disk and in the database
413 *
414 * @param array<string, mixed> $file The uploaded file
415 * @param boolean $ajax If the image comes from an ajax call (dnd)
416 * @param ?array<string, mixed> $cropping Cropping properties
417 *
418 * @return bool|int
419 */
420 public function store(array $file, bool $ajax = false, array $cropping = null): bool|int
421 {
422 $this->cropping = $cropping;
423 return $this->trait_store($file, $ajax);
424 }
425
426 /**
427 * Build destination path
428 *
429 * @return string
430 */
431 protected function buildDestPath(): string
432 {
433 return $this->dest_dir . $this->id . '.' . $this->extension;
434 }
435
436 /**
437 * Get file mime type
438 *
439 * @param string $file File
440 *
441 * @return string
442 */
443 public static function getMimeType(string $file): string
444 {
445 $info = getimagesize($file);
446 if ($info !== false) {
447 return $info['mime'];
448 }
449
450 //fallback if file is not an image
451 return static::trait_getMimeType($file);
452 }
453
454 /**
455 * Write file on disk
456 *
457 * @param string $tmpfile Temporary file
458 * @param bool $ajax If the file comes from an ajax call (dnd)
459 *
460 * @return bool|int
461 */
462 public function writeOnDisk(string $tmpfile, bool $ajax): bool|int
463 {
464 global $zdb;
465
466 $this->setDestDir($this->store_path);
467 $current = getimagesize($tmpfile);
468
469 // Source image must have minimum dimensions to match the cropping process requirements
470 // and ensure the final picture will fit the maximum allowed resizing dimensions.
471 if (isset($this->cropping['ratio']) && isset($this->cropping['focus'])) {
472 if ($current[0] < $this->mincropsize || $current[1] < $this->mincropsize) {
473 $min_current = min($current[0], $current[1]);
474 Analog::log(
475 '[' . get_class($this) . '] Image is too small. The minimum image side size allowed is ' .
476 $this->mincropsize . 'px, but current is ' . $min_current . 'px.',
477 Analog::ERROR
478 );
479 return self::IMAGE_TOO_SMALL;
480 } else {
481 Analog::log('[' . get_class($this) . '] Image dimensions are OK, proceed', Analog::DEBUG);
482 }
483 }
484 $this->delete();
485
486 $result = $this->trait_writeOnDisk($tmpfile, $ajax);
487 if ($result !== true) {
488 return $result;
489 }
490
491 // current[0] gives width ; current[1] gives height
492 if ($current[0] > $this->max_width || $current[1] > $this->max_height) {
493 /** FIXME: what if image cannot be resized?
494 Should'nt we want to stop the process here? */
495 $this->resizeImage($this->buildDestPath(), $this->extension, null, $this->cropping);
496 }
497
498 return $this->storeInDb($zdb, $this->db_id, $this->buildDestPath(), $this->extension);
499 }
500
501 /**
502 * Stores an image in the database
503 *
504 * @param Db $zdb Database instance
505 * @param int $id Member ID
506 * @param string $file File path on disk
507 * @param string $ext File extension
508 *
509 * @return bool|int
510 */
511 private function storeInDb(Db $zdb, int $id, string $file, string $ext): bool|int
512 {
513 $f = fopen($file, 'r');
514 $picture = '';
515 while ($r = fread($f, 8192)) {
516 $picture .= $r;
517 }
518 fclose($f);
519
520 $class = get_class($this);
521
522 try {
523 $zdb->connection->beginTransaction();
524
525 if (isset($this->insert_stmt)) {
526 $stmt = $this->insert_stmt;
527 } else {
528 $insert = $zdb->insert($this->tbl_prefix . $class::TABLE);
529 $insert->values(
530 array(
531 $class::PK => ':' . $class::PK,
532 'picture' => ':picture',
533 'format' => ':format'
534 )
535 );
536 $stmt = $zdb->sql->prepareStatementForSqlObject($insert);
537 $container = $stmt->getParameterContainer();
538 $container->offsetSet(
539 'picture', //'picture',
540 ':picture',
541 $container::TYPE_LOB
542 );
543 $stmt->setParameterContainer($container);
544 $this->insert_stmt = $stmt;
545 }
546
547 $stmt->execute(
548 array(
549 $class::PK => $id,
550 'picture' => $picture,
551 'format' => $ext
552 )
553 );
554 $zdb->connection->commit();
555 $this->has_picture = true;
556 } catch (Throwable $e) {
557 $zdb->connection->rollBack();
558 Analog::log(
559 'An error occurred storing picture in database: ' .
560 $e->getMessage(),
561 Analog::ERROR
562 );
563 return self::SQL_ERROR;
564 }
565
566 return true;
567 }
568
569 /**
570 * Check for missing images in database
571 *
572 * @param Db $zdb Database instance
573 *
574 * @return void
575 */
576 public function missingInDb(Db $zdb): void
577 {
578 $existing_disk = array();
579
580 //retrieve files on disk
581 if ($handle = opendir($this->store_path)) {
582 while (false !== ($entry = readdir($handle))) {
583 $reg = "/^(\d+)\.(" .
584 implode('|', $this->allowed_extensions) . ")$/i";
585 if (preg_match($reg, $entry, $matches)) {
586 $id = $matches[1];
587 $extension = strtolower($matches[2]);
588 if ($extension == 'jpeg') {
589 //jpeg is an allowed extension,
590 //but we change it to jpg to reduce further tests :)
591 $extension = 'jpg';
592 }
593 $existing_disk[$id] = array(
594 'name' => $entry,
595 'id' => $id,
596 'ext' => $extension
597 );
598 }
599 }
600 closedir($handle);
601
602 if (count($existing_disk) === 0) {
603 //no image on disk, nothing to do :)
604 return;
605 }
606
607 //retrieve files in database
608 $class = get_class($this);
609 $select = $zdb->select($this->tbl_prefix . $class::TABLE);
610 $select
611 ->columns(array($class::PK))
612 ->where->in($class::PK, array_keys($existing_disk));
613
614 $results = $zdb->execute($select);
615
616 $existing_db = array();
617 foreach ($results as $result) {
618 $existing_db[] = (int)$result[self::PK];
619 }
620
621 $existing_diff = array_diff(array_keys($existing_disk), $existing_db);
622
623 //retrieve valid members ids
624 $members = new Members();
625 $valids = $members->getArrayList(
626 array_map('intval', $existing_diff),
627 null,
628 false,
629 false,
630 array(self::PK)
631 );
632
633 foreach ($valids as $valid) {
634 /** @var ArrayObject<string,mixed> $valid */
635 $file = $existing_disk[$valid->id_adh];
636 $this->storeInDb(
637 $zdb,
638 (int)$file['id'],
639 $this->store_path . $file['id'] . '.' . $file['ext'],
640 $file['ext']
641 );
642 }
643 } else {
644 Analog::log(
645 'Something went wrong opening images directory ' .
646 $this->store_path,
647 Analog::ERROR
648 );
649 }
650 }
651
652 /**
653 * Resize and eventually crop the image if it exceeds max allowed sizes
654 *
655 * @param string $source The source image
656 * @param string $ext File's extension
657 * @param ?string $dest The destination image.
658 * If null, we'll use the source image. Defaults to null
659 * @param ?array<string, mixed> $cropping Cropping properties
660 *
661 * @return boolean
662 */
663 private function resizeImage(string $source, string $ext, string $dest = null, array $cropping = null): bool
664 {
665 $class = get_class($this);
666
667 if (!function_exists("gd_info")) {
668 Analog::log(
669 '[' . $class . '] GD is not present - ' .
670 'pictures could not be resized!',
671 Analog::ERROR
672 );
673 return false;
674 }
675
676 $gdinfo = gd_info();
677 $h = $this->max_height;
678 $w = $this->max_width;
679 if ($dest == null) {
680 $dest = $source;
681 }
682
683 switch (strtolower($ext)) {
684 case 'jpg':
685 if (!$gdinfo['JPEG Support']) {
686 Analog::log(
687 '[' . $class . '] GD has no JPEG Support - ' .
688 'pictures could not be resized!',
689 Analog::ERROR
690 );
691 return false;
692 }
693 break;
694 case 'png':
695 if (!$gdinfo['PNG Support']) {
696 Analog::log(
697 '[' . $class . '] GD has no PNG Support - ' .
698 'pictures could not be resized!',
699 Analog::ERROR
700 );
701 return false;
702 }
703 break;
704 case 'gif':
705 if (!$gdinfo['GIF Create Support']) {
706 Analog::log(
707 '[' . $class . '] GD has no GIF Support - ' .
708 'pictures could not be resized!',
709 Analog::ERROR
710 );
711 return false;
712 }
713 break;
714 case 'webp':
715 if (!$gdinfo['WebP Support']) {
716 Analog::log(
717 '[' . $class . '] GD has no WebP Support - ' .
718 'pictures could not be resized!',
719 Analog::ERROR
720 );
721 return false;
722 }
723 break;
724
725 default:
726 return false;
727 }
728
729 list($cur_width, $cur_height, $cur_type, $curattr)
730 = getimagesize($source);
731
732 $ratio = $cur_width / $cur_height;
733
734 // Define cropping variables if necessary.
735 $thumb_cropped = false;
736 // Cropping is based on the smallest side of the source in order to
737 // provide as less focusing options as possible if the source doesn't
738 // fit the final ratio (center, top, bottom, left, right).
739 $min_size = min($cur_width, $cur_height);
740 // Cropping dimensions.
741 $crop_width = $min_size;
742 $crop_height = $min_size;
743 // Cropping focus.
744 $crop_x = 0;
745 $crop_y = 0;
746 if (isset($cropping['ratio']) && isset($cropping['focus'])) {
747 // Calculate cropping dimensions
748 switch ($cropping['ratio']) {
749 case 'portrait_ratio':
750 // Calculate cropping dimensions
751 if ($ratio < 1) {
752 $crop_height = ceil($crop_width * 4 / 3);
753 } else {
754 $crop_width = ceil($crop_height * 3 / 4);
755 }
756 // Calculate resizing dimensions
757 $w = ceil($h * 3 / 4);
758 break;
759 case 'landscape_ratio':
760 // Calculate cropping dimensions
761 if ($ratio > 1) {
762 $crop_width = ceil($crop_height * 4 / 3);
763 } else {
764 $crop_height = ceil($crop_width * 3 / 4);
765 }
766 // Calculate resizing dimensions
767 $h = ceil($w * 3 / 4);
768 break;
769 }
770 // Calculate focus coordinates
771 switch ($cropping['focus']) {
772 case 'center':
773 if ($ratio > 1) {
774 $crop_x = ceil(($cur_width - $crop_width) / 2);
775 } elseif ($ratio == 1) {
776 $crop_x = ceil(($cur_width - $crop_width) / 2);
777 $crop_y = ceil(($cur_height - $crop_height) / 2);
778 } else {
779 $crop_y = ceil(($cur_height - $crop_height) / 2);
780 }
781 break;
782 case 'top':
783 $crop_x = ceil(($cur_width - $crop_width) / 2);
784 break;
785 case 'bottom':
786 $crop_y = $cur_height - $crop_height;
787 break;
788 case 'right':
789 $crop_x = $cur_width - $crop_width;
790 break;
791 }
792 // Cropped image.
793 $thumb_cropped = imagecreatetruecolor($crop_width, $crop_height);
794 // Cropped ratio.
795 $ratio = $crop_width / $crop_height;
796 // Otherwise, calculate image size according to the source's ratio.
797 } else {
798 if ($cur_width > $cur_height) {
799 $h = round($w / $ratio);
800 } else {
801 $w = round($h * $ratio);
802 }
803 }
804
805 // Resized image.
806 $thumb = imagecreatetruecolor($w, $h);
807
808 $image = false;
809 switch ($ext) {
810 case 'jpg':
811 $image = imagecreatefromjpeg($source);
812 // Crop
813 if ($thumb_cropped !== false) {
814 // First, crop.
815 imagecopyresampled($thumb_cropped, $image, 0, 0, $crop_x, $crop_y, $cur_width, $cur_height, $cur_width, $cur_height);
816 // Then, resize.
817 imagecopyresampled($thumb, $thumb_cropped, 0, 0, 0, 0, $w, $h, $crop_width, $crop_height);
818 // Resize
819 } else {
820 imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
821 }
822 imagejpeg($thumb, $dest);
823 break;
824 case 'png':
825 $image = imagecreatefrompng($source);
826 // Turn off alpha blending and set alpha flag. That prevent alpha
827 // transparency to be saved as an arbitrary color (black in my tests)
828 imagealphablending($image, false);
829 imagesavealpha($image, true);
830 imagealphablending($thumb, false);
831 imagesavealpha($thumb, true);
832 // Crop
833 if ($thumb_cropped !== false) {
834 imagealphablending($thumb_cropped, false);
835 imagesavealpha($thumb_cropped, true);
836 // First, crop.
837 imagecopyresampled($thumb_cropped, $image, 0, 0, $crop_x, $crop_y, $cur_width, $cur_height, $cur_width, $cur_height);
838 // Then, resize.
839 imagecopyresampled($thumb, $thumb_cropped, 0, 0, 0, 0, $w, $h, $crop_width, $crop_height);
840 // Resize
841 } else {
842 imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
843 }
844 imagepng($thumb, $dest);
845 break;
846 case 'gif':
847 $image = imagecreatefromgif($source);
848 // Crop
849 if ($thumb_cropped !== false) {
850 // First, crop.
851 imagecopyresampled($thumb_cropped, $image, 0, 0, $crop_x, $crop_y, $cur_width, $cur_height, $cur_width, $cur_height);
852 // Then, resize.
853 imagecopyresampled($thumb, $thumb_cropped, 0, 0, 0, 0, $w, $h, $crop_width, $crop_height);
854 // Resize
855 } else {
856 imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
857 }
858 imagegif($thumb, $dest);
859 break;
860 case 'webp':
861 $image = imagecreatefromwebp($source);
862 // Crop
863 if ($thumb_cropped !== false) {
864 // First, crop.
865 imagecopyresampled($thumb_cropped, $image, 0, 0, $crop_x, $crop_y, $cur_width, $cur_height, $cur_width, $cur_height);
866 // Then, resize.
867 imagecopyresampled($thumb, $thumb_cropped, 0, 0, 0, 0, $w, $h, $crop_width, $crop_height);
868 // Resize
869 } else {
870 imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
871 }
872 imagewebp($thumb, $dest);
873 break;
874 }
875
876 return true;
877 }
878
879 /**
880 * Returns current file optimal height (resized)
881 *
882 * @return int optimal height
883 */
884 public function getOptimalHeight(): int
885 {
886 return (int)round($this->optimal_height, 1);
887 }
888
889 /**
890 * Returns current file height
891 *
892 * @return int current height
893 */
894 public function getHeight(): int
895 {
896 return $this->height;
897 }
898
899 /**
900 * Returns current file optimal width (resized)
901 *
902 * @return int optimal width
903 */
904 public function getOptimalWidth(): int
905 {
906 return (int)round($this->optimal_width, 1);
907 }
908
909 /**
910 * Returns current file width
911 *
912 * @return int current width
913 */
914 public function getWidth(): int
915 {
916 return $this->width;
917 }
918
919 /**
920 * Returns current file format
921 *
922 * @return string
923 */
924 public function getFormat(): string
925 {
926 return $this->format;
927 }
928
929 /**
930 * Have we got a picture ?
931 *
932 * @return bool True if a picture matches adherent's id, false otherwise
933 */
934 public function hasPicture(): bool
935 {
936 return $this->has_picture;
937 }
938
939 /**
940 * Returns current file full path
941 *
942 * @return string full file path
943 */
944 public function getPath(): string
945 {
946 return $this->file_path;
947 }
948
949 /**
950 * Returns current mime type
951 *
952 * @return string
953 */
954 public function getMime(): string
955 {
956 return $this->mime;
957 }
958
959 /**
960 * Return textual error message
961 *
962 * @param int $code The error code
963 *
964 * @return string Localized message
965 */
966 public function getErrorMessage($code): string
967 {
968 $error = null;
969 switch ($code) {
970 case self::SQL_ERROR:
971 case self::SQL_BLOB_ERROR:
972 $error = _T("An SQL error has occurred.");
973 break;
974 }
975
976 if ($error === null) {
977 $error = $this->getErrorMessageFromCode($code);
978 }
979
980 return $error;
981 }
982 }