From 5dc4e669699c8b5a43e4bb41ca2227308864510f Mon Sep 17 00:00:00 2001 From: Oliver Vogel Date: Sun, 26 Jun 2022 20:09:21 +0200 Subject: [PATCH] Extended Textwriter to handle multi line text --- src/Drivers/Abstract/AbstractFont.php | 4 ++ src/Drivers/Gd/Font.php | 2 +- src/Drivers/Gd/Modifiers/TextWriter.php | 69 ++++++---------------- src/Geometry/Size.php | 28 +++++++++ src/Typography/Line.php | 9 ++- src/Typography/TextBlock.php | 76 +++++++++++++++++++++++++ tests/Drivers/Gd/FontTest.php | 30 ++++++++++ tests/Geometry/PointTest.php | 5 ++ tests/Typography/LineTest.php | 12 ++++ tests/Typography/TextBlockTest.php | 53 ++++++++++++++++- 10 files changed, 234 insertions(+), 54 deletions(-) create mode 100644 tests/Drivers/Gd/FontTest.php diff --git a/src/Drivers/Abstract/AbstractFont.php b/src/Drivers/Abstract/AbstractFont.php index dc246737..2b505952 100644 --- a/src/Drivers/Abstract/AbstractFont.php +++ b/src/Drivers/Abstract/AbstractFont.php @@ -2,9 +2,13 @@ namespace Intervention\Image\Drivers\Abstract; +use Intervention\Image\Geometry\Point; +use Intervention\Image\Geometry\Polygon; +use Intervention\Image\Geometry\Size; use Intervention\Image\Interfaces\ColorInterface; use Intervention\Image\Interfaces\FontInterface; use Intervention\Image\Traits\CanHandleInput; +use Intervention\Image\Typography\TextBlock; abstract class AbstractFont implements FontInterface { diff --git a/src/Drivers/Gd/Font.php b/src/Drivers/Gd/Font.php index e524dcfe..79ee8f81 100644 --- a/src/Drivers/Gd/Font.php +++ b/src/Drivers/Gd/Font.php @@ -15,7 +15,7 @@ class Font extends AbstractFont } /** - * Calculate size of bounding box of current text + * Calculate size of bounding box of given text * * @return Polygon */ diff --git a/src/Drivers/Gd/Modifiers/TextWriter.php b/src/Drivers/Gd/Modifiers/TextWriter.php index 6d2f4d4d..b08b90b4 100644 --- a/src/Drivers/Gd/Modifiers/TextWriter.php +++ b/src/Drivers/Gd/Modifiers/TextWriter.php @@ -5,84 +5,51 @@ namespace Intervention\Image\Drivers\Gd\Modifiers; use Intervention\Image\Drivers\Abstract\AbstractTextWriter; use Intervention\Image\Drivers\Gd\Font; use Intervention\Image\Exceptions\FontException; -use Intervention\Image\Geometry\Polygon; -use Intervention\Image\Geometry\Size; use Intervention\Image\Interfaces\ImageInterface; class TextWriter extends AbstractTextWriter { public function apply(ImageInterface $image): ImageInterface { - $box = $this->getBoundingBox(); - $position = clone $box->last(); - $leading = $this->getFont()->leadingInPixels(); + $lines = $this->getTextBlock(); + $boundingBox = $lines->getBoundingBox($this->getFont(), $this->position); + $lines->alignByFont($this->getFont(), $boundingBox->last()); foreach ($image as $frame) { if ($this->font->hasFilename()) { - $position->moveY($this->getFont()->capHeight()); - $posx = $position->getX(); - $posy = $position->getY(); - foreach ($this->getTextBlock() as $line) { + foreach ($lines as $line) { imagettftext( $frame->getCore(), $this->getFont()->getSize(), $this->getFont()->getAngle() * (-1), - $posx, - $posy, + $line->getPosition()->getX(), + $line->getPosition()->getY(), $this->getFont()->getColor()->toInt(), $this->getFont()->getFilename(), $line ); - $posy += $leading; } // debug - imagepolygon($frame->getCore(), $box->toArray(), 0); + imagepolygon($frame->getCore(), $boundingBox->toArray(), 0); } else { - imagestring( - $frame->getCore(), - $this->getFont()->getGdFont(), - $position->getX(), - $position->getY(), - $this->text, - $this->font->getColor()->toInt() - ); + foreach ($lines as $line) { + imagestring( + $frame->getCore(), + $this->getFont()->getGdFont(), + $line->getPosition()->getX(), + $line->getPosition()->getY(), + $line, + $this->font->getColor()->toInt() + ); + imagepolygon($frame->getCore(), $boundingBox->toArray(), 0); + } } } return $image; } - private function getBoundingBox(): Polygon - { - $size = new Size( - $this->getTextBlock()->longestLine()->width($this->font), - $this->getFont()->leadingInPixels() * $this->getTextBlock()->count() - ); - - $poly = $size->toPolygon(); - $poly->setPivotPoint($this->position); - $poly->align($this->getFont()->getAlign()); - $poly->valign($this->getFont()->getValign()); - - return $poly; - } - - // private function getAlignedPosition(): Point - // { - // $poly = $this->font->getBoxSize($this->text); - // $poly->setPivotPoint($this->position); - // - // $poly->align($this->font->getAlign()); - // $poly->valign($this->font->getValign()); - // - // if ($this->font->getAngle() != 0) { - // $poly->rotate($this->font->getAngle()); - // } - // - // return $poly->last(); - // } - private function getFont(): Font { if (!is_a($this->font, Font::class)) { diff --git a/src/Geometry/Size.php b/src/Geometry/Size.php index 532940b5..07c15d06 100644 --- a/src/Geometry/Size.php +++ b/src/Geometry/Size.php @@ -40,6 +40,34 @@ class Size implements SizeInterface return $this; } + public function addWidth(int $value): SizeInterface + { + $this->width = $this->width + $value; + + return $this; + } + + public function subWidth(int $value): SizeInterface + { + $this->width = $this->width - $value; + + return $this; + } + + public function addHeight(int $value): SizeInterface + { + $this->height = $this->height + $value; + + return $this; + } + + public function subHeight(int $value): SizeInterface + { + $this->height = $this->height - $value; + + return $this; + } + /** * Get current pivot point * diff --git a/src/Typography/Line.php b/src/Typography/Line.php index f4c05a01..10e98665 100644 --- a/src/Typography/Line.php +++ b/src/Typography/Line.php @@ -19,7 +19,14 @@ class Line return $this->position; } - public function width(FontInterface $font): int + public function setPosition(Point $point): self + { + $this->position = $point; + + return $this; + } + + public function widthInFont(FontInterface $font): int { return $font->getBoxSize($this->text)->width(); } diff --git a/src/Typography/TextBlock.php b/src/Typography/TextBlock.php index c77c29c8..b6174b1a 100644 --- a/src/Typography/TextBlock.php +++ b/src/Typography/TextBlock.php @@ -3,6 +3,10 @@ namespace Intervention\Image\Typography; use Intervention\Image\Collection; +use Intervention\Image\Geometry\Point; +use Intervention\Image\Geometry\Polygon; +use Intervention\Image\Geometry\Size; +use Intervention\Image\Interfaces\FontInterface; class TextBlock extends Collection { @@ -13,11 +17,83 @@ class TextBlock extends Collection } } + /** + * Set position of each line in text block + * according to given font settings. + * + * @param FontInterface $font + * @param Point $pivot + * @return TextBlock + */ + public function alignByFont(FontInterface $font, Point $pivot = null): self + { + $pivot = $pivot ? $pivot : new Point(); + + $leading = $font->leadingInPixels(); + $x = $pivot->getX(); + $y = $font->hasFilename() ? $pivot->getY() + $font->capHeight() : $pivot->getY(); + + $x_adjustment = 0; + $total_width = $this->longestLine()->widthInFont($font); + foreach ($this as $line) { + $x_adjustment = $font->getAlign() == 'left' ? 0 : $total_width - $line->widthInFont($font); + $x_adjustment = $font->getAlign() == 'right' ? intval(round($x_adjustment)) : $x_adjustment; + $x_adjustment = $font->getAlign() == 'center' ? intval(round($x_adjustment / 2)) : $x_adjustment; + $position = new Point($x + $x_adjustment, $y); + $position->rotate($font->getAngle(), $pivot); + $line->setPosition($position); + $y += $leading; + } + + return $this; + } + + public function getBoundingBox(FontInterface $font, Point $pivot = null): Polygon + { + $pivot = $pivot ? $pivot : new Point(); + + // bounding box + $box = (new Size( + $this->longestLine()->widthInFont($font), + $font->leadingInPixels() * ($this->count() - 1) + $font->capHeight() + ))->toPolygon(); + + // set pivot + $box->setPivotPoint($pivot); + + // align + $box->align($font->getAlign()); + $box->valign($font->getValign()); + + $box->rotate($font->getAngle()); + + return $box; + } + + /** + * Return array of lines in text block + * + * @return array + */ public function lines(): array { return $this->items; } + public function getLine($key): ?Line + { + if (!array_key_exists($key, $this->lines())) { + return null; + } + + return $this->lines()[$key]; + } + + /** + * Return line with most characters of text block + * + * @return Line + */ public function longestLine(): Line { $lines = $this->lines(); diff --git a/tests/Drivers/Gd/FontTest.php b/tests/Drivers/Gd/FontTest.php new file mode 100644 index 00000000..62e02e48 --- /dev/null +++ b/tests/Drivers/Gd/FontTest.php @@ -0,0 +1,30 @@ +size(12); + $this->assertEquals(9, $font->getSize()); + } + + public function testGetGdFont(): void + { + $font = new Font(); + $this->assertEquals(1, $font->getGdFont()); + $font->filename(12); + $this->assertEquals(12, $font->getGdFont()); + } + + public function testCapHeight(): void + { + $font = new Font(); + $this->assertEquals(8, $font->capHeight()); + } +} diff --git a/tests/Geometry/PointTest.php b/tests/Geometry/PointTest.php index 8f95d81d..98db4393 100644 --- a/tests/Geometry/PointTest.php +++ b/tests/Geometry/PointTest.php @@ -81,5 +81,10 @@ class PointTest extends TestCase $point->rotate(90, new Point(0, 0)); $this->assertEquals(-200, $point->getX()); $this->assertEquals(300, $point->getY()); + + $point = new Point(0, 74); + $point->rotate(45, new Point(0, 0)); + $this->assertEquals(-52, $point->getX()); + $this->assertEquals(52, $point->getY()); } } diff --git a/tests/Typography/LineTest.php b/tests/Typography/LineTest.php index fb7de727..b4c2de6a 100644 --- a/tests/Typography/LineTest.php +++ b/tests/Typography/LineTest.php @@ -2,6 +2,7 @@ namespace Intervention\Image\Tests\Typography; +use Intervention\Image\Geometry\Point; use Intervention\Image\Tests\TestCase; use Intervention\Image\Typography\Line; @@ -18,4 +19,15 @@ class LineTest extends TestCase $line = new Line('foo'); $this->assertEquals('foo', (string) $line); } + + public function testSetGetPosition(): void + { + $line = new Line('foo'); + $this->assertEquals(0, $line->getPosition()->getX()); + $this->assertEquals(0, $line->getPosition()->getY()); + + $line->setPosition(new Point(10, 11)); + $this->assertEquals(10, $line->getPosition()->getX()); + $this->assertEquals(11, $line->getPosition()->getY()); + } } diff --git a/tests/Typography/TextBlockTest.php b/tests/Typography/TextBlockTest.php index bb4a901c..36fb2779 100644 --- a/tests/Typography/TextBlockTest.php +++ b/tests/Typography/TextBlockTest.php @@ -4,6 +4,10 @@ namespace Intervention\Image\Tests\Typography; use Intervention\Image\Tests\TestCase; use Intervention\Image\Typography\TextBlock; +use Intervention\Image\Drivers\Abstract\AbstractFont; +use Intervention\Image\Geometry\Point; +use Intervention\Image\Geometry\Polygon; +use Mockery; class TextBlockTest extends TestCase { @@ -11,10 +15,11 @@ class TextBlockTest extends TestCase { return new TextBlock(<<getTestBlock(); @@ -27,4 +32,50 @@ class TextBlockTest extends TestCase $block = $this->getTestBlock(); $this->assertCount(3, $block->lines()); } + + public function testGetLine(): void + { + $block = $this->getTestBlock(); + $this->assertEquals('foo', $block->getLine(0)); + $this->assertEquals('FooBar', $block->getLine(1)); + $this->assertEquals('bar', $block->getLine(2)); + } + + public function testAlignByFont(): void + { + $font = Mockery::mock(AbstractFont::class) + ->shouldAllowMockingProtectedMethods() + ->makePartial(); + + $font->shouldReceive('getBoxSize')->andReturn( + new Polygon([ + new Point(-1, -29), + new Point(141, -29), + new Point(141, 98), + new Point(-1, 98), + ]) + ); + + // $font->shouldReceive('capHeight')->andReturn(22); + + $font->shouldReceive('leadingInPixels')->andReturn(74); + $font->angle(45); + + $block = $this->getTestBlock(); // before + $this->assertEquals(0, $block->getLine(0)->getPosition()->getX()); + $this->assertEquals(0, $block->getLine(0)->getPosition()->getY()); + $this->assertEquals(0, $block->getLine(1)->getPosition()->getX()); + $this->assertEquals(0, $block->getLine(1)->getPosition()->getY()); + $this->assertEquals(0, $block->getLine(2)->getPosition()->getX()); + $this->assertEquals(0, $block->getLine(2)->getPosition()->getY()); + + $result = $block->alignByFont($font); // after + $this->assertInstanceOf(TextBlock::class, $result); + $this->assertEquals(0, $block->getLine(0)->getPosition()->getX()); + $this->assertEquals(0, $block->getLine(0)->getPosition()->getY()); + $this->assertEquals(-52, $block->getLine(1)->getPosition()->getX()); + $this->assertEquals(52, $block->getLine(1)->getPosition()->getY()); + $this->assertEquals(-104, $block->getLine(2)->getPosition()->getX()); + $this->assertEquals(104, $block->getLine(2)->getPosition()->getY()); + } }