@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
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}