Correct a parameter order swap in "diffusion.historyquery" for Mercurial
[phabricator.git] / src / applications / files / transform / PhabricatorFileImageTransform.php
blob468eae0e03d3e09bc2b1781d1e9a5cd0f6c824c8
1 <?php
3 abstract class PhabricatorFileImageTransform extends PhabricatorFileTransform {
5 private $file;
6 private $data;
7 private $image;
8 private $imageX;
9 private $imageY;
11 /**
12 * Get an estimate of the transformed dimensions of a file.
14 * @param PhabricatorFile File to transform.
15 * @return list<int, int>|null Width and height, if available.
17 public function getTransformedDimensions(PhabricatorFile $file) {
18 return null;
21 public function canApplyTransform(PhabricatorFile $file) {
22 if (!$file->isViewableImage()) {
23 return false;
26 if (!$file->isTransformableImage()) {
27 return false;
30 return true;
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;
41 protected function getFileProperties() {
42 return array();
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) {
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);
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);
63 $off_x = ($dst_w - $cpy_w) / 2;
64 $off_y = ($dst_h - $cpy_h) / 2;
66 if ($this->shouldUseImagemagick()) {
67 $argv = array();
68 $argv[] = '-coalesce';
69 $argv[] = '-shave';
70 $argv[] = $src_x.'x'.$src_y;
71 $argv[] = '-resize';
73 if ($scale_up) {
74 $argv[] = $dst_w.'x'.$dst_h;
75 } else {
76 $argv[] = $dst_w.'x'.$dst_h.'>';
79 $argv[] = '-bordercolor';
80 $argv[] = 'rgba(255, 255, 255, 0)';
81 $argv[] = '-border';
82 $argv[] = $off_x.'x'.$off_y;
84 return $this->applyImagemagick($argv);
87 $src = $this->getImage();
88 $dst = $this->newEmptyImage($dst_w, $dst_h);
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();
101 if ($ok === false) {
102 throw new Exception(
103 pht(
104 'Failed to imagecopyresampled() image: %s',
105 $errors));
108 $data = PhabricatorImageTransformer::saveImageDataInAnyFormat(
109 $dst,
110 $this->file->getMimeType());
112 return $this->newFileFromData($data);
115 protected function applyImagemagick(array $argv) {
116 $tmp = new TempFile();
117 Filesystem::writeFile($tmp, $this->getData());
119 $out = new TempFile();
121 $future = new ExecFuture('convert %s %Ls %s', $tmp, $argv, $out);
122 // Don't spend more than 60 seconds resizing; just fail if it takes longer
123 // than that.
124 $future->setTimeout(60)->resolvex();
126 $data = Filesystem::readFile($out);
128 return $this->newFileFromData($data);
133 * Create a new @{class:PhabricatorFile} from raw data.
135 * @param string Raw file data.
137 protected function newFileFromData($data) {
138 if ($this->file) {
139 $name = $this->file->getName();
140 } else {
141 $name = 'default.png';
144 $defaults = array(
145 'canCDN' => true,
146 'name' => $this->getTransformKey().'-'.$name,
149 $properties = $this->getFileProperties() + $defaults;
151 return PhabricatorFile::newFromFileData($data, $properties);
156 * Create a new image filled with transparent pixels.
158 * @param int Desired image width.
159 * @param int Desired image height.
160 * @return resource New image resource.
162 protected function newEmptyImage($w, $h) {
163 $w = (int)$w;
164 $h = (int)$h;
166 if (($w <= 0) || ($h <= 0)) {
167 throw new Exception(
168 pht('Can not create an image with nonpositive dimensions.'));
171 $trap = new PhutilErrorTrap();
172 $img = @imagecreatetruecolor($w, $h);
173 $errors = $trap->getErrorsAsString();
174 $trap->destroy();
175 if ($img === false) {
176 throw new Exception(
177 pht(
178 'Unable to imagecreatetruecolor() a new empty image: %s',
179 $errors));
182 $trap = new PhutilErrorTrap();
183 $ok = @imagesavealpha($img, true);
184 $errors = $trap->getErrorsAsString();
185 $trap->destroy();
186 if ($ok === false) {
187 throw new Exception(
188 pht(
189 'Unable to imagesavealpha() a new empty image: %s',
190 $errors));
193 $trap = new PhutilErrorTrap();
194 $color = @imagecolorallocatealpha($img, 255, 255, 255, 127);
195 $errors = $trap->getErrorsAsString();
196 $trap->destroy();
197 if ($color === false) {
198 throw new Exception(
199 pht(
200 'Unable to imagecolorallocatealpha() a new empty image: %s',
201 $errors));
204 $trap = new PhutilErrorTrap();
205 $ok = @imagefill($img, 0, 0, $color);
206 $errors = $trap->getErrorsAsString();
207 $trap->destroy();
208 if ($ok === false) {
209 throw new Exception(
210 pht(
211 'Unable to imagefill() a new empty image: %s',
212 $errors));
215 return $img;
220 * Get the pixel dimensions of the image being transformed.
222 * @return list<int, int> Width and height of the image.
224 protected function getImageDimensions() {
225 if ($this->imageX === null) {
226 $image = $this->getImage();
228 $trap = new PhutilErrorTrap();
229 $x = @imagesx($image);
230 $y = @imagesy($image);
231 $errors = $trap->getErrorsAsString();
232 $trap->destroy();
234 if (($x === false) || ($y === false) || ($x <= 0) || ($y <= 0)) {
235 throw new Exception(
236 pht(
237 'Unable to determine image dimensions with '.
238 'imagesx()/imagesy(): %s',
239 $errors));
242 $this->imageX = $x;
243 $this->imageY = $y;
246 return array($this->imageX, $this->imageY);
251 * Get the raw file data for the image being transformed.
253 * @return string Raw file data.
255 protected function getData() {
256 if ($this->data !== null) {
257 return $this->data;
260 $file = $this->file;
262 $max_size = (1024 * 1024 * 16);
263 $img_size = $file->getByteSize();
264 if ($img_size > $max_size) {
265 throw new Exception(
266 pht(
267 'This image is too large to transform. The transform limit is %s '.
268 'bytes, but the image size is %s bytes.',
269 new PhutilNumber($max_size),
270 new PhutilNumber($img_size)));
273 $data = $file->loadFileData();
274 $this->data = $data;
275 return $this->data;
280 * Get the GD image resource for the image being transformed.
282 * @return resource GD image resource.
284 protected function getImage() {
285 if ($this->image !== null) {
286 return $this->image;
289 if (!function_exists('imagecreatefromstring')) {
290 throw new Exception(
291 pht(
292 'Unable to transform image: the imagecreatefromstring() function '.
293 'is not available. Install or enable the "gd" extension for PHP.'));
296 $data = $this->getData();
297 $data = (string)$data;
299 // First, we're going to write the file to disk and use getimagesize()
300 // to determine its dimensions without actually loading the pixel data
301 // into memory. For very large images, we'll bail out.
303 // In particular, this defuses a resource exhaustion attack where the
304 // attacker uploads a 40,000 x 40,000 pixel PNGs of solid white. These
305 // kinds of files compress extremely well, but require a huge amount
306 // of memory and CPU to process.
308 $tmp = new TempFile();
309 Filesystem::writeFile($tmp, $data);
310 $tmp_path = (string)$tmp;
312 $trap = new PhutilErrorTrap();
313 $info = @getimagesize($tmp_path);
314 $errors = $trap->getErrorsAsString();
315 $trap->destroy();
317 unset($tmp);
319 if ($info === false) {
320 throw new Exception(
321 pht(
322 'Unable to get image information with getimagesize(): %s',
323 $errors));
326 list($width, $height) = $info;
327 if (($width <= 0) || ($height <= 0)) {
328 throw new Exception(
329 pht(
330 'Unable to determine image width and height with getimagesize().'));
333 $max_pixels = (4096 * 4096);
334 $img_pixels = ($width * $height);
336 if ($img_pixels > $max_pixels) {
337 throw new Exception(
338 pht(
339 'This image (with dimensions %spx x %spx) is too large to '.
340 'transform. The image has %s pixels, but transforms are limited '.
341 'to images with %s or fewer pixels.',
342 new PhutilNumber($width),
343 new PhutilNumber($height),
344 new PhutilNumber($img_pixels),
345 new PhutilNumber($max_pixels)));
348 $trap = new PhutilErrorTrap();
349 $image = @imagecreatefromstring($data);
350 $errors = $trap->getErrorsAsString();
351 $trap->destroy();
353 if ($image === false) {
354 throw new Exception(
355 pht(
356 'Unable to load image data with imagecreatefromstring(): %s',
357 $errors));
360 $this->image = $image;
361 return $this->image;
364 private function shouldUseImagemagick() {
365 if (!PhabricatorEnv::getEnvConfig('files.enable-imagemagick')) {
366 return false;
369 if ($this->file->getMimeType() != 'image/gif') {
370 return false;
373 // Don't try to preserve the animation in huge GIFs.
374 list($x, $y) = $this->getImageDimensions();
375 if (($x * $y) > (512 * 512)) {
376 return false;
379 return true;