From 684f5e6eb6e2660510c98f8eb0af2eb576f53c7b Mon Sep 17 00:00:00 2001 From: Oliver Vogel Date: Sat, 3 Feb 2024 14:51:17 +0100 Subject: [PATCH] Implement text wrapping --- src/Drivers/AbstractFontProcessor.php | 55 ++++++++++++++++++++++- src/Drivers/Imagick/FontProcessor.php | 5 +++ src/Interfaces/FontInterface.php | 15 +++++++ src/Interfaces/FontProcessorInterface.php | 4 +- src/Typography/Font.php | 13 ++++++ src/Typography/FontFactory.php | 7 +++ src/Typography/Line.php | 37 +++++++++++++-- src/Typography/TextBlock.php | 13 ++++++ tests/Typography/LineTest.php | 21 ++++++++- 9 files changed, 161 insertions(+), 9 deletions(-) diff --git a/src/Drivers/AbstractFontProcessor.php b/src/Drivers/AbstractFontProcessor.php index ecef320d..37fea91c 100644 --- a/src/Drivers/AbstractFontProcessor.php +++ b/src/Drivers/AbstractFontProcessor.php @@ -9,6 +9,7 @@ use Intervention\Image\Geometry\Rectangle; use Intervention\Image\Interfaces\FontInterface; use Intervention\Image\Interfaces\FontProcessorInterface; use Intervention\Image\Interfaces\PointInterface; +use Intervention\Image\Typography\Line; use Intervention\Image\Typography\TextBlock; abstract class AbstractFontProcessor implements FontProcessorInterface @@ -20,7 +21,7 @@ abstract class AbstractFontProcessor implements FontProcessorInterface */ 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); $leading = $this->leading($font); @@ -74,6 +75,58 @@ abstract class AbstractFontProcessor implements FontProcessorInterface 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 * diff --git a/src/Drivers/Imagick/FontProcessor.php b/src/Drivers/Imagick/FontProcessor.php index 1d24537c..fb76622c 100644 --- a/src/Drivers/Imagick/FontProcessor.php +++ b/src/Drivers/Imagick/FontProcessor.php @@ -40,6 +40,11 @@ class FontProcessor extends AbstractFontProcessor ); } + /** + * {@inheritdoc} + * + * @see FontProcessorInterface::nativeFontSize() + */ public function nativeFontSize(FontInterface $font): float { return $font->size(); diff --git a/src/Interfaces/FontInterface.php b/src/Interfaces/FontInterface.php index 3bfffba0..71be76f7 100644 --- a/src/Interfaces/FontInterface.php +++ b/src/Interfaces/FontInterface.php @@ -117,4 +117,19 @@ interface FontInterface * @return 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; } diff --git a/src/Interfaces/FontProcessorInterface.php b/src/Interfaces/FontProcessorInterface.php index 9adc8ebd..52ef4759 100644 --- a/src/Interfaces/FontProcessorInterface.php +++ b/src/Interfaces/FontProcessorInterface.php @@ -16,8 +16,8 @@ interface FontProcessorInterface public function boxSize(string $text, FontInterface $font): SizeInterface; /** - * Build TextBlock object from text string and align every line - * according to text modifier's font object and position. + * Build TextBlock object from text string and align every line according + * to text modifier's font object and position. * * @param string $text * @param FontInterface $font diff --git a/src/Typography/Font.php b/src/Typography/Font.php index e19d27ef..8dd40b02 100644 --- a/src/Typography/Font.php +++ b/src/Typography/Font.php @@ -15,6 +15,7 @@ class Font implements FontInterface protected string $alignment = 'left'; protected string $valignment = 'bottom'; protected float $lineHeight = 1.25; + protected ?int $wrapWidth = null; public function __construct(?string $filename = null) { @@ -184,4 +185,16 @@ class Font implements FontInterface { return $this->lineHeight; } + + public function setWrapWidth(?int $width): FontInterface + { + $this->wrapWidth = $width; + + return $this; + } + + public function wrapWidth(): ?int + { + return $this->wrapWidth; + } } diff --git a/src/Typography/FontFactory.php b/src/Typography/FontFactory.php index 274fe174..e19f124c 100644 --- a/src/Typography/FontFactory.php +++ b/src/Typography/FontFactory.php @@ -77,4 +77,11 @@ class FontFactory return $this; } + + public function wrap(int $width): self + { + $this->font->setWrapWidth($width); + + return $this; + } } diff --git a/src/Typography/Line.php b/src/Typography/Line.php index 2060ba01..52e2724a 100644 --- a/src/Typography/Line.php +++ b/src/Typography/Line.php @@ -4,13 +4,17 @@ declare(strict_types=1); namespace Intervention\Image\Typography; +use ArrayIterator; +use Countable; use Intervention\Image\Geometry\Point; 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 = []; @@ -22,10 +26,35 @@ class Line * @return void */ public function __construct( - string $text, + ?string $text = null, protected PointInterface $position = new Point() ) { - $this->segments = explode(" ", $text); + if (is_string($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); } /** diff --git a/src/Typography/TextBlock.php b/src/Typography/TextBlock.php index 3f6857d6..46252d64 100644 --- a/src/Typography/TextBlock.php +++ b/src/Typography/TextBlock.php @@ -25,6 +25,19 @@ class TextBlock extends Collection 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 * diff --git a/tests/Typography/LineTest.php b/tests/Typography/LineTest.php index b450435f..69b7413f 100644 --- a/tests/Typography/LineTest.php +++ b/tests/Typography/LineTest.php @@ -18,8 +18,8 @@ class LineTest extends TestCase public function testToString(): void { - $line = new Line('foo'); - $this->assertEquals('foo', (string) $line); + $line = new Line('foo bar'); + $this->assertEquals('foo bar', (string) $line); } public function testSetGetPosition(): void @@ -35,10 +35,27 @@ class LineTest extends TestCase public function testCount(): void { + $line = new Line(); + $this->assertEquals(0, $line->count()); + $line = new Line("foo"); $this->assertEquals(1, $line->count()); $line = new Line("foo bar"); $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()); + } }