1<?php 2/** 3 * @copyright Copyright (c) 2016, ownCloud, Inc. 4 * 5 * @author Bartek Przybylski <bart.p.pl@gmail.com> 6 * @author Bart Visscher <bartv@thisnet.nl> 7 * @author Björn Schießle <bjoern@schiessle.org> 8 * @author Byron Marohn <combustible@live.com> 9 * @author Christopher Schäpers <kondou@ts.unde.re> 10 * @author Christoph Wurst <christoph@winzerhof-wurst.at> 11 * @author Georg Ehrke <oc.list@georgehrke.com> 12 * @author J0WI <J0WI@users.noreply.github.com> 13 * @author j-ed <juergen@eisfair.org> 14 * @author Joas Schilling <coding@schilljs.com> 15 * @author Johannes Willnecker <johannes@willnecker.com> 16 * @author Jörn Friedrich Dreyer <jfd@butonic.de> 17 * @author Julius Härtl <jus@bitgrid.net> 18 * @author Lukas Reschke <lukas@statuscode.ch> 19 * @author Morris Jobke <hey@morrisjobke.de> 20 * @author Olivier Paroz <github@oparoz.com> 21 * @author Robin Appelman <robin@icewind.nl> 22 * @author Roeland Jago Douma <roeland@famdouma.nl> 23 * @author Samuel CHEMLA <chemla.samuel@gmail.com> 24 * @author Thomas Müller <thomas.mueller@tmit.eu> 25 * @author Thomas Tanghus <thomas@tanghus.net> 26 * 27 * @license AGPL-3.0 28 * 29 * This code is free software: you can redistribute it and/or modify 30 * it under the terms of the GNU Affero General Public License, version 3, 31 * as published by the Free Software Foundation. 32 * 33 * This program is distributed in the hope that it will be useful, 34 * but WITHOUT ANY WARRANTY; without even the implied warranty of 35 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 36 * GNU Affero General Public License for more details. 37 * 38 * You should have received a copy of the GNU Affero General Public License, version 3, 39 * along with this program. If not, see <http://www.gnu.org/licenses/> 40 * 41 */ 42use OCP\IImage; 43 44/** 45 * Class for basic image manipulation 46 */ 47class OC_Image implements \OCP\IImage { 48 /** @var false|resource */ 49 protected $resource = false; // tmp resource. 50 /** @var int */ 51 protected $imageType = IMAGETYPE_PNG; // Default to png if file type isn't evident. 52 /** @var string */ 53 protected $mimeType = 'image/png'; // Default to png 54 /** @var int */ 55 protected $bitDepth = 24; 56 /** @var null|string */ 57 protected $filePath = null; 58 /** @var finfo */ 59 private $fileInfo; 60 /** @var \OCP\ILogger */ 61 private $logger; 62 /** @var \OCP\IConfig */ 63 private $config; 64 /** @var array */ 65 private $exif; 66 67 /** 68 * Constructor. 69 * 70 * @param resource|string $imageRef The path to a local file, a base64 encoded string or a resource created by 71 * an imagecreate* function. 72 * @param \OCP\ILogger $logger 73 * @param \OCP\IConfig $config 74 * @throws \InvalidArgumentException in case the $imageRef parameter is not null 75 */ 76 public function __construct($imageRef = null, \OCP\ILogger $logger = null, \OCP\IConfig $config = null) { 77 $this->logger = $logger; 78 if ($logger === null) { 79 $this->logger = \OC::$server->getLogger(); 80 } 81 $this->config = $config; 82 if ($config === null) { 83 $this->config = \OC::$server->getConfig(); 84 } 85 86 if (\OC_Util::fileInfoLoaded()) { 87 $this->fileInfo = new finfo(FILEINFO_MIME_TYPE); 88 } 89 90 if ($imageRef !== null) { 91 throw new \InvalidArgumentException('The first parameter in the constructor is not supported anymore. Please use any of the load* methods of the image object to load an image.'); 92 } 93 } 94 95 /** 96 * Determine whether the object contains an image resource. 97 * 98 * @return bool 99 */ 100 public function valid() { // apparently you can't name a method 'empty'... 101 if (is_resource($this->resource)) { 102 return true; 103 } 104 if (is_object($this->resource) && get_class($this->resource) === \GdImage::class) { 105 return true; 106 } 107 108 return false; 109 } 110 111 /** 112 * Returns the MIME type of the image or an empty string if no image is loaded. 113 * 114 * @return string 115 */ 116 public function mimeType() { 117 return $this->valid() ? $this->mimeType : ''; 118 } 119 120 /** 121 * Returns the width of the image or -1 if no image is loaded. 122 * 123 * @return int 124 */ 125 public function width() { 126 return $this->valid() ? imagesx($this->resource) : -1; 127 } 128 129 /** 130 * Returns the height of the image or -1 if no image is loaded. 131 * 132 * @return int 133 */ 134 public function height() { 135 return $this->valid() ? imagesy($this->resource) : -1; 136 } 137 138 /** 139 * Returns the width when the image orientation is top-left. 140 * 141 * @return int 142 */ 143 public function widthTopLeft() { 144 $o = $this->getOrientation(); 145 $this->logger->debug('OC_Image->widthTopLeft() Orientation: ' . $o, ['app' => 'core']); 146 switch ($o) { 147 case -1: 148 case 1: 149 case 2: // Not tested 150 case 3: 151 case 4: // Not tested 152 return $this->width(); 153 case 5: // Not tested 154 case 6: 155 case 7: // Not tested 156 case 8: 157 return $this->height(); 158 } 159 return $this->width(); 160 } 161 162 /** 163 * Returns the height when the image orientation is top-left. 164 * 165 * @return int 166 */ 167 public function heightTopLeft() { 168 $o = $this->getOrientation(); 169 $this->logger->debug('OC_Image->heightTopLeft() Orientation: ' . $o, ['app' => 'core']); 170 switch ($o) { 171 case -1: 172 case 1: 173 case 2: // Not tested 174 case 3: 175 case 4: // Not tested 176 return $this->height(); 177 case 5: // Not tested 178 case 6: 179 case 7: // Not tested 180 case 8: 181 return $this->width(); 182 } 183 return $this->height(); 184 } 185 186 /** 187 * Outputs the image. 188 * 189 * @param string $mimeType 190 * @return bool 191 */ 192 public function show($mimeType = null) { 193 if ($mimeType === null) { 194 $mimeType = $this->mimeType(); 195 } 196 header('Content-Type: ' . $mimeType); 197 return $this->_output(null, $mimeType); 198 } 199 200 /** 201 * Saves the image. 202 * 203 * @param string $filePath 204 * @param string $mimeType 205 * @return bool 206 */ 207 208 public function save($filePath = null, $mimeType = null) { 209 if ($mimeType === null) { 210 $mimeType = $this->mimeType(); 211 } 212 if ($filePath === null) { 213 if ($this->filePath === null) { 214 $this->logger->error(__METHOD__ . '(): called with no path.', ['app' => 'core']); 215 return false; 216 } else { 217 $filePath = $this->filePath; 218 } 219 } 220 return $this->_output($filePath, $mimeType); 221 } 222 223 /** 224 * Outputs/saves the image. 225 * 226 * @param string $filePath 227 * @param string $mimeType 228 * @return bool 229 * @throws Exception 230 */ 231 private function _output($filePath = null, $mimeType = null) { 232 if ($filePath) { 233 if (!file_exists(dirname($filePath))) { 234 mkdir(dirname($filePath), 0777, true); 235 } 236 $isWritable = is_writable(dirname($filePath)); 237 if (!$isWritable) { 238 $this->logger->error(__METHOD__ . '(): Directory \'' . dirname($filePath) . '\' is not writable.', ['app' => 'core']); 239 return false; 240 } elseif ($isWritable && file_exists($filePath) && !is_writable($filePath)) { 241 $this->logger->error(__METHOD__ . '(): File \'' . $filePath . '\' is not writable.', ['app' => 'core']); 242 return false; 243 } 244 } 245 if (!$this->valid()) { 246 return false; 247 } 248 249 $imageType = $this->imageType; 250 if ($mimeType !== null) { 251 switch ($mimeType) { 252 case 'image/gif': 253 $imageType = IMAGETYPE_GIF; 254 break; 255 case 'image/jpeg': 256 $imageType = IMAGETYPE_JPEG; 257 break; 258 case 'image/png': 259 $imageType = IMAGETYPE_PNG; 260 break; 261 case 'image/x-xbitmap': 262 $imageType = IMAGETYPE_XBM; 263 break; 264 case 'image/bmp': 265 case 'image/x-ms-bmp': 266 $imageType = IMAGETYPE_BMP; 267 break; 268 default: 269 throw new Exception('\OC_Image::_output(): "' . $mimeType . '" is not supported when forcing a specific output format'); 270 } 271 } 272 273 switch ($imageType) { 274 case IMAGETYPE_GIF: 275 $retVal = imagegif($this->resource, $filePath); 276 break; 277 case IMAGETYPE_JPEG: 278 $retVal = imagejpeg($this->resource, $filePath, $this->getJpegQuality()); 279 break; 280 case IMAGETYPE_PNG: 281 $retVal = imagepng($this->resource, $filePath); 282 break; 283 case IMAGETYPE_XBM: 284 if (function_exists('imagexbm')) { 285 $retVal = imagexbm($this->resource, $filePath); 286 } else { 287 throw new Exception('\OC_Image::_output(): imagexbm() is not supported.'); 288 } 289 290 break; 291 case IMAGETYPE_WBMP: 292 $retVal = imagewbmp($this->resource, $filePath); 293 break; 294 case IMAGETYPE_BMP: 295 $retVal = imagebmp($this->resource, $filePath, $this->bitDepth); 296 break; 297 default: 298 $retVal = imagepng($this->resource, $filePath); 299 } 300 return $retVal; 301 } 302 303 /** 304 * Prints the image when called as $image(). 305 */ 306 public function __invoke() { 307 return $this->show(); 308 } 309 310 /** 311 * @param resource|\GdImage $resource 312 * @throws \InvalidArgumentException in case the supplied resource does not have the type "gd" 313 */ 314 public function setResource($resource) { 315 // For PHP<8 316 if (is_resource($resource) && get_resource_type($resource) === 'gd') { 317 $this->resource = $resource; 318 return; 319 } 320 // PHP 8 has real objects for GD stuff 321 if (is_object($resource) && get_class($resource) === \GdImage::class) { 322 $this->resource = $resource; 323 return; 324 } 325 throw new \InvalidArgumentException('Supplied resource is not of type "gd".'); 326 } 327 328 /** 329 * @return resource|\GdImage Returns the image resource in any. 330 */ 331 public function resource() { 332 return $this->resource; 333 } 334 335 /** 336 * @return string Returns the mimetype of the data. Returns the empty string 337 * if the data is not valid. 338 */ 339 public function dataMimeType() { 340 if (!$this->valid()) { 341 return ''; 342 } 343 344 switch ($this->mimeType) { 345 case 'image/png': 346 case 'image/jpeg': 347 case 'image/gif': 348 return $this->mimeType; 349 default: 350 return 'image/png'; 351 } 352 } 353 354 /** 355 * @return null|string Returns the raw image data. 356 */ 357 public function data() { 358 if (!$this->valid()) { 359 return null; 360 } 361 ob_start(); 362 switch ($this->mimeType) { 363 case "image/png": 364 $res = imagepng($this->resource); 365 break; 366 case "image/jpeg": 367 $quality = $this->getJpegQuality(); 368 if ($quality !== null) { 369 $res = imagejpeg($this->resource, null, $quality); 370 } else { 371 $res = imagejpeg($this->resource); 372 } 373 break; 374 case "image/gif": 375 $res = imagegif($this->resource); 376 break; 377 default: 378 $res = imagepng($this->resource); 379 $this->logger->info('OC_Image->data. Could not guess mime-type, defaulting to png', ['app' => 'core']); 380 break; 381 } 382 if (!$res) { 383 $this->logger->error('OC_Image->data. Error getting image data.', ['app' => 'core']); 384 } 385 return ob_get_clean(); 386 } 387 388 /** 389 * @return string - base64 encoded, which is suitable for embedding in a VCard. 390 */ 391 public function __toString() { 392 return base64_encode($this->data()); 393 } 394 395 /** 396 * @return int|null 397 */ 398 protected function getJpegQuality() { 399 $quality = $this->config->getAppValue('preview', 'jpeg_quality', 90); 400 if ($quality !== null) { 401 $quality = min(100, max(10, (int) $quality)); 402 } 403 return $quality; 404 } 405 406 /** 407 * (I'm open for suggestions on better method name ;) 408 * Get the orientation based on EXIF data. 409 * 410 * @return int The orientation or -1 if no EXIF data is available. 411 */ 412 public function getOrientation() { 413 if ($this->exif !== null) { 414 return $this->exif['Orientation']; 415 } 416 417 if ($this->imageType !== IMAGETYPE_JPEG) { 418 $this->logger->debug('OC_Image->fixOrientation() Image is not a JPEG.', ['app' => 'core']); 419 return -1; 420 } 421 if (!is_callable('exif_read_data')) { 422 $this->logger->debug('OC_Image->fixOrientation() Exif module not enabled.', ['app' => 'core']); 423 return -1; 424 } 425 if (!$this->valid()) { 426 $this->logger->debug('OC_Image->fixOrientation() No image loaded.', ['app' => 'core']); 427 return -1; 428 } 429 if (is_null($this->filePath) || !is_readable($this->filePath)) { 430 $this->logger->debug('OC_Image->fixOrientation() No readable file path set.', ['app' => 'core']); 431 return -1; 432 } 433 $exif = @exif_read_data($this->filePath, 'IFD0'); 434 if (!$exif) { 435 return -1; 436 } 437 if (!isset($exif['Orientation'])) { 438 return -1; 439 } 440 $this->exif = $exif; 441 return $exif['Orientation']; 442 } 443 444 public function readExif($data) { 445 if (!is_callable('exif_read_data')) { 446 $this->logger->debug('OC_Image->fixOrientation() Exif module not enabled.', ['app' => 'core']); 447 return; 448 } 449 if (!$this->valid()) { 450 $this->logger->debug('OC_Image->fixOrientation() No image loaded.', ['app' => 'core']); 451 return; 452 } 453 454 $exif = @exif_read_data('data://image/jpeg;base64,' . base64_encode($data)); 455 if (!$exif) { 456 return; 457 } 458 if (!isset($exif['Orientation'])) { 459 return; 460 } 461 $this->exif = $exif; 462 } 463 464 /** 465 * (I'm open for suggestions on better method name ;) 466 * Fixes orientation based on EXIF data. 467 * 468 * @return bool 469 */ 470 public function fixOrientation() { 471 $o = $this->getOrientation(); 472 $this->logger->debug('OC_Image->fixOrientation() Orientation: ' . $o, ['app' => 'core']); 473 $rotate = 0; 474 $flip = false; 475 switch ($o) { 476 case -1: 477 return false; //Nothing to fix 478 case 1: 479 $rotate = 0; 480 break; 481 case 2: 482 $rotate = 0; 483 $flip = true; 484 break; 485 case 3: 486 $rotate = 180; 487 break; 488 case 4: 489 $rotate = 180; 490 $flip = true; 491 break; 492 case 5: 493 $rotate = 90; 494 $flip = true; 495 break; 496 case 6: 497 $rotate = 270; 498 break; 499 case 7: 500 $rotate = 270; 501 $flip = true; 502 break; 503 case 8: 504 $rotate = 90; 505 break; 506 } 507 if ($flip && function_exists('imageflip')) { 508 imageflip($this->resource, IMG_FLIP_HORIZONTAL); 509 } 510 if ($rotate) { 511 $res = imagerotate($this->resource, $rotate, 0); 512 if ($res) { 513 if (imagealphablending($res, true)) { 514 if (imagesavealpha($res, true)) { 515 imagedestroy($this->resource); 516 $this->resource = $res; 517 return true; 518 } else { 519 $this->logger->debug('OC_Image->fixOrientation() Error during alpha-saving', ['app' => 'core']); 520 return false; 521 } 522 } else { 523 $this->logger->debug('OC_Image->fixOrientation() Error during alpha-blending', ['app' => 'core']); 524 return false; 525 } 526 } else { 527 $this->logger->debug('OC_Image->fixOrientation() Error during orientation fixing', ['app' => 'core']); 528 return false; 529 } 530 } 531 return false; 532 } 533 534 /** 535 * Loads an image from an open file handle. 536 * It is the responsibility of the caller to position the pointer at the correct place and to close the handle again. 537 * 538 * @param resource $handle 539 * @return resource|\GdImage|false An image resource or false on error 540 */ 541 public function loadFromFileHandle($handle) { 542 $contents = stream_get_contents($handle); 543 if ($this->loadFromData($contents)) { 544 return $this->resource; 545 } 546 return false; 547 } 548 549 /** 550 * Loads an image from a local file. 551 * 552 * @param bool|string $imagePath The path to a local file. 553 * @return bool|resource|\GdImage An image resource or false on error 554 */ 555 public function loadFromFile($imagePath = false) { 556 // exif_imagetype throws "read error!" if file is less than 12 byte 557 if (is_bool($imagePath) || !@is_file($imagePath) || !file_exists($imagePath) || filesize($imagePath) < 12 || !is_readable($imagePath)) { 558 return false; 559 } 560 $iType = exif_imagetype($imagePath); 561 switch ($iType) { 562 case IMAGETYPE_GIF: 563 if (imagetypes() & IMG_GIF) { 564 $this->resource = imagecreatefromgif($imagePath); 565 if ($this->resource) { 566 // Preserve transparency 567 imagealphablending($this->resource, true); 568 imagesavealpha($this->resource, true); 569 } else { 570 $this->logger->debug('OC_Image->loadFromFile, GIF image not valid: ' . $imagePath, ['app' => 'core']); 571 } 572 } else { 573 $this->logger->debug('OC_Image->loadFromFile, GIF images not supported: ' . $imagePath, ['app' => 'core']); 574 } 575 break; 576 case IMAGETYPE_JPEG: 577 if (imagetypes() & IMG_JPG) { 578 if (getimagesize($imagePath) !== false) { 579 $this->resource = @imagecreatefromjpeg($imagePath); 580 } else { 581 $this->logger->debug('OC_Image->loadFromFile, JPG image not valid: ' . $imagePath, ['app' => 'core']); 582 } 583 } else { 584 $this->logger->debug('OC_Image->loadFromFile, JPG images not supported: ' . $imagePath, ['app' => 'core']); 585 } 586 break; 587 case IMAGETYPE_PNG: 588 if (imagetypes() & IMG_PNG) { 589 $this->resource = @imagecreatefrompng($imagePath); 590 if ($this->resource) { 591 // Preserve transparency 592 imagealphablending($this->resource, true); 593 imagesavealpha($this->resource, true); 594 } else { 595 $this->logger->debug('OC_Image->loadFromFile, PNG image not valid: ' . $imagePath, ['app' => 'core']); 596 } 597 } else { 598 $this->logger->debug('OC_Image->loadFromFile, PNG images not supported: ' . $imagePath, ['app' => 'core']); 599 } 600 break; 601 case IMAGETYPE_XBM: 602 if (imagetypes() & IMG_XPM) { 603 $this->resource = @imagecreatefromxbm($imagePath); 604 } else { 605 $this->logger->debug('OC_Image->loadFromFile, XBM/XPM images not supported: ' . $imagePath, ['app' => 'core']); 606 } 607 break; 608 case IMAGETYPE_WBMP: 609 if (imagetypes() & IMG_WBMP) { 610 $this->resource = @imagecreatefromwbmp($imagePath); 611 } else { 612 $this->logger->debug('OC_Image->loadFromFile, WBMP images not supported: ' . $imagePath, ['app' => 'core']); 613 } 614 break; 615 case IMAGETYPE_BMP: 616 $this->resource = $this->imagecreatefrombmp($imagePath); 617 break; 618 case IMAGETYPE_WEBP: 619 if (imagetypes() & IMG_WEBP) { 620 $this->resource = @imagecreatefromwebp($imagePath); 621 } else { 622 $this->logger->debug('OC_Image->loadFromFile, webp images not supported: ' . $imagePath, ['app' => 'core']); 623 } 624 break; 625 /* 626 case IMAGETYPE_TIFF_II: // (intel byte order) 627 break; 628 case IMAGETYPE_TIFF_MM: // (motorola byte order) 629 break; 630 case IMAGETYPE_JPC: 631 break; 632 case IMAGETYPE_JP2: 633 break; 634 case IMAGETYPE_JPX: 635 break; 636 case IMAGETYPE_JB2: 637 break; 638 case IMAGETYPE_SWC: 639 break; 640 case IMAGETYPE_IFF: 641 break; 642 case IMAGETYPE_ICO: 643 break; 644 case IMAGETYPE_SWF: 645 break; 646 case IMAGETYPE_PSD: 647 break; 648 */ 649 default: 650 651 // this is mostly file created from encrypted file 652 $this->resource = imagecreatefromstring(file_get_contents($imagePath)); 653 $iType = IMAGETYPE_PNG; 654 $this->logger->debug('OC_Image->loadFromFile, Default', ['app' => 'core']); 655 break; 656 } 657 if ($this->valid()) { 658 $this->imageType = $iType; 659 $this->mimeType = image_type_to_mime_type($iType); 660 $this->filePath = $imagePath; 661 } 662 return $this->resource; 663 } 664 665 /** 666 * Loads an image from a string of data. 667 * 668 * @param string $str A string of image data as read from a file. 669 * @return bool|resource|\GdImage An image resource or false on error 670 */ 671 public function loadFromData($str) { 672 if (!is_string($str)) { 673 return false; 674 } 675 $this->resource = @imagecreatefromstring($str); 676 if ($this->fileInfo) { 677 $this->mimeType = $this->fileInfo->buffer($str); 678 } 679 if ($this->valid()) { 680 imagealphablending($this->resource, false); 681 imagesavealpha($this->resource, true); 682 } 683 684 if (!$this->resource) { 685 $this->logger->debug('OC_Image->loadFromFile, could not load', ['app' => 'core']); 686 return false; 687 } 688 return $this->resource; 689 } 690 691 /** 692 * Loads an image from a base64 encoded string. 693 * 694 * @param string $str A string base64 encoded string of image data. 695 * @return bool|resource|\GdImage An image resource or false on error 696 */ 697 public function loadFromBase64($str) { 698 if (!is_string($str)) { 699 return false; 700 } 701 $data = base64_decode($str); 702 if ($data) { // try to load from string data 703 $this->resource = @imagecreatefromstring($data); 704 if ($this->fileInfo) { 705 $this->mimeType = $this->fileInfo->buffer($data); 706 } 707 if (!$this->resource) { 708 $this->logger->debug('OC_Image->loadFromBase64, could not load', ['app' => 'core']); 709 return false; 710 } 711 return $this->resource; 712 } else { 713 return false; 714 } 715 } 716 717 /** 718 * Create a new image from file or URL 719 * 720 * @link http://www.programmierer-forum.de/function-imagecreatefrombmp-laeuft-mit-allen-bitraten-t143137.htm 721 * @version 1.00 722 * @param string $fileName <p> 723 * Path to the BMP image. 724 * </p> 725 * @return bool|resource|\GdImage an image resource identifier on success, <b>FALSE</b> on errors. 726 */ 727 private function imagecreatefrombmp($fileName) { 728 if (!($fh = fopen($fileName, 'rb'))) { 729 $this->logger->warning('imagecreatefrombmp: Can not open ' . $fileName, ['app' => 'core']); 730 return false; 731 } 732 // read file header 733 $meta = unpack('vtype/Vfilesize/Vreserved/Voffset', fread($fh, 14)); 734 // check for bitmap 735 if ($meta['type'] != 19778) { 736 fclose($fh); 737 $this->logger->warning('imagecreatefrombmp: Can not open ' . $fileName . ' is not a bitmap!', ['app' => 'core']); 738 return false; 739 } 740 // read image header 741 $meta += unpack('Vheadersize/Vwidth/Vheight/vplanes/vbits/Vcompression/Vimagesize/Vxres/Vyres/Vcolors/Vimportant', fread($fh, 40)); 742 // read additional 16bit header 743 if ($meta['bits'] == 16) { 744 $meta += unpack('VrMask/VgMask/VbMask', fread($fh, 12)); 745 } 746 // set bytes and padding 747 $meta['bytes'] = $meta['bits'] / 8; 748 $this->bitDepth = $meta['bits']; //remember the bit depth for the imagebmp call 749 $meta['decal'] = 4 - (4 * (($meta['width'] * $meta['bytes'] / 4) - floor($meta['width'] * $meta['bytes'] / 4))); 750 if ($meta['decal'] == 4) { 751 $meta['decal'] = 0; 752 } 753 // obtain imagesize 754 if ($meta['imagesize'] < 1) { 755 $meta['imagesize'] = $meta['filesize'] - $meta['offset']; 756 // in rare cases filesize is equal to offset so we need to read physical size 757 if ($meta['imagesize'] < 1) { 758 $meta['imagesize'] = @filesize($fileName) - $meta['offset']; 759 if ($meta['imagesize'] < 1) { 760 fclose($fh); 761 $this->logger->warning('imagecreatefrombmp: Can not obtain file size of ' . $fileName . ' is not a bitmap!', ['app' => 'core']); 762 return false; 763 } 764 } 765 } 766 // calculate colors 767 $meta['colors'] = !$meta['colors'] ? pow(2, $meta['bits']) : $meta['colors']; 768 // read color palette 769 $palette = []; 770 if ($meta['bits'] < 16) { 771 $palette = unpack('l' . $meta['colors'], fread($fh, $meta['colors'] * 4)); 772 // in rare cases the color value is signed 773 if ($palette[1] < 0) { 774 foreach ($palette as $i => $color) { 775 $palette[$i] = $color + 16777216; 776 } 777 } 778 } 779 // create gd image 780 $im = imagecreatetruecolor($meta['width'], $meta['height']); 781 if ($im == false) { 782 fclose($fh); 783 $this->logger->warning( 784 'imagecreatefrombmp: imagecreatetruecolor failed for file "' . $fileName . '" with dimensions ' . $meta['width'] . 'x' . $meta['height'], 785 ['app' => 'core']); 786 return false; 787 } 788 789 $data = fread($fh, $meta['imagesize']); 790 $p = 0; 791 $vide = chr(0); 792 $y = $meta['height'] - 1; 793 $error = 'imagecreatefrombmp: ' . $fileName . ' has not enough data!'; 794 // loop through the image data beginning with the lower left corner 795 while ($y >= 0) { 796 $x = 0; 797 while ($x < $meta['width']) { 798 switch ($meta['bits']) { 799 case 32: 800 case 24: 801 if (!($part = substr($data, $p, 3))) { 802 $this->logger->warning($error, ['app' => 'core']); 803 return $im; 804 } 805 $color = @unpack('V', $part . $vide); 806 break; 807 case 16: 808 if (!($part = substr($data, $p, 2))) { 809 fclose($fh); 810 $this->logger->warning($error, ['app' => 'core']); 811 return $im; 812 } 813 $color = @unpack('v', $part); 814 $color[1] = (($color[1] & 0xf800) >> 8) * 65536 + (($color[1] & 0x07e0) >> 3) * 256 + (($color[1] & 0x001f) << 3); 815 break; 816 case 8: 817 $color = @unpack('n', $vide . ($data[$p] ?? '')); 818 $color[1] = isset($palette[$color[1] + 1]) ? $palette[$color[1] + 1] : $palette[1]; 819 break; 820 case 4: 821 $color = @unpack('n', $vide . ($data[floor($p)] ?? '')); 822 $color[1] = ($p * 2) % 2 == 0 ? $color[1] >> 4 : $color[1] & 0x0F; 823 $color[1] = isset($palette[$color[1] + 1]) ? $palette[$color[1] + 1] : $palette[1]; 824 break; 825 case 1: 826 $color = @unpack('n', $vide . ($data[floor($p)] ?? '')); 827 switch (($p * 8) % 8) { 828 case 0: 829 $color[1] = $color[1] >> 7; 830 break; 831 case 1: 832 $color[1] = ($color[1] & 0x40) >> 6; 833 break; 834 case 2: 835 $color[1] = ($color[1] & 0x20) >> 5; 836 break; 837 case 3: 838 $color[1] = ($color[1] & 0x10) >> 4; 839 break; 840 case 4: 841 $color[1] = ($color[1] & 0x8) >> 3; 842 break; 843 case 5: 844 $color[1] = ($color[1] & 0x4) >> 2; 845 break; 846 case 6: 847 $color[1] = ($color[1] & 0x2) >> 1; 848 break; 849 case 7: 850 $color[1] = ($color[1] & 0x1); 851 break; 852 } 853 $color[1] = isset($palette[$color[1] + 1]) ? $palette[$color[1] + 1] : $palette[1]; 854 break; 855 default: 856 fclose($fh); 857 $this->logger->warning('imagecreatefrombmp: ' . $fileName . ' has ' . $meta['bits'] . ' bits and this is not supported!', ['app' => 'core']); 858 return false; 859 } 860 imagesetpixel($im, $x, $y, $color[1]); 861 $x++; 862 $p += $meta['bytes']; 863 } 864 $y--; 865 $p += $meta['decal']; 866 } 867 fclose($fh); 868 return $im; 869 } 870 871 /** 872 * Resizes the image preserving ratio. 873 * 874 * @param integer $maxSize The maximum size of either the width or height. 875 * @return bool 876 */ 877 public function resize($maxSize) { 878 $result = $this->resizeNew($maxSize); 879 imagedestroy($this->resource); 880 $this->resource = $result; 881 return $this->valid(); 882 } 883 884 /** 885 * @param $maxSize 886 * @return resource|bool|\GdImage 887 */ 888 private function resizeNew($maxSize) { 889 if (!$this->valid()) { 890 $this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']); 891 return false; 892 } 893 $widthOrig = imagesx($this->resource); 894 $heightOrig = imagesy($this->resource); 895 $ratioOrig = $widthOrig / $heightOrig; 896 897 if ($ratioOrig > 1) { 898 $newHeight = round($maxSize / $ratioOrig); 899 $newWidth = $maxSize; 900 } else { 901 $newWidth = round($maxSize * $ratioOrig); 902 $newHeight = $maxSize; 903 } 904 905 return $this->preciseResizeNew((int)round($newWidth), (int)round($newHeight)); 906 } 907 908 /** 909 * @param int $width 910 * @param int $height 911 * @return bool 912 */ 913 public function preciseResize(int $width, int $height): bool { 914 $result = $this->preciseResizeNew($width, $height); 915 imagedestroy($this->resource); 916 $this->resource = $result; 917 return $this->valid(); 918 } 919 920 921 /** 922 * @param int $width 923 * @param int $height 924 * @return resource|bool|\GdImage 925 */ 926 public function preciseResizeNew(int $width, int $height) { 927 if (!($width > 0) || !($height > 0)) { 928 $this->logger->info(__METHOD__ . '(): Requested image size not bigger than 0', ['app' => 'core']); 929 return false; 930 } 931 if (!$this->valid()) { 932 $this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']); 933 return false; 934 } 935 $widthOrig = imagesx($this->resource); 936 $heightOrig = imagesy($this->resource); 937 $process = imagecreatetruecolor($width, $height); 938 if ($process === false) { 939 $this->logger->error(__METHOD__ . '(): Error creating true color image', ['app' => 'core']); 940 return false; 941 } 942 943 // preserve transparency 944 if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) { 945 imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127)); 946 imagealphablending($process, false); 947 imagesavealpha($process, true); 948 } 949 950 $res = imagecopyresampled($process, $this->resource, 0, 0, 0, 0, $width, $height, $widthOrig, $heightOrig); 951 if ($res === false) { 952 $this->logger->error(__METHOD__ . '(): Error re-sampling process image', ['app' => 'core']); 953 imagedestroy($process); 954 return false; 955 } 956 return $process; 957 } 958 959 /** 960 * Crops the image to the middle square. If the image is already square it just returns. 961 * 962 * @param int $size maximum size for the result (optional) 963 * @return bool for success or failure 964 */ 965 public function centerCrop($size = 0) { 966 if (!$this->valid()) { 967 $this->logger->error('OC_Image->centerCrop, No image loaded', ['app' => 'core']); 968 return false; 969 } 970 $widthOrig = imagesx($this->resource); 971 $heightOrig = imagesy($this->resource); 972 if ($widthOrig === $heightOrig and $size == 0) { 973 return true; 974 } 975 $ratioOrig = $widthOrig / $heightOrig; 976 $width = $height = min($widthOrig, $heightOrig); 977 978 if ($ratioOrig > 1) { 979 $x = ($widthOrig / 2) - ($width / 2); 980 $y = 0; 981 } else { 982 $y = ($heightOrig / 2) - ($height / 2); 983 $x = 0; 984 } 985 if ($size > 0) { 986 $targetWidth = $size; 987 $targetHeight = $size; 988 } else { 989 $targetWidth = $width; 990 $targetHeight = $height; 991 } 992 $process = imagecreatetruecolor($targetWidth, $targetHeight); 993 if ($process == false) { 994 $this->logger->error('OC_Image->centerCrop, Error creating true color image', ['app' => 'core']); 995 imagedestroy($process); 996 return false; 997 } 998 999 // preserve transparency 1000 if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) { 1001 imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127)); 1002 imagealphablending($process, false); 1003 imagesavealpha($process, true); 1004 } 1005 1006 imagecopyresampled($process, $this->resource, 0, 0, $x, $y, $targetWidth, $targetHeight, $width, $height); 1007 if ($process == false) { 1008 $this->logger->error('OC_Image->centerCrop, Error re-sampling process image ' . $width . 'x' . $height, ['app' => 'core']); 1009 imagedestroy($process); 1010 return false; 1011 } 1012 imagedestroy($this->resource); 1013 $this->resource = $process; 1014 return true; 1015 } 1016 1017 /** 1018 * Crops the image from point $x$y with dimension $wx$h. 1019 * 1020 * @param int $x Horizontal position 1021 * @param int $y Vertical position 1022 * @param int $w Width 1023 * @param int $h Height 1024 * @return bool for success or failure 1025 */ 1026 public function crop(int $x, int $y, int $w, int $h): bool { 1027 $result = $this->cropNew($x, $y, $w, $h); 1028 imagedestroy($this->resource); 1029 $this->resource = $result; 1030 return $this->valid(); 1031 } 1032 1033 /** 1034 * Crops the image from point $x$y with dimension $wx$h. 1035 * 1036 * @param int $x Horizontal position 1037 * @param int $y Vertical position 1038 * @param int $w Width 1039 * @param int $h Height 1040 * @return resource | bool 1041 */ 1042 public function cropNew(int $x, int $y, int $w, int $h) { 1043 if (!$this->valid()) { 1044 $this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']); 1045 return false; 1046 } 1047 $process = imagecreatetruecolor($w, $h); 1048 if ($process == false) { 1049 $this->logger->error(__METHOD__ . '(): Error creating true color image', ['app' => 'core']); 1050 imagedestroy($process); 1051 return false; 1052 } 1053 1054 // preserve transparency 1055 if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) { 1056 imagecolortransparent($process, imagecolorallocatealpha($process, 0, 0, 0, 127)); 1057 imagealphablending($process, false); 1058 imagesavealpha($process, true); 1059 } 1060 1061 imagecopyresampled($process, $this->resource, 0, 0, $x, $y, $w, $h, $w, $h); 1062 if ($process == false) { 1063 $this->logger->error(__METHOD__ . '(): Error re-sampling process image ' . $w . 'x' . $h, ['app' => 'core']); 1064 imagedestroy($process); 1065 return false; 1066 } 1067 return $process; 1068 } 1069 1070 /** 1071 * Resizes the image to fit within a boundary while preserving ratio. 1072 * 1073 * Warning: Images smaller than $maxWidth x $maxHeight will end up being scaled up 1074 * 1075 * @param integer $maxWidth 1076 * @param integer $maxHeight 1077 * @return bool 1078 */ 1079 public function fitIn($maxWidth, $maxHeight) { 1080 if (!$this->valid()) { 1081 $this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']); 1082 return false; 1083 } 1084 $widthOrig = imagesx($this->resource); 1085 $heightOrig = imagesy($this->resource); 1086 $ratio = $widthOrig / $heightOrig; 1087 1088 $newWidth = min($maxWidth, $ratio * $maxHeight); 1089 $newHeight = min($maxHeight, $maxWidth / $ratio); 1090 1091 $this->preciseResize((int)round($newWidth), (int)round($newHeight)); 1092 return true; 1093 } 1094 1095 /** 1096 * Shrinks larger images to fit within specified boundaries while preserving ratio. 1097 * 1098 * @param integer $maxWidth 1099 * @param integer $maxHeight 1100 * @return bool 1101 */ 1102 public function scaleDownToFit($maxWidth, $maxHeight) { 1103 if (!$this->valid()) { 1104 $this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']); 1105 return false; 1106 } 1107 $widthOrig = imagesx($this->resource); 1108 $heightOrig = imagesy($this->resource); 1109 1110 if ($widthOrig > $maxWidth || $heightOrig > $maxHeight) { 1111 return $this->fitIn($maxWidth, $maxHeight); 1112 } 1113 1114 return false; 1115 } 1116 1117 public function copy(): IImage { 1118 $image = new OC_Image(null, $this->logger, $this->config); 1119 $image->resource = imagecreatetruecolor($this->width(), $this->height()); 1120 imagecopy( 1121 $image->resource(), 1122 $this->resource(), 1123 0, 1124 0, 1125 0, 1126 0, 1127 $this->width(), 1128 $this->height() 1129 ); 1130 1131 return $image; 1132 } 1133 1134 public function cropCopy(int $x, int $y, int $w, int $h): IImage { 1135 $image = new OC_Image(null, $this->logger, $this->config); 1136 $image->imageType = $this->imageType; 1137 $image->mimeType = $this->mimeType; 1138 $image->bitDepth = $this->bitDepth; 1139 $image->resource = $this->cropNew($x, $y, $w, $h); 1140 1141 return $image; 1142 } 1143 1144 public function preciseResizeCopy(int $width, int $height): IImage { 1145 $image = new OC_Image(null, $this->logger, $this->config); 1146 $image->imageType = $this->imageType; 1147 $image->mimeType = $this->mimeType; 1148 $image->bitDepth = $this->bitDepth; 1149 $image->resource = $this->preciseResizeNew($width, $height); 1150 1151 return $image; 1152 } 1153 1154 public function resizeCopy(int $maxSize): IImage { 1155 $image = new OC_Image(null, $this->logger, $this->config); 1156 $image->imageType = $this->imageType; 1157 $image->mimeType = $this->mimeType; 1158 $image->bitDepth = $this->bitDepth; 1159 $image->resource = $this->resizeNew($maxSize); 1160 1161 return $image; 1162 } 1163 1164 /** 1165 * Destroys the current image and resets the object 1166 */ 1167 public function destroy() { 1168 if ($this->valid()) { 1169 imagedestroy($this->resource); 1170 } 1171 $this->resource = null; 1172 } 1173 1174 public function __destruct() { 1175 $this->destroy(); 1176 } 1177} 1178 1179if (!function_exists('imagebmp')) { 1180 /** 1181 * Output a BMP image to either the browser or a file 1182 * 1183 * @link http://www.ugia.cn/wp-data/imagebmp.php 1184 * @author legend <legendsky@hotmail.com> 1185 * @link http://www.programmierer-forum.de/imagebmp-gute-funktion-gefunden-t143716.htm 1186 * @author mgutt <marc@gutt.it> 1187 * @version 1.00 1188 * @param resource|\GdImage $im 1189 * @param string $fileName [optional] <p>The path to save the file to.</p> 1190 * @param int $bit [optional] <p>Bit depth, (default is 24).</p> 1191 * @param int $compression [optional] 1192 * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure. 1193 */ 1194 function imagebmp($im, $fileName = '', $bit = 24, $compression = 0) { 1195 if (!in_array($bit, [1, 4, 8, 16, 24, 32])) { 1196 $bit = 24; 1197 } elseif ($bit == 32) { 1198 $bit = 24; 1199 } 1200 $bits = (int)pow(2, $bit); 1201 imagetruecolortopalette($im, true, $bits); 1202 $width = imagesx($im); 1203 $height = imagesy($im); 1204 $colorsNum = imagecolorstotal($im); 1205 $rgbQuad = ''; 1206 if ($bit <= 8) { 1207 for ($i = 0; $i < $colorsNum; $i++) { 1208 $colors = imagecolorsforindex($im, $i); 1209 $rgbQuad .= chr($colors['blue']) . chr($colors['green']) . chr($colors['red']) . "\0"; 1210 } 1211 $bmpData = ''; 1212 if ($compression == 0 || $bit < 8) { 1213 $compression = 0; 1214 $extra = ''; 1215 $padding = 4 - ceil($width / (8 / $bit)) % 4; 1216 if ($padding % 4 != 0) { 1217 $extra = str_repeat("\0", $padding); 1218 } 1219 for ($j = $height - 1; $j >= 0; $j--) { 1220 $i = 0; 1221 while ($i < $width) { 1222 $bin = 0; 1223 $limit = $width - $i < 8 / $bit ? (8 / $bit - $width + $i) * $bit : 0; 1224 for ($k = 8 - $bit; $k >= $limit; $k -= $bit) { 1225 $index = imagecolorat($im, $i, $j); 1226 $bin |= $index << $k; 1227 $i++; 1228 } 1229 $bmpData .= chr($bin); 1230 } 1231 $bmpData .= $extra; 1232 } 1233 } // RLE8 1234 elseif ($compression == 1 && $bit == 8) { 1235 for ($j = $height - 1; $j >= 0; $j--) { 1236 $lastIndex = null; 1237 $sameNum = 0; 1238 for ($i = 0; $i <= $width; $i++) { 1239 $index = imagecolorat($im, $i, $j); 1240 if ($index !== $lastIndex || $sameNum > 255) { 1241 if ($sameNum != 0) { 1242 $bmpData .= chr($sameNum) . chr($lastIndex); 1243 } 1244 $lastIndex = $index; 1245 $sameNum = 1; 1246 } else { 1247 $sameNum++; 1248 } 1249 } 1250 $bmpData .= "\0\0"; 1251 } 1252 $bmpData .= "\0\1"; 1253 } 1254 $sizeQuad = strlen($rgbQuad); 1255 $sizeData = strlen($bmpData); 1256 } else { 1257 $extra = ''; 1258 $padding = 4 - ($width * ($bit / 8)) % 4; 1259 if ($padding % 4 != 0) { 1260 $extra = str_repeat("\0", $padding); 1261 } 1262 $bmpData = ''; 1263 for ($j = $height - 1; $j >= 0; $j--) { 1264 for ($i = 0; $i < $width; $i++) { 1265 $index = imagecolorat($im, $i, $j); 1266 $colors = imagecolorsforindex($im, $index); 1267 if ($bit == 16) { 1268 $bin = 0 << $bit; 1269 $bin |= ($colors['red'] >> 3) << 10; 1270 $bin |= ($colors['green'] >> 3) << 5; 1271 $bin |= $colors['blue'] >> 3; 1272 $bmpData .= pack("v", $bin); 1273 } else { 1274 $bmpData .= pack("c*", $colors['blue'], $colors['green'], $colors['red']); 1275 } 1276 } 1277 $bmpData .= $extra; 1278 } 1279 $sizeQuad = 0; 1280 $sizeData = strlen($bmpData); 1281 $colorsNum = 0; 1282 } 1283 $fileHeader = 'BM' . pack('V3', 54 + $sizeQuad + $sizeData, 0, 54 + $sizeQuad); 1284 $infoHeader = pack('V3v2V*', 0x28, $width, $height, 1, $bit, $compression, $sizeData, 0, 0, $colorsNum, 0); 1285 if ($fileName != '') { 1286 $fp = fopen($fileName, 'wb'); 1287 fwrite($fp, $fileHeader . $infoHeader . $rgbQuad . $bmpData); 1288 fclose($fp); 1289 return true; 1290 } 1291 echo $fileHeader . $infoHeader . $rgbQuad . $bmpData; 1292 return true; 1293 } 1294} 1295 1296if (!function_exists('exif_imagetype')) { 1297 /** 1298 * Workaround if exif_imagetype does not exist 1299 * 1300 * @link https://www.php.net/manual/en/function.exif-imagetype.php#80383 1301 * @param string $fileName 1302 * @return string|boolean 1303 */ 1304 function exif_imagetype($fileName) { 1305 if (($info = getimagesize($fileName)) !== false) { 1306 return $info[2]; 1307 } 1308 return false; 1309 } 1310} 1311