Path: blob/master/src/applications/files/transform/PhabricatorFileImageTransform.php
12242 views
<?php12abstract class PhabricatorFileImageTransform extends PhabricatorFileTransform {34private $file;5private $data;6private $image;7private $imageX;8private $imageY;910/**11* Get an estimate of the transformed dimensions of a file.12*13* @param PhabricatorFile File to transform.14* @return list<int, int>|null Width and height, if available.15*/16public function getTransformedDimensions(PhabricatorFile $file) {17return null;18}1920public function canApplyTransform(PhabricatorFile $file) {21if (!$file->isViewableImage()) {22return false;23}2425if (!$file->isTransformableImage()) {26return false;27}2829return true;30}3132protected function willTransformFile(PhabricatorFile $file) {33$this->file = $file;34$this->data = null;35$this->image = null;36$this->imageX = null;37$this->imageY = null;38}3940protected function getFileProperties() {41return array();42}4344protected function applyCropAndScale(45$dst_w, $dst_h,46$src_x, $src_y,47$src_w, $src_h,48$use_w, $use_h,49$scale_up) {5051// Figure out the effective destination width, height, and offsets.52$cpy_w = min($dst_w, $use_w);53$cpy_h = min($dst_h, $use_h);5455// If we aren't scaling up, and are copying a very small source image,56// we're just going to center it in the destination image.57if (!$scale_up) {58$cpy_w = min($cpy_w, $src_w);59$cpy_h = min($cpy_h, $src_h);60}6162$off_x = ($dst_w - $cpy_w) / 2;63$off_y = ($dst_h - $cpy_h) / 2;6465if ($this->shouldUseImagemagick()) {66$argv = array();67$argv[] = '-coalesce';68$argv[] = '-shave';69$argv[] = $src_x.'x'.$src_y;70$argv[] = '-resize';7172if ($scale_up) {73$argv[] = $dst_w.'x'.$dst_h;74} else {75$argv[] = $dst_w.'x'.$dst_h.'>';76}7778$argv[] = '-bordercolor';79$argv[] = 'rgba(255, 255, 255, 0)';80$argv[] = '-border';81$argv[] = $off_x.'x'.$off_y;8283return $this->applyImagemagick($argv);84}8586$src = $this->getImage();87$dst = $this->newEmptyImage($dst_w, $dst_h);8889$trap = new PhutilErrorTrap();90$ok = @imagecopyresampled(91$dst,92$src,93$off_x, $off_y,94$src_x, $src_y,95$cpy_w, $cpy_h,96$src_w, $src_h);97$errors = $trap->getErrorsAsString();98$trap->destroy();99100if ($ok === false) {101throw new Exception(102pht(103'Failed to imagecopyresampled() image: %s',104$errors));105}106107$data = PhabricatorImageTransformer::saveImageDataInAnyFormat(108$dst,109$this->file->getMimeType());110111return $this->newFileFromData($data);112}113114protected function applyImagemagick(array $argv) {115$tmp = new TempFile();116Filesystem::writeFile($tmp, $this->getData());117118$out = new TempFile();119120$future = new ExecFuture('convert %s %Ls %s', $tmp, $argv, $out);121// Don't spend more than 60 seconds resizing; just fail if it takes longer122// than that.123$future->setTimeout(60)->resolvex();124125$data = Filesystem::readFile($out);126127return $this->newFileFromData($data);128}129130131/**132* Create a new @{class:PhabricatorFile} from raw data.133*134* @param string Raw file data.135*/136protected function newFileFromData($data) {137if ($this->file) {138$name = $this->file->getName();139} else {140$name = 'default.png';141}142143$defaults = array(144'canCDN' => true,145'name' => $this->getTransformKey().'-'.$name,146);147148$properties = $this->getFileProperties() + $defaults;149150return PhabricatorFile::newFromFileData($data, $properties);151}152153154/**155* Create a new image filled with transparent pixels.156*157* @param int Desired image width.158* @param int Desired image height.159* @return resource New image resource.160*/161protected function newEmptyImage($w, $h) {162$w = (int)$w;163$h = (int)$h;164165if (($w <= 0) || ($h <= 0)) {166throw new Exception(167pht('Can not create an image with nonpositive dimensions.'));168}169170$trap = new PhutilErrorTrap();171$img = @imagecreatetruecolor($w, $h);172$errors = $trap->getErrorsAsString();173$trap->destroy();174if ($img === false) {175throw new Exception(176pht(177'Unable to imagecreatetruecolor() a new empty image: %s',178$errors));179}180181$trap = new PhutilErrorTrap();182$ok = @imagesavealpha($img, true);183$errors = $trap->getErrorsAsString();184$trap->destroy();185if ($ok === false) {186throw new Exception(187pht(188'Unable to imagesavealpha() a new empty image: %s',189$errors));190}191192$trap = new PhutilErrorTrap();193$color = @imagecolorallocatealpha($img, 255, 255, 255, 127);194$errors = $trap->getErrorsAsString();195$trap->destroy();196if ($color === false) {197throw new Exception(198pht(199'Unable to imagecolorallocatealpha() a new empty image: %s',200$errors));201}202203$trap = new PhutilErrorTrap();204$ok = @imagefill($img, 0, 0, $color);205$errors = $trap->getErrorsAsString();206$trap->destroy();207if ($ok === false) {208throw new Exception(209pht(210'Unable to imagefill() a new empty image: %s',211$errors));212}213214return $img;215}216217218/**219* Get the pixel dimensions of the image being transformed.220*221* @return list<int, int> Width and height of the image.222*/223protected function getImageDimensions() {224if ($this->imageX === null) {225$image = $this->getImage();226227$trap = new PhutilErrorTrap();228$x = @imagesx($image);229$y = @imagesy($image);230$errors = $trap->getErrorsAsString();231$trap->destroy();232233if (($x === false) || ($y === false) || ($x <= 0) || ($y <= 0)) {234throw new Exception(235pht(236'Unable to determine image dimensions with '.237'imagesx()/imagesy(): %s',238$errors));239}240241$this->imageX = $x;242$this->imageY = $y;243}244245return array($this->imageX, $this->imageY);246}247248249/**250* Get the raw file data for the image being transformed.251*252* @return string Raw file data.253*/254protected function getData() {255if ($this->data !== null) {256return $this->data;257}258259$file = $this->file;260261$max_size = (1024 * 1024 * 16);262$img_size = $file->getByteSize();263if ($img_size > $max_size) {264throw new Exception(265pht(266'This image is too large to transform. The transform limit is %s '.267'bytes, but the image size is %s bytes.',268new PhutilNumber($max_size),269new PhutilNumber($img_size)));270}271272$data = $file->loadFileData();273$this->data = $data;274return $this->data;275}276277278/**279* Get the GD image resource for the image being transformed.280*281* @return resource GD image resource.282*/283protected function getImage() {284if ($this->image !== null) {285return $this->image;286}287288if (!function_exists('imagecreatefromstring')) {289throw new Exception(290pht(291'Unable to transform image: the imagecreatefromstring() function '.292'is not available. Install or enable the "gd" extension for PHP.'));293}294295$data = $this->getData();296$data = (string)$data;297298// First, we're going to write the file to disk and use getimagesize()299// to determine its dimensions without actually loading the pixel data300// into memory. For very large images, we'll bail out.301302// In particular, this defuses a resource exhaustion attack where the303// attacker uploads a 40,000 x 40,000 pixel PNGs of solid white. These304// kinds of files compress extremely well, but require a huge amount305// of memory and CPU to process.306307$tmp = new TempFile();308Filesystem::writeFile($tmp, $data);309$tmp_path = (string)$tmp;310311$trap = new PhutilErrorTrap();312$info = @getimagesize($tmp_path);313$errors = $trap->getErrorsAsString();314$trap->destroy();315316unset($tmp);317318if ($info === false) {319throw new Exception(320pht(321'Unable to get image information with getimagesize(): %s',322$errors));323}324325list($width, $height) = $info;326if (($width <= 0) || ($height <= 0)) {327throw new Exception(328pht(329'Unable to determine image width and height with getimagesize().'));330}331332$max_pixels = (4096 * 4096);333$img_pixels = ($width * $height);334335if ($img_pixels > $max_pixels) {336throw new Exception(337pht(338'This image (with dimensions %spx x %spx) is too large to '.339'transform. The image has %s pixels, but transforms are limited '.340'to images with %s or fewer pixels.',341new PhutilNumber($width),342new PhutilNumber($height),343new PhutilNumber($img_pixels),344new PhutilNumber($max_pixels)));345}346347$trap = new PhutilErrorTrap();348$image = @imagecreatefromstring($data);349$errors = $trap->getErrorsAsString();350$trap->destroy();351352if ($image === false) {353throw new Exception(354pht(355'Unable to load image data with imagecreatefromstring(): %s',356$errors));357}358359$this->image = $image;360return $this->image;361}362363private function shouldUseImagemagick() {364if (!PhabricatorEnv::getEnvConfig('files.enable-imagemagick')) {365return false;366}367368if ($this->file->getMimeType() != 'image/gif') {369return false;370}371372// Don't try to preserve the animation in huge GIFs.373list($x, $y) = $this->getImageDimensions();374if (($x * $y) > (512 * 512)) {375return false;376}377378return true;379}380381}382383384