Page MenuHomePhabricator

D19201.diff
No OneTemporary

D19201.diff

diff --git a/src/applications/files/PhabricatorImageTransformer.php b/src/applications/files/PhabricatorImageTransformer.php
--- a/src/applications/files/PhabricatorImageTransformer.php
+++ b/src/applications/files/PhabricatorImageTransformer.php
@@ -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 )-------------------------------------------------- */
diff --git a/src/applications/macro/engine/PhabricatorMemeEngine.php b/src/applications/macro/engine/PhabricatorMemeEngine.php
--- a/src/applications/macro/engine/PhabricatorMemeEngine.php
+++ b/src/applications/macro/engine/PhabricatorMemeEngine.php
@@ -8,6 +8,7 @@
private $belowText;
private $templateFile;
+ private $metrics;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
@@ -68,11 +69,7 @@
$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 @@
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);
+ }
+
+
}

File Metadata

Mime Type
text/plain
Expires
Mon, Feb 3, 8:15 PM (21 h, 23 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7087700
Default Alt Text
D19201.diff (12 KB)

Event Timeline