diff --git a/src/applications/files/transform/PhabricatorFileImageTransform.php b/src/applications/files/transform/PhabricatorFileImageTransform.php index ae5cbad305..4878b9e2c7 100644 --- a/src/applications/files/transform/PhabricatorFileImageTransform.php +++ b/src/applications/files/transform/PhabricatorFileImageTransform.php @@ -1,301 +1,302 @@ isViewableImage()) { return false; } if (!$file->isTransformableImage()) { return false; } return true; } protected function willTransformFile(PhabricatorFile $file) { $this->file = $file; $this->data = null; $this->image = null; $this->imageX = null; $this->imageY = null; } protected function applyCropAndScale( $dst_w, $dst_h, $src_x, $src_y, - $src_w, $src_h) { + $src_w, $src_h, + $use_w, $use_h) { // Figure out the effective destination width, height, and offsets. We // never want to scale images up, so if we're copying a very small source // image we're just going to center it in the destination image. - $cpy_w = min($dst_w, $src_w); - $cpy_h = min($dst_h, $src_h); + $cpy_w = min($dst_w, $src_w, $use_w); + $cpy_h = min($dst_h, $src_h, $use_h); $off_x = ($dst_w - $cpy_w) / 2; $off_y = ($dst_h - $cpy_h) / 2; // TODO: Support imagemagick for animated GIFs. $src = $this->getImage(); $dst = $this->newEmptyImage($dst_w, $dst_h); $trap = new PhutilErrorTrap(); $ok = @imagecopyresampled( $dst, $src, $off_x, $off_y, $src_x, $src_y, $cpy_w, $cpy_h, $src_w, $src_h); $errors = $trap->getErrorsAsString(); $trap->destroy(); if ($ok === false) { throw new Exception( pht( 'Failed to imagecopyresampled() image: %s', $errors)); } $data = PhabricatorImageTransformer::saveImageDataInAnyFormat( $dst, $this->file->getMimeType()); return $this->newFileFromData($data); } /** * Create a new @{class:PhabricatorFile} from raw data. * * @param string Raw file data. */ protected function newFileFromData($data) { $name = $this->getTransformKey().'-'.$this->file->getName(); return PhabricatorFile::newFromFileData( $data, array( 'name' => $name, 'canCDN' => true, )); } /** * Create a new image filled with transparent pixels. * * @param int Desired image width. * @param int Desired image height. * @return resource New image resource. */ protected function newEmptyImage($w, $h) { $w = (int)$w; $h = (int)$h; if (($w <= 0) || ($h <= 0)) { throw new Exception( pht('Can not create an image with nonpositive dimensions.')); } $trap = new PhutilErrorTrap(); $img = @imagecreatetruecolor($w, $h); $errors = $trap->getErrorsAsString(); $trap->destroy(); if ($img === false) { throw new Exception( pht( 'Unable to imagecreatetruecolor() a new empty image: %s', $errors)); } $trap = new PhutilErrorTrap(); $ok = @imagesavealpha($img, true); $errors = $trap->getErrorsAsString(); $trap->destroy(); if ($ok === false) { throw new Exception( pht( 'Unable to imagesavealpha() a new empty image: %s', $errors)); } $trap = new PhutilErrorTrap(); $color = @imagecolorallocatealpha($img, 255, 255, 255, 127); $errors = $trap->getErrorsAsString(); $trap->destroy(); if ($color === false) { throw new Exception( pht( 'Unable to imagecolorallocatealpha() a new empty image: %s', $errors)); } $trap = new PhutilErrorTrap(); $ok = @imagefill($img, 0, 0, $color); $errors = $trap->getErrorsAsString(); $trap->destroy(); if ($ok === false) { throw new Exception( pht( 'Unable to imagefill() a new empty image: %s', $errors)); } return $img; } /** * Get the pixel dimensions of the image being transformed. * * @return list Width and height of the image. */ protected function getImageDimensions() { if ($this->imageX === null) { $image = $this->getImage(); $trap = new PhutilErrorTrap(); $x = @imagesx($image); $y = @imagesy($image); $errors = $trap->getErrorsAsString(); $trap->destroy(); if (($x === false) || ($y === false) || ($x <= 0) || ($y <= 0)) { throw new Exception( pht( 'Unable to determine image dimensions with '. 'imagesx()/imagesy(): %s', $errors)); } $this->imageX = $x; $this->imageY = $y; } return array($this->imageX, $this->imageY); } /** * Get the raw file data for the image being transformed. * * @return string Raw file data. */ protected function getData() { if ($this->data !== null) { return $this->data; } $file = $this->file; $max_size = (1024 * 1024 * 4); $img_size = $file->getByteSize(); if ($img_size > $max_size) { throw new Exception( pht( 'This image is too large to transform. The transform limit is %s '. 'bytes, but the image size is %s bytes.', new PhutilNumber($max_size), new PhutilNumber($img_size))); } $data = $file->loadFileData(); $this->data = $data; return $this->data; } /** * Get the GD image resource for the image being transformed. * * @return resource GD image resource. */ protected function getImage() { if ($this->image !== null) { return $this->image; } if (!function_exists('imagecreatefromstring')) { throw new Exception( pht( 'Unable to transform image: the imagecreatefromstring() function '. 'is not available. Install or enable the "gd" extension for PHP.')); } $data = $this->getData(); $data = (string)$data; // First, we're going to write the file to disk and use getimagesize() // to determine its dimensions without actually loading the pixel data // into memory. For very large images, we'll bail out. // In particular, this defuses a resource exhaustion attack where the // attacker uploads a 40,000 x 40,000 pixel PNGs of solid white. These // kinds of files compress extremely well, but require a huge amount // of memory and CPU to process. $tmp = new TempFile(); Filesystem::writeFile($tmp, $data); $tmp_path = (string)$tmp; $trap = new PhutilErrorTrap(); $info = @getimagesize($tmp_path); $errors = $trap->getErrorsAsString(); $trap->destroy(); unset($tmp); if ($info === false) { throw new Exception( pht( 'Unable to get image information with getimagesize(): %s', $errors)); } list($width, $height) = $info; if (($width <= 0) || ($height <= 0)) { throw new Exception( pht( 'Unable to determine image width and height with getimagesize().')); } $max_pixels = (4096 * 4096); $img_pixels = ($width * $height); if ($img_pixels > $max_pixels) { throw new Exception( pht( 'This image (with dimensions %spx x %spx) is too large to '. 'transform. The image has %s pixels, but transforms are limited '. 'to images with %s or fewer pixels.', new PhutilNumber($width), new PhutilNumber($height), new PhutilNumber($img_pixels), new PhutilNumber($max_pixels))); } $trap = new PhutilErrorTrap(); $image = @imagecreatefromstring($data); $errors = $trap->getErrorsAsString(); $trap->destroy(); if ($image === false) { throw new Exception( pht( 'Unable to load image data with imagecreatefromstring(): %s', $errors)); } $this->image = $image; return $this->image; } } diff --git a/src/applications/files/transform/PhabricatorFileThumbnailTransform.php b/src/applications/files/transform/PhabricatorFileThumbnailTransform.php index 2921869c2c..45d7ad670a 100644 --- a/src/applications/files/transform/PhabricatorFileThumbnailTransform.php +++ b/src/applications/files/transform/PhabricatorFileThumbnailTransform.php @@ -1,110 +1,140 @@ name = $name; return $this; } public function setKey($key) { $this->key = $key; return $this; } public function setDimensions($x, $y) { $this->dstX = $x; $this->dstY = $y; return $this; } public function getTransformName() { return $this->name; } public function getTransformKey() { return $this->key; } public function generateTransforms() { return array( id(new PhabricatorFileThumbnailTransform()) ->setName(pht("Profile (100px \xC3\x97 100px)")) ->setKey(self::TRANSFORM_PROFILE) ->setDimensions(100, 100), id(new PhabricatorFileThumbnailTransform()) ->setName(pht("Pinboard (280px \xC3\x97 210px)")) ->setKey(self::TRANSFORM_PINBOARD) ->setDimensions(280, 210), id(new PhabricatorFileThumbnailTransform()) ->setName(pht('Thumbgrid (100px)')) ->setKey(self::TRANSFORM_THUMBGRID) ->setDimensions(100, null), id(new PhabricatorFileThumbnailTransform()) ->setName(pht('Preview (220px)')) ->setKey(self::TRANSFORM_PREVIEW) ->setDimensions(220, null), ); } public function applyTransform(PhabricatorFile $file) { - $xformer = new PhabricatorImageTransformer(); - if ($this->dstY === null) { - return $xformer->executePreviewTransform($file, $this->dstX); - } - $this->willTransformFile($file); list($src_x, $src_y) = $this->getImageDimensions(); $dst_x = $this->dstX; $dst_y = $this->dstY; - // Figure out how much we'd have to scale the image down along each - // dimension to get the entire thing to fit. - $scale_x = min(($dst_x / $src_x), 1); - $scale_y = min(($dst_y / $src_y), 1); - - if ($scale_x > $scale_y) { - // This image is relatively tall and narrow. We're going to crop off the - // top and bottom. + if ($dst_y === null) { + // If we only have one dimension, it represents a maximum dimension. + // The other dimension of the transform is scaled appropriately, except + // that we never generate images with crazily extreme aspect ratios. + if ($src_x < $src_y) { + // This is a tall, narrow image. Use the maximum dimension for the + // height and scale the width. + $use_y = $dst_x; + $dst_y = $dst_x; + + $use_x = $dst_y * ($src_x / $src_y); + $dst_x = max($dst_y / 4, $use_x); + } else { + // This is a short, wide image. Use the maximum dimension for the width + // and scale the height. + $use_x = $dst_x; + + $use_y = $dst_x * ($src_y / $src_x); + $dst_y = max($dst_x / 4, $use_y); + } + + // In this mode, we always copy the entire source image. We may generate + // margins in the output. $copy_x = $src_x; - $copy_y = min($src_y, $dst_y / $scale_x); - } else { - // This image is relatively short and wide. We're going to crop off the - // left and right. - $copy_x = min($src_x, $dst_x / $scale_y); $copy_y = $src_y; + } else { + // Otherwise, both dimensions are fixed. Figure out how much we'd have to + // scale the image down along each dimension to get the entire thing to + // fit. + $scale_x = min(($dst_x / $src_x), 1); + $scale_y = min(($dst_y / $src_y), 1); + + if ($scale_x > $scale_y) { + // This image is relatively tall and narrow. We're going to crop off the + // top and bottom. + $copy_x = $src_x; + $copy_y = min($src_y, $dst_y / $scale_x); + } else { + // This image is relatively short and wide. We're going to crop off the + // left and right. + $copy_x = min($src_x, $dst_x / $scale_y); + $copy_y = $src_y; + } + + // In this mode, we always use the entire destination image. We may + // crop the source input. + $use_x = $dst_x; + $use_y = $dst_y; } return $this->applyCropAndScale( $dst_x, $dst_y, ($src_x - $copy_x) / 2, ($src_y - $copy_y) / 2, $copy_x, - $copy_y); + $copy_y, + $use_x, + $use_y); } public function getDefaultTransform(PhabricatorFile $file) { $x = (int)$this->dstX; $y = (int)$this->dstY; $name = 'image-'.$x.'x'.nonempty($y, $x).'.png'; $root = dirname(phutil_get_library_root('phabricator')); $data = Filesystem::readFile($root.'/resources/builtin/'.$name); return $this->newFileFromData($data); } }