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