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

Configure Feed

Select the types of activity you want to include in your feed.

Somewhat improve meme transform code so it is merely very bad

Summary: Depends on D19200. Fixes T5258. Ref T13101. Attempt to simplify and modernize this code and improve error handling.

Test Plan: did real hard dank memes

Maniphest Tasks: T13101, T5258

Differential Revision: https://secure.phabricator.com/D19201

+223 -192
-187
src/applications/files/PhabricatorImageTransformer.php
··· 6 6 */ 7 7 final class PhabricatorImageTransformer extends Phobject { 8 8 9 - public function executeMemeTransform( 10 - PhabricatorFile $file, 11 - $upper_text, 12 - $lower_text) { 13 - $image = $this->applyMemeToFile($file, $upper_text, $lower_text); 14 - return PhabricatorFile::newFromFileData( 15 - $image, 16 - array( 17 - 'name' => 'meme-'.$file->getName(), 18 - 'ttl.relative' => phutil_units('24 hours in seconds'), 19 - 'canCDN' => true, 20 - )); 21 - } 22 - 23 - private function applyMemeToFile( 24 - PhabricatorFile $file, 25 - $upper_text, 26 - $lower_text) { 27 - $data = $file->loadFileData(); 28 - 29 - $img_type = $file->getMimeType(); 30 - $imagemagick = PhabricatorEnv::getEnvConfig('files.enable-imagemagick'); 31 - 32 - if ($img_type != 'image/gif' || $imagemagick == false) { 33 - return $this->applyMemeTo( 34 - $data, $upper_text, $lower_text, $img_type); 35 - } 36 - 37 - $data = $file->loadFileData(); 38 - $input = new TempFile(); 39 - Filesystem::writeFile($input, $data); 40 - 41 - list($out) = execx('convert %s info:', $input); 42 - $split = phutil_split_lines($out); 43 - if (count($split) > 1) { 44 - return $this->applyMemeWithImagemagick( 45 - $input, 46 - $upper_text, 47 - $lower_text, 48 - count($split), 49 - $img_type); 50 - } else { 51 - return $this->applyMemeTo($data, $upper_text, $lower_text, $img_type); 52 - } 53 - } 54 - 55 - private function applyMemeTo( 56 - $data, 57 - $upper_text, 58 - $lower_text, 59 - $mime_type) { 60 - $img = imagecreatefromstring($data); 61 - 62 - // Some PNGs have color palettes, and allocating the dark border color 63 - // fails and gives us whatever's first in the color table. Copy the image 64 - // to a fresh truecolor canvas before working with it. 65 - 66 - $truecolor = imagecreatetruecolor(imagesx($img), imagesy($img)); 67 - imagecopy($truecolor, $img, 0, 0, 0, 0, imagesx($img), imagesy($img)); 68 - $img = $truecolor; 69 - 70 - $phabricator_root = dirname(phutil_get_library_root('phabricator')); 71 - $font_root = $phabricator_root.'/resources/font/'; 72 - $font_path = $font_root.'tuffy.ttf'; 73 - if (Filesystem::pathExists($font_root.'impact.ttf')) { 74 - $font_path = $font_root.'impact.ttf'; 75 - } 76 - $text_color = imagecolorallocate($img, 255, 255, 255); 77 - $border_color = imagecolorallocatealpha($img, 0, 0, 0, 110); 78 - $border_width = 4; 79 - $font_max = 200; 80 - $font_min = 5; 81 - for ($i = $font_max; $i > $font_min; $i--) { 82 - $fit = $this->doesTextBoundingBoxFitInImage( 83 - $img, 84 - $upper_text, 85 - $i, 86 - $font_path); 87 - if ($fit['doesfit']) { 88 - $x = ($fit['imgwidth'] - $fit['txtwidth']) / 2; 89 - $y = $fit['txtheight'] + 10; 90 - $this->makeImageWithTextBorder($img, 91 - $i, 92 - $x, 93 - $y, 94 - $text_color, 95 - $border_color, 96 - $border_width, 97 - $font_path, 98 - $upper_text); 99 - break; 100 - } 101 - } 102 - for ($i = $font_max; $i > $font_min; $i--) { 103 - $fit = $this->doesTextBoundingBoxFitInImage($img, 104 - $lower_text, $i, $font_path); 105 - if ($fit['doesfit']) { 106 - $x = ($fit['imgwidth'] - $fit['txtwidth']) / 2; 107 - $y = $fit['imgheight'] - 10; 108 - $this->makeImageWithTextBorder( 109 - $img, 110 - $i, 111 - $x, 112 - $y, 113 - $text_color, 114 - $border_color, 115 - $border_width, 116 - $font_path, 117 - $lower_text); 118 - break; 119 - } 120 - } 121 - return self::saveImageDataInAnyFormat($img, $mime_type); 122 - } 123 - 124 - private function makeImageWithTextBorder($img, $font_size, $x, $y, 125 - $color, $stroke_color, $bw, $font, $text) { 126 - $angle = 0; 127 - $bw = abs($bw); 128 - for ($c1 = $x - $bw; $c1 <= $x + $bw; $c1++) { 129 - for ($c2 = $y - $bw; $c2 <= $y + $bw; $c2++) { 130 - if (!(($c1 == $x - $bw || $x + $bw) && 131 - $c2 == $y - $bw || $c2 == $y + $bw)) { 132 - $bg = imagettftext($img, $font_size, 133 - $angle, $c1, $c2, $stroke_color, $font, $text); 134 - } 135 - } 136 - } 137 - imagettftext($img, $font_size, $angle, 138 - $x , $y, $color , $font, $text); 139 - } 140 - 141 - private function doesTextBoundingBoxFitInImage($img, 142 - $text, $font_size, $font_path) { 143 - // Default Angle = 0 144 - $angle = 0; 145 - 146 - $bbox = imagettfbbox($font_size, $angle, $font_path, $text); 147 - $text_height = abs($bbox[3] - $bbox[5]); 148 - $text_width = abs($bbox[0] - $bbox[2]); 149 - return array( 150 - 'doesfit' => ($text_height * 1.05 <= imagesy($img) / 2 151 - && $text_width * 1.05 <= imagesx($img)), 152 - 'txtwidth' => $text_width, 153 - 'txtheight' => $text_height, 154 - 'imgwidth' => imagesx($img), 155 - 'imgheight' => imagesy($img), 156 - ); 157 - } 158 - 159 - private function applyMemeWithImagemagick( 160 - $input, 161 - $above, 162 - $below, 163 - $count, 164 - $img_type) { 165 - 166 - $output = new TempFile(); 167 - $future = new ExecFuture( 168 - 'convert %s -coalesce +adjoin %s_%s', 169 - $input, 170 - $input, 171 - '%09d'); 172 - $future->setTimeout(10)->resolvex(); 173 - 174 - $output_files = array(); 175 - for ($ii = 0; $ii < $count; $ii++) { 176 - $frame_name = sprintf('%s_%09d', $input, $ii); 177 - $output_name = sprintf('%s_%09d', $output, $ii); 178 - 179 - $output_files[] = $output_name; 180 - 181 - $frame_data = Filesystem::readFile($frame_name); 182 - $memed_frame_data = $this->applyMemeTo( 183 - $frame_data, 184 - $above, 185 - $below, 186 - $img_type); 187 - Filesystem::writeFile($output_name, $memed_frame_data); 188 - } 189 - 190 - $future = new ExecFuture('convert -loop 0 %Ls %s', $output_files, $output); 191 - $future->setTimeout(10)->resolvex(); 192 - 193 - return Filesystem::readFile($output); 194 - } 195 - 196 9 197 10 /* -( Saving Image Data )-------------------------------------------------- */ 198 11
+223 -5
src/applications/macro/engine/PhabricatorMemeEngine.php
··· 8 8 private $belowText; 9 9 10 10 private $templateFile; 11 + private $metrics; 11 12 12 13 public function setViewer(PhabricatorUser $viewer) { 13 14 $this->viewer = $viewer; ··· 68 69 69 70 $hash = $this->newTransformHash(); 70 71 71 - $transformer = new PhabricatorImageTransformer(); 72 - $asset = $transformer->executeMemeTransform( 73 - $template, 74 - $this->getAboveText(), 75 - $this->getBelowText()); 72 + $asset = $this->newAssetFile($template); 76 73 77 74 $xfile = id(new PhabricatorTransformedFile()) 78 75 ->setOriginalPHID($template->getPHID()) ··· 159 156 160 157 return $this->templateFile; 161 158 } 159 + 160 + private function newAssetFile(PhabricatorFile $template) { 161 + $data = $this->newAssetData($template); 162 + return PhabricatorFile::newFromFileData( 163 + $data, 164 + array( 165 + 'name' => 'meme-'.$template->getName(), 166 + 'ttl.relative' => phutil_units('24 hours in seconds'), 167 + 'canCDN' => true, 168 + )); 169 + } 170 + 171 + private function newAssetData(PhabricatorFile $template) { 172 + $template_data = $template->loadFileData(); 173 + 174 + $result = $this->newImagemagickAsset($template, $template_data); 175 + if ($result) { 176 + return $result; 177 + } 178 + 179 + return $this->newGDAsset($template, $template_data); 180 + } 181 + 182 + private function newImagemagickAsset( 183 + PhabricatorFile $template, 184 + $template_data) { 185 + 186 + // We're only going to use Imagemagick on GIFs. 187 + $mime_type = $template->getMimeType(); 188 + if ($mime_type != 'image/gif') { 189 + return null; 190 + } 191 + 192 + // We're only going to use Imagemagick if it is actually available. 193 + $available = PhabricatorEnv::getEnvConfig('files.enable-imagemagick'); 194 + if (!$available) { 195 + return null; 196 + } 197 + 198 + // Test of the GIF is an animated GIF. If it's a flat GIF, we'll fall 199 + // back to GD. 200 + $input = new TempFile(); 201 + Filesystem::writeFile($input, $template_data); 202 + list($err, $out) = exec_manual('convert %s info:', $input); 203 + if ($err) { 204 + return null; 205 + } 206 + 207 + $split = phutil_split_lines($out); 208 + $frames = count($split); 209 + if ($frames <= 1) { 210 + return null; 211 + } 212 + 213 + // Split the frames apart, transform each frame, then merge them back 214 + // together. 215 + $output = new TempFile(); 216 + 217 + $future = new ExecFuture( 218 + 'convert %s -coalesce +adjoin %s_%s', 219 + $input, 220 + $input, 221 + '%09d'); 222 + $future->setTimeout(10)->resolvex(); 223 + 224 + $output_files = array(); 225 + for ($ii = 0; $ii < $frames; $ii++) { 226 + $frame_name = sprintf('%s_%09d', $input, $ii); 227 + $output_name = sprintf('%s_%09d', $output, $ii); 228 + 229 + $output_files[] = $output_name; 230 + 231 + $frame_data = Filesystem::readFile($frame_name); 232 + $memed_frame_data = $this->newGDAsset($template, $frame_data); 233 + Filesystem::writeFile($output_name, $memed_frame_data); 234 + } 235 + 236 + $future = new ExecFuture('convert -loop 0 %Ls %s', $output_files, $output); 237 + $future->setTimeout(10)->resolvex(); 238 + 239 + return Filesystem::readFile($output); 240 + } 241 + 242 + private function newGDAsset(PhabricatorFile $template, $data) { 243 + $img = imagecreatefromstring($data); 244 + if (!$img) { 245 + throw new Exception( 246 + pht('Failed to imagecreatefromstring() image template data.')); 247 + } 248 + 249 + $dx = imagesx($img); 250 + $dy = imagesy($img); 251 + 252 + $metrics = $this->getMetrics($dx, $dy); 253 + $font = $this->getFont(); 254 + $size = $metrics['size']; 255 + 256 + $above = $this->getAboveText(); 257 + if (strlen($above)) { 258 + $x = (int)floor(($dx - $metrics['text']['above']['width']) / 2); 259 + $y = $metrics['text']['above']['height'] + 12; 260 + 261 + $this->drawText($img, $font, $metrics['size'], $x, $y, $above); 262 + } 263 + 264 + $below = $this->getBelowText(); 265 + if (strlen($below)) { 266 + $x = (int)floor(($dx - $metrics['text']['below']['width']) / 2); 267 + $y = $dy - 12 - $metrics['text']['below']['descend']; 268 + 269 + $this->drawText($img, $font, $metrics['size'], $x, $y, $below); 270 + } 271 + 272 + return PhabricatorImageTransformer::saveImageDataInAnyFormat( 273 + $img, 274 + $template->getMimeType()); 275 + } 276 + 277 + private function getFont() { 278 + $phabricator_root = dirname(phutil_get_library_root('phabricator')); 279 + 280 + $font_root = $phabricator_root.'/resources/font/'; 281 + if (Filesystem::pathExists($font_root.'impact.ttf')) { 282 + $font_path = $font_root.'impact.ttf'; 283 + } else { 284 + $font_path = $font_root.'tuffy.ttf'; 285 + } 286 + 287 + return $font_path; 288 + } 289 + 290 + private function getMetrics($dim_x, $dim_y) { 291 + if ($this->metrics === null) { 292 + $font = $this->getFont(); 293 + 294 + $font_max = 72; 295 + $font_min = 5; 296 + 297 + $last = null; 298 + $cursor = floor(($font_max + $font_min) / 2); 299 + $min = $font_min; 300 + $max = $font_max; 301 + 302 + $texts = array( 303 + 'above' => $this->getAboveText(), 304 + 'below' => $this->getBelowText(), 305 + ); 306 + 307 + $metrics = null; 308 + $best = null; 309 + while (true) { 310 + $all_fit = true; 311 + $text_metrics = array(); 312 + foreach ($texts as $key => $text) { 313 + $box = imagettfbbox($cursor, 0, $font, $text); 314 + $height = abs($box[3] - $box[5]); 315 + $width = abs($box[0] - $box[2]); 316 + 317 + // This is the number of pixels below the baseline that the 318 + // text extends, for example if it has a "y". 319 + $descend = $box[3]; 320 + 321 + if ($height > $dim_y) { 322 + $all_fit = false; 323 + break; 324 + } 325 + 326 + if ($width > $dim_x) { 327 + $all_fit = false; 328 + break; 329 + } 330 + 331 + $text_metrics[$key]['width'] = $width; 332 + $text_metrics[$key]['height'] = $height; 333 + $text_metrics[$key]['descend'] = $descend; 334 + } 335 + 336 + if ($all_fit || $best === null) { 337 + $best = $cursor; 338 + $metrics = $text_metrics; 339 + } 340 + 341 + if ($all_fit) { 342 + $min = $cursor; 343 + } else { 344 + $max = $cursor; 345 + } 346 + 347 + $last = $cursor; 348 + $cursor = floor(($max + $min) / 2); 349 + if ($cursor === $last) { 350 + break; 351 + } 352 + } 353 + 354 + $this->metrics = array( 355 + 'size' => $best, 356 + 'text' => $metrics, 357 + ); 358 + } 359 + 360 + return $this->metrics; 361 + } 362 + 363 + private function drawText($img, $font, $size, $x, $y, $text) { 364 + $text_color = imagecolorallocate($img, 255, 255, 255); 365 + $border_color = imagecolorallocate($img, 0, 0, 0); 366 + 367 + $border = 2; 368 + for ($xx = ($x - $border); $xx <= ($x + $border); $xx += $border) { 369 + for ($yy = ($y - $border); $yy <= ($y + $border); $yy += $border) { 370 + if (($xx === $x) && ($yy === $y)) { 371 + continue; 372 + } 373 + imagettftext($img, $size, 0, $xx, $yy, $border_color, $font, $text); 374 + } 375 + } 376 + 377 + imagettftext($img, $size, 0, $x, $y, $text_color, $font, $text); 378 + } 379 + 162 380 163 381 }