mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-09 16:32:39 +01:00
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
This commit is contained in:
parent
c7408f2797
commit
a3d282d33e
2 changed files with 223 additions and 192 deletions
|
@ -6,193 +6,6 @@
|
|||
*/
|
||||
final class PhabricatorImageTransformer extends Phobject {
|
||||
|
||||
public function executeMemeTransform(
|
||||
PhabricatorFile $file,
|
||||
$upper_text,
|
||||
$lower_text) {
|
||||
$image = $this->applyMemeToFile($file, $upper_text, $lower_text);
|
||||
return PhabricatorFile::newFromFileData(
|
||||
$image,
|
||||
array(
|
||||
'name' => 'meme-'.$file->getName(),
|
||||
'ttl.relative' => phutil_units('24 hours in seconds'),
|
||||
'canCDN' => true,
|
||||
));
|
||||
}
|
||||
|
||||
private function applyMemeToFile(
|
||||
PhabricatorFile $file,
|
||||
$upper_text,
|
||||
$lower_text) {
|
||||
$data = $file->loadFileData();
|
||||
|
||||
$img_type = $file->getMimeType();
|
||||
$imagemagick = PhabricatorEnv::getEnvConfig('files.enable-imagemagick');
|
||||
|
||||
if ($img_type != 'image/gif' || $imagemagick == false) {
|
||||
return $this->applyMemeTo(
|
||||
$data, $upper_text, $lower_text, $img_type);
|
||||
}
|
||||
|
||||
$data = $file->loadFileData();
|
||||
$input = new TempFile();
|
||||
Filesystem::writeFile($input, $data);
|
||||
|
||||
list($out) = execx('convert %s info:', $input);
|
||||
$split = phutil_split_lines($out);
|
||||
if (count($split) > 1) {
|
||||
return $this->applyMemeWithImagemagick(
|
||||
$input,
|
||||
$upper_text,
|
||||
$lower_text,
|
||||
count($split),
|
||||
$img_type);
|
||||
} else {
|
||||
return $this->applyMemeTo($data, $upper_text, $lower_text, $img_type);
|
||||
}
|
||||
}
|
||||
|
||||
private function applyMemeTo(
|
||||
$data,
|
||||
$upper_text,
|
||||
$lower_text,
|
||||
$mime_type) {
|
||||
$img = imagecreatefromstring($data);
|
||||
|
||||
// Some PNGs have color palettes, and allocating the dark border color
|
||||
// fails and gives us whatever's first in the color table. Copy the image
|
||||
// to a fresh truecolor canvas before working with it.
|
||||
|
||||
$truecolor = imagecreatetruecolor(imagesx($img), imagesy($img));
|
||||
imagecopy($truecolor, $img, 0, 0, 0, 0, imagesx($img), imagesy($img));
|
||||
$img = $truecolor;
|
||||
|
||||
$phabricator_root = dirname(phutil_get_library_root('phabricator'));
|
||||
$font_root = $phabricator_root.'/resources/font/';
|
||||
$font_path = $font_root.'tuffy.ttf';
|
||||
if (Filesystem::pathExists($font_root.'impact.ttf')) {
|
||||
$font_path = $font_root.'impact.ttf';
|
||||
}
|
||||
$text_color = imagecolorallocate($img, 255, 255, 255);
|
||||
$border_color = imagecolorallocatealpha($img, 0, 0, 0, 110);
|
||||
$border_width = 4;
|
||||
$font_max = 200;
|
||||
$font_min = 5;
|
||||
for ($i = $font_max; $i > $font_min; $i--) {
|
||||
$fit = $this->doesTextBoundingBoxFitInImage(
|
||||
$img,
|
||||
$upper_text,
|
||||
$i,
|
||||
$font_path);
|
||||
if ($fit['doesfit']) {
|
||||
$x = ($fit['imgwidth'] - $fit['txtwidth']) / 2;
|
||||
$y = $fit['txtheight'] + 10;
|
||||
$this->makeImageWithTextBorder($img,
|
||||
$i,
|
||||
$x,
|
||||
$y,
|
||||
$text_color,
|
||||
$border_color,
|
||||
$border_width,
|
||||
$font_path,
|
||||
$upper_text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
for ($i = $font_max; $i > $font_min; $i--) {
|
||||
$fit = $this->doesTextBoundingBoxFitInImage($img,
|
||||
$lower_text, $i, $font_path);
|
||||
if ($fit['doesfit']) {
|
||||
$x = ($fit['imgwidth'] - $fit['txtwidth']) / 2;
|
||||
$y = $fit['imgheight'] - 10;
|
||||
$this->makeImageWithTextBorder(
|
||||
$img,
|
||||
$i,
|
||||
$x,
|
||||
$y,
|
||||
$text_color,
|
||||
$border_color,
|
||||
$border_width,
|
||||
$font_path,
|
||||
$lower_text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return self::saveImageDataInAnyFormat($img, $mime_type);
|
||||
}
|
||||
|
||||
private function makeImageWithTextBorder($img, $font_size, $x, $y,
|
||||
$color, $stroke_color, $bw, $font, $text) {
|
||||
$angle = 0;
|
||||
$bw = abs($bw);
|
||||
for ($c1 = $x - $bw; $c1 <= $x + $bw; $c1++) {
|
||||
for ($c2 = $y - $bw; $c2 <= $y + $bw; $c2++) {
|
||||
if (!(($c1 == $x - $bw || $x + $bw) &&
|
||||
$c2 == $y - $bw || $c2 == $y + $bw)) {
|
||||
$bg = imagettftext($img, $font_size,
|
||||
$angle, $c1, $c2, $stroke_color, $font, $text);
|
||||
}
|
||||
}
|
||||
}
|
||||
imagettftext($img, $font_size, $angle,
|
||||
$x , $y, $color , $font, $text);
|
||||
}
|
||||
|
||||
private function doesTextBoundingBoxFitInImage($img,
|
||||
$text, $font_size, $font_path) {
|
||||
// Default Angle = 0
|
||||
$angle = 0;
|
||||
|
||||
$bbox = imagettfbbox($font_size, $angle, $font_path, $text);
|
||||
$text_height = abs($bbox[3] - $bbox[5]);
|
||||
$text_width = abs($bbox[0] - $bbox[2]);
|
||||
return array(
|
||||
'doesfit' => ($text_height * 1.05 <= imagesy($img) / 2
|
||||
&& $text_width * 1.05 <= imagesx($img)),
|
||||
'txtwidth' => $text_width,
|
||||
'txtheight' => $text_height,
|
||||
'imgwidth' => imagesx($img),
|
||||
'imgheight' => imagesy($img),
|
||||
);
|
||||
}
|
||||
|
||||
private function applyMemeWithImagemagick(
|
||||
$input,
|
||||
$above,
|
||||
$below,
|
||||
$count,
|
||||
$img_type) {
|
||||
|
||||
$output = new TempFile();
|
||||
$future = new ExecFuture(
|
||||
'convert %s -coalesce +adjoin %s_%s',
|
||||
$input,
|
||||
$input,
|
||||
'%09d');
|
||||
$future->setTimeout(10)->resolvex();
|
||||
|
||||
$output_files = array();
|
||||
for ($ii = 0; $ii < $count; $ii++) {
|
||||
$frame_name = sprintf('%s_%09d', $input, $ii);
|
||||
$output_name = sprintf('%s_%09d', $output, $ii);
|
||||
|
||||
$output_files[] = $output_name;
|
||||
|
||||
$frame_data = Filesystem::readFile($frame_name);
|
||||
$memed_frame_data = $this->applyMemeTo(
|
||||
$frame_data,
|
||||
$above,
|
||||
$below,
|
||||
$img_type);
|
||||
Filesystem::writeFile($output_name, $memed_frame_data);
|
||||
}
|
||||
|
||||
$future = new ExecFuture('convert -loop 0 %Ls %s', $output_files, $output);
|
||||
$future->setTimeout(10)->resolvex();
|
||||
|
||||
return Filesystem::readFile($output);
|
||||
}
|
||||
|
||||
|
||||
/* -( Saving Image Data )-------------------------------------------------- */
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ final class PhabricatorMemeEngine extends Phobject {
|
|||
private $belowText;
|
||||
|
||||
private $templateFile;
|
||||
private $metrics;
|
||||
|
||||
public function setViewer(PhabricatorUser $viewer) {
|
||||
$this->viewer = $viewer;
|
||||
|
@ -68,11 +69,7 @@ final class PhabricatorMemeEngine extends Phobject {
|
|||
|
||||
$hash = $this->newTransformHash();
|
||||
|
||||
$transformer = new PhabricatorImageTransformer();
|
||||
$asset = $transformer->executeMemeTransform(
|
||||
$template,
|
||||
$this->getAboveText(),
|
||||
$this->getBelowText());
|
||||
$asset = $this->newAssetFile($template);
|
||||
|
||||
$xfile = id(new PhabricatorTransformedFile())
|
||||
->setOriginalPHID($template->getPHID())
|
||||
|
@ -160,4 +157,225 @@ final class PhabricatorMemeEngine extends Phobject {
|
|||
return $this->templateFile;
|
||||
}
|
||||
|
||||
private function newAssetFile(PhabricatorFile $template) {
|
||||
$data = $this->newAssetData($template);
|
||||
return PhabricatorFile::newFromFileData(
|
||||
$data,
|
||||
array(
|
||||
'name' => 'meme-'.$template->getName(),
|
||||
'ttl.relative' => phutil_units('24 hours in seconds'),
|
||||
'canCDN' => true,
|
||||
));
|
||||
}
|
||||
|
||||
private function newAssetData(PhabricatorFile $template) {
|
||||
$template_data = $template->loadFileData();
|
||||
|
||||
$result = $this->newImagemagickAsset($template, $template_data);
|
||||
if ($result) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $this->newGDAsset($template, $template_data);
|
||||
}
|
||||
|
||||
private function newImagemagickAsset(
|
||||
PhabricatorFile $template,
|
||||
$template_data) {
|
||||
|
||||
// We're only going to use Imagemagick on GIFs.
|
||||
$mime_type = $template->getMimeType();
|
||||
if ($mime_type != 'image/gif') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We're only going to use Imagemagick if it is actually available.
|
||||
$available = PhabricatorEnv::getEnvConfig('files.enable-imagemagick');
|
||||
if (!$available) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Test of the GIF is an animated GIF. If it's a flat GIF, we'll fall
|
||||
// back to GD.
|
||||
$input = new TempFile();
|
||||
Filesystem::writeFile($input, $template_data);
|
||||
list($err, $out) = exec_manual('convert %s info:', $input);
|
||||
if ($err) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$split = phutil_split_lines($out);
|
||||
$frames = count($split);
|
||||
if ($frames <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Split the frames apart, transform each frame, then merge them back
|
||||
// together.
|
||||
$output = new TempFile();
|
||||
|
||||
$future = new ExecFuture(
|
||||
'convert %s -coalesce +adjoin %s_%s',
|
||||
$input,
|
||||
$input,
|
||||
'%09d');
|
||||
$future->setTimeout(10)->resolvex();
|
||||
|
||||
$output_files = array();
|
||||
for ($ii = 0; $ii < $frames; $ii++) {
|
||||
$frame_name = sprintf('%s_%09d', $input, $ii);
|
||||
$output_name = sprintf('%s_%09d', $output, $ii);
|
||||
|
||||
$output_files[] = $output_name;
|
||||
|
||||
$frame_data = Filesystem::readFile($frame_name);
|
||||
$memed_frame_data = $this->newGDAsset($template, $frame_data);
|
||||
Filesystem::writeFile($output_name, $memed_frame_data);
|
||||
}
|
||||
|
||||
$future = new ExecFuture('convert -loop 0 %Ls %s', $output_files, $output);
|
||||
$future->setTimeout(10)->resolvex();
|
||||
|
||||
return Filesystem::readFile($output);
|
||||
}
|
||||
|
||||
private function newGDAsset(PhabricatorFile $template, $data) {
|
||||
$img = imagecreatefromstring($data);
|
||||
if (!$img) {
|
||||
throw new Exception(
|
||||
pht('Failed to imagecreatefromstring() image template data.'));
|
||||
}
|
||||
|
||||
$dx = imagesx($img);
|
||||
$dy = imagesy($img);
|
||||
|
||||
$metrics = $this->getMetrics($dx, $dy);
|
||||
$font = $this->getFont();
|
||||
$size = $metrics['size'];
|
||||
|
||||
$above = $this->getAboveText();
|
||||
if (strlen($above)) {
|
||||
$x = (int)floor(($dx - $metrics['text']['above']['width']) / 2);
|
||||
$y = $metrics['text']['above']['height'] + 12;
|
||||
|
||||
$this->drawText($img, $font, $metrics['size'], $x, $y, $above);
|
||||
}
|
||||
|
||||
$below = $this->getBelowText();
|
||||
if (strlen($below)) {
|
||||
$x = (int)floor(($dx - $metrics['text']['below']['width']) / 2);
|
||||
$y = $dy - 12 - $metrics['text']['below']['descend'];
|
||||
|
||||
$this->drawText($img, $font, $metrics['size'], $x, $y, $below);
|
||||
}
|
||||
|
||||
return PhabricatorImageTransformer::saveImageDataInAnyFormat(
|
||||
$img,
|
||||
$template->getMimeType());
|
||||
}
|
||||
|
||||
private function getFont() {
|
||||
$phabricator_root = dirname(phutil_get_library_root('phabricator'));
|
||||
|
||||
$font_root = $phabricator_root.'/resources/font/';
|
||||
if (Filesystem::pathExists($font_root.'impact.ttf')) {
|
||||
$font_path = $font_root.'impact.ttf';
|
||||
} else {
|
||||
$font_path = $font_root.'tuffy.ttf';
|
||||
}
|
||||
|
||||
return $font_path;
|
||||
}
|
||||
|
||||
private function getMetrics($dim_x, $dim_y) {
|
||||
if ($this->metrics === null) {
|
||||
$font = $this->getFont();
|
||||
|
||||
$font_max = 72;
|
||||
$font_min = 5;
|
||||
|
||||
$last = null;
|
||||
$cursor = floor(($font_max + $font_min) / 2);
|
||||
$min = $font_min;
|
||||
$max = $font_max;
|
||||
|
||||
$texts = array(
|
||||
'above' => $this->getAboveText(),
|
||||
'below' => $this->getBelowText(),
|
||||
);
|
||||
|
||||
$metrics = null;
|
||||
$best = null;
|
||||
while (true) {
|
||||
$all_fit = true;
|
||||
$text_metrics = array();
|
||||
foreach ($texts as $key => $text) {
|
||||
$box = imagettfbbox($cursor, 0, $font, $text);
|
||||
$height = abs($box[3] - $box[5]);
|
||||
$width = abs($box[0] - $box[2]);
|
||||
|
||||
// This is the number of pixels below the baseline that the
|
||||
// text extends, for example if it has a "y".
|
||||
$descend = $box[3];
|
||||
|
||||
if ($height > $dim_y) {
|
||||
$all_fit = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($width > $dim_x) {
|
||||
$all_fit = false;
|
||||
break;
|
||||
}
|
||||
|
||||
$text_metrics[$key]['width'] = $width;
|
||||
$text_metrics[$key]['height'] = $height;
|
||||
$text_metrics[$key]['descend'] = $descend;
|
||||
}
|
||||
|
||||
if ($all_fit || $best === null) {
|
||||
$best = $cursor;
|
||||
$metrics = $text_metrics;
|
||||
}
|
||||
|
||||
if ($all_fit) {
|
||||
$min = $cursor;
|
||||
} else {
|
||||
$max = $cursor;
|
||||
}
|
||||
|
||||
$last = $cursor;
|
||||
$cursor = floor(($max + $min) / 2);
|
||||
if ($cursor === $last) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->metrics = array(
|
||||
'size' => $best,
|
||||
'text' => $metrics,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->metrics;
|
||||
}
|
||||
|
||||
private function drawText($img, $font, $size, $x, $y, $text) {
|
||||
$text_color = imagecolorallocate($img, 255, 255, 255);
|
||||
$border_color = imagecolorallocate($img, 0, 0, 0);
|
||||
|
||||
$border = 2;
|
||||
for ($xx = ($x - $border); $xx <= ($x + $border); $xx += $border) {
|
||||
for ($yy = ($y - $border); $yy <= ($y + $border); $yy += $border) {
|
||||
if (($xx === $x) && ($yy === $y)) {
|
||||
continue;
|
||||
}
|
||||
imagettftext($img, $size, 0, $xx, $yy, $border_color, $font, $text);
|
||||
}
|
||||
}
|
||||
|
||||
imagettftext($img, $size, 0, $x, $y, $text_color, $font, $text);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue