1<?php 2/** 3 * @author Andreas Fischer <bantu@owncloud.com> 4 * @author Bartek Przybylski <bart.p.pl@gmail.com> 5 * @author Bart Visscher <bartv@thisnet.nl> 6 * @author Björn Schießle <bjoern@schiessle.org> 7 * @author Byron Marohn <combustible@live.com> 8 * @author Christopher Schäpers <kondou@ts.unde.re> 9 * @author Georg Ehrke <georg@owncloud.com> 10 * @author j-ed <juergen@eisfair.org> 11 * @author Joas Schilling <coding@schilljs.com> 12 * @author Johannes Willnecker <johannes@willnecker.com> 13 * @author Jörn Friedrich Dreyer <jfd@butonic.de> 14 * @author Lukas Reschke <lukas@statuscode.ch> 15 * @author Morris Jobke <hey@morrisjobke.de> 16 * @author Olivier Paroz <github@oparoz.com> 17 * @author Robin Appelman <icewind@owncloud.com> 18 * @author Thomas Müller <thomas.mueller@tmit.eu> 19 * @author Thomas Tanghus <thomas@tanghus.net> 20 * @author Victor Dubiniuk <dubiniuk@owncloud.com> 21 * 22 * @copyright Copyright (c) 2018, ownCloud GmbH 23 * @license AGPL-3.0 24 * 25 * This code is free software: you can redistribute it and/or modify 26 * it under the terms of the GNU Affero General Public License, version 3, 27 * as published by the Free Software Foundation. 28 * 29 * This program is distributed in the hope that it will be useful, 30 * but WITHOUT ANY WARRANTY; without even the implied warranty of 31 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 32 * GNU Affero General Public License for more details. 33 * 34 * You should have received a copy of the GNU Affero General Public License, version 3, 35 * along with this program. If not, see <http://www.gnu.org/licenses/> 36 * 37 */ 38 39use OC\Image\BmpToResource; 40 41/** 42 * Class for basic image manipulation 43 */ 44class OC_Image implements \OCP\IImage { 45 /** @var false|resource */ 46 protected $resource = false; // tmp resource. 47 /** @var int */ 48 protected $imageType = IMAGETYPE_PNG; // Default to png if file type isn't evident. 49 /** @var string */ 50 protected $mimeType = 'image/png'; // Default to png 51 /** @var int */ 52 protected $bitDepth = 24; 53 /** @var null|string */ 54 protected $filePath = null; 55 /** @var finfo */ 56 private $fileInfo; 57 /** @var \OCP\ILogger */ 58 private $logger; 59 60 /** 61 * Get mime type for an image file. 62 * 63 * @param string|null $filePath The path to a local image file. 64 * @return string The mime type if the it could be determined, otherwise an empty string. 65 */ 66 public static function getMimeTypeForFile($filePath) { 67 // exif_imagetype throws "read error!" if file is less than 12 byte 68 if ($filePath !== null && \filesize($filePath) > 11) { 69 $imageType = \exif_imagetype($filePath); 70 } else { 71 $imageType = false; 72 } 73 return $imageType ? \image_type_to_mime_type($imageType) : ''; 74 } 75 76 /** 77 * Constructor. 78 * 79 * @param resource|string $imageRef The path to a local file, a base64 encoded string or a resource created by 80 * an imagecreate* function. 81 * @param \OCP\ILogger $logger 82 */ 83 public function __construct($imageRef = null, $logger = null) { 84 $this->logger = $logger; 85 if ($logger === null) { 86 $this->logger = \OC::$server->getLogger(); 87 } 88 89 if (\OC_Util::fileInfoLoaded()) { 90 $this->fileInfo = new finfo(FILEINFO_MIME_TYPE); 91 } 92 93 if ($imageRef !== null) { 94 $this->load($imageRef); 95 } 96 } 97 98 /** 99 * Determine whether the object contains an image resource. 100 * 101 * @return bool 102 */ 103 public function valid() { // apparently you can't name a method 'empty'... 104 return \is_resource($this->resource); 105 } 106 107 /** 108 * Returns the MIME type of the image or an empty string if no image is loaded. 109 * 110 * @return string 111 */ 112 public function mimeType() { 113 return $this->valid() ? $this->mimeType : ''; 114 } 115 116 /** 117 * Returns the width of the image or -1 if no image is loaded. 118 * 119 * @return int 120 */ 121 public function width() { 122 return $this->valid() ? \imagesx($this->resource) : -1; 123 } 124 125 /** 126 * Returns the height of the image or -1 if no image is loaded. 127 * 128 * @return int 129 */ 130 public function height() { 131 return $this->valid() ? \imagesy($this->resource) : -1; 132 } 133 134 /** 135 * Returns the width when the image orientation is top-left. 136 * 137 * @return int 138 */ 139 public function widthTopLeft() { 140 $o = $this->getOrientation(); 141 $this->logger->debug('OC_Image->widthTopLeft() Orientation: ' . $o, ['app' => 'core']); 142 switch ($o) { 143 case -1: 144 case 1: 145 case 2: // Not tested 146 case 3: 147 case 4: // Not tested 148 return $this->width(); 149 case 5: // Not tested 150 case 6: 151 case 7: // Not tested 152 case 8: 153 return $this->height(); 154 } 155 return $this->width(); 156 } 157 158 /** 159 * Returns the height when the image orientation is top-left. 160 * 161 * @return int 162 */ 163 public function heightTopLeft() { 164 $o = $this->getOrientation(); 165 $this->logger->debug('OC_Image->heightTopLeft() Orientation: ' . $o, ['app' => 'core']); 166 switch ($o) { 167 case -1: 168 case 1: 169 case 2: // Not tested 170 case 3: 171 case 4: // Not tested 172 return $this->height(); 173 case 5: // Not tested 174 case 6: 175 case 7: // Not tested 176 case 8: 177 return $this->width(); 178 } 179 return $this->height(); 180 } 181 182 /** 183 * Outputs the image. 184 * 185 * @param string $mimeType 186 * @return bool 187 */ 188 public function show($mimeType = null) { 189 if ($mimeType === null) { 190 $mimeType = $this->mimeType(); 191 } 192 \header('Content-Type: ' . $mimeType); 193 return $this->_output(null, $mimeType); 194 } 195 196 /** 197 * Saves the image. 198 * 199 * @param string $filePath 200 * @param string $mimeType 201 * @return bool 202 */ 203 204 public function save($filePath = null, $mimeType = null) { 205 if ($mimeType === null) { 206 $mimeType = $this->mimeType(); 207 } 208 if ($filePath === null && $this->filePath === null) { 209 $this->logger->error(__METHOD__ . '(): called with no path.', ['app' => 'core']); 210 return false; 211 } elseif ($filePath === null && $this->filePath !== null) { 212 $filePath = $this->filePath; 213 } 214 return $this->_output($filePath, $mimeType); 215 } 216 217 /** 218 * Outputs/saves the image. 219 * 220 * @param string $filePath 221 * @param string $mimeType 222 * @return bool 223 * @throws Exception 224 */ 225 private function _output($filePath = null, $mimeType = null) { 226 if ($filePath) { 227 if (!\file_exists(\dirname($filePath))) { 228 \mkdir(\dirname($filePath), 0777, true); 229 } 230 if (!\is_writable(\dirname($filePath))) { 231 $this->logger->error(__METHOD__ . '(): Directory \'' . \dirname($filePath) . '\' is not writable.', ['app' => 'core']); 232 return false; 233 } elseif (\is_writable(\dirname($filePath)) && \file_exists($filePath) && !\is_writable($filePath)) { 234 $this->logger->error(__METHOD__ . '(): File \'' . $filePath . '\' is not writable.', ['app' => 'core']); 235 return false; 236 } 237 } 238 if (!$this->valid()) { 239 return false; 240 } 241 242 $imageType = $this->imageType; 243 if ($mimeType !== null) { 244 switch ($mimeType) { 245 case 'image/gif': 246 $imageType = IMAGETYPE_GIF; 247 break; 248 case 'image/jpeg': 249 $imageType = IMAGETYPE_JPEG; 250 break; 251 case 'image/png': 252 $imageType = IMAGETYPE_PNG; 253 break; 254 case 'image/x-xbitmap': 255 $imageType = IMAGETYPE_XBM; 256 break; 257 case 'image/bmp': 258 case 'image/x-ms-bmp': 259 $imageType = IMAGETYPE_BMP; 260 break; 261 default: 262 throw new Exception('\OC_Image::_output(): "' . $mimeType . '" is not supported when forcing a specific output format'); 263 } 264 } 265 266 switch ($imageType) { 267 case IMAGETYPE_GIF: 268 $retVal = \imagegif($this->resource, $filePath); 269 break; 270 case IMAGETYPE_JPEG: 271 $retVal = \imagejpeg($this->resource, $filePath); 272 break; 273 case IMAGETYPE_PNG: 274 $retVal = \imagepng($this->resource, $filePath); 275 break; 276 case IMAGETYPE_XBM: 277 if (\function_exists('imagexbm')) { 278 $retVal = \imagexbm($this->resource, $filePath); 279 } else { 280 throw new Exception('\OC_Image::_output(): imagexbm() is not supported.'); 281 } 282 283 break; 284 case IMAGETYPE_WBMP: 285 $retVal = \imagewbmp($this->resource, $filePath); 286 break; 287 case IMAGETYPE_BMP: 288 $retVal = \imagebmp($this->resource, $filePath, $this->bitDepth); 289 break; 290 default: 291 $retVal = \imagepng($this->resource, $filePath); 292 } 293 return $retVal; 294 } 295 296 /** 297 * Prints the image when called as $image(). 298 */ 299 public function __invoke() { 300 return $this->show(); 301 } 302 303 /** 304 * @return resource Returns the image resource in any. 305 */ 306 public function resource() { 307 return $this->resource; 308 } 309 310 /** 311 * @return null|string Returns the raw image data. 312 */ 313 public function data() { 314 if (!$this->valid()) { 315 return null; 316 } 317 \ob_start(); 318 switch ($this->mimeType) { 319 case "image/png": 320 $res = \imagepng($this->resource); 321 break; 322 case "image/jpeg": 323 $res = \imagejpeg($this->resource); 324 break; 325 case "image/gif": 326 $res = \imagegif($this->resource); 327 break; 328 default: 329 $res = \imagepng($this->resource); 330 $this->logger->info('OC_Image->data. Could not guess mime-type, defaulting to png', ['app' => 'core']); 331 break; 332 } 333 if (!$res) { 334 $this->logger->error('OC_Image->data. Error getting image data.', ['app' => 'core']); 335 } 336 return \ob_get_clean(); 337 } 338 339 /** 340 * @return string - base64 encoded, which is suitable for embedding in a VCard. 341 */ 342 public function __toString() { 343 return \base64_encode($this->data()); 344 } 345 346 /** 347 * (I'm open for suggestions on better method name ;) 348 * Get the orientation based on EXIF data. 349 * 350 * @return int The orientation or -1 if no EXIF data is available. 351 */ 352 public function getOrientation() { 353 if ($this->imageType !== IMAGETYPE_JPEG) { 354 $this->logger->debug('OC_Image->fixOrientation() Image is not a JPEG.', ['app' => 'core']); 355 return -1; 356 } 357 if (!\is_callable('exif_read_data')) { 358 $this->logger->debug('OC_Image->fixOrientation() Exif module not enabled.', ['app' => 'core']); 359 return -1; 360 } 361 if (!$this->valid()) { 362 $this->logger->debug('OC_Image->fixOrientation() No image loaded.', ['app' => 'core']); 363 return -1; 364 } 365 if ($this->filePath === null || !\is_readable($this->filePath)) { 366 $this->logger->debug('OC_Image->fixOrientation() No readable file path set.', ['app' => 'core']); 367 return -1; 368 } 369 $exif = @\exif_read_data($this->filePath, 'IFD0'); 370 if (!$exif) { 371 return -1; 372 } 373 if (!isset($exif['Orientation'])) { 374 return -1; 375 } 376 return $exif['Orientation']; 377 } 378 379 /** 380 * (I'm open for suggestions on better method name ;) 381 * Fixes orientation based on EXIF data. 382 * 383 * @return bool. 384 */ 385 public function fixOrientation() { 386 $o = $this->getOrientation(); 387 $this->logger->debug('OC_Image->fixOrientation() Orientation: ' . $o, ['app' => 'core']); 388 $rotate = 0; 389 $flip = false; 390 switch ($o) { 391 case -1: 392 return false; //Nothing to fix 393 case 1: 394 $rotate = 0; 395 break; 396 case 2: 397 $rotate = 0; 398 $flip = true; 399 break; 400 case 3: 401 $rotate = 180; 402 break; 403 case 4: 404 $rotate = 180; 405 $flip = true; 406 break; 407 case 5: 408 $rotate = 90; 409 $flip = true; 410 break; 411 case 6: 412 $rotate = 270; 413 break; 414 case 7: 415 $rotate = 270; 416 $flip = true; 417 break; 418 case 8: 419 $rotate = 90; 420 break; 421 } 422 if ($flip && \function_exists('imageflip')) { 423 \imageflip($this->resource, IMG_FLIP_HORIZONTAL); 424 } 425 if ($rotate) { 426 $res = \imagerotate($this->resource, $rotate, 0); 427 if ($res) { 428 if (\imagealphablending($res, true)) { 429 if (\imagesavealpha($res, true)) { 430 \imagedestroy($this->resource); 431 $this->resource = $res; 432 return true; 433 } else { 434 $this->logger->debug('OC_Image->fixOrientation() Error during alpha-saving', ['app' => 'core']); 435 return false; 436 } 437 } else { 438 $this->logger->debug('OC_Image->fixOrientation() Error during alpha-blending', ['app' => 'core']); 439 return false; 440 } 441 } else { 442 $this->logger->debug('OC_Image->fixOrientation() Error during orientation fixing', ['app' => 'core']); 443 return false; 444 } 445 } 446 return false; 447 } 448 449 /** 450 * Loads an image from a local file, a base64 encoded string or a resource created by an imagecreate* function. 451 * 452 * @param resource|string $imageRef The path to a local file, a base64 encoded string or a resource created by an imagecreate* function or a file resource (file handle ). 453 * @return resource|false An image resource or false on error 454 */ 455 public function load($imageRef) { 456 if (\is_resource($imageRef)) { 457 if (\get_resource_type($imageRef) == 'gd') { 458 $this->resource = $imageRef; 459 return $this->resource; 460 } elseif (\in_array(\get_resource_type($imageRef), ['file', 'stream'])) { 461 return $this->loadFromFileHandle($imageRef); 462 } 463 } elseif ($this->loadFromBase64($imageRef) !== false) { 464 return $this->resource; 465 } elseif ($this->loadFromFile($imageRef) !== false) { 466 return $this->resource; 467 } elseif ($this->loadFromData($imageRef) !== false) { 468 return $this->resource; 469 } 470 $this->logger->debug(__METHOD__ . '(): could not load anything. Giving up!', ['app' => 'core']); 471 return false; 472 } 473 474 /** 475 * Loads an image from an open file handle. 476 * It is the responsibility of the caller to position the pointer at the correct place and to close the handle again. 477 * 478 * @param resource $handle 479 * @return resource|false An image resource or false on error 480 */ 481 public function loadFromFileHandle($handle) { 482 $contents = \stream_get_contents($handle); 483 if ($this->loadFromData($contents)) { 484 return $this->resource; 485 } 486 return false; 487 } 488 489 /** 490 * Loads an image from a local file. 491 * 492 * @param bool|string $imagePath The path to a local file. 493 * @return bool|resource An image resource or false on error 494 */ 495 public function loadFromFile($imagePath = false) { 496 // exif_imagetype throws "read error!" if file is less than 12 byte 497 if (!@\is_file($imagePath) || !\file_exists($imagePath) || \filesize($imagePath) < 12 || !\is_readable($imagePath)) { 498 return false; 499 } 500 $iType = \exif_imagetype($imagePath); 501 switch ($iType) { 502 case IMAGETYPE_GIF: 503 if (\imagetypes() & IMG_GIF) { 504 $this->resource = \imagecreatefromgif($imagePath); 505 // Preserve transparency 506 \imagealphablending($this->resource, true); 507 \imagesavealpha($this->resource, true); 508 } else { 509 $this->logger->debug('OC_Image->loadFromFile, GIF images not supported: ' . $imagePath, ['app' => 'core']); 510 } 511 break; 512 case IMAGETYPE_JPEG: 513 if (\imagetypes() & IMG_JPG) { 514 $this->resource = \imagecreatefromjpeg($imagePath); 515 } else { 516 $this->logger->debug('OC_Image->loadFromFile, JPG images not supported: ' . $imagePath, ['app' => 'core']); 517 } 518 break; 519 case IMAGETYPE_PNG: 520 if (\imagetypes() & IMG_PNG) { 521 $this->resource = \imagecreatefrompng($imagePath); 522 // Preserve transparency 523 \imagealphablending($this->resource, true); 524 \imagesavealpha($this->resource, true); 525 } else { 526 $this->logger->debug('OC_Image->loadFromFile, PNG images not supported: ' . $imagePath, ['app' => 'core']); 527 } 528 break; 529 case IMAGETYPE_XBM: 530 if (\imagetypes() & IMG_XPM) { 531 $this->resource = \imagecreatefromxbm($imagePath); 532 } else { 533 $this->logger->debug('OC_Image->loadFromFile, XBM/XPM images not supported: ' . $imagePath, ['app' => 'core']); 534 } 535 break; 536 case IMAGETYPE_WBMP: 537 if (\imagetypes() & IMG_WBMP) { 538 $this->resource = \imagecreatefromwbmp($imagePath); 539 } else { 540 $this->logger->debug('OC_Image->loadFromFile, WBMP images not supported: ' . $imagePath, ['app' => 'core']); 541 } 542 break; 543 case IMAGETYPE_BMP: 544 $this->resource = $this->imagecreatefrombmp($imagePath); 545 break; 546 /* 547 case IMAGETYPE_TIFF_II: // (intel byte order) 548 break; 549 case IMAGETYPE_TIFF_MM: // (motorola byte order) 550 break; 551 case IMAGETYPE_JPC: 552 break; 553 case IMAGETYPE_JP2: 554 break; 555 case IMAGETYPE_JPX: 556 break; 557 case IMAGETYPE_JB2: 558 break; 559 case IMAGETYPE_SWC: 560 break; 561 case IMAGETYPE_IFF: 562 break; 563 case IMAGETYPE_ICO: 564 break; 565 case IMAGETYPE_SWF: 566 break; 567 case IMAGETYPE_PSD: 568 break; 569 */ 570 default: 571 572 // this is mostly file created from encrypted file 573 $this->resource = \imagecreatefromstring(\OC\Files\Filesystem::file_get_contents(\OC\Files\Filesystem::getLocalPath($imagePath))); 574 $iType = IMAGETYPE_PNG; 575 $this->logger->debug('OC_Image->loadFromFile, Default', ['app' => 'core']); 576 break; 577 } 578 if ($this->valid()) { 579 $this->imageType = $iType; 580 $this->mimeType = \image_type_to_mime_type($iType); 581 $this->filePath = $imagePath; 582 } 583 return $this->resource; 584 } 585 586 /** 587 * Loads an image from a string of data. 588 * 589 * @param string $str A string of image data as read from a file. 590 * @return bool|resource An image resource or false on error 591 */ 592 public function loadFromData($str) { 593 if (\is_resource($str)) { 594 return false; 595 } 596 $this->resource = @\imagecreatefromstring($str); 597 if ($this->fileInfo) { 598 $this->mimeType = $this->fileInfo->buffer($str); 599 } 600 if (\is_resource($this->resource)) { 601 \imagealphablending($this->resource, false); 602 \imagesavealpha($this->resource, true); 603 } 604 605 if (!$this->resource) { 606 $this->logger->debug('OC_Image->loadFromFile, could not load', ['app' => 'core']); 607 return false; 608 } 609 return $this->resource; 610 } 611 612 /** 613 * Loads an image from a base64 encoded string. 614 * 615 * @param string $str A string base64 encoded string of image data. 616 * @return bool|resource An image resource or false on error 617 */ 618 public function loadFromBase64($str) { 619 if (!\is_string($str)) { 620 return false; 621 } 622 $data = \base64_decode($str); 623 if ($data) { // try to load from string data 624 $this->resource = @\imagecreatefromstring($data); 625 if ($this->fileInfo) { 626 $this->mimeType = $this->fileInfo->buffer($data); 627 } 628 if (!$this->resource) { 629 $this->logger->debug('OC_Image->loadFromBase64, could not load', ['app' => 'core']); 630 return false; 631 } 632 return $this->resource; 633 } else { 634 return false; 635 } 636 } 637 638 /** 639 * Create a new image from file or URL 640 * @param string $fileName <p> 641 * Path to the BMP image. 642 * @return bool|resource an image resource identifier on success, <b>FALSE</b> on errors. 643 */ 644 private function imagecreatefrombmp($fileName) { 645 try { 646 $bmp = new BmpToResource($fileName); 647 $imageHandle = $bmp->toResource(); 648 $imageDetails = $bmp->getHeader(); 649 $this->bitDepth = $imageDetails['bits']; //remember the bit depth for the imagebmp call 650 } catch (\Exception $e) { 651 $this->logger->warning($e->getMessage(), ['app' => 'core']); 652 return false; 653 } 654 655 return $imageHandle; 656 } 657 658 /** 659 * Resizes the image preserving ratio. 660 * 661 * @param integer $maxSize The maximum size of either the width or height. 662 * @return bool 663 */ 664 public function resize($maxSize) { 665 if (!$this->valid()) { 666 $this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']); 667 return false; 668 } 669 $widthOrig = \imagesx($this->resource); 670 $heightOrig = \imagesy($this->resource); 671 $ratioOrig = $widthOrig / $heightOrig; 672 673 if ($ratioOrig > 1) { 674 $newHeight = \round($maxSize / $ratioOrig); 675 $newWidth = $maxSize; 676 } else { 677 $newWidth = \round($maxSize * $ratioOrig); 678 $newHeight = $maxSize; 679 } 680 681 $this->preciseResize(\round($newWidth), \round($newHeight)); 682 return true; 683 } 684 685 /** 686 * @param int $width 687 * @param int $height 688 * @return bool 689 */ 690 public function preciseResize($width, $height) { 691 if (!$this->valid()) { 692 $this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']); 693 return false; 694 } 695 $widthOrig = \imagesx($this->resource); 696 $heightOrig = \imagesy($this->resource); 697 $process = \imagecreatetruecolor($width, $height); 698 699 if ($process == false) { 700 $this->logger->error(__METHOD__ . '(): Error creating true color image', ['app' => 'core']); 701 \imagedestroy($process); 702 return false; 703 } 704 705 // preserve transparency 706 if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) { 707 \imagecolortransparent($process, \imagecolorallocatealpha($process, 0, 0, 0, 127)); 708 \imagealphablending($process, false); 709 \imagesavealpha($process, true); 710 } 711 712 \imagecopyresampled($process, $this->resource, 0, 0, 0, 0, $width, $height, $widthOrig, $heightOrig); 713 if ($process == false) { 714 $this->logger->error(__METHOD__ . '(): Error re-sampling process image', ['app' => 'core']); 715 \imagedestroy($process); 716 return false; 717 } 718 \imagedestroy($this->resource); 719 $this->resource = $process; 720 return true; 721 } 722 723 /** 724 * Crops the image to the middle square. If the image is already square it just returns. 725 * 726 * @param int $size maximum size for the result (optional) 727 * @return bool for success or failure 728 */ 729 public function centerCrop($size = 0) { 730 if (!$this->valid()) { 731 $this->logger->error('OC_Image->centerCrop, No image loaded', ['app' => 'core']); 732 return false; 733 } 734 $widthOrig = \imagesx($this->resource); 735 $heightOrig = \imagesy($this->resource); 736 if ($widthOrig === $heightOrig and $size == 0) { 737 return true; 738 } 739 $ratioOrig = $widthOrig / $heightOrig; 740 $width = $height = \min($widthOrig, $heightOrig); 741 742 if ($ratioOrig > 1) { 743 $x = ($widthOrig / 2) - ($width / 2); 744 $y = 0; 745 } else { 746 $y = ($heightOrig / 2) - ($height / 2); 747 $x = 0; 748 } 749 if ($size > 0) { 750 $targetWidth = $size; 751 $targetHeight = $size; 752 } else { 753 $targetWidth = $width; 754 $targetHeight = $height; 755 } 756 $process = \imagecreatetruecolor($targetWidth, $targetHeight); 757 if ($process == false) { 758 $this->logger->error('OC_Image->centerCrop, Error creating true color image', ['app' => 'core']); 759 \imagedestroy($process); 760 return false; 761 } 762 763 // preserve transparency 764 if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) { 765 \imagecolortransparent($process, \imagecolorallocatealpha($process, 0, 0, 0, 127)); 766 \imagealphablending($process, false); 767 \imagesavealpha($process, true); 768 } 769 770 \imagecopyresampled($process, $this->resource, 0, 0, $x, $y, $targetWidth, $targetHeight, $width, $height); 771 if ($process == false) { 772 $this->logger->error('OC_Image->centerCrop, Error re-sampling process image ' . $width . 'x' . $height, ['app' => 'core']); 773 \imagedestroy($process); 774 return false; 775 } 776 \imagedestroy($this->resource); 777 $this->resource = $process; 778 return true; 779 } 780 781 /** 782 * Crops the image from point $x$y with dimension $wx$h. 783 * 784 * @param int $x Horizontal position 785 * @param int $y Vertical position 786 * @param int $w Width 787 * @param int $h Height 788 * @return bool for success or failure 789 */ 790 public function crop($x, $y, $w, $h) { 791 if (!$this->valid()) { 792 $this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']); 793 return false; 794 } 795 $process = \imagecreatetruecolor($w, $h); 796 if ($process == false) { 797 $this->logger->error(__METHOD__ . '(): Error creating true color image', ['app' => 'core']); 798 \imagedestroy($process); 799 return false; 800 } 801 802 // preserve transparency 803 if ($this->imageType == IMAGETYPE_GIF or $this->imageType == IMAGETYPE_PNG) { 804 \imagecolortransparent($process, \imagecolorallocatealpha($process, 0, 0, 0, 127)); 805 \imagealphablending($process, false); 806 \imagesavealpha($process, true); 807 } 808 809 \imagecopyresampled($process, $this->resource, 0, 0, $x, $y, $w, $h, $w, $h); 810 if ($process == false) { 811 $this->logger->error(__METHOD__ . '(): Error re-sampling process image ' . $w . 'x' . $h, ['app' => 'core']); 812 \imagedestroy($process); 813 return false; 814 } 815 \imagedestroy($this->resource); 816 $this->resource = $process; 817 return true; 818 } 819 820 /** 821 * Resizes the image to fit within a boundary while preserving ratio. 822 * 823 * Warning: Images smaller than $maxWidth x $maxHeight will end up being scaled up 824 * 825 * @param integer $maxWidth 826 * @param integer $maxHeight 827 * @return bool 828 */ 829 public function fitIn($maxWidth, $maxHeight) { 830 if (!$this->valid()) { 831 $this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']); 832 return false; 833 } 834 $widthOrig = \imagesx($this->resource); 835 $heightOrig = \imagesy($this->resource); 836 $ratio = $widthOrig / $heightOrig; 837 838 $newWidth = \min($maxWidth, $ratio * $maxHeight); 839 $newHeight = \min($maxHeight, $maxWidth / $ratio); 840 841 $this->preciseResize(\round($newWidth), \round($newHeight)); 842 return true; 843 } 844 845 /** 846 * Shrinks larger images to fit within specified boundaries while preserving ratio. 847 * 848 * @param integer $maxWidth 849 * @param integer $maxHeight 850 * @return bool 851 */ 852 public function scaleDownToFit($maxWidth, $maxHeight) { 853 if (!$this->valid()) { 854 $this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']); 855 return false; 856 } 857 $widthOrig = \imagesx($this->resource); 858 $heightOrig = \imagesy($this->resource); 859 860 if ($widthOrig > $maxWidth || $heightOrig > $maxHeight) { 861 return $this->fitIn($maxWidth, $maxHeight); 862 } 863 864 return false; 865 } 866 867 /** 868 * Destroys the current image and resets the object 869 */ 870 public function destroy() { 871 if ($this->valid()) { 872 \imagedestroy($this->resource); 873 } 874 $this->resource = null; 875 } 876 877 public function __destruct() { 878 $this->destroy(); 879 } 880} 881 882if (!\function_exists('imagebmp')) { 883 /** 884 * Output a BMP image to either the browser or a file 885 * 886 * @link http://www.ugia.cn/wp-data/imagebmp.php 887 * @author legend <legendsky@hotmail.com> 888 * @link http://www.programmierer-forum.de/imagebmp-gute-funktion-gefunden-t143716.htm 889 * @author mgutt <marc@gutt.it> 890 * @version 1.00 891 * @param resource $im 892 * @param string $fileName [optional] <p>The path to save the file to.</p> 893 * @param int $bit [optional] <p>Bit depth, (default is 24).</p> 894 * @param int $compression [optional] 895 * @return bool <b>TRUE</b> on success or <b>FALSE</b> on failure. 896 */ 897 function imagebmp($im, $fileName = '', $bit = 24, $compression = 0) { 898 if (!\in_array($bit, [1, 4, 8, 16, 24, 32])) { 899 $bit = 24; 900 } elseif ($bit == 32) { 901 $bit = 24; 902 } 903 $bits = \pow(2, $bit); 904 \imagetruecolortopalette($im, true, $bits); 905 $width = \imagesx($im); 906 $height = \imagesy($im); 907 $colorsNum = \imagecolorstotal($im); 908 $rgbQuad = ''; 909 if ($bit <= 8) { 910 for ($i = 0; $i < $colorsNum; $i++) { 911 $colors = \imagecolorsforindex($im, $i); 912 $rgbQuad .= \chr($colors['blue']) . \chr($colors['green']) . \chr($colors['red']) . "\0"; 913 } 914 $bmpData = ''; 915 if ($compression == 0 || $bit < 8) { 916 $compression = 0; 917 $extra = ''; 918 $padding = 4 - \ceil($width / (8 / $bit)) % 4; 919 if ($padding % 4 != 0) { 920 $extra = \str_repeat("\0", $padding); 921 } 922 for ($j = $height - 1; $j >= 0; $j--) { 923 $i = 0; 924 while ($i < $width) { 925 $bin = 0; 926 $limit = $width - $i < 8 / $bit ? (8 / $bit - $width + $i) * $bit : 0; 927 for ($k = 8 - $bit; $k >= $limit; $k -= $bit) { 928 $index = \imagecolorat($im, $i, $j); 929 $bin |= $index << $k; 930 $i++; 931 } 932 $bmpData .= \chr($bin); 933 } 934 $bmpData .= $extra; 935 } 936 } // RLE8 937 elseif ($compression == 1 && $bit == 8) { 938 for ($j = $height - 1; $j >= 0; $j--) { 939 $lastIndex = "\0"; 940 $sameNum = 0; 941 for ($i = 0; $i <= $width; $i++) { 942 $index = \imagecolorat($im, $i, $j); 943 if ($index !== $lastIndex || $sameNum > 255) { 944 if ($sameNum != 0) { 945 $bmpData .= \chr($sameNum) . \chr($lastIndex); 946 } 947 $lastIndex = $index; 948 $sameNum = 1; 949 } else { 950 $sameNum++; 951 } 952 } 953 $bmpData .= "\0\0"; 954 } 955 $bmpData .= "\0\1"; 956 } 957 $sizeQuad = \strlen($rgbQuad); 958 $sizeData = \strlen($bmpData); 959 } else { 960 $extra = ''; 961 $padding = 4 - ($width * ($bit / 8)) % 4; 962 if ($padding % 4 != 0) { 963 $extra = \str_repeat("\0", $padding); 964 } 965 $bmpData = ''; 966 for ($j = $height - 1; $j >= 0; $j--) { 967 for ($i = 0; $i < $width; $i++) { 968 $index = \imagecolorat($im, $i, $j); 969 $colors = \imagecolorsforindex($im, $index); 970 if ($bit == 16) { 971 $bin = 0 << $bit; 972 $bin |= ($colors['red'] >> 3) << 10; 973 $bin |= ($colors['green'] >> 3) << 5; 974 $bin |= $colors['blue'] >> 3; 975 $bmpData .= \pack("v", $bin); 976 } else { 977 $bmpData .= \pack("c*", $colors['blue'], $colors['green'], $colors['red']); 978 } 979 } 980 $bmpData .= $extra; 981 } 982 $sizeQuad = 0; 983 $sizeData = \strlen($bmpData); 984 $colorsNum = 0; 985 } 986 $fileHeader = 'BM' . \pack('V3', 54 + $sizeQuad + $sizeData, 0, 54 + $sizeQuad); 987 $infoHeader = \pack('V3v2V*', 0x28, $width, $height, 1, $bit, $compression, $sizeData, 0, 0, $colorsNum, 0); 988 if ($fileName != '') { 989 $fp = \fopen($fileName, 'wb'); 990 \fwrite($fp, $fileHeader . $infoHeader . $rgbQuad . $bmpData); 991 \fclose($fp); 992 return true; 993 } 994 echo $fileHeader . $infoHeader . $rgbQuad . $bmpData; 995 return true; 996 } 997} 998 999if (!\function_exists('exif_imagetype')) { 1000 /** 1001 * Workaround if exif_imagetype does not exist 1002 * 1003 * @link http://www.php.net/manual/en/function.exif-imagetype.php#80383 1004 * @param string $fileName 1005 * @return string|boolean 1006 */ 1007 function exif_imagetype($fileName) { 1008 if (($info = \getimagesize($fileName)) !== false) { 1009 return $info[2]; 1010 } 1011 return false; 1012 } 1013} 1014