[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/files/ -> PhabricatorImageTransformer.php (source)

   1  <?php
   2  
   3  /**
   4   * @task enormous Detecting Enormous Images
   5   * @task save     Saving Image Data
   6   */
   7  final class PhabricatorImageTransformer {
   8  
   9    public function executeMemeTransform(
  10      PhabricatorFile $file,
  11      $upper_text,
  12      $lower_text) {
  13      $image = $this->applyMemeToFile($file, $upper_text, $lower_text);
  14      return PhabricatorFile::newFromFileData(
  15        $image,
  16        array(
  17          'name' => 'meme-'.$file->getName(),
  18          'ttl' => time() + 60 * 60 * 24,
  19          'canCDN' => true,
  20        ));
  21    }
  22  
  23    public function executeThumbTransform(
  24      PhabricatorFile $file,
  25      $x,
  26      $y) {
  27  
  28      $image = $this->crudelyScaleTo($file, $x, $y);
  29  
  30      return PhabricatorFile::newFromFileData(
  31        $image,
  32        array(
  33          'name' => 'thumb-'.$file->getName(),
  34          'canCDN' => true,
  35        ));
  36    }
  37  
  38    public function executeProfileTransform(
  39      PhabricatorFile $file,
  40      $x,
  41      $min_y,
  42      $max_y) {
  43  
  44      $image = $this->crudelyCropTo($file, $x, $min_y, $max_y);
  45  
  46      return PhabricatorFile::newFromFileData(
  47        $image,
  48        array(
  49          'name' => 'profile-'.$file->getName(),
  50          'canCDN' => true,
  51        ));
  52    }
  53  
  54    public function executePreviewTransform(
  55      PhabricatorFile $file,
  56      $size) {
  57  
  58      $image = $this->generatePreview($file, $size);
  59  
  60      return PhabricatorFile::newFromFileData(
  61        $image,
  62        array(
  63          'name' => 'preview-'.$file->getName(),
  64          'canCDN' => true,
  65        ));
  66    }
  67  
  68    public function executeConpherenceTransform(
  69      PhabricatorFile $file,
  70      $top,
  71      $left,
  72      $width,
  73      $height) {
  74  
  75      $image = $this->crasslyCropTo(
  76        $file,
  77        $top,
  78        $left,
  79        $width,
  80        $height);
  81  
  82      return PhabricatorFile::newFromFileData(
  83        $image,
  84        array(
  85          'name' => 'conpherence-'.$file->getName(),
  86          'canCDN' => true,
  87        ));
  88    }
  89  
  90    private function crudelyCropTo(PhabricatorFile $file, $x, $min_y, $max_y) {
  91      $data = $file->loadFileData();
  92      $img = imagecreatefromstring($data);
  93      $sx = imagesx($img);
  94      $sy = imagesy($img);
  95  
  96      $scaled_y = ($x / $sx) * $sy;
  97      if ($scaled_y > $max_y) {
  98        // This image is very tall and thin.
  99        $scaled_y = $max_y;
 100      } else if ($scaled_y < $min_y) {
 101        // This image is very short and wide.
 102        $scaled_y = $min_y;
 103      }
 104  
 105      $cropped = $this->applyScaleWithImagemagick($file, $x, $scaled_y);
 106      if ($cropped != null) {
 107        return $cropped;
 108      }
 109  
 110      $img = $this->applyScaleTo(
 111        $file,
 112        $x,
 113        $scaled_y);
 114  
 115      return self::saveImageDataInAnyFormat($img, $file->getMimeType());
 116    }
 117  
 118    private function crasslyCropTo(PhabricatorFile $file, $top, $left, $w, $h) {
 119      $data = $file->loadFileData();
 120      $src = imagecreatefromstring($data);
 121      $dst = $this->getBlankDestinationFile($w, $h);
 122  
 123      $scale = self::getScaleForCrop($file, $w, $h);
 124      $orig_x = $left / $scale;
 125      $orig_y = $top / $scale;
 126      $orig_w = $w / $scale;
 127      $orig_h = $h / $scale;
 128  
 129      imagecopyresampled(
 130        $dst,
 131        $src,
 132        0, 0,
 133        $orig_x, $orig_y,
 134        $w, $h,
 135        $orig_w, $orig_h);
 136  
 137      return self::saveImageDataInAnyFormat($dst, $file->getMimeType());
 138    }
 139  
 140  
 141    /**
 142     * Very crudely scale an image up or down to an exact size.
 143     */
 144    private function crudelyScaleTo(PhabricatorFile $file, $dx, $dy) {
 145      $scaled = $this->applyScaleWithImagemagick($file, $dx, $dy);
 146  
 147      if ($scaled != null) {
 148        return $scaled;
 149      }
 150  
 151      $dst = $this->applyScaleTo($file, $dx, $dy);
 152      return self::saveImageDataInAnyFormat($dst, $file->getMimeType());
 153    }
 154  
 155    private function getBlankDestinationFile($dx, $dy) {
 156      $dst = imagecreatetruecolor($dx, $dy);
 157      imagesavealpha($dst, true);
 158      imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127));
 159  
 160      return $dst;
 161    }
 162  
 163    private function applyScaleTo(PhabricatorFile $file, $dx, $dy) {
 164      $data = $file->loadFileData();
 165      $src = imagecreatefromstring($data);
 166  
 167      $x = imagesx($src);
 168      $y = imagesy($src);
 169  
 170      $scale = min(($dx / $x), ($dy / $y), 1);
 171  
 172      $sdx = $scale * $x;
 173      $sdy = $scale * $y;
 174  
 175      $dst = $this->getBlankDestinationFile($dx, $dy);
 176      imagesavealpha($dst, true);
 177      imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127));
 178  
 179      imagecopyresampled(
 180        $dst,
 181        $src,
 182        ($dx - $sdx) / 2,  ($dy - $sdy) / 2,
 183        0, 0,
 184        $sdx, $sdy,
 185        $x, $y);
 186  
 187      return $dst;
 188  
 189    }
 190  
 191    public static function getPreviewDimensions(PhabricatorFile $file, $size) {
 192      $metadata = $file->getMetadata();
 193      $x = idx($metadata, PhabricatorFile::METADATA_IMAGE_WIDTH);
 194      $y = idx($metadata, PhabricatorFile::METADATA_IMAGE_HEIGHT);
 195  
 196      if (!$x || !$y) {
 197        $data = $file->loadFileData();
 198        $src = imagecreatefromstring($data);
 199  
 200        $x = imagesx($src);
 201        $y = imagesy($src);
 202      }
 203  
 204      $scale = min($size / $x, $size / $y, 1);
 205  
 206      $dx = max($size / 4, $scale * $x);
 207      $dy = max($size / 4, $scale * $y);
 208  
 209      $sdx = $scale * $x;
 210      $sdy = $scale * $y;
 211  
 212      return array(
 213        'x' => $x,
 214        'y' => $y,
 215        'dx' => $dx,
 216        'dy' => $dy,
 217        'sdx' => $sdx,
 218        'sdy' => $sdy,
 219      );
 220    }
 221  
 222    public static function getScaleForCrop(
 223      PhabricatorFile $file,
 224      $des_width,
 225      $des_height) {
 226  
 227      $metadata = $file->getMetadata();
 228      $width = $metadata[PhabricatorFile::METADATA_IMAGE_WIDTH];
 229      $height = $metadata[PhabricatorFile::METADATA_IMAGE_HEIGHT];
 230  
 231      if ($height < $des_height) {
 232        $scale = $height / $des_height;
 233      } else if ($width < $des_width) {
 234        $scale = $width / $des_width;
 235      } else {
 236        $scale_x = $des_width / $width;
 237        $scale_y = $des_height / $height;
 238        $scale = max($scale_x, $scale_y);
 239      }
 240  
 241      return $scale;
 242    }
 243  
 244    private function generatePreview(PhabricatorFile $file, $size) {
 245      $data = $file->loadFileData();
 246      $src = imagecreatefromstring($data);
 247  
 248      $dimensions = self::getPreviewDimensions($file, $size);
 249      $x = $dimensions['x'];
 250      $y = $dimensions['y'];
 251      $dx = $dimensions['dx'];
 252      $dy = $dimensions['dy'];
 253      $sdx = $dimensions['sdx'];
 254      $sdy = $dimensions['sdy'];
 255  
 256      $dst = $this->getBlankDestinationFile($dx, $dy);
 257  
 258      imagecopyresampled(
 259        $dst,
 260        $src,
 261        ($dx - $sdx) / 2, ($dy - $sdy) / 2,
 262        0, 0,
 263        $sdx, $sdy,
 264        $x, $y);
 265  
 266      return self::saveImageDataInAnyFormat($dst, $file->getMimeType());
 267    }
 268  
 269    private function applyMemeToFile(
 270      PhabricatorFile $file,
 271      $upper_text,
 272      $lower_text) {
 273      $data = $file->loadFileData();
 274  
 275      $img_type = $file->getMimeType();
 276      $imagemagick = PhabricatorEnv::getEnvConfig('files.enable-imagemagick');
 277  
 278      if ($img_type != 'image/gif' || $imagemagick == false) {
 279        return $this->applyMemeTo(
 280          $data, $upper_text, $lower_text, $img_type);
 281      }
 282  
 283      $data = $file->loadFileData();
 284      $input = new TempFile();
 285      Filesystem::writeFile($input, $data);
 286  
 287      list($out) = execx('convert %s info:', $input);
 288      $split = phutil_split_lines($out);
 289      if (count($split) > 1) {
 290        return $this->applyMemeWithImagemagick(
 291          $input,
 292          $upper_text,
 293          $lower_text,
 294          count($split),
 295          $img_type);
 296      } else {
 297        return $this->applyMemeTo($data, $upper_text, $lower_text, $img_type);
 298      }
 299    }
 300  
 301    private function applyMemeTo(
 302      $data,
 303      $upper_text,
 304      $lower_text,
 305      $mime_type) {
 306      $img = imagecreatefromstring($data);
 307  
 308      // Some PNGs have color palettes, and allocating the dark border color
 309      // fails and gives us whatever's first in the color table. Copy the image
 310      // to a fresh truecolor canvas before working with it.
 311  
 312      $truecolor = imagecreatetruecolor(imagesx($img), imagesy($img));
 313      imagecopy($truecolor, $img, 0, 0, 0, 0, imagesx($img), imagesy($img));
 314      $img = $truecolor;
 315  
 316      $phabricator_root = dirname(phutil_get_library_root('phabricator'));
 317      $font_root = $phabricator_root.'/resources/font/';
 318      $font_path = $font_root.'tuffy.ttf';
 319      if (Filesystem::pathExists($font_root.'impact.ttf')) {
 320        $font_path = $font_root.'impact.ttf';
 321      }
 322      $text_color = imagecolorallocate($img, 255, 255, 255);
 323      $border_color = imagecolorallocatealpha($img, 0, 0, 0, 110);
 324      $border_width = 4;
 325      $font_max = 200;
 326      $font_min = 5;
 327      for ($i = $font_max; $i > $font_min; $i--) {
 328        $fit = $this->doesTextBoundingBoxFitInImage(
 329          $img,
 330          $upper_text,
 331          $i,
 332          $font_path);
 333        if ($fit['doesfit']) {
 334          $x = ($fit['imgwidth'] - $fit['txtwidth']) / 2;
 335          $y = $fit['txtheight'] + 10;
 336          $this->makeImageWithTextBorder($img,
 337            $i,
 338            $x,
 339            $y,
 340            $text_color,
 341            $border_color,
 342            $border_width,
 343            $font_path,
 344            $upper_text);
 345          break;
 346        }
 347      }
 348      for ($i = $font_max; $i > $font_min; $i--) {
 349        $fit = $this->doesTextBoundingBoxFitInImage($img,
 350          $lower_text, $i, $font_path);
 351        if ($fit['doesfit']) {
 352          $x = ($fit['imgwidth'] - $fit['txtwidth']) / 2;
 353          $y = $fit['imgheight'] - 10;
 354          $this->makeImageWithTextBorder(
 355            $img,
 356            $i,
 357            $x,
 358            $y,
 359            $text_color,
 360            $border_color,
 361            $border_width,
 362            $font_path,
 363            $lower_text);
 364          break;
 365        }
 366      }
 367      return self::saveImageDataInAnyFormat($img, $mime_type);
 368    }
 369  
 370    private function makeImageWithTextBorder($img, $font_size, $x, $y,
 371      $color, $stroke_color, $bw, $font, $text) {
 372      $angle = 0;
 373      $bw = abs($bw);
 374      for ($c1 = $x - $bw; $c1 <= $x + $bw; $c1++) {
 375        for ($c2 = $y - $bw; $c2 <= $y + $bw; $c2++) {
 376          if (!(($c1 == $x - $bw || $x + $bw) &&
 377            $c2 == $y - $bw || $c2 == $y + $bw)) {
 378            $bg = imagettftext($img, $font_size,
 379              $angle, $c1, $c2, $stroke_color, $font, $text);
 380            }
 381          }
 382        }
 383      imagettftext($img, $font_size, $angle,
 384              $x , $y, $color , $font, $text);
 385    }
 386  
 387    private function doesTextBoundingBoxFitInImage($img,
 388      $text, $font_size, $font_path) {
 389      // Default Angle = 0
 390      $angle = 0;
 391  
 392      $bbox = imagettfbbox($font_size, $angle, $font_path, $text);
 393      $text_height = abs($bbox[3] - $bbox[5]);
 394      $text_width = abs($bbox[0] - $bbox[2]);
 395      return array(
 396        'doesfit' => ($text_height * 1.05 <= imagesy($img) / 2
 397          && $text_width * 1.05 <= imagesx($img)),
 398        'txtwidth' => $text_width,
 399        'txtheight' => $text_height,
 400        'imgwidth' => imagesx($img),
 401        'imgheight' => imagesy($img),
 402      );
 403    }
 404  
 405    private function applyScaleWithImagemagick(PhabricatorFile $file, $dx, $dy) {
 406      $img_type = $file->getMimeType();
 407      $imagemagick = PhabricatorEnv::getEnvConfig('files.enable-imagemagick');
 408  
 409      if ($img_type != 'image/gif' || $imagemagick == false) {
 410        return null;
 411      }
 412  
 413      $data = $file->loadFileData();
 414      $src = imagecreatefromstring($data);
 415  
 416      $x = imagesx($src);
 417      $y = imagesy($src);
 418  
 419      if (self::isEnormousGIF($x, $y)) {
 420        return null;
 421      }
 422  
 423      $scale = min(($dx / $x), ($dy / $y), 1);
 424  
 425      $sdx = $scale * $x;
 426      $sdy = $scale * $y;
 427  
 428      $input = new TempFile();
 429      Filesystem::writeFile($input, $data);
 430  
 431      $resized = new TempFile();
 432  
 433      $future = new ExecFuture(
 434        'convert %s -coalesce -resize %sX%s%s %s',
 435        $input,
 436        $sdx,
 437        $sdy,
 438        '!',
 439        $resized);
 440  
 441      // Don't spend more than 10 seconds resizing; just fail if it takes longer
 442      // than that.
 443      $future->setTimeout(10)->resolvex();
 444  
 445      return Filesystem::readFile($resized);
 446    }
 447  
 448    private function applyMemeWithImagemagick(
 449      $input,
 450      $above,
 451      $below,
 452      $count,
 453      $img_type) {
 454  
 455      $output = new TempFile();
 456      $future = new ExecFuture(
 457        'convert %s -coalesce +adjoin %s_%s',
 458        $input,
 459        $input,
 460        '%09d');
 461      $future->setTimeout(10)->resolvex();
 462  
 463      $output_files = array();
 464      for ($ii = 0; $ii < $count; $ii++) {
 465        $frame_name = sprintf('%s_%09d', $input, $ii);
 466        $output_name = sprintf('%s_%09d', $output, $ii);
 467  
 468        $output_files[] = $output_name;
 469  
 470        $frame_data = Filesystem::readFile($frame_name);
 471        $memed_frame_data = $this->applyMemeTo(
 472          $frame_data,
 473          $above,
 474          $below,
 475          $img_type);
 476        Filesystem::writeFile($output_name, $memed_frame_data);
 477      }
 478  
 479      $future = new ExecFuture('convert -loop 0 %Ls %s', $output_files, $output);
 480      $future->setTimeout(10)->resolvex();
 481  
 482      return Filesystem::readFile($output);
 483    }
 484  
 485  /* -(  Detecting Enormous Files  )------------------------------------------- */
 486  
 487  
 488    /**
 489     * Determine if an image is enormous (too large to transform).
 490     *
 491     * Attackers can perform a denial of service attack by uploading highly
 492     * compressible images with enormous dimensions but a very small filesize.
 493     * Transforming them (e.g., into thumbnails) may consume huge quantities of
 494     * memory and CPU relative to the resources required to transmit the file.
 495     *
 496     * In general, we respond to these images by declining to transform them, and
 497     * using a default thumbnail instead.
 498     *
 499     * @param int Width of the image, in pixels.
 500     * @param int Height of the image, in pixels.
 501     * @return bool True if this image is enormous (too large to transform).
 502     * @task enormous
 503     */
 504    public static function isEnormousImage($x, $y) {
 505      // This is just a sanity check, but if we don't have valid dimensions we
 506      // shouldn't be trying to transform the file.
 507      if (($x <= 0) || ($y <= 0)) {
 508        return true;
 509      }
 510  
 511      return ($x * $y) > (4096 * 4096);
 512    }
 513  
 514  
 515    /**
 516     * Determine if a GIF is enormous (too large to transform).
 517     *
 518     * For discussion, see @{method:isEnormousImage}. We need to be more
 519     * careful about GIFs, because they can also have a large number of frames
 520     * despite having a very small filesize. We're more conservative about
 521     * calling GIFs enormous than about calling images in general enormous.
 522     *
 523     * @param int Width of the GIF, in pixels.
 524     * @param int Height of the GIF, in pixels.
 525     * @return bool True if this image is enormous (too large to transform).
 526     * @task enormous
 527     */
 528    public static function isEnormousGIF($x, $y) {
 529      if (self::isEnormousImage($x, $y)) {
 530        return true;
 531      }
 532  
 533      return ($x * $y) > (800 * 800);
 534    }
 535  
 536  
 537  /* -(  Saving Image Data  )-------------------------------------------------- */
 538  
 539  
 540    /**
 541     * Save an image resource to a string representation suitable for storage or
 542     * transmission as an image file.
 543     *
 544     * Optionally, you can specify a preferred MIME type like `"image/png"`.
 545     * Generally, you should specify the MIME type of the original file if you're
 546     * applying file transformations. The MIME type may not be honored if
 547     * Phabricator can not encode images in the given format (based on available
 548     * extensions), but can save images in another format.
 549     *
 550     * @param   resource  GD image resource.
 551     * @param   string?   Optionally, preferred mime type.
 552     * @return  string    Bytes of an image file.
 553     * @task save
 554     */
 555    public static function saveImageDataInAnyFormat($data, $preferred_mime = '') {
 556      $preferred = null;
 557      switch ($preferred_mime) {
 558        case 'image/gif':
 559          $preferred = self::saveImageDataAsGIF($data);
 560          break;
 561        case 'image/png':
 562          $preferred = self::saveImageDataAsPNG($data);
 563          break;
 564      }
 565  
 566      if ($preferred !== null) {
 567        return $preferred;
 568      }
 569  
 570      $data = self::saveImageDataAsJPG($data);
 571      if ($data !== null) {
 572        return $data;
 573      }
 574  
 575      $data = self::saveImageDataAsPNG($data);
 576      if ($data !== null) {
 577        return $data;
 578      }
 579  
 580      $data = self::saveImageDataAsGIF($data);
 581      if ($data !== null) {
 582        return $data;
 583      }
 584  
 585      throw new Exception(pht('Failed to save image data into any format.'));
 586    }
 587  
 588  
 589    /**
 590     * Save an image in PNG format, returning the file data as a string.
 591     *
 592     * @param resource      GD image resource.
 593     * @return string|null  PNG file as a string, or null on failure.
 594     * @task save
 595     */
 596    private static function saveImageDataAsPNG($image) {
 597      if (!function_exists('imagepng')) {
 598        return null;
 599      }
 600  
 601      ob_start();
 602      $result = imagepng($image, null, 9);
 603      $output = ob_get_clean();
 604  
 605      if (!$result) {
 606        return null;
 607      }
 608  
 609      return $output;
 610    }
 611  
 612  
 613    /**
 614     * Save an image in GIF format, returning the file data as a string.
 615     *
 616     * @param resource      GD image resource.
 617     * @return string|null  GIF file as a string, or null on failure.
 618     * @task save
 619     */
 620    private static function saveImageDataAsGIF($image) {
 621      if (!function_exists('imagegif')) {
 622        return null;
 623      }
 624  
 625      ob_start();
 626      $result = imagegif($image);
 627      $output = ob_get_clean();
 628  
 629      if (!$result) {
 630        return null;
 631      }
 632  
 633      return $output;
 634    }
 635  
 636  
 637    /**
 638     * Save an image in JPG format, returning the file data as a string.
 639     *
 640     * @param resource      GD image resource.
 641     * @return string|null  JPG file as a string, or null on failure.
 642     * @task save
 643     */
 644    private static function saveImageDataAsJPG($image) {
 645      if (!function_exists('imagejpeg')) {
 646        return null;
 647      }
 648  
 649      ob_start();
 650      $result = imagejpeg($image);
 651      $output = ob_get_clean();
 652  
 653      if (!$result) {
 654        return null;
 655      }
 656  
 657      return $output;
 658    }
 659  
 660  
 661  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1