diff --git a/resources/font/tuffy.ttf b/resources/font/tuffy.ttf new file mode 100644 index 0000000000..9df064674f Binary files /dev/null and b/resources/font/tuffy.ttf differ diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index ae6eef9c8c..cdaba398dc 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -925,6 +925,7 @@ phutil_register_library_map(array( 'PhabricatorMacroEditController' => 'applications/macro/controller/PhabricatorMacroEditController.php', 'PhabricatorMacroEditor' => 'applications/macro/editor/PhabricatorMacroEditor.php', 'PhabricatorMacroListController' => 'applications/macro/controller/PhabricatorMacroListController.php', + 'PhabricatorMacroMemeController' => 'applications/macro/controller/PhabricatorMacroMemeController.php', 'PhabricatorMacroReplyHandler' => 'applications/macro/mail/PhabricatorMacroReplyHandler.php', 'PhabricatorMacroTransaction' => 'applications/macro/storage/PhabricatorMacroTransaction.php', 'PhabricatorMacroTransactionComment' => 'applications/macro/storage/PhabricatorMacroTransactionComment.php', @@ -2308,6 +2309,7 @@ phutil_register_library_map(array( 'PhabricatorMacroEditController' => 'PhabricatorMacroController', 'PhabricatorMacroEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorMacroListController' => 'PhabricatorMacroController', + 'PhabricatorMacroMemeController' => 'PhabricatorMacroController', 'PhabricatorMacroReplyHandler' => 'PhabricatorMailReplyHandler', 'PhabricatorMacroTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorMacroTransactionComment' => 'PhabricatorApplicationTransactionComment', diff --git a/src/applications/files/PhabricatorImageTransformer.php b/src/applications/files/PhabricatorImageTransformer.php index 2bf565d43f..4f0f86edee 100644 --- a/src/applications/files/PhabricatorImageTransformer.php +++ b/src/applications/files/PhabricatorImageTransformer.php @@ -2,6 +2,18 @@ final class PhabricatorImageTransformer { + public function executeMemeTransform( + PhabricatorFile $file, + $upper_text, + $lower_text) { + $image = $this->applyMemeTo($file, $upper_text, $lower_text); + return PhabricatorFile::newFromFileData( + $image, + array( + 'name' => 'meme-'.$file->getName(), + )); + } + public function executeThumbTransform( PhabricatorFile $file, $x, @@ -134,6 +146,97 @@ final class PhabricatorImageTransformer { return $this->saveImageDataInAnyFormat($dst, $file->getMimeType()); } + private function applyMemeTo( + PhabricatorFile $file, + $upper_text, + $lower_text) { + $data = $file->loadFileData(); + $img = imagecreatefromstring($data); + $phabricator_root = dirname(phutil_get_library_root('phabricator')); + $font_path = $phabricator_root.'/resources/font/tuffy.ttf'; + $white = imagecolorallocate($img, 255, 255, 255); + $black = imagecolorallocate($img, 0, 0, 0); + $border_width = 3; + $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, + $white, + $black, + $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, + $white, + $black, + $border_width, + $font_path, + $lower_text); + break; + } + } + return $this->saveImageDataInAnyFormat($img, $file->getMimeType()); + } + + 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) + && $text_width * 1.05 <= imagesx($img)), + "txtwidth" => $text_width, + "txtheight" => $text_height, + "imgwidth" => imagesx($img), + "imgheight" => imagesy($img), + ); + } + private function saveImageDataInAnyFormat($data, $preferred_mime = '') { switch ($preferred_mime) { case 'image/gif': // GIF doesn't support true color. diff --git a/src/applications/macro/application/PhabricatorApplicationMacro.php b/src/applications/macro/application/PhabricatorApplicationMacro.php index a2f68437e1..1edca56440 100644 --- a/src/applications/macro/application/PhabricatorApplicationMacro.php +++ b/src/applications/macro/application/PhabricatorApplicationMacro.php @@ -31,6 +31,7 @@ final class PhabricatorApplicationMacro extends PhabricatorApplication { 'comment/(?P[1-9]\d*)/' => 'PhabricatorMacroCommentController', 'edit/(?P[1-9]\d*)/' => 'PhabricatorMacroEditController', 'disable/(?P[1-9]\d*)/' => 'PhabricatorMacroDisableController', + 'meme/' => 'PhabricatorMacroMemeController', ), ); } diff --git a/src/applications/macro/controller/PhabricatorMacroMemeController.php b/src/applications/macro/controller/PhabricatorMacroMemeController.php new file mode 100644 index 0000000000..bfaa50c666 --- /dev/null +++ b/src/applications/macro/controller/PhabricatorMacroMemeController.php @@ -0,0 +1,45 @@ +getRequest(); + $macro_name = $request->getStr('macro'); + $upper_text = $request->getStr('uppertext'); + $lower_text = $request->getStr('lowertext'); + $user = $request->getUser(); + $macro = id(new PhabricatorFileImageMacro()) + ->loadOneWhere('name=%s', $macro_name); + if (!$macro) { + return new Aphront404Response(); + } + $file = id(new PhabricatorFile())->loadOneWhere( + 'phid = %s', + $macro->getFilePHID()); + + $upper_text = strtoupper($upper_text); + $lower_text = strtoupper($lower_text); + $mixed_text = $upper_text.":".$lower_text; + $hash = "meme".hash("sha256", $mixed_text); + $xform = id(new PhabricatorTransformedFile()) + ->loadOneWhere('originalphid=%s and transform=%s', + $file->getPHID(), $hash); + + if ($xform) { + $memefile = id(new PhabricatorFile())->loadOneWhere( + 'phid = %s', $xform->getTransformedPHID()); + return id(new AphrontRedirectResponse())->setURI($memefile->getBestURI()); + } + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $transformers = (new PhabricatorImageTransformer()); + $newfile = $transformers + ->executeMemeTransform($file, $upper_text, $lower_text); + $xfile = new PhabricatorTransformedFile(); + $xfile->setOriginalPHID($file->getPHID()); + $xfile->setTransformedPHID($newfile->getPHID()); + $xfile->setTransform($hash); + $xfile->save(); + return id(new AphrontRedirectResponse())->setURI($newfile->getBestURI()); + } +}