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