mirror of
https://github.com/Intervention/image.git
synced 2025-08-29 16:50:07 +02:00
Extended Textwriter to handle multi line text
This commit is contained in:
@@ -2,9 +2,13 @@
|
|||||||
|
|
||||||
namespace Intervention\Image\Drivers\Abstract;
|
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\ColorInterface;
|
||||||
use Intervention\Image\Interfaces\FontInterface;
|
use Intervention\Image\Interfaces\FontInterface;
|
||||||
use Intervention\Image\Traits\CanHandleInput;
|
use Intervention\Image\Traits\CanHandleInput;
|
||||||
|
use Intervention\Image\Typography\TextBlock;
|
||||||
|
|
||||||
abstract class AbstractFont implements FontInterface
|
abstract class AbstractFont implements FontInterface
|
||||||
{
|
{
|
||||||
|
@@ -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
|
* @return Polygon
|
||||||
*/
|
*/
|
||||||
|
@@ -5,84 +5,51 @@ namespace Intervention\Image\Drivers\Gd\Modifiers;
|
|||||||
use Intervention\Image\Drivers\Abstract\AbstractTextWriter;
|
use Intervention\Image\Drivers\Abstract\AbstractTextWriter;
|
||||||
use Intervention\Image\Drivers\Gd\Font;
|
use Intervention\Image\Drivers\Gd\Font;
|
||||||
use Intervention\Image\Exceptions\FontException;
|
use Intervention\Image\Exceptions\FontException;
|
||||||
use Intervention\Image\Geometry\Polygon;
|
|
||||||
use Intervention\Image\Geometry\Size;
|
|
||||||
use Intervention\Image\Interfaces\ImageInterface;
|
use Intervention\Image\Interfaces\ImageInterface;
|
||||||
|
|
||||||
class TextWriter extends AbstractTextWriter
|
class TextWriter extends AbstractTextWriter
|
||||||
{
|
{
|
||||||
public function apply(ImageInterface $image): ImageInterface
|
public function apply(ImageInterface $image): ImageInterface
|
||||||
{
|
{
|
||||||
$box = $this->getBoundingBox();
|
$lines = $this->getTextBlock();
|
||||||
$position = clone $box->last();
|
$boundingBox = $lines->getBoundingBox($this->getFont(), $this->position);
|
||||||
$leading = $this->getFont()->leadingInPixels();
|
$lines->alignByFont($this->getFont(), $boundingBox->last());
|
||||||
|
|
||||||
foreach ($image as $frame) {
|
foreach ($image as $frame) {
|
||||||
if ($this->font->hasFilename()) {
|
if ($this->font->hasFilename()) {
|
||||||
$position->moveY($this->getFont()->capHeight());
|
foreach ($lines as $line) {
|
||||||
$posx = $position->getX();
|
|
||||||
$posy = $position->getY();
|
|
||||||
foreach ($this->getTextBlock() as $line) {
|
|
||||||
imagettftext(
|
imagettftext(
|
||||||
$frame->getCore(),
|
$frame->getCore(),
|
||||||
$this->getFont()->getSize(),
|
$this->getFont()->getSize(),
|
||||||
$this->getFont()->getAngle() * (-1),
|
$this->getFont()->getAngle() * (-1),
|
||||||
$posx,
|
$line->getPosition()->getX(),
|
||||||
$posy,
|
$line->getPosition()->getY(),
|
||||||
$this->getFont()->getColor()->toInt(),
|
$this->getFont()->getColor()->toInt(),
|
||||||
$this->getFont()->getFilename(),
|
$this->getFont()->getFilename(),
|
||||||
$line
|
$line
|
||||||
);
|
);
|
||||||
$posy += $leading;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// debug
|
// debug
|
||||||
imagepolygon($frame->getCore(), $box->toArray(), 0);
|
imagepolygon($frame->getCore(), $boundingBox->toArray(), 0);
|
||||||
} else {
|
} else {
|
||||||
|
foreach ($lines as $line) {
|
||||||
imagestring(
|
imagestring(
|
||||||
$frame->getCore(),
|
$frame->getCore(),
|
||||||
$this->getFont()->getGdFont(),
|
$this->getFont()->getGdFont(),
|
||||||
$position->getX(),
|
$line->getPosition()->getX(),
|
||||||
$position->getY(),
|
$line->getPosition()->getY(),
|
||||||
$this->text,
|
$line,
|
||||||
$this->font->getColor()->toInt()
|
$this->font->getColor()->toInt()
|
||||||
);
|
);
|
||||||
|
imagepolygon($frame->getCore(), $boundingBox->toArray(), 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $image;
|
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
|
private function getFont(): Font
|
||||||
{
|
{
|
||||||
if (!is_a($this->font, Font::class)) {
|
if (!is_a($this->font, Font::class)) {
|
||||||
|
@@ -40,6 +40,34 @@ class Size implements SizeInterface
|
|||||||
return $this;
|
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
|
* Get current pivot point
|
||||||
*
|
*
|
||||||
|
@@ -19,7 +19,14 @@ class Line
|
|||||||
return $this->position;
|
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();
|
return $font->getBoxSize($this->text)->width();
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,10 @@
|
|||||||
namespace Intervention\Image\Typography;
|
namespace Intervention\Image\Typography;
|
||||||
|
|
||||||
use Intervention\Image\Collection;
|
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
|
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
|
public function lines(): array
|
||||||
{
|
{
|
||||||
return $this->items;
|
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
|
public function longestLine(): Line
|
||||||
{
|
{
|
||||||
$lines = $this->lines();
|
$lines = $this->lines();
|
||||||
|
30
tests/Drivers/Gd/FontTest.php
Normal file
30
tests/Drivers/Gd/FontTest.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Intervention\Image\Tests\Drivers\Gd;
|
||||||
|
|
||||||
|
use Intervention\Image\Drivers\Gd\Font;
|
||||||
|
use Intervention\Image\Tests\TestCase;
|
||||||
|
|
||||||
|
class FontTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testGetSize(): void
|
||||||
|
{
|
||||||
|
$font = new Font();
|
||||||
|
$font->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());
|
||||||
|
}
|
||||||
|
}
|
@@ -81,5 +81,10 @@ class PointTest extends TestCase
|
|||||||
$point->rotate(90, new Point(0, 0));
|
$point->rotate(90, new Point(0, 0));
|
||||||
$this->assertEquals(-200, $point->getX());
|
$this->assertEquals(-200, $point->getX());
|
||||||
$this->assertEquals(300, $point->getY());
|
$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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Intervention\Image\Tests\Typography;
|
namespace Intervention\Image\Tests\Typography;
|
||||||
|
|
||||||
|
use Intervention\Image\Geometry\Point;
|
||||||
use Intervention\Image\Tests\TestCase;
|
use Intervention\Image\Tests\TestCase;
|
||||||
use Intervention\Image\Typography\Line;
|
use Intervention\Image\Typography\Line;
|
||||||
|
|
||||||
@@ -18,4 +19,15 @@ class LineTest extends TestCase
|
|||||||
$line = new Line('foo');
|
$line = new Line('foo');
|
||||||
$this->assertEquals('foo', (string) $line);
|
$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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,10 @@ namespace Intervention\Image\Tests\Typography;
|
|||||||
|
|
||||||
use Intervention\Image\Tests\TestCase;
|
use Intervention\Image\Tests\TestCase;
|
||||||
use Intervention\Image\Typography\TextBlock;
|
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
|
class TextBlockTest extends TestCase
|
||||||
{
|
{
|
||||||
@@ -11,10 +15,11 @@ class TextBlockTest extends TestCase
|
|||||||
{
|
{
|
||||||
return new TextBlock(<<<EOF
|
return new TextBlock(<<<EOF
|
||||||
foo
|
foo
|
||||||
|
FooBar
|
||||||
bar
|
bar
|
||||||
baz
|
|
||||||
EOF);
|
EOF);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testConstructor(): void
|
public function testConstructor(): void
|
||||||
{
|
{
|
||||||
$block = $this->getTestBlock();
|
$block = $this->getTestBlock();
|
||||||
@@ -27,4 +32,50 @@ class TextBlockTest extends TestCase
|
|||||||
$block = $this->getTestBlock();
|
$block = $this->getTestBlock();
|
||||||
$this->assertCount(3, $block->lines());
|
$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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user