@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at upstream/main 399 lines 10 kB view raw
1<?php 2 3abstract class PhabricatorFileImageTransform extends PhabricatorFileTransform { 4 5 private $file; 6 private $data; 7 private $image; 8 private $imageX; 9 private $imageY; 10 11 /** 12 * Get an estimate of the transformed dimensions of a file. 13 * 14 * @param PhabricatorFile $file File to transform. 15 * @return list<int, int>|null Width and height, if available. 16 */ 17 public function getTransformedDimensions(PhabricatorFile $file) { 18 return null; 19 } 20 21 public function canApplyTransform(PhabricatorFile $file) { 22 if (!$file->isViewableImage()) { 23 return false; 24 } 25 26 if (!$file->isTransformableImage()) { 27 return false; 28 } 29 30 return true; 31 } 32 33 protected function willTransformFile(PhabricatorFile $file) { 34 $this->file = $file; 35 $this->data = null; 36 $this->image = null; 37 $this->imageX = null; 38 $this->imageY = null; 39 } 40 41 protected function getFileProperties() { 42 return array(); 43 } 44 45 protected function applyCropAndScale( 46 $dst_w, $dst_h, 47 $src_x, $src_y, 48 $src_w, $src_h, 49 $use_w, $use_h, 50 $scale_up) { 51 52 // Figure out the effective destination width, height, and offsets. 53 $cpy_w = min($dst_w, $use_w); 54 $cpy_h = min($dst_h, $use_h); 55 56 // If we aren't scaling up, and are copying a very small source image, 57 // we're just going to center it in the destination image. 58 if (!$scale_up) { 59 $cpy_w = min($cpy_w, $src_w); 60 $cpy_h = min($cpy_h, $src_h); 61 } 62 63 $off_x = ($dst_w - $cpy_w) / 2; 64 $off_y = ($dst_h - $cpy_h) / 2; 65 66 if ($this->shouldUseImagemagick()) { 67 $argv = array(); 68 $argv[] = '-coalesce'; 69 $argv[] = '-shave'; 70 $argv[] = $src_x.'x'.$src_y; 71 $argv[] = '-resize'; 72 73 if ($scale_up) { 74 $argv[] = $dst_w.'x'.$dst_h; 75 } else { 76 $argv[] = $dst_w.'x'.$dst_h.'>'; 77 } 78 79 $argv[] = '-bordercolor'; 80 $argv[] = 'rgba(255, 255, 255, 0)'; 81 $argv[] = '-border'; 82 $argv[] = $off_x.'x'.$off_y; 83 84 return $this->applyImagemagick($argv); 85 } 86 87 $src = $this->getImage(); 88 $dst = $this->newEmptyImage($dst_w, $dst_h); 89 90 $trap = new PhutilErrorTrap(); 91 $ok = @imagecopyresampled( 92 $dst, 93 $src, 94 $off_x, $off_y, 95 $src_x, $src_y, 96 $cpy_w, $cpy_h, 97 $src_w, $src_h); 98 $errors = $trap->getErrorsAsString(); 99 $trap->destroy(); 100 101 if ($ok === false) { 102 throw new Exception( 103 pht( 104 'Failed to imagecopyresampled() image: %s', 105 $errors)); 106 } 107 108 $data = PhabricatorImageTransformer::saveImageDataInAnyFormat( 109 $dst, 110 $this->file->getMimeType()); 111 112 return $this->newFileFromData($data); 113 } 114 115 protected function applyImagemagick(array $argv) { 116 $tmp = new TempFile(); 117 Filesystem::writeFile($tmp, $this->getData()); 118 119 $out = new TempFile(); 120 121 $binary = id(new PhabricatorImagemagickSetupCheck()) 122 ->getImageMagickBinaryName(); 123 124 $future = new ExecFuture('%s %s %Ls %s', $binary, $tmp, $argv, $out); 125 // Don't spend more than 60 seconds resizing; just fail if it takes longer 126 // than that. 127 $future->setTimeout(60)->resolvex(); 128 129 $data = Filesystem::readFile($out); 130 131 return $this->newFileFromData($data); 132 } 133 134 135 /** 136 * Create a new @{class:PhabricatorFile} from raw data. 137 * 138 * @param string $data Raw file data. 139 */ 140 protected function newFileFromData($data) { 141 if ($this->file) { 142 $name = $this->file->getName(); 143 $inherit_properties = array( 144 'viewPolicy' => $this->file->getViewPolicy(), 145 ); 146 } else { 147 $name = 'default.png'; 148 $inherit_properties = array(); 149 } 150 151 $defaults = array( 152 'canCDN' => true, 153 'name' => $this->getTransformKey().'-'.$name, 154 ); 155 156 $properties = $this->getFileProperties() + $inherit_properties + $defaults; 157 158 return PhabricatorFile::newFromFileData($data, $properties); 159 } 160 161 162 /** 163 * Create a new image filled with transparent pixels. 164 * 165 * @param int $w Desired image width. 166 * @param int $h Desired image height. 167 * @return GdImage|resource New GD image resource. 168 */ 169 protected function newEmptyImage($w, $h) { 170 $w = (int)$w; 171 $h = (int)$h; 172 173 if (($w <= 0) || ($h <= 0)) { 174 throw new Exception( 175 pht('Can not create an image with nonpositive dimensions.')); 176 } 177 178 $trap = new PhutilErrorTrap(); 179 $img = @imagecreatetruecolor($w, $h); 180 $errors = $trap->getErrorsAsString(); 181 $trap->destroy(); 182 if ($img === false) { 183 throw new Exception( 184 pht( 185 'Unable to imagecreatetruecolor() a new empty image: %s', 186 $errors)); 187 } 188 189 $trap = new PhutilErrorTrap(); 190 $ok = @imagesavealpha($img, true); 191 $errors = $trap->getErrorsAsString(); 192 $trap->destroy(); 193 if ($ok === false) { 194 throw new Exception( 195 pht( 196 'Unable to imagesavealpha() a new empty image: %s', 197 $errors)); 198 } 199 200 $trap = new PhutilErrorTrap(); 201 $color = @imagecolorallocatealpha($img, 255, 255, 255, 127); 202 $errors = $trap->getErrorsAsString(); 203 $trap->destroy(); 204 if ($color === false) { 205 throw new Exception( 206 pht( 207 'Unable to imagecolorallocatealpha() a new empty image: %s', 208 $errors)); 209 } 210 211 $trap = new PhutilErrorTrap(); 212 $ok = @imagefill($img, 0, 0, $color); 213 $errors = $trap->getErrorsAsString(); 214 $trap->destroy(); 215 if ($ok === false) { 216 throw new Exception( 217 pht( 218 'Unable to imagefill() a new empty image: %s', 219 $errors)); 220 } 221 222 return $img; 223 } 224 225 226 /** 227 * Get the pixel dimensions of the image being transformed. 228 * 229 * @return list<int, int> Width and height of the image. 230 */ 231 protected function getImageDimensions() { 232 if ($this->imageX === null) { 233 $image = $this->getImage(); 234 235 $trap = new PhutilErrorTrap(); 236 $x = @imagesx($image); 237 $y = @imagesy($image); 238 $errors = $trap->getErrorsAsString(); 239 $trap->destroy(); 240 241 if (($x === false) || ($y === false) || ($x <= 0) || ($y <= 0)) { 242 throw new Exception( 243 pht( 244 'Unable to determine image dimensions with '. 245 'imagesx()/imagesy(): %s', 246 $errors)); 247 } 248 249 $this->imageX = $x; 250 $this->imageY = $y; 251 } 252 253 return array($this->imageX, $this->imageY); 254 } 255 256 257 /** 258 * Get the raw file data for the image being transformed. 259 * 260 * @return string Raw file data. 261 */ 262 protected function getData() { 263 if ($this->data !== null) { 264 return $this->data; 265 } 266 267 $file = $this->file; 268 269 $max_size = (1024 * 1024 * 16); 270 $img_size = $file->getByteSize(); 271 if ($img_size > $max_size) { 272 throw new Exception( 273 pht( 274 'This image is too large to transform. The transform limit is %s '. 275 'bytes, but the image size is %s bytes.', 276 new PhutilNumber($max_size), 277 new PhutilNumber($img_size))); 278 } 279 280 $data = $file->loadFileData(); 281 $this->data = $data; 282 return $this->data; 283 } 284 285 286 /** 287 * Get the GD image resource for the image being transformed. 288 * 289 * @return GdImage|resource GD image resource. 290 */ 291 protected function getImage() { 292 if ($this->image !== null) { 293 return $this->image; 294 } 295 296 if (!function_exists('imagecreatefromstring')) { 297 throw new Exception( 298 pht( 299 'Unable to transform image: the imagecreatefromstring() function '. 300 'is not available. Install or enable the "gd" extension for PHP.')); 301 } 302 303 $data = $this->getData(); 304 $data = (string)$data; 305 306 // First, we're going to write the file to disk and use getimagesize() 307 // to determine its dimensions without actually loading the pixel data 308 // into memory. For very large images, we'll bail out. 309 310 // In particular, this defuses a resource exhaustion attack where the 311 // attacker uploads a 40,000 x 40,000 pixel PNGs of solid white. These 312 // kinds of files compress extremely well, but require a huge amount 313 // of memory and CPU to process. 314 315 $tmp = new TempFile(); 316 Filesystem::writeFile($tmp, $data); 317 $tmp_path = (string)$tmp; 318 319 $trap = new PhutilErrorTrap(); 320 $info = @getimagesize($tmp_path); 321 $errors = $trap->getErrorsAsString(); 322 $trap->destroy(); 323 324 unset($tmp); 325 326 if ($info === false) { 327 throw new Exception( 328 pht( 329 'Unable to get image information with getimagesize(): %s', 330 $errors)); 331 } 332 333 list($width, $height) = $info; 334 if (($width <= 0) || ($height <= 0)) { 335 throw new Exception( 336 pht( 337 'Unable to determine image width and height with getimagesize().')); 338 } 339 340 $max_pixels_array = $this->getMaxTransformDimensions(); 341 $max_pixels = ($max_pixels_array[0] * $max_pixels_array[1]); 342 $img_pixels = ($width * $height); 343 344 if ($img_pixels > $max_pixels) { 345 throw new Exception( 346 pht( 347 'This image (with dimensions %spx x %spx) is too large to '. 348 'transform. The image has %s pixels, but transforms are limited '. 349 'to images with %s or fewer pixels.', 350 new PhutilNumber($width), 351 new PhutilNumber($height), 352 new PhutilNumber($img_pixels), 353 new PhutilNumber($max_pixels))); 354 } 355 356 $trap = new PhutilErrorTrap(); 357 $image = @imagecreatefromstring($data); 358 $errors = $trap->getErrorsAsString(); 359 $trap->destroy(); 360 361 if ($image === false) { 362 throw new Exception( 363 pht( 364 'Unable to load image data with imagecreatefromstring(): %s', 365 $errors)); 366 } 367 368 $this->image = $image; 369 return $this->image; 370 } 371 372 /** 373 * Get maximum supported image dimensions in pixels for transforming 374 * 375 * @return array<int> Maximum width and height 376 */ 377 public function getMaxTransformDimensions() { 378 return array(8160, 6144); 379 } 380 381 private function shouldUseImagemagick() { 382 if (!PhabricatorEnv::getEnvConfig('files.enable-imagemagick')) { 383 return false; 384 } 385 386 if ($this->file->getMimeType() != 'image/gif') { 387 return false; 388 } 389 390 // Don't try to preserve the animation in huge GIFs. 391 list($x, $y) = $this->getImageDimensions(); 392 if (($x * $y) > (512 * 512)) { 393 return false; 394 } 395 396 return true; 397 } 398 399}