[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |