Changeset View
Changeset View
Standalone View
Standalone View
src/applications/files/PhabricatorImageTransformer.php
| <?php | <?php | ||||
| /** | /** | ||||
| * @task enormous Detecting Enormous Images | * @task enormous Detecting Enormous Images | ||||
| * @task save Saving Image Data | * @task save Saving Image Data | ||||
| */ | */ | ||||
| final class PhabricatorImageTransformer extends Phobject { | 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 )-------------------------------------------------- */ | /* -( Saving Image Data )-------------------------------------------------- */ | ||||
| /** | /** | ||||
| * Save an image resource to a string representation suitable for storage or | * Save an image resource to a string representation suitable for storage or | ||||
| * transmission as an image file. | * transmission as an image file. | ||||
| * | * | ||||
| ▲ Show 20 Lines • Show All 122 Lines • Show Last 20 Lines | |||||