Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
/lib/ -> gdlib.php (source)

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]

   1  <?php
   2  
   3  // This file is part of Moodle - http://moodle.org/
   4  //
   5  // Moodle is free software: you can redistribute it and/or modify
   6  // it under the terms of the GNU General Public License as published by
   7  // the Free Software Foundation, either version 3 of the License, or
   8  // (at your option) any later version.
   9  //
  10  // Moodle is distributed in the hope that it will be useful,
  11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13  // GNU General Public License for more details.
  14  //
  15  // You should have received a copy of the GNU General Public License
  16  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  17  
  18  /**
  19   * gdlib.php - Collection of routines in Moodle related to
  20   * processing images using GD
  21   *
  22   * @package   core
  23   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
  24   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  25   */
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  /**
  30   * Copies a rectangular portion of the source image to another rectangle in the destination image
  31   *
  32   * This function calls imagecopyresampled() if it is available and GD version is 2 at least.
  33   * Otherwise it reimplements the same behaviour. See the PHP manual page for more info.
  34   *
  35   * @link http://php.net/manual/en/function.imagecopyresampled.php
  36   * @param resource $dst_img the destination GD image resource
  37   * @param resource $src_img the source GD image resource
  38   * @param int $dst_x vthe X coordinate of the upper left corner in the destination image
  39   * @param int $dst_y the Y coordinate of the upper left corner in the destination image
  40   * @param int $src_x the X coordinate of the upper left corner in the source image
  41   * @param int $src_y the Y coordinate of the upper left corner in the source image
  42   * @param int $dst_w the width of the destination rectangle
  43   * @param int $dst_h the height of the destination rectangle
  44   * @param int $src_w the width of the source rectangle
  45   * @param int $src_h the height of the source rectangle
  46   * @return bool tru on success, false otherwise
  47   */
  48  function imagecopybicubic($dst_img, $src_img, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h) {
  49      global $CFG;
  50  
  51      if (function_exists('imagecopyresampled')) {
  52         return imagecopyresampled($dst_img, $src_img, $dst_x, $dst_y, $src_x, $src_y,
  53                                   $dst_w, $dst_h, $src_w, $src_h);
  54      }
  55  
  56      $totalcolors = imagecolorstotal($src_img);
  57      for ($i=0; $i<$totalcolors; $i++) {
  58          if ($colors = imagecolorsforindex($src_img, $i)) {
  59              imagecolorallocate($dst_img, $colors['red'], $colors['green'], $colors['blue']);
  60          }
  61      }
  62  
  63      $scalex = ($src_w - 1) / $dst_w;
  64      $scaley = ($src_h - 1) / $dst_h;
  65  
  66      $scalex2 = $scalex / 2.0;
  67      $scaley2 = $scaley / 2.0;
  68  
  69      for ($j = 0; $j < $dst_h; $j++) {
  70          $sy = $j * $scaley;
  71  
  72          for ($i = 0; $i < $dst_w; $i++) {
  73              $sx = $i * $scalex;
  74  
  75              $c1 = imagecolorsforindex($src_img, imagecolorat($src_img, (int)$sx, (int)$sy + $scaley2));
  76              $c2 = imagecolorsforindex($src_img, imagecolorat($src_img, (int)$sx, (int)$sy));
  77              $c3 = imagecolorsforindex($src_img, imagecolorat($src_img, (int)$sx + $scalex2, (int)$sy + $scaley2));
  78              $c4 = imagecolorsforindex($src_img, imagecolorat($src_img, (int)$sx + $scalex2, (int)$sy));
  79  
  80              $red = (int) (($c1['red'] + $c2['red'] + $c3['red'] + $c4['red']) / 4);
  81              $green = (int) (($c1['green'] + $c2['green'] + $c3['green'] + $c4['green']) / 4);
  82              $blue = (int) (($c1['blue'] + $c2['blue'] + $c3['blue'] + $c4['blue']) / 4);
  83  
  84              $color = imagecolorclosest($dst_img, $red, $green, $blue);
  85              imagesetpixel($dst_img, $i + $dst_x, $j + $dst_y, $color);
  86          }
  87      }
  88  }
  89  
  90  /**
  91   * Stores optimised icon images in icon file area.
  92   *
  93   * Since 2.9 this function will generate an icon in the same format as the original file when possible.
  94   * To counter that behaviour, you can use the argument $preferpng to generate a PNG icon.
  95   *
  96   * @param context $context
  97   * @param string $component
  98   * @param string filearea
  99   * @param int $itemid
 100   * @param string $originalfile
 101   * @param boolean $preferpng When true, it will try to generate a PNG file regardless of the original file.
 102   * @return mixed new unique revision number or false if not saved
 103   */
 104  function process_new_icon($context, $component, $filearea, $itemid, $originalfile, $preferpng = false) {
 105      global $CFG;
 106  
 107      if (!is_file($originalfile)) {
 108          return false;
 109      }
 110  
 111      $imageinfo = getimagesize($originalfile);
 112      $imagefnc = '';
 113  
 114      if (empty($imageinfo)) {
 115          return false;
 116      }
 117  
 118      $image = new stdClass();
 119      $image->width  = $imageinfo[0];
 120      $image->height = $imageinfo[1];
 121      $image->type   = $imageinfo[2];
 122  
 123      $t = null;
 124      switch ($image->type) {
 125          case IMAGETYPE_GIF:
 126              if (function_exists('imagecreatefromgif')) {
 127                  $im = imagecreatefromgif($originalfile);
 128              } else {
 129                  debugging('GIF not supported on this server');
 130                  return false;
 131              }
 132              // Guess transparent colour from GIF.
 133              $transparent = imagecolortransparent($im);
 134              if ($transparent != -1) {
 135                  $t = imagecolorsforindex($im, $transparent);
 136              }
 137              break;
 138          case IMAGETYPE_JPEG:
 139              if (function_exists('imagecreatefromjpeg')) {
 140                  $im = imagecreatefromjpeg($originalfile);
 141              } else {
 142                  debugging('JPEG not supported on this server');
 143                  return false;
 144              }
 145              // If the user uploads a jpeg them we should process as a jpeg if possible.
 146              if (!$preferpng && function_exists('imagejpeg')) {
 147                  $imagefnc = 'imagejpeg';
 148                  $imageext = '.jpg';
 149                  $filters = null; // Not used.
 150                  $quality = 90;
 151              }
 152              break;
 153          case IMAGETYPE_PNG:
 154              if (function_exists('imagecreatefrompng')) {
 155                  $im = imagecreatefrompng($originalfile);
 156              } else {
 157                  debugging('PNG not supported on this server');
 158                  return false;
 159              }
 160              break;
 161          default:
 162              return false;
 163      }
 164  
 165      // The conversion has not been decided yet, let's apply defaults (png with fallback to jpg).
 166      if (empty($imagefnc)) {
 167          if (function_exists('imagepng')) {
 168              $imagefnc = 'imagepng';
 169              $imageext = '.png';
 170              $filters = PNG_NO_FILTER;
 171              $quality = 1;
 172          } else if (function_exists('imagejpeg')) {
 173              $imagefnc = 'imagejpeg';
 174              $imageext = '.jpg';
 175              $filters = null; // Not used.
 176              $quality = 90;
 177          } else {
 178              debugging('Jpeg and png not supported on this server, please fix server configuration');
 179              return false;
 180          }
 181      }
 182  
 183      if (function_exists('imagecreatetruecolor')) {
 184          $im1 = imagecreatetruecolor(100, 100);
 185          $im2 = imagecreatetruecolor(35, 35);
 186          $im3 = imagecreatetruecolor(512, 512);
 187          if ($image->type != IMAGETYPE_JPEG and $imagefnc === 'imagepng') {
 188              if ($t) {
 189                  // Transparent GIF hacking...
 190                  $transparentcolour = imagecolorallocate($im1 , $t['red'] , $t['green'] , $t['blue']);
 191                  imagecolortransparent($im1 , $transparentcolour);
 192                  $transparentcolour = imagecolorallocate($im2 , $t['red'] , $t['green'] , $t['blue']);
 193                  imagecolortransparent($im2 , $transparentcolour);
 194                  $transparentcolour = imagecolorallocate($im3 , $t['red'] , $t['green'] , $t['blue']);
 195                  imagecolortransparent($im3 , $transparentcolour);
 196              }
 197  
 198              imagealphablending($im1, false);
 199              $color = imagecolorallocatealpha($im1, 0, 0,  0, 127);
 200              imagefill($im1, 0, 0,  $color);
 201              imagesavealpha($im1, true);
 202  
 203              imagealphablending($im2, false);
 204              $color = imagecolorallocatealpha($im2, 0, 0,  0, 127);
 205              imagefill($im2, 0, 0,  $color);
 206              imagesavealpha($im2, true);
 207  
 208              imagealphablending($im3, false);
 209              $color = imagecolorallocatealpha($im3, 0, 0,  0, 127);
 210              imagefill($im3, 0, 0,  $color);
 211              imagesavealpha($im3, true);
 212          }
 213      } else {
 214          $im1 = imagecreate(100, 100);
 215          $im2 = imagecreate(35, 35);
 216          $im3 = imagecreate(512, 512);
 217      }
 218  
 219      $cx = floor($image->width / 2);
 220      $cy = floor($image->height / 2);
 221  
 222      if ($image->width < $image->height) {
 223          $half = floor($image->width / 2.0);
 224      } else {
 225          $half = floor($image->height / 2.0);
 226      }
 227  
 228      imagecopybicubic($im1, $im, 0, 0, $cx - $half, $cy - $half, 100, 100, $half * 2, $half * 2);
 229      imagecopybicubic($im2, $im, 0, 0, $cx - $half, $cy - $half, 35, 35, $half * 2, $half * 2);
 230      imagecopybicubic($im3, $im, 0, 0, $cx - $half, $cy - $half, 512, 512, $half * 2, $half * 2);
 231  
 232      $fs = get_file_storage();
 233  
 234      $icon = array('contextid'=>$context->id, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>'/');
 235  
 236      if ($imagefnc === 'imagejpeg') {
 237          // Function imagejpeg() accepts less arguments than imagepng() but we need to make $imagefnc accept the same
 238          // number of arguments, otherwise PHP8 throws an error.
 239          $imagefnc = function($image, $filename, $quality, $unused) {
 240              return imagejpeg($image, $filename, $quality);
 241          };
 242      }
 243  
 244      ob_start();
 245      if (!$imagefnc($im1, NULL, $quality, $filters)) {
 246          // keep old icons
 247          ob_end_clean();
 248          return false;
 249      }
 250      $data = ob_get_clean();
 251      imagedestroy($im1);
 252      $icon['filename'] = 'f1'.$imageext;
 253      $fs->delete_area_files($context->id, $component, $filearea, $itemid);
 254      $file1 = $fs->create_file_from_string($icon, $data);
 255  
 256      ob_start();
 257      if (!$imagefnc($im2, NULL, $quality, $filters)) {
 258          ob_end_clean();
 259          $fs->delete_area_files($context->id, $component, $filearea, $itemid);
 260          return false;
 261      }
 262      $data = ob_get_clean();
 263      imagedestroy($im2);
 264      $icon['filename'] = 'f2'.$imageext;
 265      $fs->create_file_from_string($icon, $data);
 266  
 267      ob_start();
 268      if (!$imagefnc($im3, NULL, $quality, $filters)) {
 269          ob_end_clean();
 270          $fs->delete_area_files($context->id, $component, $filearea, $itemid);
 271          return false;
 272      }
 273      $data = ob_get_clean();
 274      imagedestroy($im3);
 275      $icon['filename'] = 'f3'.$imageext;
 276      $fs->create_file_from_string($icon, $data);
 277  
 278      return $file1->get_id();
 279  }
 280  
 281  
 282  /**
 283   * Resize an image from an image path.
 284   *
 285   * This maintains the aspect ratio of the image.
 286   * This will not enlarge the image.
 287   *
 288   * @param string $filepath The full path to the original image file.
 289   * @param int|null $width The max width of the resized image, or null to only use the height.
 290   * @param int|null $height The max height of the resized image, or null to only use the width.
 291   * @param bool $forcecanvas Whether the final dimensions should be set to $width and $height.
 292   * @return string|bool False if a problem occurs, else the resized image data.
 293   */
 294  function resize_image($filepath, $width, $height, $forcecanvas = false) {
 295      if (empty($filepath)) {
 296          return false;
 297      }
 298  
 299      // Fetch the image information for this image.
 300      $imageinfo = @getimagesize($filepath);
 301      if (empty($imageinfo)) {
 302          return false;
 303      }
 304  
 305      // Create a new image from the file.
 306      $original = @imagecreatefromstring(file_get_contents($filepath));
 307  
 308      // Generate the thumbnail.
 309      return resize_image_from_image($original, $imageinfo, $width, $height, $forcecanvas);
 310  }
 311  
 312  /**
 313   * Resize an image from an image object.
 314   *
 315   * @param resource $original The image to work on.
 316   * @param array $imageinfo Contains [0] => originalwidth, [1] => originalheight.
 317   * @param int|null $width The max width of the resized image, or null to only use the height.
 318   * @param int|null $height The max height of the resized image, or null to only use the width.
 319   * @param bool $forcecanvas Whether the final dimensions should be set to $width and $height.
 320   * @return string|bool False if a problem occurs, else the resized image data.
 321   */
 322  function resize_image_from_image($original, $imageinfo, $width, $height, $forcecanvas = false) {
 323      global $CFG;
 324  
 325      if (empty($width) && empty($height) || ($forcecanvas && (empty($width) || empty($height)))) {
 326          // We need do not have the required ddimensions to work with.
 327          return false;
 328      }
 329  
 330      if (empty($imageinfo)) {
 331          return false;
 332      }
 333  
 334      $originalwidth  = $imageinfo[0];
 335      $originalheight = $imageinfo[1];
 336      if (empty($originalwidth) or empty($originalheight)) {
 337          return false;
 338      }
 339  
 340      if (function_exists('imagepng')) {
 341          $imagefnc = 'imagepng';
 342          $filters = PNG_NO_FILTER;
 343          $quality = 1;
 344      } else if (function_exists('imagejpeg')) {
 345          $imagefnc = 'imagejpeg';
 346          $filters = null;
 347          $quality = 90;
 348      } else {
 349          debugging('Neither JPEG nor PNG are supported at this server, please fix the system configuration.');
 350          return false;
 351      }
 352  
 353      if (empty($height)) {
 354          $ratio = $width / $originalwidth;
 355      } else if (empty($width)) {
 356          $ratio = $height / $originalheight;
 357      } else {
 358          $ratio = min($width / $originalwidth, $height / $originalheight);
 359      }
 360  
 361      if ($ratio < 1) {
 362          $targetwidth    = floor($originalwidth * $ratio);
 363          $targetheight   = floor($originalheight * $ratio);
 364      } else {
 365          // Do not enlarge the original file if it is smaller than the requested thumbnail size.
 366          $targetwidth    = $originalwidth;
 367          $targetheight   = $originalheight;
 368      }
 369  
 370      $canvaswidth = $targetwidth;
 371      $canvasheight = $targetheight;
 372      $dstx = 0;
 373      $dsty = 0;
 374  
 375      if ($forcecanvas) {
 376          $canvaswidth = $width;
 377          $canvasheight = $height;
 378          $dstx = floor(($width - $targetwidth) / 2);
 379          $dsty = floor(($height - $targetheight) / 2);
 380      }
 381  
 382      if (function_exists('imagecreatetruecolor')) {
 383          $newimage = imagecreatetruecolor($canvaswidth, $canvasheight);
 384          if ($imagefnc === 'imagepng') {
 385              imagealphablending($newimage, false);
 386              imagefill($newimage, 0, 0, imagecolorallocatealpha($newimage, 0, 0, 0, 127));
 387              imagesavealpha($newimage, true);
 388          }
 389      } else {
 390          $newimage = imagecreate($canvaswidth, $canvasheight);
 391      }
 392  
 393      imagecopybicubic($newimage, $original, $dstx, $dsty, 0, 0, $targetwidth, $targetheight, $originalwidth, $originalheight);
 394  
 395      if ($imagefnc === 'imagejpeg') {
 396          // Function imagejpeg() accepts less arguments than imagepng() but we need to make $imagefnc accept the same
 397          // number of arguments, otherwise PHP8 throws an error.
 398          $imagefnc = function($image, $filename, $quality, $unused) {
 399              return imagejpeg($image, $filename, $quality);
 400          };
 401      }
 402  
 403      // Capture the image as a string object, rather than straight to file.
 404      ob_start();
 405      if (!$imagefnc($newimage, null, $quality, $filters)) {
 406          ob_end_clean();
 407          return false;
 408      }
 409      $data = ob_get_clean();
 410      imagedestroy($original);
 411      imagedestroy($newimage);
 412  
 413      return $data;
 414  }
 415  
 416  /**
 417   * Generates a thumbnail for the given image
 418   *
 419   * If the GD library has at least version 2 and PNG support is available, the returned data
 420   * is the content of a transparent PNG file containing the thumbnail. Otherwise, the function
 421   * returns contents of a JPEG file with black background containing the thumbnail.
 422   *
 423   * @param string $filepath the full path to the original image file
 424   * @param int $width the width of the requested thumbnail
 425   * @param int $height the height of the requested thumbnail
 426   * @return string|bool false if a problem occurs, the thumbnail image data otherwise
 427   */
 428  function generate_image_thumbnail($filepath, $width, $height) {
 429      return resize_image($filepath, $width, $height, true);
 430  }
 431  
 432  /**
 433   * Generates a thumbnail for the given image string.
 434   *
 435   * If the GD library has at least version 2 and PNG support is available, the returned data
 436   * is the content of a transparent PNG file containing the thumbnail. Otherwise, the function
 437   * returns contents of a JPEG file with black background containing the thumbnail.
 438   *
 439   * @param   string $filedata The image content as a string
 440   * @param   int $width the width of the requested thumbnail
 441   * @param   int $height the height of the requested thumbnail
 442   * @return  string|bool false if a problem occurs, the thumbnail image data otherwise
 443   */
 444  function generate_image_thumbnail_from_string($filedata, $width, $height) {
 445      if (empty($filedata) or empty($width) or empty($height)) {
 446          return false;
 447      }
 448  
 449      // Fetch the image information for this image.
 450      $imageinfo = @getimagesizefromstring($filedata);
 451      if (empty($imageinfo)) {
 452          return false;
 453      }
 454  
 455      // Create a new image from the file.
 456      $original = @imagecreatefromstring($filedata);
 457  
 458      // Generate the thumbnail.
 459      return generate_image_thumbnail_from_image($original, $imageinfo, $width, $height);
 460  }
 461  
 462  /**
 463   * Generates a thumbnail for the given image string.
 464   *
 465   * If the GD library has at least version 2 and PNG support is available, the returned data
 466   * is the content of a transparent PNG file containing the thumbnail. Otherwise, the function
 467   * returns contents of a JPEG file with black background containing the thumbnail.
 468   *
 469   * @param   resource $original The image to work on.
 470   * @param   array $imageinfo Contains [0] => originalwidth, [1] => originalheight.
 471   * @param   int $width The width of the requested thumbnail.
 472   * @param   int $height The height of the requested thumbnail.
 473   * @return  string|bool False if a problem occurs, the thumbnail image data otherwise.
 474   */
 475  function generate_image_thumbnail_from_image($original, $imageinfo, $width, $height) {
 476      return resize_image_from_image($original, $imageinfo, $width, $height, true);
 477  }