Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Method to calculate drawn text and automatically fit font size #30

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
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;
}
}
}
}