Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/files/transform/PhabricatorFileImageTransform.php
12242 views
1
<?php
2
3
abstract 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 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
$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();
125
126
$data = Filesystem::readFile($out);
127
128
return $this->newFileFromData($data);
129
}
130
131
132
/**
133
* Create a new @{class:PhabricatorFile} from raw data.
134
*
135
* @param string Raw file data.
136
*/
137
protected function newFileFromData($data) {
138
if ($this->file) {
139
$name = $this->file->getName();
140
} else {
141
$name = 'default.png';
142
}
143
144
$defaults = array(
145
'canCDN' => true,
146
'name' => $this->getTransformKey().'-'.$name,
147
);
148
149
$properties = $this->getFileProperties() + $defaults;
150
151
return PhabricatorFile::newFromFileData($data, $properties);
152
}
153
154
155
/**
156
* Create a new image filled with transparent pixels.
157
*
158
* @param int Desired image width.
159
* @param int Desired image height.
160
* @return resource New image resource.
161
*/
162
protected function newEmptyImage($w, $h) {
163
$w = (int)$w;
164
$h = (int)$h;
165
166
if (($w <= 0) || ($h <= 0)) {
167
throw new Exception(
168
pht('Can not create an image with nonpositive dimensions.'));
169
}
170
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));
180
}
181
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));
191
}
192
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));
202
}
203
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));
213
}
214
215
return $img;
216
}
217
218
219
/**
220
* Get the pixel dimensions of the image being transformed.
221
*
222
* @return list<int, int> Width and height of the image.
223
*/
224
protected function getImageDimensions() {
225
if ($this->imageX === null) {
226
$image = $this->getImage();
227
228
$trap = new PhutilErrorTrap();
229
$x = @imagesx($image);
230
$y = @imagesy($image);
231
$errors = $trap->getErrorsAsString();
232
$trap->destroy();
233
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));
240
}
241
242
$this->imageX = $x;
243
$this->imageY = $y;
244
}
245
246
return array($this->imageX, $this->imageY);
247
}
248
249
250
/**
251
* Get the raw file data for the image being transformed.
252
*
253
* @return string Raw file data.
254
*/
255
protected function getData() {
256
if ($this->data !== null) {
257
return $this->data;
258
}
259
260
$file = $this->file;
261
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)));
271
}
272
273
$data = $file->loadFileData();
274
$this->data = $data;
275
return $this->data;
276
}
277
278
279
/**
280
* Get the GD image resource for the image being transformed.
281
*
282
* @return resource GD image resource.
283
*/
284
protected function getImage() {
285
if ($this->image !== null) {
286
return $this->image;
287
}
288
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.'));
294
}
295
296
$data = $this->getData();
297
$data = (string)$data;
298
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.
302
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.
307
308
$tmp = new TempFile();
309
Filesystem::writeFile($tmp, $data);
310
$tmp_path = (string)$tmp;
311
312
$trap = new PhutilErrorTrap();
313
$info = @getimagesize($tmp_path);
314
$errors = $trap->getErrorsAsString();
315
$trap->destroy();
316
317
unset($tmp);
318
319
if ($info === false) {
320
throw new Exception(
321
pht(
322
'Unable to get image information with getimagesize(): %s',
323
$errors));
324
}
325
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().'));
331
}
332
333
$max_pixels = (4096 * 4096);
334
$img_pixels = ($width * $height);
335
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)));
346
}
347
348
$trap = new PhutilErrorTrap();
349
$image = @imagecreatefromstring($data);
350
$errors = $trap->getErrorsAsString();
351
$trap->destroy();
352
353
if ($image === false) {
354
throw new Exception(
355
pht(
356
'Unable to load image data with imagecreatefromstring(): %s',
357
$errors));
358
}
359
360
$this->image = $image;
361
return $this->image;
362
}
363
364
private function shouldUseImagemagick() {
365
if (!PhabricatorEnv::getEnvConfig('files.enable-imagemagick')) {
366
return false;
367
}
368
369
if ($this->file->getMimeType() != 'image/gif') {
370
return false;
371
}
372
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;
377
}
378
379
return true;
380
}
381
382
}
383
384