Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/macro/engine/PhabricatorMemeEngine.php
12241 views
1
<?php
2
3
final class PhabricatorMemeEngine extends Phobject {
4
5
private $viewer;
6
private $template;
7
private $aboveText;
8
private $belowText;
9
10
private $templateFile;
11
private $metrics;
12
13
public function setViewer(PhabricatorUser $viewer) {
14
$this->viewer = $viewer;
15
return $this;
16
}
17
18
public function getViewer() {
19
return $this->viewer;
20
}
21
22
public function setTemplate($template) {
23
$this->template = $template;
24
return $this;
25
}
26
27
public function getTemplate() {
28
return $this->template;
29
}
30
31
public function setAboveText($above_text) {
32
$this->aboveText = $above_text;
33
return $this;
34
}
35
36
public function getAboveText() {
37
return $this->aboveText;
38
}
39
40
public function setBelowText($below_text) {
41
$this->belowText = $below_text;
42
return $this;
43
}
44
45
public function getBelowText() {
46
return $this->belowText;
47
}
48
49
public function getGenerateURI() {
50
$params = array(
51
'macro' => $this->getTemplate(),
52
'above' => $this->getAboveText(),
53
'below' => $this->getBelowText(),
54
);
55
56
return new PhutilURI('/macro/meme/', $params);
57
}
58
59
public function newAsset() {
60
$cache = $this->loadCachedFile();
61
if ($cache) {
62
return $cache;
63
}
64
65
$template = $this->loadTemplateFile();
66
if (!$template) {
67
throw new Exception(
68
pht(
69
'Template "%s" is not a valid template.',
70
$template));
71
}
72
73
$hash = $this->newTransformHash();
74
75
$asset = $this->newAssetFile($template);
76
77
$xfile = id(new PhabricatorTransformedFile())
78
->setOriginalPHID($template->getPHID())
79
->setTransformedPHID($asset->getPHID())
80
->setTransform($hash);
81
82
try {
83
$caught = null;
84
85
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
86
try {
87
$xfile->save();
88
} catch (Exception $ex) {
89
$caught = $ex;
90
}
91
unset($unguarded);
92
93
if ($caught) {
94
throw $caught;
95
}
96
97
return $asset;
98
} catch (AphrontDuplicateKeyQueryException $ex) {
99
$xfile = $this->loadCachedFile();
100
if (!$xfile) {
101
throw $ex;
102
}
103
return $xfile;
104
}
105
}
106
107
private function newTransformHash() {
108
$properties = array(
109
'kind' => 'meme',
110
'above' => $this->getAboveText(),
111
'below' => $this->getBelowText(),
112
);
113
114
$properties = phutil_json_encode($properties);
115
116
return PhabricatorHash::digestForIndex($properties);
117
}
118
119
public function loadCachedFile() {
120
$viewer = $this->getViewer();
121
122
$template_file = $this->loadTemplateFile();
123
if (!$template_file) {
124
return null;
125
}
126
127
$hash = $this->newTransformHash();
128
129
$xform = id(new PhabricatorTransformedFile())->loadOneWhere(
130
'originalPHID = %s AND transform = %s',
131
$template_file->getPHID(),
132
$hash);
133
if (!$xform) {
134
return null;
135
}
136
137
return id(new PhabricatorFileQuery())
138
->setViewer($viewer)
139
->withPHIDs(array($xform->getTransformedPHID()))
140
->executeOne();
141
}
142
143
private function loadTemplateFile() {
144
if ($this->templateFile === null) {
145
$viewer = $this->getViewer();
146
$template = $this->getTemplate();
147
148
$macro = id(new PhabricatorMacroQuery())
149
->setViewer($viewer)
150
->withNames(array($template))
151
->needFiles(true)
152
->executeOne();
153
if (!$macro) {
154
return null;
155
}
156
157
$this->templateFile = $macro->getFile();
158
}
159
160
return $this->templateFile;
161
}
162
163
private function newAssetFile(PhabricatorFile $template) {
164
$data = $this->newAssetData($template);
165
return PhabricatorFile::newFromFileData(
166
$data,
167
array(
168
'name' => 'meme-'.$template->getName(),
169
'canCDN' => true,
170
171
// In modern code these can end up linked directly in email, so let
172
// them stick around for a while.
173
'ttl.relative' => phutil_units('30 days in seconds'),
174
));
175
}
176
177
private function newAssetData(PhabricatorFile $template) {
178
$template_data = $template->loadFileData();
179
180
// When we aren't adding text, just return the data unmodified. This saves
181
// us from doing expensive stitching when we aren't actually making any
182
// changes to the image.
183
$above_text = coalesce($this->getAboveText(), '');
184
$below_text = coalesce($this->getBelowText(), '');
185
if (!strlen(trim($above_text)) && !strlen(trim($below_text))) {
186
return $template_data;
187
}
188
189
$result = $this->newImagemagickAsset($template, $template_data);
190
if ($result) {
191
return $result;
192
}
193
194
return $this->newGDAsset($template, $template_data);
195
}
196
197
private function newImagemagickAsset(
198
PhabricatorFile $template,
199
$template_data) {
200
201
// We're only going to use Imagemagick on GIFs.
202
$mime_type = $template->getMimeType();
203
if ($mime_type != 'image/gif') {
204
return null;
205
}
206
207
// We're only going to use Imagemagick if it is actually available.
208
$available = PhabricatorEnv::getEnvConfig('files.enable-imagemagick');
209
if (!$available) {
210
return null;
211
}
212
213
// Test of the GIF is an animated GIF. If it's a flat GIF, we'll fall
214
// back to GD.
215
$input = new TempFile();
216
Filesystem::writeFile($input, $template_data);
217
list($err, $out) = exec_manual('convert %s info:', $input);
218
if ($err) {
219
return null;
220
}
221
222
$split = phutil_split_lines($out);
223
$frames = count($split);
224
if ($frames <= 1) {
225
return null;
226
}
227
228
// Split the frames apart, transform each frame, then merge them back
229
// together.
230
$output = new TempFile();
231
232
$future = new ExecFuture(
233
'convert %s -coalesce +adjoin %s_%s',
234
$input,
235
$input,
236
'%09d');
237
$future->setTimeout(10)->resolvex();
238
239
$output_files = array();
240
for ($ii = 0; $ii < $frames; $ii++) {
241
$frame_name = sprintf('%s_%09d', $input, $ii);
242
$output_name = sprintf('%s_%09d', $output, $ii);
243
244
$output_files[] = $output_name;
245
246
$frame_data = Filesystem::readFile($frame_name);
247
$memed_frame_data = $this->newGDAsset($template, $frame_data);
248
Filesystem::writeFile($output_name, $memed_frame_data);
249
}
250
251
$future = new ExecFuture(
252
'convert -dispose background -loop 0 %Ls %s',
253
$output_files,
254
$output);
255
$future->setTimeout(10)->resolvex();
256
257
return Filesystem::readFile($output);
258
}
259
260
private function newGDAsset(PhabricatorFile $template, $data) {
261
$img = imagecreatefromstring($data);
262
if (!$img) {
263
throw new Exception(
264
pht('Failed to imagecreatefromstring() image template data.'));
265
}
266
267
$dx = imagesx($img);
268
$dy = imagesy($img);
269
270
$metrics = $this->getMetrics($dx, $dy);
271
$font = $this->getFont();
272
$size = $metrics['size'];
273
274
$above = coalesce($this->getAboveText(), '');
275
if (strlen($above)) {
276
$x = (int)floor(($dx - $metrics['text']['above']['width']) / 2);
277
$y = $metrics['text']['above']['height'] + 12;
278
279
$this->drawText($img, $font, $metrics['size'], $x, $y, $above);
280
}
281
282
$below = coalesce($this->getBelowText(), '');
283
if (strlen($below)) {
284
$x = (int)floor(($dx - $metrics['text']['below']['width']) / 2);
285
$y = $dy - 12 - $metrics['text']['below']['descend'];
286
287
$this->drawText($img, $font, $metrics['size'], $x, $y, $below);
288
}
289
290
return PhabricatorImageTransformer::saveImageDataInAnyFormat(
291
$img,
292
$template->getMimeType());
293
}
294
295
private function getFont() {
296
$phabricator_root = dirname(phutil_get_library_root('phabricator'));
297
298
$font_root = $phabricator_root.'/resources/font/';
299
if (Filesystem::pathExists($font_root.'impact.ttf')) {
300
$font_path = $font_root.'impact.ttf';
301
} else {
302
$font_path = $font_root.'tuffy.ttf';
303
}
304
305
return $font_path;
306
}
307
308
private function getMetrics($dim_x, $dim_y) {
309
if ($this->metrics === null) {
310
$font = $this->getFont();
311
312
$font_max = 72;
313
$font_min = 5;
314
315
$margin_x = 16;
316
$margin_y = 16;
317
318
$last = null;
319
$cursor = floor(($font_max + $font_min) / 2);
320
$min = $font_min;
321
$max = $font_max;
322
323
$texts = array(
324
'above' => $this->getAboveText(),
325
'below' => $this->getBelowText(),
326
);
327
328
$metrics = null;
329
$best = null;
330
while (true) {
331
$all_fit = true;
332
$text_metrics = array();
333
foreach ($texts as $key => $text) {
334
$text = coalesce($text, '');
335
$box = imagettfbbox($cursor, 0, $font, $text);
336
$height = abs($box[3] - $box[5]);
337
$width = abs($box[0] - $box[2]);
338
339
// This is the number of pixels below the baseline that the
340
// text extends, for example if it has a "y".
341
$descend = $box[3];
342
343
if (($height + $margin_y) > $dim_y) {
344
$all_fit = false;
345
break;
346
}
347
348
if (($width + $margin_x) > $dim_x) {
349
$all_fit = false;
350
break;
351
}
352
353
$text_metrics[$key]['width'] = $width;
354
$text_metrics[$key]['height'] = $height;
355
$text_metrics[$key]['descend'] = $descend;
356
}
357
358
if ($all_fit || $best === null) {
359
$best = $cursor;
360
$metrics = $text_metrics;
361
}
362
363
if ($all_fit) {
364
$min = $cursor;
365
} else {
366
$max = $cursor;
367
}
368
369
$last = $cursor;
370
$cursor = floor(($max + $min) / 2);
371
if ($cursor === $last) {
372
break;
373
}
374
}
375
376
$this->metrics = array(
377
'size' => $best,
378
'text' => $metrics,
379
);
380
}
381
382
return $this->metrics;
383
}
384
385
private function drawText($img, $font, $size, $x, $y, $text) {
386
$text_color = imagecolorallocate($img, 255, 255, 255);
387
$border_color = imagecolorallocate($img, 0, 0, 0);
388
389
$border = 2;
390
for ($xx = ($x - $border); $xx <= ($x + $border); $xx += $border) {
391
for ($yy = ($y - $border); $yy <= ($y + $border); $yy += $border) {
392
if (($xx === $x) && ($yy === $y)) {
393
continue;
394
}
395
imagettftext($img, $size, 0, $xx, $yy, $border_color, $font, $text);
396
}
397
}
398
399
imagettftext($img, $size, 0, $x, $y, $text_color, $font, $text);
400
}
401
402
403
}
404
405