@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
3final 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 = $this->getAboveText();
184 $below_text = $this->getBelowText();
185 if (($above_text === null || !phutil_nonempty_string(trim($above_text))) &&
186 ($below_text === null || !phutil_nonempty_string(trim($below_text)))) {
187 return $template_data;
188 }
189
190 $result = $this->newImagemagickAsset($template, $template_data);
191 if ($result) {
192 return $result;
193 }
194
195 return $this->newGDAsset($template, $template_data);
196 }
197
198 private function newImagemagickAsset(
199 PhabricatorFile $template,
200 $template_data) {
201
202 // We're only going to use Imagemagick on GIFs.
203 $mime_type = $template->getMimeType();
204 if ($mime_type != 'image/gif') {
205 return null;
206 }
207
208 // We're only going to use Imagemagick if it is actually available.
209 $available = PhabricatorEnv::getEnvConfig('files.enable-imagemagick');
210 if (!$available) {
211 return null;
212 }
213
214 $binary = id(new PhabricatorImagemagickSetupCheck())
215 ->getImageMagickBinaryName();
216
217 // Test of the GIF is an animated GIF. If it's a flat GIF, we'll fall
218 // back to GD.
219 $input = new TempFile();
220 Filesystem::writeFile($input, $template_data);
221 list($err, $out) = exec_manual('%s %s info:', $binary, $input);
222 if ($err) {
223 return null;
224 }
225
226 $split = phutil_split_lines($out);
227 $frames = count($split);
228 if ($frames <= 1) {
229 return null;
230 }
231
232 // Split the frames apart, transform each frame, then merge them back
233 // together.
234 $output = new TempFile();
235
236 $future = new ExecFuture(
237 '%s %s -coalesce +adjoin %s_%s',
238 $binary,
239 $input,
240 $input,
241 '%09d');
242 $future->setTimeout(10)->resolvex();
243
244 $output_files = array();
245 for ($ii = 0; $ii < $frames; $ii++) {
246 $frame_name = sprintf('%s_%09d', $input, $ii);
247 $output_name = sprintf('%s_%09d', $output, $ii);
248
249 $output_files[] = $output_name;
250
251 $frame_data = Filesystem::readFile($frame_name);
252 $memed_frame_data = $this->newGDAsset($template, $frame_data);
253 Filesystem::writeFile($output_name, $memed_frame_data);
254 }
255
256 $future = new ExecFuture(
257 '%s -dispose background -loop 0 %Ls %s',
258 $binary,
259 $output_files,
260 $output);
261 $future->setTimeout(10)->resolvex();
262
263 return Filesystem::readFile($output);
264 }
265
266 private function newGDAsset(PhabricatorFile $template, $data) {
267 $img = imagecreatefromstring($data);
268 if (!$img) {
269 throw new Exception(
270 pht('Failed to imagecreatefromstring() image template data.'));
271 }
272
273 $dx = imagesx($img);
274 $dy = imagesy($img);
275
276 $metrics = $this->getMetrics($dx, $dy);
277 $font = $this->getFont();
278 $size = $metrics['size'];
279
280 $above = $this->getAboveText();
281 if ($above !== null && phutil_nonempty_string(trim($above))) {
282 $x = (int)floor(($dx - $metrics['text']['above']['width']) / 2);
283 $y = $metrics['text']['above']['height'] + 12;
284
285 $this->drawText($img, $font, $metrics['size'], $x, $y, $above);
286 }
287
288 $below = $this->getBelowText();
289 if ($below !== null && phutil_nonempty_string(trim($below))) {
290 $x = (int)floor(($dx - $metrics['text']['below']['width']) / 2);
291 $y = $dy - 12 - $metrics['text']['below']['descend'];
292
293 $this->drawText($img, $font, $metrics['size'], $x, $y, $below);
294 }
295
296 return PhabricatorImageTransformer::saveImageDataInAnyFormat(
297 $img,
298 $template->getMimeType());
299 }
300
301 private function getFont() {
302 $phabricator_root = dirname(phutil_get_library_root('phabricator'));
303
304 $font_root = $phabricator_root.'/resources/font/';
305 if (Filesystem::pathExists($font_root.'impact.ttf')) {
306 $font_path = $font_root.'impact.ttf';
307 } else {
308 $font_path = $font_root.'tuffy.ttf';
309 }
310
311 return $font_path;
312 }
313
314 private function getMetrics($dim_x, $dim_y) {
315 if ($this->metrics === null) {
316 $font = $this->getFont();
317
318 $font_max = 72;
319 $font_min = 5;
320
321 $margin_x = 16;
322 $margin_y = 16;
323
324 $last = null;
325 $cursor = floor(($font_max + $font_min) / 2);
326 $min = $font_min;
327 $max = $font_max;
328
329 $texts = array(
330 'above' => $this->getAboveText(),
331 'below' => $this->getBelowText(),
332 );
333
334 $metrics = null;
335 $best = null;
336 while (true) {
337 $all_fit = true;
338 $text_metrics = array();
339 foreach ($texts as $key => $text) {
340 if (phutil_nonempty_string($text)) {
341 $box = imagettfbbox($cursor, 0, $font, $text);
342 $height = abs($box[3] - $box[5]);
343 $width = abs($box[0] - $box[2]);
344
345 // This is the number of pixels below the baseline that the
346 // text extends, for example if it has a "y".
347 $descend = $box[3];
348
349 if (($height + $margin_y) > $dim_y) {
350 $all_fit = false;
351 break;
352 }
353
354 if (($width + $margin_x) > $dim_x) {
355 $all_fit = false;
356 break;
357 }
358
359 $text_metrics[$key]['width'] = $width;
360 $text_metrics[$key]['height'] = $height;
361 $text_metrics[$key]['descend'] = $descend;
362 }
363 }
364
365 if ($all_fit || $best === null) {
366 $best = $cursor;
367 $metrics = $text_metrics;
368 }
369
370 if ($all_fit) {
371 $min = $cursor;
372 } else {
373 $max = $cursor;
374 }
375
376 $last = $cursor;
377 $cursor = floor(($max + $min) / 2);
378 if ($cursor === $last) {
379 break;
380 }
381 }
382
383 $this->metrics = array(
384 'size' => $best,
385 'text' => $metrics,
386 );
387 }
388
389 return $this->metrics;
390 }
391
392 private function drawText($img, $font, $size, $x, $y, $text) {
393 $text_color = imagecolorallocate($img, 255, 255, 255);
394 $border_color = imagecolorallocate($img, 0, 0, 0);
395
396 $border = 2;
397 for ($xx = ($x - $border); $xx <= ($x + $border); $xx += $border) {
398 for ($yy = ($y - $border); $yy <= ($y + $border); $yy += $border) {
399 if (($xx === $x) && ($yy === $y)) {
400 continue;
401 }
402 imagettftext($img, $size, 0, $xx, $yy, $border_color, $font, $text);
403 }
404 }
405
406 imagettftext($img, $size, 0, $x, $y, $text_color, $font, $text);
407 }
408
409
410}