Skip to content
228 changes: 185 additions & 43 deletions src/Box.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

namespace GDText;

use GDText\Struct\Point;
Expand Down Expand Up @@ -56,6 +57,11 @@ class Box
*/
protected $baseline = 0.2;

/**
* @var int|float
*/
protected $spacing = 0;

/**
* @var string
*/
Expand Down Expand Up @@ -120,7 +126,7 @@ public function setStrokeColor(Color $color)
{
$this->strokeColor = $color;
}

/**
* @param int $v Stroke size in *pixels*
*/
Expand All @@ -130,16 +136,16 @@ public function setStrokeSize($v)
}

/**
* @param Color $color Shadow color
* @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.
* @param Color $color Shadow color
* @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)
{
$this->textShadow = array(
'color' => $color,
$this->textShadow = [
'color' => $color,
'offset' => new Point($xShift, $yShift)
);
];
}

/**
Expand All @@ -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)
Expand All @@ -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.');
Expand All @@ -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)
Expand All @@ -217,19 +234,93 @@ 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, $maxFontSize = -1, $minFontSize = -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 (($minFontSize == -1 || $usedFontSize > $minFontSize) &&
($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 (($maxFontSize > 0 && $usedFontSize < $maxFontSize)
&& $rectangle->getHeight() < $this->box->getHeight()
&& $rectangle->getWidth() < $this->box->getWidth());

$usedFontSize -= $precision * 2;
}
$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.');
}

switch ($this->textWrapping) {
case TextWrapping::NoWrap:
$lines = array($text);
$lines = [$text];
break;
case TextWrapping::WrapWithOverflow:
default:
Expand Down Expand Up @@ -261,6 +352,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) {
Expand All @@ -280,7 +375,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;

Expand Down Expand Up @@ -308,52 +403,63 @@ 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);
}

/**
* 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) {
// Check every line if it needs to be wrapped
$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;
Expand Down Expand Up @@ -383,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

Expand All @@ -406,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);
Expand All @@ -416,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;
}
}
}
}