diff --git a/README.md b/README.md index 4a4358f..e59f9ce 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ # PHP-add-text-to-image Add text to an existing or new image. PHP >= 7.0 -```shell -composer require ghostff/php-text-to-image -``` - ### Usage ```php $text1 = Text::from('Text One')->color(231, 81, 0); @@ -41,6 +37,76 @@ Writing to a new image: // Or save to a file (new TextToImage())->setDimension(350, 350)->setBackgroundColor(0, 0, 0)->addTexts($text1, $text2, $text3, $text)->render(__DIR__ . '/tmp.png'); ``` +Writing with background layer: +```php +$text = Text::from('Text example')->position(10, 25); +$bg_layer = BackgroundLayer::create()->color(0,0,0, 85)->position(0,0,null, 40); +$text_to_image = new TextToImage(__DIR__ . '/default.png'); +$text_to_image->addTexts($text)->addBackgroundLayer($bg_layer)->render(__DIR__ . '/tmp.png'); +``` +Example function: +```php +/** + * Print text on image with background layer. Supports multiline. + * @param $path string Image path + * @param $text string Text that splitted with "\r\n" will be wrapped on next line. + * @param $text_position string 'top' - text will be on the top of image, 'bottom' - on the bottom + * @return bool + */ + public static function setText($path, $text, $text_position = 'top') + { + $bg_layer_y2 = 0; + $bg_layer_y_increment = 29; + $text_font_size = 14; + $font_path = 'arial.ttf'; //path to your font + $pos_x = 10; + $pos_y_increment = 25; + $pos_y = 0; + $bg_layer_x = 0; + $bg_layer_y = 0; + $text_image = new TextToImage($path); + $max_length = number_format(0.075 * $text_image->getWidth()); // 75 letter / 1000px + if ($text_position == 'bottom') { + $pos_y = $text_image->getHeight() + $pos_y_increment / 1.5; + $bg_layer_y = $text_image->getHeight(); + $bg_layer_y2 = null; + } + $text_array_tmp = explode("\r\n", $text); + $text_array = []; + foreach ($text_array_tmp as $tmp_value) { //generating array of rows based on the letters count + while (mb_strlen($tmp_value) > $max_length) { + $offset = mb_strpos($tmp_value, ' ', $max_length); + if ($offset === false) { + break; + } + $sub_text = mb_substr($tmp_value, 0, $offset); + $text_array[] = $sub_text; + $tmp_value = mb_substr($tmp_value, $offset); + } + $text_array[] = $tmp_value; + } + unset($text_array_tmp); + if ($text_position == 'bottom') { + $text_array = array_reverse($text_array); + } + foreach ($text_array as $value) { //printing text + if ($text_position == 'bottom') { + $pos_y -= $pos_y_increment; + $bg_layer_y -= $bg_layer_y_increment; + } else { + $pos_y += $pos_y_increment; + $bg_layer_y2 += $bg_layer_y_increment; + } + + $txt1 = Text::from($value)->position($pos_x, $pos_y)->font( $text_font_size, $font_path, true); + $text_image->addTexts($txt1); + } + $bg_layer = BackgroundLayer::create()->color(0, 0, 0, 85)->position($bg_layer_x, $bg_layer_y, null, $bg_layer_y2); //printing bg layer + $text_image->addBackgroundLayer($bg_layer); + + $text_image->render($path); + } +``` ---- diff --git a/src/BackgroundLayer.php b/src/BackgroundLayer.php new file mode 100644 index 0000000..68541cd --- /dev/null +++ b/src/BackgroundLayer.php @@ -0,0 +1,90 @@ +position_x1, $this->position_y1, $this->position_x2, $this->position_y2]; + } + + public function getColor(): array { + return $this->color; + } + + public static function create(): self + { + return new self(); + } + + /** + * Generic Set. + * + * @param int $position_x1 Left X position. + * @param int $position_y1 Left Y position. + * @param int $position_x2 Right X position. If value is null than it will equals the width of image. + * @param int $position_y2 Right Y position. If value is null than it will equals the height of image. + * @param array $color Text color [r, g, b] + * @return $this + */ + public function set( + int $position_x1 = 0, + int $position_y1 = 0, + int $position_x2 = null, + int $position_y2 = null, + array $color = [255, 255, 255, 255] + ): self + { + $this->position_x1 = $position_x1; + $this->position_y1 = $position_y1; + $this->position_x2 = $position_x2; + $this->position_y2 = $position_y2; + $this->color = $color + [0, 0, 0, 255]; + + return $this; + } + + /** + * Sets the position of added background layer. + * + * @param int $x1 Left X position. + * @param int $y1 Left Y position. + * @param int $x2 Right X position. If value is null than it will equals the width of image. + * @param int $y2 Right Y position. If value is null than it will equals the height of image. + * @return $this + */ + public function position(int $x1, int $y1 = 0, int $x2=null, int $y2=null): self + { + $this->position_x1 = $x1; + $this->position_y1 = $y1; + $this->position_x2 = $x2; + $this->position_y2 = $y2; + + return $this; + } + + /** + * Sets a color for background layer. + * The red, green and blue parameters are integers between 0 and 255 or hexadecimals between 0x00 and 0xFF. + * + * @param int $r Value of red component . + * @param int $g Value of green component. + * @param int $b Value of blue component. + * @param int $a A value between 0 and 255. 255 indicates completely opaque while 0 indicates completely transparent. + * + * @return $this + */ + public function color(int $r = 255, int $g = 255, int $b = 255, int $a = 255): self + { + $this->color = [$r, $g, $b, $a]; + + return $this; + } + +} \ No newline at end of file diff --git a/src/TextToImage.php b/src/TextToImage.php index 0df948f..4ad7a40 100644 --- a/src/TextToImage.php +++ b/src/TextToImage.php @@ -6,24 +6,66 @@ class TextToImage { - private $from = ''; - private $texts = []; - private $width = 200; - private $height = 200; + private $from = ''; + private $texts = []; + private $width = null; + private $height = null; private $bg_color = [255, 255, 255, 127]; + /** + * @var BackgroundLayer[] + */ + private $bg_layers = []; - public function getHeight(): int { + public function getHeight(): int + { + $this->loadDimensions(); return $this->height; } - public function getWidth(): int { + public function getWidth(): int + { + $this->loadDimensions(); return $this->width; } + protected function loadDimensions() + { + if ($this->from == '') { + if (!isset($this->height) || !isset($this->width)) { + $this->width = 200; + $this->height = 200; + } + return; + } + if (isset($this->height) && isset($this->width)) { + return; + } + $this->height = $this->width = 0; + if (!is_readable($this->from)) { + throw new \RuntimeException('Image to write text to not specified or does not exist.'); + } + + $ext = $this->getExtension($this->from); + if ($ext == 'jpg' || $ext == 'jpeg') { + $image = imagecreatefromjpeg($this->from); + } elseif ($ext == 'png') { + $image = imagecreatefrompng($this->from); + } elseif ($ext == 'gif') { + $image = imagecreatefromgif($this->from); + } else { + throw new \RuntimeException("{$ext} not supported, implement it yourself."); + } + if ($this->height === 0) { + $this->height = imagesy($image); + $this->width = imagesx($image); + } + imagedestroy($image); + } + /** * Constructor. * - * @param string $from The image text will be added to. If not specified a blank image 200x200 will be created. + * @param string $from The image text will be added to. If not specified a blank image 200x200 will be created. */ public function __construct(string $from = '') { @@ -34,14 +76,14 @@ public function __construct(string $from = '') * Set background image dimension. * Note: This is not evaluated if a $path is specified in a constructor. * - * @param int $width The image width. - * @param int $height The image height. + * @param int $width the image width + * @param int $height the image height * * @return $this */ public function setDimension(int $width, int $height): self { - $this->width = $width; + $this->width = $width; $this->height = $height; return $this; @@ -51,10 +93,10 @@ public function setDimension(int $width, int $height): self * Set the background color of created background image. * Note: This is not evaluated if a path is specified in a constructor. * - * @param int $r Value of red component . - * @param int $g Value of green component. - * @param int $b Value of blue component. - * @param int $a A value between 0 and 255. 255 indicates completely opaque while 0 indicates completely transparent. + * @param int $r value of red component + * @param int $g value of green component + * @param int $b value of blue component + * @param int $a A value between 0 and 255. 255 indicates completely opaque while 0 indicates completely transparent. * * @return $this */ @@ -66,9 +108,22 @@ public function setBackgroundColor(int $r = 255, int $g = 255, int $b = 255, int } /** - * Adds texts to specified or background image. + * $transparent = imagecolorallocatealpha($image, 0,0,0, 80);. + imagefilledrectangle($image, 0, 0, $imageX, 60, $transparent); + * @param int $position_x + * @param int $position_y * - * @param Text ...$text + * @return $this + */ + public function addBackgroundLayer(BackgroundLayer $bg_layer): self + { + $this->bg_layers[] = $bg_layer; + + return $this; + } + + /** + * Adds texts to specified or background image. * * @return $this */ @@ -82,24 +137,20 @@ public function addTexts(Text ...$text): self /** * Renders modified image to a file or return contents. * - * @param string|null $save_as If specified, image content will be saved at the provided path. - * @param string|null $ext Image processor. possible values: jpg|jpeg|png|gif - * - * @return string + * @param string|null $save_as if specified, image content will be saved at the provided path + * @param string|null $ext Image processor. possible values: jpg|jpeg|png|gif */ public function render(string $save_as = null, string $ext = null): string { - if ($this->from == '') - { + if ($this->from == '') { + $this->loadDimensions(); $image = @imagecreate($this->height, $this->width); imagecolorallocatealpha($image, ...$this->bg_color); $ext = $ext ?: $this->getExtension($save_as ?? '.png'); - } - else - { + } else { $this->height = $this->width = 0; - if (! is_readable($this->from)) { - throw new RuntimeException('Image to write text to not specified or does not exist.'); + if (!is_readable($this->from)) { + throw new \RuntimeException('Image to write text to not specified or does not exist.'); } $ext = $this->getExtension($this->from); @@ -110,28 +161,40 @@ public function render(string $save_as = null, string $ext = null): string } elseif ($ext == 'gif') { $image = imagecreatefromgif($this->from); } else { - throw new RuntimeException("{$ext} not supported, implement it yourself."); + throw new \RuntimeException("{$ext} not supported, implement it yourself."); } } + foreach ($this->bg_layers as $bg_layer) { + $bg_layer_color = imagecolorallocatealpha($image, ...$bg_layer->getColor()); + $bg_layer_position = $bg_layer->getPosition(); + imagefilledrectangle( + $image, + $bg_layer_position[0], + $bg_layer_position[1], + (isset($bg_layer_position[2])) ? $bg_layer_position[2] : imagesx($image), + (isset($bg_layer_position[3])) ? $bg_layer_position[3] : imagesy($image), + $bg_layer_color + ); + } + /** @var Text $text */ - foreach ($this->texts as $text) - { - if (($font = $text->getFont()) !== '' && ! is_readable($font)) { - throw new RuntimeException("Font \"{$font}\" not found."); + foreach ($this->texts as $text) { + if (($font = $text->getFont()) !== '' && !is_readable($font)) { + throw new \RuntimeException("Font \"{$font}\" not found."); } if ($this->height === 0) { - $this->height = imagesx($image); - $this->width = imagesy($image); + $this->height = imagesy($image); + $this->width = imagesx($image); } $closure = $text->getCallback(); $closure && $closure($this, $text, $image); - list($position_x, $position_y) = $text->getPosition(); - if (! empty($shadow_color = $text->getShadowColor())) { - list($x, $y) = $text->getShadow(); + [$position_x, $position_y] = $text->getPosition(); + if (!empty($shadow_color = $text->getShadowColor())) { + [$x, $y] = $text->getShadow(); $this->write($image, $text, $x + $position_x, $y + $position_y, $shadow_color); } @@ -140,7 +203,7 @@ public function render(string $save_as = null, string $ext = null): string imagesavealpha($image, true); - $save_as = $save_as ?? fopen('php://memory','r+'); + $save_as = $save_as ?? fopen('php://memory', 'r+'); if ($ext == 'jpg' || $ext == 'jpeg') { imagejpeg($image, $save_as); } elseif ($ext == 'png') { @@ -152,6 +215,7 @@ public function render(string $save_as = null, string $ext = null): string imagedestroy($image); if (is_resource($save_as)) { rewind($save_as); + return stream_get_contents($save_as); } @@ -160,14 +224,33 @@ public function render(string $save_as = null, string $ext = null): string /** * Gets files extension. - * - * @param string $filename - * - * @return string */ private function getExtension(string $filename): string { - return (($position = strrpos($filename,'.')) !== false) ? substr($filename,$position + 1) : ''; + return (($position = strrpos($filename, '.')) !== false) ? mb_strtolower(substr($filename, $position + 1)) : ''; + } + + private function calculateTextBox($text, $font_file, $font_size, $font_angle) + { + /************ + simple function that calculates the *exact* bounding box (single pixel precision). + The function returns an associative array with these keys: + left, top: coordinates you will pass to imagettftext + width, height: dimension of the image you have to create + *************/ + $rect = imagettfbbox($font_size, $font_angle, $font_file, $text); + $minX = min([$rect[0], $rect[2], $rect[4], $rect[6]]); + $maxX = max([$rect[0], $rect[2], $rect[4], $rect[6]]); + $minY = min([$rect[1], $rect[3], $rect[5], $rect[7]]); + $maxY = max([$rect[1], $rect[3], $rect[5], $rect[7]]); + + return [ + 'left' => abs($minX) - 1, + 'top' => abs($minY) - 1, + 'width' => $maxX - $minX, + 'height' => $maxY - $minY, + 'box' => $rect, + ]; } /** @@ -175,31 +258,35 @@ private function getExtension(string $filename): string * * @param resource $image * @param int $font_size - * @param int $x - * @param int $y * @param string $text - * @param array $color * @param string|null $font * @param float $degrees */ private function write($image, Text $text, int $x, int $y, array $color) { - $color[3] = 127 - ($color[3] >> 1); - $font = $text->getFont(); - $label = $text->getText(); - $rotation = $text->getRotation(); + $color[3] = 127 - ($color[3] >> 1); + $font = $text->getFont(); + $label = $text->getText(); + $rotation = $text->getRotation(); $font_size = $text->getFontSize(); + $scale_font_size = $text->shouldScaleFontSize(); - if ($font !== '') - { - imagettftext($image, $font_size, -$rotation, $x, $y, imagecolorallocatealpha($image, ...$color), $font, $label); + if ($scale_font_size) { + $bbox = $this->calculateTextBox($label, $font, $font_size, $rotation); + $image_width = imagesx($image); + $scale = $image_width / $bbox['width']; + if ($scale < 1) { + $font_size = $font_size * $scale * 0.95; + } } - elseif ($rotation > 0) - { + + if ($font !== '') { + imagettftext($image, $font_size, -$rotation, $x, $y, imagecolorallocatealpha($image, ...$color), $font, $label); + } elseif ($rotation > 0) { // create a tmp image - $text_width = imagefontwidth($font_size) * strlen($label); + $text_width = imagefontwidth($font_size) * strlen($label); $text_height = imagefontheight($font_size); - $text_image = imagecreate($text_width + 3, $text_height + 3); + $text_image = imagecreate($text_width + 3, $text_height + 3); imagecolorallocatealpha($text_image, 0, 0, 0, 127); // write to the temp image @@ -209,10 +296,8 @@ private function write($image, Text $text, int $x, int $y, array $color) // copy the temp image back to the real image imagecopy($image, $text_image, $x, $y, 0, 0, imagesx($text_image), imagesy($text_image)); imagedestroy($text_image); - } - else - { + } else { imagestring($image, $font_size, $x, $y, $label, imagecolorallocatealpha($image, ...$color)); } } -} \ No newline at end of file +}