1
0
mirror of https://github.com/Intervention/image.git synced 2025-08-26 07:14:31 +02:00

Implement text wrapping

This commit is contained in:
Oliver Vogel
2024-02-03 14:51:17 +01:00
parent ac7389fa96
commit 684f5e6eb6
9 changed files with 161 additions and 9 deletions

View File

@@ -9,6 +9,7 @@ use Intervention\Image\Geometry\Rectangle;
use Intervention\Image\Interfaces\FontInterface; use Intervention\Image\Interfaces\FontInterface;
use Intervention\Image\Interfaces\FontProcessorInterface; use Intervention\Image\Interfaces\FontProcessorInterface;
use Intervention\Image\Interfaces\PointInterface; use Intervention\Image\Interfaces\PointInterface;
use Intervention\Image\Typography\Line;
use Intervention\Image\Typography\TextBlock; use Intervention\Image\Typography\TextBlock;
abstract class AbstractFontProcessor implements FontProcessorInterface abstract class AbstractFontProcessor implements FontProcessorInterface
@@ -20,7 +21,7 @@ abstract class AbstractFontProcessor implements FontProcessorInterface
*/ */
public function textBlock(string $text, FontInterface $font, PointInterface $position): TextBlock public function textBlock(string $text, FontInterface $font, PointInterface $position): TextBlock
{ {
$lines = new TextBlock($text); $lines = $this->wrapTextBlock(new TextBlock($text), $font);
$pivot = $this->buildPivot($lines, $font, $position); $pivot = $this->buildPivot($lines, $font, $position);
$leading = $this->leading($font); $leading = $this->leading($font);
@@ -74,6 +75,58 @@ abstract class AbstractFontProcessor implements FontProcessorInterface
return intval(round($this->typographicalSize($font) * $font->lineHeight())); return intval(round($this->typographicalSize($font) * $font->lineHeight()));
} }
/**
* Reformat a text block by wrapping each line before the given maximum width
*
* @param TextBlock $block
* @param FontInterface $font
* @return TextBlock
*/
protected function wrapTextBlock(TextBlock $block, FontInterface $font): TextBlock
{
$newLines = [];
foreach ($block as $line) {
foreach ($this->wrapLine($line, $font) as $newLine) {
$newLines[] = $newLine;
}
}
return $block->setLines($newLines);
}
/**
* Check if a line exceeds the given maximum width and wrap it if necessary.
* The output will be an array of formatted lines that are all within the
* maximum width.
*
* @param Line $line
* @param FontInterface $font
* @return array
*/
protected function wrapLine(Line $line, FontInterface $font): array
{
// no wrap width - no wrapping
if (is_null($font->wrapWidth())) {
return [$line];
}
$wrapped = [];
$formatedLine = new Line();
foreach ($line as $word) {
if ($font->wrapWidth() >= $this->boxSize((string) $formatedLine . ' ' . $word, $font)->width()) {
$formatedLine->add($word);
} else {
$wrapped[] = $formatedLine;
$formatedLine = new Line($word);
}
}
$wrapped[] = $formatedLine;
return $wrapped;
}
/** /**
* Build pivot point of textblock according to the font settings and based on given position * Build pivot point of textblock according to the font settings and based on given position
* *

View File

@@ -40,6 +40,11 @@ class FontProcessor extends AbstractFontProcessor
); );
} }
/**
* {@inheritdoc}
*
* @see FontProcessorInterface::nativeFontSize()
*/
public function nativeFontSize(FontInterface $font): float public function nativeFontSize(FontInterface $font): float
{ {
return $font->size(); return $font->size();

View File

@@ -117,4 +117,19 @@ interface FontInterface
* @return float * @return float
*/ */
public function lineHeight(): float; public function lineHeight(): float;
/**
* Set the wrap width with which the text is rendered
*
* @param int $width
* @return FontInterface
*/
public function setWrapWidth(?int $width): self;
/**
* Get wrap width with which the text is rendered
*
* @return null|int
*/
public function wrapWidth(): ?int;
} }

View File

@@ -16,8 +16,8 @@ interface FontProcessorInterface
public function boxSize(string $text, FontInterface $font): SizeInterface; public function boxSize(string $text, FontInterface $font): SizeInterface;
/** /**
* Build TextBlock object from text string and align every line * Build TextBlock object from text string and align every line according
* according to text modifier's font object and position. * to text modifier's font object and position.
* *
* @param string $text * @param string $text
* @param FontInterface $font * @param FontInterface $font

View File

@@ -15,6 +15,7 @@ class Font implements FontInterface
protected string $alignment = 'left'; protected string $alignment = 'left';
protected string $valignment = 'bottom'; protected string $valignment = 'bottom';
protected float $lineHeight = 1.25; protected float $lineHeight = 1.25;
protected ?int $wrapWidth = null;
public function __construct(?string $filename = null) public function __construct(?string $filename = null)
{ {
@@ -184,4 +185,16 @@ class Font implements FontInterface
{ {
return $this->lineHeight; return $this->lineHeight;
} }
public function setWrapWidth(?int $width): FontInterface
{
$this->wrapWidth = $width;
return $this;
}
public function wrapWidth(): ?int
{
return $this->wrapWidth;
}
} }

View File

@@ -77,4 +77,11 @@ class FontFactory
return $this; return $this;
} }
public function wrap(int $width): self
{
$this->font->setWrapWidth($width);
return $this;
}
} }

View File

@@ -4,13 +4,17 @@ declare(strict_types=1);
namespace Intervention\Image\Typography; namespace Intervention\Image\Typography;
use ArrayIterator;
use Countable;
use Intervention\Image\Geometry\Point; use Intervention\Image\Geometry\Point;
use Intervention\Image\Interfaces\PointInterface; use Intervention\Image\Interfaces\PointInterface;
use IteratorAggregate;
use Traversable;
class Line class Line implements IteratorAggregate, Countable
{ {
/** /**
* Segments (usually individual words) of the line * Segments (usually individual words including punctuation marks) of the line
*/ */
protected array $segments = []; protected array $segments = [];
@@ -22,11 +26,36 @@ class Line
* @return void * @return void
*/ */
public function __construct( public function __construct(
string $text, ?string $text = null,
protected PointInterface $position = new Point() protected PointInterface $position = new Point()
) { ) {
if (is_string($text)) {
$this->segments = explode(" ", $text); $this->segments = explode(" ", $text);
} }
}
/**
* Add word to current line
*
* @param string $word
* @return Line
*/
public function add(string $word): self
{
$this->segments[] = $word;
return $this;
}
/**
* Returns Iterator
*
* @return Traversable
*/
public function getIterator(): Traversable
{
return new ArrayIterator($this->segments);
}
/** /**
* Get Position of line * Get Position of line

View File

@@ -25,6 +25,19 @@ class TextBlock extends Collection
return $this->items; return $this->items;
} }
/**
* Set lines of the text block
*
* @param array $lines
* @return self
*/
public function setLines(array $lines): self
{
$this->items = $lines;
return $this;
}
/** /**
* Get line by given key * Get line by given key
* *

View File

@@ -18,8 +18,8 @@ class LineTest extends TestCase
public function testToString(): void public function testToString(): void
{ {
$line = new Line('foo'); $line = new Line('foo bar');
$this->assertEquals('foo', (string) $line); $this->assertEquals('foo bar', (string) $line);
} }
public function testSetGetPosition(): void public function testSetGetPosition(): void
@@ -35,10 +35,27 @@ class LineTest extends TestCase
public function testCount(): void public function testCount(): void
{ {
$line = new Line();
$this->assertEquals(0, $line->count());
$line = new Line("foo"); $line = new Line("foo");
$this->assertEquals(1, $line->count()); $this->assertEquals(1, $line->count());
$line = new Line("foo bar"); $line = new Line("foo bar");
$this->assertEquals(2, $line->count()); $this->assertEquals(2, $line->count());
} }
public function testAdd(): void
{
$line = new Line();
$this->assertEquals(0, $line->count());
$result = $line->add('foo');
$this->assertEquals(1, $line->count());
$this->assertEquals(1, $result->count());
$result = $line->add('bar');
$this->assertEquals(2, $line->count());
$this->assertEquals(2, $result->count());
}
} }