Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14845006
D19201.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
12 KB
Referenced Files
None
Subscribers
None
D19201.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D19201: Somewhat improve meme transform code so it is merely very bad
Attached
Detach File
Event Timeline
Log In to Comment