From 73c53d5e15fc69c781ed1d1f0b138a59adbc3f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Mar=C3=ADn?= Date: Fri, 21 Jul 2017 13:18:29 +0200 Subject: [PATCH 1/8] Method to calculate drawn text and automatically fit font size --- src/Box.php | 112 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 97 insertions(+), 15 deletions(-) diff --git a/src/Box.php b/src/Box.php index 3721a50..b28f447 100644 --- a/src/Box.php +++ b/src/Box.php @@ -120,7 +120,7 @@ public function setStrokeColor(Color $color) { $this->strokeColor = $color; } - + /** * @param int $v Stroke size in *pixels* */ @@ -217,11 +217,80 @@ public function setTextWrapping($textWrapping) $this->textWrapping = $textWrapping; } + /** * Draws the text on the picture. + * * @param string $text Text to draw. May contain newline characters. + * + * @return Rectangle Area that cover the drawn text */ public function draw($text) + { + return $this->drawText($text, true); + } + + /** + * Draws the text on the picture, fitting it to the current box + * + * @param string $text Text to draw. May contain newline characters. + * @param int $precision Increment or decrement of font size. The lower this value, the slower this method. + * + * @return Rectangle Area that cover the drawn text + */ + public function drawFitFontSize($text, $precision = -1, &$usedFontSize = null) + { + $initialFontSize = $this->fontSize; + + $usedFontSize = $this->fontSize; + $rectangle = $this->calculate($text); + + if ($rectangle->getHeight() > $this->box->getHeight() || $rectangle->getWidth() > $this->box->getWidth()) { + // Decrement font size + do { + $this->setFontSize($usedFontSize); + $rectangle = $this->calculate($text); + + $usedFontSize -= $precision; + } while ($rectangle->getHeight() > $this->box->getHeight() || $rectangle->getWidth() > $this->box->getWidth()); + + $usedFontSize += $precision; + } else { + // Increment font size + do { + $this->setFontSize($usedFontSize); + $rectangle = $this->calculate($text); + + $usedFontSize += $precision; + } while ($rectangle->getHeight() < $this->box->getHeight() && $rectangle->getWidth() < $this->box->getWidth()); + + $usedFontSize -= $precision; + $this->setFontSize($usedFontSize); + } + + $rectangle = $this->drawText($text, true); + + // Restore initial font size + $this->setFontSize($initialFontSize); + + return $rectangle; + } + + /** + * Get the area that will cover the given text + * @return Rectangle + */ + public function calculate($text) + { + return $this->drawText($text, false); + } + + /** + * Draws the text on the picture. + * @param string $text Text to draw. May contain newline characters. + * @return Rectangle + */ + protected function drawText($text, $draw) { if (!isset($this->fontFace)) { throw new \InvalidArgumentException('No path to font file has been specified.'); @@ -261,6 +330,10 @@ public function draw($text) } $n = 0; + + $drawnX = $drawnY = PHP_INT_MAX; + $drawnH = $drawnW = 0; + foreach ($lines as $line) { $box = $this->calculateBox($line); switch ($this->alignX) { @@ -280,7 +353,7 @@ public function draw($text) $xMOD = $this->box->getX() + $xAlign; $yMOD = $this->box->getY() + $yAlign + $yShift + ($n * $lineHeightPx); - if ($line && $this->backgroundColor) { + if ($draw && $line && $this->backgroundColor) { // Marks whole texbox area with given background-color $backgroundHeight = $this->fontSize; @@ -308,29 +381,38 @@ public function draw($text) ); } - if ($this->textShadow !== false) { + if ($draw) { + if ($this->textShadow !== false) { + $this->drawInternal( + new Point( + $xMOD + $this->textShadow['offset']->getX(), + $yMOD + $this->textShadow['offset']->getY() + ), + $this->textShadow['color'], + $line + ); + } + + $this->strokeText($xMOD, $yMOD, $line); $this->drawInternal( new Point( - $xMOD + $this->textShadow['offset']->getX(), - $yMOD + $this->textShadow['offset']->getY() + $xMOD, + $yMOD ), - $this->textShadow['color'], + $this->fontColor, $line ); } - $this->strokeText($xMOD, $yMOD, $line); - $this->drawInternal( - new Point( - $xMOD, - $yMOD - ), - $this->fontColor, - $line - ); + $drawnX = min($xMOD, $drawnX); + $drawnY = min($this->box->getY() + $yAlign + ($n * $lineHeightPx), $drawnY); + $drawnW = max($drawnW, $box->getWidth()); + $drawnH += $lineHeightPx; $n++; } + + return new Rectangle($drawnX, $drawnY, $drawnW, $drawnH); } /** From 9eb1721f5795edc1ae0131a0d75658d8bad529cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Mar=C3=ADn?= Date: Fri, 21 Jul 2017 13:30:47 +0200 Subject: [PATCH 2/8] Min and Max font sizes parameters for `drawFitFontSize` --- src/Box.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Box.php b/src/Box.php index b28f447..94ff141 100644 --- a/src/Box.php +++ b/src/Box.php @@ -238,7 +238,7 @@ public function draw($text) * * @return Rectangle Area that cover the drawn text */ - public function drawFitFontSize($text, $precision = -1, &$usedFontSize = null) + public function drawFitFontSize($text, $precision = -1, $maxFontSize = -1, $minFontSize = -1, &$usedFontSize = null) { $initialFontSize = $this->fontSize; @@ -252,7 +252,8 @@ public function drawFitFontSize($text, $precision = -1, &$usedFontSize = null) $rectangle = $this->calculate($text); $usedFontSize -= $precision; - } while ($rectangle->getHeight() > $this->box->getHeight() || $rectangle->getWidth() > $this->box->getWidth()); + } while (($minFontSize == -1 || $usedFontSize > $minFontSize) && + ($rectangle->getHeight() > $this->box->getHeight() || $rectangle->getWidth() > $this->box->getWidth())); $usedFontSize += $precision; } else { @@ -262,7 +263,8 @@ public function drawFitFontSize($text, $precision = -1, &$usedFontSize = null) $rectangle = $this->calculate($text); $usedFontSize += $precision; - } while ($rectangle->getHeight() < $this->box->getHeight() && $rectangle->getWidth() < $this->box->getWidth()); + } while (($maxFontSize > 0 && $usedFontSize < $maxFontSize) && $rectangle->getHeight() < $this->box->getHeight() && + $rectangle->getWidth() < $this->box->getWidth()); $usedFontSize -= $precision; $this->setFontSize($usedFontSize); From 859b4b40ca105bf16be7ad8a78a224784fcc8116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Mar=C3=ADn?= Date: Fri, 21 Jul 2017 16:42:35 +0200 Subject: [PATCH 3/8] Fixed bug in `drawFitFontSize` --- src/Box.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Box.php b/src/Box.php index 94ff141..d4b5f34 100644 --- a/src/Box.php +++ b/src/Box.php @@ -263,12 +263,13 @@ public function drawFitFontSize($text, $precision = -1, $maxFontSize = -1, $minF $rectangle = $this->calculate($text); $usedFontSize += $precision; - } while (($maxFontSize > 0 && $usedFontSize < $maxFontSize) && $rectangle->getHeight() < $this->box->getHeight() && - $rectangle->getWidth() < $this->box->getWidth()); + } while (($maxFontSize > 0 && $usedFontSize < $maxFontSize) + && $rectangle->getHeight() < $this->box->getHeight() + && $rectangle->getWidth() < $this->box->getWidth()); - $usedFontSize -= $precision; - $this->setFontSize($usedFontSize); + $usedFontSize -= $precision * 2; } + $this->setFontSize($usedFontSize); $rectangle = $this->drawText($text, true); From 3fc6da28893eb9e92b1da4a0a5d7fc95956ddd58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Mar=C3=ADn?= Date: Mon, 13 Dec 2021 10:07:36 +0100 Subject: [PATCH 4/8] feat: support for char spacing --- src/Box.php | 60 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/src/Box.php b/src/Box.php index d4b5f34..07cab75 100644 --- a/src/Box.php +++ b/src/Box.php @@ -56,6 +56,11 @@ class Box */ protected $baseline = 0.2; + /** + * @var int + */ + protected $spacing = 0; + /** * @var string */ @@ -189,6 +194,14 @@ public function setTextAlign($x = 'left', $y = 'top') $this->alignY = $y; } + /** + * @param int $spacing Spacing between characters + */ + public function setSpacing($spacing) + { + $this->spacing = $spacing; + } + /** * Sets textbox position and dimensions * @param int $x Distance in pixels from left edge of image. @@ -480,6 +493,19 @@ protected function calculateBox($text) $yLower = $bounds[1]; // lower (left|right) corner, Y position $yUpper = $bounds[5]; // upper (left|right) corner, Y position + if ($this->spacing != 0) { // Fix text width + $getBoxW = fn($bBox) => $bBox[2] - $bBox[0]; + + $xRight = $xLeft; + $testStr = 'test'; + $size = $this->getFontSizeInPoints(); + $testW = $getBoxW(imagettfbbox($size, 0, $this->fontFace, $testStr)); + foreach (mb_str_split($text) as $char) { + $fullBox = imagettfbbox($size, 0, $this->fontFace, $char . $testStr); + $xRight += $this->spacing + $getBoxW($fullBox) - $testW; + } + } + return new Rectangle( $xLeft, $yUpper, @@ -501,15 +527,29 @@ protected function strokeText($x, $y, $text) protected function drawInternal(Point $position, Color $color, $text) { - imagettftext( - $this->im, - $this->getFontSizeInPoints(), - 0, // no rotation - $position->getX(), - $position->getY(), - $color->getIndex($this->im), - $this->fontFace, - $text - ); + if ($this->spacing == 0) { + imagettftext( + $this->im, + $this->getFontSizeInPoints(), + 0, // no rotation + $position->getX(), + $position->getY(), + $color->getIndex($this->im), + $this->fontFace, + $text + ); + } else { + $getBoxW = fn($bBox) => $bBox[2] - $bBox[0]; + + $x = $position->getX(); + $testStr = 'test'; + $size = $this->getFontSizeInPoints(); + $testW = $getBoxW(imagettfbbox($size, 0, $this->fontFace, $testStr)); + foreach (mb_str_split($text) as $char) { + $fullBox = imagettfbbox($size, 0, $this->fontFace, $char . $testStr); + imagettftext($this->im, $size, 0, $x - $fullBox[0], $position->getY(), $color->getIndex($this->im), $this->fontFace, $char); + $x += $this->spacing + $getBoxW($fullBox) - $testW; + } + } } } From 3c61ed74b891fd0d2541b290e8e7885c3a840c7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Mar=C3=ADn?= Date: Mon, 13 Dec 2021 10:30:33 +0100 Subject: [PATCH 5/8] feat: support for char spacing --- src/Box.php | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/Box.php b/src/Box.php index 07cab75..b22b0b6 100644 --- a/src/Box.php +++ b/src/Box.php @@ -57,7 +57,7 @@ class Box protected $baseline = 0.2; /** - * @var int + * @var int|float */ protected $spacing = 0; @@ -195,7 +195,7 @@ public function setTextAlign($x = 'left', $y = 'top') } /** - * @param int $spacing Spacing between characters + * @param int|float $spacing Spacing between characters */ public function setSpacing($spacing) { @@ -489,23 +489,10 @@ protected function calculateBox($text) $bounds = imagettfbbox($this->getFontSizeInPoints(), 0, $this->fontFace, $text); $xLeft = $bounds[0]; // (lower|upper) left corner, X position - $xRight = $bounds[2]; // (lower|upper) right corner, X position + $xRight = $bounds[2] + (mb_strlen($text) * $this->spacing); // (lower|upper) right corner, X position $yLower = $bounds[1]; // lower (left|right) corner, Y position $yUpper = $bounds[5]; // upper (left|right) corner, Y position - if ($this->spacing != 0) { // Fix text width - $getBoxW = fn($bBox) => $bBox[2] - $bBox[0]; - - $xRight = $xLeft; - $testStr = 'test'; - $size = $this->getFontSizeInPoints(); - $testW = $getBoxW(imagettfbbox($size, 0, $this->fontFace, $testStr)); - foreach (mb_str_split($text) as $char) { - $fullBox = imagettfbbox($size, 0, $this->fontFace, $char . $testStr); - $xRight += $this->spacing + $getBoxW($fullBox) - $testW; - } - } - return new Rectangle( $xLeft, $yUpper, @@ -538,7 +525,7 @@ protected function drawInternal(Point $position, Color $color, $text) $this->fontFace, $text ); - } else { + } else { // https://stackoverflow.com/a/65254013/528065 $getBoxW = fn($bBox) => $bBox[2] - $bBox[0]; $x = $position->getX(); From 4d5d2f05db9f028f43e879738b6642a858287b2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Mar=C3=ADn?= Date: Mon, 13 Dec 2021 10:42:13 +0100 Subject: [PATCH 6/8] feat: support for char spacing --- src/Box.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Box.php b/src/Box.php index b22b0b6..fb7f2b8 100644 --- a/src/Box.php +++ b/src/Box.php @@ -533,6 +533,24 @@ protected function drawInternal(Point $position, Color $color, $text) $size = $this->getFontSizeInPoints(); $testW = $getBoxW(imagettfbbox($size, 0, $this->fontFace, $testStr)); foreach (mb_str_split($text) as $char) { + if ($this->debug) { + $bounds = imagettfbbox($size, 0, $this->fontFace, $char); + $xLeft = $bounds[0]; // (lower|upper) left corner, X position + $xRight = $bounds[2]; // (lower|upper) right corner, X position + $yLower = $bounds[1]; // lower (left|right) corner, Y position + $yUpper = $bounds[5]; // upper (left|right) corner, Y position + + $this->drawFilledRectangle( + new Rectangle( + $x - $bounds[0], + $position->getY() - ($yLower - $yUpper), + $xRight - $xLeft, + $yLower - $yUpper + ), + new Color(rand(180, 255), rand(180, 255), rand(180, 255), 80) + ); + } + $fullBox = imagettfbbox($size, 0, $this->fontFace, $char . $testStr); imagettftext($this->im, $size, 0, $x - $fullBox[0], $position->getY(), $color->getIndex($this->im), $this->fontFace, $char); $x += $this->spacing + $getBoxW($fullBox) - $testW; From 9b4913f0ce4d17ce9ff3fed6db86dd48081ad551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Mar=C3=ADn?= Date: Tue, 26 Apr 2022 17:29:16 +0200 Subject: [PATCH 7/8] fix: PHP8.1 support --- src/Box.php | 113 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 85 insertions(+), 28 deletions(-) diff --git a/src/Box.php b/src/Box.php index d4b5f34..be685b4 100644 --- a/src/Box.php +++ b/src/Box.php @@ -1,4 +1,5 @@ textShadow = array( - 'color' => $color, + $this->textShadow = [ + 'color' => $color, 'offset' => new Point($xShift, $yShift) - ); + ]; } /** @@ -152,6 +158,7 @@ public function setBackgroundColor(Color $color) /** * Allows to customize spacing between lines. + * * @param float $v Height of the single text line, in percents, proportionally to font size */ public function setLineHeight($v) @@ -169,13 +176,14 @@ public function setBaseline($v) /** * Sets text alignment inside textbox + * * @param string $x Horizontal alignment. Allowed values are: left, center, right. * @param string $y Vertical alignment. Allowed values are: top, center, bottom. */ public function setTextAlign($x = 'left', $y = 'top') { - $xAllowed = array('left', 'right', 'center'); - $yAllowed = array('top', 'bottom', 'center'); + $xAllowed = ['left', 'right', 'center']; + $yAllowed = ['top', 'bottom', 'center']; if (!in_array($x, $xAllowed)) { throw new \InvalidArgumentException('Invalid horizontal alignement value was specified.'); @@ -189,11 +197,20 @@ public function setTextAlign($x = 'left', $y = 'top') $this->alignY = $y; } + /** + * @param int|float $spacing Spacing between characters + */ + public function setSpacing($spacing) + { + $this->spacing = $spacing; + } + /** * Sets textbox position and dimensions - * @param int $x Distance in pixels from left edge of image. - * @param int $y Distance in pixels from top edge of image. - * @param int $width Width of texbox in pixels. + * + * @param int $x Distance in pixels from left edge of image. + * @param int $y Distance in pixels from top edge of image. + * @param int $width Width of texbox in pixels. * @param int $height Height of textbox in pixels. */ public function setBox($x, $y, $width, $height) @@ -290,7 +307,9 @@ public function calculate($text) /** * Draws the text on the picture. + * * @param string $text Text to draw. May contain newline characters. + * * @return Rectangle */ protected function drawText($text, $draw) @@ -301,7 +320,7 @@ protected function drawText($text, $draw) switch ($this->textWrapping) { case TextWrapping::NoWrap: - $lines = array($text); + $lines = [$text]; break; case TextWrapping::WrapWithOverflow: default: @@ -420,12 +439,14 @@ protected function drawText($text, $draw) /** * Splits overflowing text into array of strings. + * * @param string $text + * * @return string[] */ protected function wrapTextWithOverflow($text) { - $lines = array(); + $lines = []; // Split text explicitly into lines by \n, \r\n and \r $explicitLines = preg_split('/\n|\r\n?/', $text); foreach ($explicitLines as $line) { @@ -433,12 +454,12 @@ protected function wrapTextWithOverflow($text) $words = explode(" ", $line); $line = $words[0]; for ($i = 1; $i < count($words); $i++) { - $box = $this->calculateBox($line." ".$words[$i]); + $box = $this->calculateBox($line . " " . $words[$i]); if ($box->getWidth() >= $this->box->getWidth()) { $lines[] = $line; $line = $words[$i]; } else { - $line .= " ".$words[$i]; + $line .= " " . $words[$i]; } } $lines[] = $line; @@ -468,15 +489,17 @@ protected function drawFilledRectangle(Rectangle $rect, Color $color) /** * Returns the bounding box of a text. + * * @param string $text + * * @return Rectangle */ protected function calculateBox($text) { $bounds = imagettfbbox($this->getFontSizeInPoints(), 0, $this->fontFace, $text); - $xLeft = $bounds[0]; // (lower|upper) left corner, X position - $xRight = $bounds[2]; // (lower|upper) right corner, X position + $xLeft = $bounds[0]; // (lower|upper) left corner, X position + $xRight = $bounds[2] + (mb_strlen($text) * $this->spacing); // (lower|upper) right corner, X position $yLower = $bounds[1]; // lower (left|right) corner, Y position $yUpper = $bounds[5]; // upper (left|right) corner, Y position @@ -491,7 +514,9 @@ protected function calculateBox($text) protected function strokeText($x, $y, $text) { $size = $this->strokeSize; - if ($size <= 0) return; + if ($size <= 0) { + return; + } for ($c1 = $x - $size; $c1 <= $x + $size; $c1++) { for ($c2 = $y - $size; $c2 <= $y + $size; $c2++) { $this->drawInternal(new Point($c1, $c2), $this->strokeColor, $text); @@ -501,15 +526,47 @@ protected function strokeText($x, $y, $text) protected function drawInternal(Point $position, Color $color, $text) { - imagettftext( - $this->im, - $this->getFontSizeInPoints(), - 0, // no rotation - $position->getX(), - $position->getY(), - $color->getIndex($this->im), - $this->fontFace, - $text - ); + if ($this->spacing == 0) { + imagettftext( + $this->im, + $this->getFontSizeInPoints(), + 0, // no rotation + (int)round($position->getX()), + (int)round($position->getY()), + $color->getIndex($this->im), + $this->fontFace, + $text + ); + } else { // https://stackoverflow.com/a/65254013/528065 + $getBoxW = fn($bBox) => $bBox[2] - $bBox[0]; + + $x = $position->getX(); + $testStr = 'test'; + $size = $this->getFontSizeInPoints(); + $testW = $getBoxW(imagettfbbox($size, 0, $this->fontFace, $testStr)); + foreach (mb_str_split($text) as $char) { + if ($this->debug) { + $bounds = imagettfbbox($size, 0, $this->fontFace, $char); + $xLeft = $bounds[0]; // (lower|upper) left corner, X position + $xRight = $bounds[2]; // (lower|upper) right corner, X position + $yLower = $bounds[1]; // lower (left|right) corner, Y position + $yUpper = $bounds[5]; // upper (left|right) corner, Y position + + $this->drawFilledRectangle( + new Rectangle( + $x - $bounds[0], + $position->getY() - ($yLower - $yUpper), + $xRight - $xLeft, + $yLower - $yUpper + ), + new Color(rand(180, 255), rand(180, 255), rand(180, 255), 80) + ); + } + + $fullBox = imagettfbbox($size, 0, $this->fontFace, $char . $testStr); + imagettftext($this->im, $size, 0, (int)round($x - $fullBox[0]), (int)round($position->getY()), $color->getIndex($this->im), $this->fontFace, $char); + $x += $this->spacing + $getBoxW($fullBox) - $testW; + } + } } } From 7f95a4101c048152bbed3cd8bef698f8db659290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Mar=C3=ADn?= Date: Thu, 11 May 2023 11:18:28 +0200 Subject: [PATCH 8/8] refactor: PHP 8.1, strict types, use enums, add phpstan --- composer.json | 7 +- phpstan.neon | 4 + src/Box.php | 227 ++++++++++++------------------------ src/Color.php | 105 ++++++++--------- src/HorizontalAlignment.php | 11 +- src/Struct/Point.php | 28 +---- src/Struct/Rectangle.php | 50 ++------ src/TextWrapping.php | 9 +- src/VerticalAlignment.php | 11 +- tests/TestCase.php | 23 +--- tests/TextAlignmentTest.php | 4 +- tests/TextWrappingTest.php | 6 +- 12 files changed, 175 insertions(+), 310 deletions(-) create mode 100644 phpstan.neon diff --git a/composer.json b/composer.json index 34e893d..a180435 100644 --- a/composer.json +++ b/composer.json @@ -3,11 +3,12 @@ "description": "A class drawing multiline and aligned text on pictures. Uses GD extension.", "license": "MIT", "require": { - "php": ">=5.3", - "ext-gd": "*" + "php": ">=8.1", + "ext-gd": "*", + "ext-mbstring": "*" }, "require-dev": { - "phpunit/phpunit": "^6.2" + "phpunit/phpunit": "^10.1.2" }, "autoload": { "psr-4": { diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..b170ac7 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: 6 + paths: + - ./src \ No newline at end of file diff --git a/src/Box.php b/src/Box.php index be685b4..05c8c88 100644 --- a/src/Box.php +++ b/src/Box.php @@ -1,5 +1,7 @@ im = $image; $this->fontColor = new Color(0, 0, 0); @@ -98,7 +53,7 @@ public function __construct(&$image) /** * @param Color $color Font color */ - public function setFontColor(Color $color) + public function setFontColor(Color $color): void { $this->fontColor = $color; } @@ -106,7 +61,7 @@ public function setFontColor(Color $color) /** * @param string $path Path to the font file */ - public function setFontFace($path) + public function setFontFace(string $path): void { $this->fontFace = $path; } @@ -114,7 +69,7 @@ public function setFontFace($path) /** * @param int $v Font size in *pixels* */ - public function setFontSize($v) + public function setFontSize(int $v): void { $this->fontSize = $v; } @@ -122,7 +77,7 @@ public function setFontSize($v) /** * @param Color $color Stroke color */ - public function setStrokeColor(Color $color) + public function setStrokeColor(Color $color): void { $this->strokeColor = $color; } @@ -130,7 +85,7 @@ public function setStrokeColor(Color $color) /** * @param int $v Stroke size in *pixels* */ - public function setStrokeSize($v) + public function setStrokeSize(int $v): void { $this->strokeSize = $v; } @@ -140,7 +95,7 @@ public function setStrokeSize($v) * @param int $xShift Relative shadow position in pixels. Positive values move shadow to right, negative to left. * @param int $yShift Relative shadow position in pixels. Positive values move shadow to bottom, negative to up. */ - public function setTextShadow(Color $color, $xShift, $yShift) + public function setTextShadow(Color $color, int $xShift, int $yShift): void { $this->textShadow = [ 'color' => $color, @@ -151,7 +106,7 @@ public function setTextShadow(Color $color, $xShift, $yShift) /** * @param Color $color Font color */ - public function setBackgroundColor(Color $color) + public function setBackgroundColor(Color $color): void { $this->backgroundColor = $color; } @@ -161,7 +116,7 @@ public function setBackgroundColor(Color $color) * * @param float $v Height of the single text line, in percents, proportionally to font size */ - public function setLineHeight($v) + public function setLineHeight(float $v): void { $this->lineHeight = $v; } @@ -169,7 +124,7 @@ public function setLineHeight($v) /** * @param float $v Position of baseline, in percents, proportionally to line height measuring from the bottom. */ - public function setBaseline($v) + public function setBaseline(float $v): void { $this->baseline = $v; } @@ -177,20 +132,17 @@ public function setBaseline($v) /** * Sets text alignment inside textbox * - * @param string $x Horizontal alignment. Allowed values are: left, center, right. - * @param string $y Vertical alignment. Allowed values are: top, center, bottom. + * @param HorizontalAlignment|string $x Horizontal alignment. Allowed values are: left, center, right. + * @param VerticalAlignment|string $y Vertical alignment. Allowed values are: top, center, bottom. */ - public function setTextAlign($x = 'left', $y = 'top') + public function setTextAlign(HorizontalAlignment|string $x = HorizontalAlignment::Left, VerticalAlignment|string $y = VerticalAlignment::Top): void { - $xAllowed = ['left', 'right', 'center']; - $yAllowed = ['top', 'bottom', 'center']; - - if (!in_array($x, $xAllowed)) { - throw new \InvalidArgumentException('Invalid horizontal alignement value was specified.'); + if (is_string($x)) { + $x = HorizontalAlignment::from($x); } - if (!in_array($y, $yAllowed)) { - throw new \InvalidArgumentException('Invalid vertical alignement value was specified.'); + if (is_string($y)) { + $y = VerticalAlignment::from($y); } $this->alignX = $x; @@ -198,9 +150,9 @@ public function setTextAlign($x = 'left', $y = 'top') } /** - * @param int|float $spacing Spacing between characters + * @param float|int $spacing Spacing between characters */ - public function setSpacing($spacing) + public function setSpacing(float|int $spacing): void { $this->spacing = $spacing; } @@ -213,7 +165,7 @@ public function setSpacing($spacing) * @param int $width Width of texbox in pixels. * @param int $height Height of textbox in pixels. */ - public function setBox($x, $y, $width, $height) + public function setBox(int $x, int $y, int $width, int $height): void { $this->box = new Rectangle($x, $y, $width, $height); } @@ -221,15 +173,12 @@ public function setBox($x, $y, $width, $height) /** * Enables debug mode. Whole textbox and individual lines will be filled with random colors. */ - public function enableDebug() + public function enableDebug(): void { $this->debug = true; } - /** - * @param int $textWrapping - */ - public function setTextWrapping($textWrapping) + public function setTextWrapping(TextWrapping $textWrapping): void { $this->textWrapping = $textWrapping; } @@ -242,7 +191,7 @@ public function setTextWrapping($textWrapping) * * @return Rectangle Area that cover the drawn text */ - public function draw($text) + public function draw(string $text): Rectangle { return $this->drawText($text, true); } @@ -255,7 +204,7 @@ public function draw($text) * * @return Rectangle Area that cover the drawn text */ - public function drawFitFontSize($text, $precision = -1, $maxFontSize = -1, $minFontSize = -1, &$usedFontSize = null) + public function drawFitFontSize(string $text, int $precision = -1, int $maxFontSize = -1, int $minFontSize = -1, int &$usedFontSize = null): Rectangle { $initialFontSize = $this->fontSize; @@ -298,9 +247,8 @@ public function drawFitFontSize($text, $precision = -1, $maxFontSize = -1, $minF /** * Get the area that will cover the given text - * @return Rectangle */ - public function calculate($text) + public function calculate(string $text): Rectangle { return $this->drawText($text, false); } @@ -309,24 +257,17 @@ public function calculate($text) * Draws the text on the picture. * * @param string $text Text to draw. May contain newline characters. - * - * @return Rectangle */ - protected function drawText($text, $draw) + protected function drawText(string $text, bool $draw): Rectangle { if (!isset($this->fontFace)) { throw new \InvalidArgumentException('No path to font file has been specified.'); } - switch ($this->textWrapping) { - case TextWrapping::NoWrap: - $lines = [$text]; - break; - case TextWrapping::WrapWithOverflow: - default: - $lines = $this->wrapTextWithOverflow($text); - break; - } + $lines = match ($this->textWrapping) { + TextWrapping::NoWrap => [$text], + default => $this->wrapTextWithOverflow($text), + }; if ($this->debug) { // Marks whole texbox area with color @@ -339,17 +280,11 @@ protected function drawText($text, $draw) $lineHeightPx = $this->lineHeight * $this->fontSize; $textHeight = count($lines) * $lineHeightPx; - switch ($this->alignY) { - case VerticalAlignment::Center: - $yAlign = ($this->box->getHeight() / 2) - ($textHeight / 2); - break; - case VerticalAlignment::Bottom: - $yAlign = $this->box->getHeight() - $textHeight; - break; - case VerticalAlignment::Top: - default: - $yAlign = 0; - } + $yAlign = match ($this->alignY) { + VerticalAlignment::Center => ($this->box->getHeight() / 2) - ($textHeight / 2), + VerticalAlignment::Bottom => $this->box->getHeight() - $textHeight, + default => 0, + }; $n = 0; @@ -358,22 +293,16 @@ protected function drawText($text, $draw) foreach ($lines as $line) { $box = $this->calculateBox($line); - switch ($this->alignX) { - case HorizontalAlignment::Center: - $xAlign = ($this->box->getWidth() - $box->getWidth()) / 2; - break; - case HorizontalAlignment::Right: - $xAlign = ($this->box->getWidth() - $box->getWidth()); - break; - case HorizontalAlignment::Left: - default: - $xAlign = 0; - } + $xAlign = match ($this->alignX) { + HorizontalAlignment::Center => (int)round(($this->box->getWidth() - $box->getWidth()) / 2), + HorizontalAlignment::Right => ($this->box->getWidth() - $box->getWidth()), + default => 0, + }; $yShift = $lineHeightPx * (1 - $this->baseline); // current line X and Y position $xMOD = $this->box->getX() + $xAlign; - $yMOD = $this->box->getY() + $yAlign + $yShift + ($n * $lineHeightPx); + $yMOD = (int)round($this->box->getY() + $yAlign + $yShift + ($n * $lineHeightPx)); if ($draw && $line && $this->backgroundColor) { // Marks whole texbox area with given background-color @@ -382,7 +311,10 @@ protected function drawText($text, $draw) $this->drawFilledRectangle( new Rectangle( $xMOD, - $this->box->getY() + $yAlign + ($n * $lineHeightPx) + ($lineHeightPx - $backgroundHeight) + (1 - $this->lineHeight) * 13 * (1 / 50 * $this->fontSize), + (int)round( + $this->box->getY() + $yAlign + ($n * $lineHeightPx) + ($lineHeightPx - $backgroundHeight) + + (1 - $this->lineHeight) * 13 * (1 / 50 * $this->fontSize) + ), $box->getWidth(), $backgroundHeight ), @@ -395,16 +327,16 @@ protected function drawText($text, $draw) $this->drawFilledRectangle( new Rectangle( $xMOD, - $this->box->getY() + $yAlign + ($n * $lineHeightPx), + (int)round($this->box->getY() + $yAlign + ($n * $lineHeightPx)), $box->getWidth(), - $lineHeightPx + (int)round($lineHeightPx) ), new Color(rand(1, 180), rand(1, 180), rand(1, 180)) ); } if ($draw) { - if ($this->textShadow !== false) { + if (isset($this->textShadow)) { $this->drawInternal( new Point( $xMOD + $this->textShadow['offset']->getX(), @@ -434,17 +366,15 @@ protected function drawText($text, $draw) $n++; } - return new Rectangle($drawnX, $drawnY, $drawnW, $drawnH); + return new Rectangle((int)round($drawnX), (int)round($drawnY), $drawnW, (int)round($drawnH)); } /** * Splits overflowing text into array of strings. * - * @param string $text - * * @return string[] */ - protected function wrapTextWithOverflow($text) + protected function wrapTextWithOverflow(string $text): array { $lines = []; // Split text explicitly into lines by \n, \r\n and \r @@ -467,15 +397,12 @@ protected function wrapTextWithOverflow($text) return $lines; } - /** - * @return float - */ - protected function getFontSizeInPoints() + protected function getFontSizeInPoints(): float { return 0.75 * $this->fontSize; } - protected function drawFilledRectangle(Rectangle $rect, Color $color) + protected function drawFilledRectangle(Rectangle $rect, Color $color): void { imagefilledrectangle( $this->im, @@ -489,12 +416,8 @@ protected function drawFilledRectangle(Rectangle $rect, Color $color) /** * Returns the bounding box of a text. - * - * @param string $text - * - * @return Rectangle */ - protected function calculateBox($text) + protected function calculateBox(string $text): Rectangle { $bounds = imagettfbbox($this->getFontSizeInPoints(), 0, $this->fontFace, $text); @@ -506,12 +429,12 @@ protected function calculateBox($text) return new Rectangle( $xLeft, $yUpper, - $xRight - $xLeft, + (int)round($xRight - $xLeft), $yLower - $yUpper ); } - protected function strokeText($x, $y, $text) + protected function strokeText(int $x, int $y, string $text): void { $size = $this->strokeSize; if ($size <= 0) { @@ -524,7 +447,7 @@ protected function strokeText($x, $y, $text) } } - protected function drawInternal(Point $position, Color $color, $text) + protected function drawInternal(Point $position, Color $color, string $text): void { if ($this->spacing == 0) { imagettftext( diff --git a/src/Color.php b/src/Color.php index 5efee50..adda5f0 100644 --- a/src/Color.php +++ b/src/Color.php @@ -1,4 +1,7 @@ red = $red; - $this->green = $green; - $this->blue = $blue; - $this->alpha = $alpha; } /** * Parses string to Color object representation. + * * @param string $str String with color information, ex. #000000 + * * @return Color * @todo Add parsing of CSS-like strings: rgb(), rgba(), hsl() */ - public static function parseString($str) + public static function parseString(string $str): Color { $str = str_replace('#', '', $str); if (strlen($str) == 6) { $r = hexdec(substr($str, 0, 2)); $g = hexdec(substr($str, 2, 2)); $b = hexdec(substr($str, 4, 2)); - } else if (strlen($str) == 3) { + } elseif (strlen($str) == 3) { $r = hexdec(str_repeat(substr($str, 0, 1), 2)); $g = hexdec(str_repeat(substr($str, 1, 1), 2)); $b = hexdec(str_repeat(substr($str, 2, 1), 2)); @@ -69,14 +50,15 @@ public static function parseString($str) * @param float $h Hue * @param float $s Saturation * @param float $l Light + * * @return Color */ - public static function fromHsl($h, $s, $l) + public static function fromHsl(float $h, float $s, float $l): Color { $fromFloat = function (array $rgb) { foreach ($rgb as &$v) { $v = (int)round($v * 255); - }; + } return new Color($rgb[0], $rgb[1], $rgb[2]); }; @@ -84,55 +66,60 @@ public static function fromHsl($h, $s, $l) // If saturation is 0, the given color is grey and only // lightness is relevant. if ($s == 0) { - return $fromFloat(array($l, $l, $l)); + return $fromFloat([$l, $l, $l]); } // Else calculate r, g, b according to hue. // Check http://en.wikipedia.org/wiki/HSL_and_HSV#From_HSL for details $chroma = (1 - abs(2 * $l - 1)) * $s; - $h_ = $h * 6; - $x = $chroma * (1 - abs((fmod($h_,2)) - 1)); // Note: fmod because % (modulo) returns int value!! - $m = $l - round($chroma/2, 10); // Bugfix for strange float behaviour (e.g. $l=0.17 and $s=1) - - if ($h_ >= 0 && $h_ < 1) $rgb = array(($chroma + $m), ($x + $m), $m); - elseif ($h_ >= 1 && $h_ < 2) $rgb = array(($x + $m), ($chroma + $m), $m); - elseif ($h_ >= 2 && $h_ < 3) $rgb = array($m, ($chroma + $m), ($x + $m)); - elseif ($h_ >= 3 && $h_ < 4) $rgb = array($m, ($x + $m), ($chroma + $m)); - elseif ($h_ >= 4 && $h_ < 5) $rgb = array(($x + $m), $m, ($chroma + $m)); - elseif ($h_ >= 5 && $h_ < 6) $rgb = array(($chroma + $m), $m, ($x + $m)); - else throw new \InvalidArgumentException('Invalid hue, it should be a value between 0 and 1.'); + $h_ = $h * 6; + $x = $chroma * (1 - abs((fmod($h_, 2)) - 1)); // Note: fmod because % (modulo) returns int value!! + $m = $l - round($chroma / 2, 10); // Bugfix for strange float behaviour (e.g. $l=0.17 and $s=1) + + if ($h_ >= 0 && $h_ < 1) { + $rgb = [($chroma + $m), ($x + $m), $m]; + } elseif ($h_ >= 1 && $h_ < 2) { + $rgb = [($x + $m), ($chroma + $m), $m]; + } elseif ($h_ >= 2 && $h_ < 3) { + $rgb = [$m, ($chroma + $m), ($x + $m)]; + } elseif ($h_ >= 3 && $h_ < 4) { + $rgb = [$m, ($x + $m), ($chroma + $m)]; + } elseif ($h_ >= 4 && $h_ < 5) { + $rgb = [($x + $m), $m, ($chroma + $m)]; + } elseif ($h_ >= 5 && $h_ < 6) { + $rgb = [($chroma + $m), $m, ($x + $m)]; + } else { + throw new \InvalidArgumentException('Invalid hue, it should be a value between 0 and 1.'); + } return $fromFloat($rgb); } /** - * @param resource $image GD image resource - * @return int Returns the index of the specified color+alpha in the palette of the image, + * @param \GdImage $image GD image resource + * + * @return int|false Returns the index of the specified color+alpha in the palette of the image, * or index of allocated color if the color does not exist in the image's palette. */ - public function getIndex($image) + public function getIndex(\GdImage $image): int|false { $index = $this->hasAlphaChannel() - ? imagecolorexactalpha( - $image, $this->red, $this->green, $this->blue, $this->alpha) - : imagecolorexact( - $image, $this->red, $this->green, $this->blue); + ? imagecolorexactalpha($image, $this->red, $this->green, $this->blue, $this->alpha) + : imagecolorexact($image, $this->red, $this->green, $this->blue); if ($index !== -1) { return $index; } return $this->hasAlphaChannel() - ? imagecolorallocatealpha( - $image, $this->red, $this->green, $this->blue, $this->alpha) - : imagecolorallocate( - $image, $this->red, $this->green, $this->blue); + ? imagecolorallocatealpha($image, $this->red, $this->green, $this->blue, $this->alpha) + : imagecolorallocate($image, $this->red, $this->green, $this->blue); } /** * @return bool TRUE when alpha channel is specified, FALSE otherwise */ - public function hasAlphaChannel() + public function hasAlphaChannel(): bool { return $this->alpha !== null; } @@ -140,8 +127,8 @@ public function hasAlphaChannel() /** * @return int[] */ - public function toArray() + public function toArray(): array { - return array($this->red, $this->green, $this->blue); + return [$this->red, $this->green, $this->blue]; } } \ No newline at end of file diff --git a/src/HorizontalAlignment.php b/src/HorizontalAlignment.php index 3a4aee7..9b70897 100644 --- a/src/HorizontalAlignment.php +++ b/src/HorizontalAlignment.php @@ -1,9 +1,12 @@ x = $x; - $this->y = $y; } - /** - * @return int - */ - public function getX() + public function getX(): int { return $this->x; } - /** - * @return int - */ - public function getY() + public function getY(): int { return $this->y; } diff --git a/src/Struct/Rectangle.php b/src/Struct/Rectangle.php index c24e708..69aa279 100644 --- a/src/Struct/Rectangle.php +++ b/src/Struct/Rectangle.php @@ -1,77 +1,45 @@ width = $width; - $this->height = $height; } - /** - * @return int - */ - public function getWidth() + public function getWidth(): int { return $this->width; } - /** - * @return int - */ - public function getHeight() + public function getHeight(): int { return $this->height; } - /** - * @return int - */ - public function getLeft() + public function getLeft(): int { return $this->getX(); } - /** - * @return int - */ - public function getTop() + public function getTop(): int { return $this->getY(); } - /** - * @return int - */ - public function getRight() + public function getRight(): int { return $this->getX() + $this->width; } - /** - * @return int - */ - public function getBottom() + public function getBottom(): int { return $this->getY() + $this->height; } diff --git a/src/TextWrapping.php b/src/TextWrapping.php index a0b04bd..1d390fa 100644 --- a/src/TextWrapping.php +++ b/src/TextWrapping.php @@ -1,8 +1,11 @@ setTextAlign($x, $y); $box->draw("Owls are birds from the order Strigiformes, which includes about 200 species."); - $this->assertImageEquals("test_align_{$y}_{$x}.png", $im); + $this->assertImageEquals("test_align_{$y->value}_{$x->value}.png", $im); } } } diff --git a/tests/TextWrappingTest.php b/tests/TextWrappingTest.php index b9d2578..44e0e71 100644 --- a/tests/TextWrappingTest.php +++ b/tests/TextWrappingTest.php @@ -3,11 +3,13 @@ use GDText\Box; use GDText\Color; +use GDText\HorizontalAlignment; use GDText\TextWrapping; +use GDText\VerticalAlignment; class TextWrappingTest extends TestCase { - protected function mockBox($im) + protected function mockBox($im): Box { imagealphablending($im, true); imagesavealpha($im, true); @@ -17,7 +19,7 @@ protected function mockBox($im) $box->setFontColor(new Color(255, 75, 140)); $box->setFontSize(16); $box->setBox(0, 135, imagesx($im), 70); - $box->setTextAlign('left', 'top'); + $box->setTextAlign(HorizontalAlignment::Left, VerticalAlignment::Top); return $box; }