mirror of
https://github.com/Intervention/image.git
synced 2025-08-22 13:32:56 +02:00
Refactor font processing
This commit is contained in:
@@ -6,15 +6,15 @@ use Intervention\Image\Geometry\Point;
|
||||
use Intervention\Image\Geometry\Polygon;
|
||||
use Intervention\Image\Geometry\Rectangle;
|
||||
use Intervention\Image\Interfaces\FontInterface;
|
||||
use Intervention\Image\Interfaces\FontProcessorInterface;
|
||||
use Intervention\Image\Typography\Line;
|
||||
use Intervention\Image\Typography\TextBlock;
|
||||
use Intervention\Image\Typography\Line;
|
||||
|
||||
abstract class AbstractFontProcessor implements FontProcessorInterface
|
||||
/**
|
||||
* @property FontInterface $font
|
||||
*/
|
||||
abstract class AbstractTextModifier extends DriverModifier
|
||||
{
|
||||
public function __construct(protected FontInterface $font)
|
||||
{
|
||||
}
|
||||
abstract protected function boxSize(string $text): Polygon;
|
||||
|
||||
public function leadingInPixels(): int
|
||||
{
|
@@ -7,8 +7,6 @@ use Intervention\Image\Image;
|
||||
use Intervention\Image\Interfaces\ColorInterface;
|
||||
use Intervention\Image\Interfaces\ColorProcessorInterface;
|
||||
use Intervention\Image\Interfaces\ColorspaceInterface;
|
||||
use Intervention\Image\Interfaces\FontInterface;
|
||||
use Intervention\Image\Interfaces\FontProcessorInterface;
|
||||
use Intervention\Image\Interfaces\ImageInterface;
|
||||
|
||||
class Driver extends AbstractDriver
|
||||
@@ -45,9 +43,4 @@ class Driver extends AbstractDriver
|
||||
{
|
||||
return new ColorProcessor($colorspace);
|
||||
}
|
||||
|
||||
public function fontProcessor(FontInterface $font): FontProcessorInterface
|
||||
{
|
||||
return new FontProcessor($font);
|
||||
}
|
||||
}
|
||||
|
@@ -1,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Drivers\Gd;
|
||||
|
||||
use Intervention\Image\Drivers\AbstractFontProcessor;
|
||||
use Intervention\Image\Geometry\Point;
|
||||
use Intervention\Image\Geometry\Polygon;
|
||||
use Intervention\Image\Geometry\Rectangle;
|
||||
|
||||
class FontProcessor extends AbstractFontProcessor
|
||||
{
|
||||
/**
|
||||
* Calculate size of bounding box of given text
|
||||
*
|
||||
* @return Polygon
|
||||
*/
|
||||
public function boxSize(string $text): Polygon
|
||||
{
|
||||
if (!$this->font->hasFilename()) {
|
||||
// calculate box size from gd font
|
||||
$box = new Rectangle(0, 0);
|
||||
$chars = mb_strlen($text);
|
||||
if ($chars > 0) {
|
||||
$box->setWidth($chars * $this->getGdFontWidth());
|
||||
$box->setHeight($this->getGdFontHeight());
|
||||
}
|
||||
return $box;
|
||||
}
|
||||
|
||||
// calculate box size from font file with angle 0
|
||||
$box = imageftbbox(
|
||||
$this->adjustedSize(),
|
||||
0,
|
||||
$this->font->filename(),
|
||||
$text
|
||||
);
|
||||
|
||||
// build polygon from points
|
||||
$polygon = new Polygon();
|
||||
$polygon->addPoint(new Point($box[6], $box[7]));
|
||||
$polygon->addPoint(new Point($box[4], $box[5]));
|
||||
$polygon->addPoint(new Point($box[2], $box[3]));
|
||||
$polygon->addPoint(new Point($box[0], $box[1]));
|
||||
|
||||
return $polygon;
|
||||
}
|
||||
|
||||
public function adjustedSize(): float
|
||||
{
|
||||
return floatval(ceil($this->font->size() * .75));
|
||||
}
|
||||
|
||||
public function getGdFont(): int
|
||||
{
|
||||
if (is_numeric($this->font->filename())) {
|
||||
return intval($this->font->filename());
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
protected function getGdFontWidth(): int
|
||||
{
|
||||
return $this->getGdFont() + 4;
|
||||
}
|
||||
|
||||
protected function getGdFontHeight(): int
|
||||
{
|
||||
switch ($this->getGdFont()) {
|
||||
case 2:
|
||||
return 14;
|
||||
|
||||
case 3:
|
||||
return 14;
|
||||
|
||||
case 4:
|
||||
return 16;
|
||||
|
||||
case 5:
|
||||
return 16;
|
||||
|
||||
default:
|
||||
case 1:
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,10 +2,11 @@
|
||||
|
||||
namespace Intervention\Image\Drivers\Gd\Modifiers;
|
||||
|
||||
use Intervention\Image\Drivers\DriverModifier;
|
||||
use Intervention\Image\Drivers\Gd\FontProcessor;
|
||||
use Intervention\Image\Drivers\AbstractTextModifier;
|
||||
use Intervention\Image\Interfaces\ImageInterface;
|
||||
use Intervention\Image\Geometry\Point;
|
||||
use Intervention\Image\Geometry\Polygon;
|
||||
use Intervention\Image\Geometry\Rectangle;
|
||||
use Intervention\Image\Interfaces\FontInterface;
|
||||
|
||||
/**
|
||||
@@ -13,12 +14,11 @@ use Intervention\Image\Interfaces\FontInterface;
|
||||
* @property string $text
|
||||
* @property FontInterface $font
|
||||
*/
|
||||
class TextModifier extends DriverModifier
|
||||
class TextModifier extends AbstractTextModifier
|
||||
{
|
||||
public function apply(ImageInterface $image): ImageInterface
|
||||
{
|
||||
$processor = $this->fontProcessor();
|
||||
$lines = $processor->alignedTextBlock($this->position, $this->text);
|
||||
$lines = $this->alignedTextBlock($this->position, $this->text);
|
||||
|
||||
$color = $this->driver()->colorProcessor($image->colorspace())->colorToNative(
|
||||
$this->driver()->handleInput($this->font->color())
|
||||
@@ -29,7 +29,7 @@ class TextModifier extends DriverModifier
|
||||
foreach ($lines as $line) {
|
||||
imagettftext(
|
||||
$frame->native(),
|
||||
$processor->adjustedSize(),
|
||||
$this->adjustedSize(),
|
||||
$this->font->angle() * -1,
|
||||
$line->position()->x(),
|
||||
$line->position()->y(),
|
||||
@@ -42,7 +42,7 @@ class TextModifier extends DriverModifier
|
||||
foreach ($lines as $line) {
|
||||
imagestring(
|
||||
$frame->native(),
|
||||
$processor->getGdFont(),
|
||||
$this->getGdFont(),
|
||||
$line->position()->x(),
|
||||
$line->position()->y(),
|
||||
$line,
|
||||
@@ -55,8 +55,79 @@ class TextModifier extends DriverModifier
|
||||
return $image;
|
||||
}
|
||||
|
||||
private function fontProcessor(): FontProcessor
|
||||
/**
|
||||
* Calculate size of bounding box of given text
|
||||
*
|
||||
* @return Polygon
|
||||
*/
|
||||
protected function boxSize(string $text): Polygon
|
||||
{
|
||||
return $this->driver()->fontProcessor($this->font);
|
||||
if (!$this->font->hasFilename()) {
|
||||
// calculate box size from gd font
|
||||
$box = new Rectangle(0, 0);
|
||||
$chars = mb_strlen($text);
|
||||
if ($chars > 0) {
|
||||
$box->setWidth($chars * $this->getGdFontWidth());
|
||||
$box->setHeight($this->getGdFontHeight());
|
||||
}
|
||||
return $box;
|
||||
}
|
||||
|
||||
// calculate box size from font file with angle 0
|
||||
$box = imageftbbox(
|
||||
$this->adjustedSize(),
|
||||
0,
|
||||
$this->font->filename(),
|
||||
$text
|
||||
);
|
||||
|
||||
// build polygon from points
|
||||
$polygon = new Polygon();
|
||||
$polygon->addPoint(new Point($box[6], $box[7]));
|
||||
$polygon->addPoint(new Point($box[4], $box[5]));
|
||||
$polygon->addPoint(new Point($box[2], $box[3]));
|
||||
$polygon->addPoint(new Point($box[0], $box[1]));
|
||||
|
||||
return $polygon;
|
||||
}
|
||||
|
||||
private function adjustedSize(): float
|
||||
{
|
||||
return floatval(ceil($this->font->size() * .75));
|
||||
}
|
||||
|
||||
private function getGdFont(): int
|
||||
{
|
||||
if (is_numeric($this->font->filename())) {
|
||||
return intval($this->font->filename());
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
private function getGdFontWidth(): int
|
||||
{
|
||||
return $this->getGdFont() + 4;
|
||||
}
|
||||
|
||||
private function getGdFontHeight(): int
|
||||
{
|
||||
switch ($this->getGdFont()) {
|
||||
case 2:
|
||||
return 14;
|
||||
|
||||
case 3:
|
||||
return 14;
|
||||
|
||||
case 4:
|
||||
return 16;
|
||||
|
||||
case 5:
|
||||
return 16;
|
||||
|
||||
default:
|
||||
case 1:
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -9,8 +9,6 @@ use Intervention\Image\Image;
|
||||
use Intervention\Image\Interfaces\ColorInterface;
|
||||
use Intervention\Image\Interfaces\ColorProcessorInterface;
|
||||
use Intervention\Image\Interfaces\ColorspaceInterface;
|
||||
use Intervention\Image\Interfaces\FontInterface;
|
||||
use Intervention\Image\Interfaces\FontProcessorInterface;
|
||||
use Intervention\Image\Interfaces\ImageInterface;
|
||||
|
||||
class Driver extends AbstractDriver
|
||||
@@ -43,9 +41,4 @@ class Driver extends AbstractDriver
|
||||
{
|
||||
return new ColorProcessor($colorspace);
|
||||
}
|
||||
|
||||
public function fontProcessor(FontInterface $font): FontProcessorInterface
|
||||
{
|
||||
return new FontProcessor($font);
|
||||
}
|
||||
}
|
||||
|
@@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Drivers\Imagick;
|
||||
|
||||
use Imagick;
|
||||
use ImagickDraw;
|
||||
use ImagickPixel;
|
||||
use Intervention\Image\Drivers\AbstractFontProcessor;
|
||||
use Intervention\Image\Exceptions\FontException;
|
||||
use Intervention\Image\Geometry\Polygon;
|
||||
use Intervention\Image\Geometry\Rectangle;
|
||||
|
||||
class FontProcessor extends AbstractFontProcessor
|
||||
{
|
||||
/**
|
||||
* Calculate box size of current font
|
||||
*
|
||||
* @return Polygon
|
||||
*/
|
||||
public function boxSize(string $text): Polygon
|
||||
{
|
||||
// no text - no box size
|
||||
if (mb_strlen($text) === 0) {
|
||||
return (new Rectangle(0, 0));
|
||||
}
|
||||
|
||||
$draw = $this->toImagickDraw();
|
||||
$draw->setStrokeAntialias(true);
|
||||
$draw->setTextAntialias(true);
|
||||
$dimensions = (new Imagick())->queryFontMetrics($draw, $text);
|
||||
|
||||
return (new Rectangle(
|
||||
intval(round($dimensions['textWidth'])),
|
||||
intval(round($dimensions['ascender'] + $dimensions['descender'])),
|
||||
));
|
||||
}
|
||||
|
||||
public function toImagickDraw(?ImagickPixel $color = null): ImagickDraw
|
||||
{
|
||||
if (!$this->font->hasFilename()) {
|
||||
throw new FontException('No font file specified.');
|
||||
}
|
||||
|
||||
$draw = new ImagickDraw();
|
||||
$draw->setStrokeAntialias(true);
|
||||
$draw->setTextAntialias(true);
|
||||
$draw->setFont($this->font->filename());
|
||||
$draw->setFontSize($this->font->size());
|
||||
$draw->setTextAlignment(Imagick::ALIGN_LEFT);
|
||||
|
||||
if ($color) {
|
||||
$draw->setFillColor($color);
|
||||
}
|
||||
|
||||
return $draw;
|
||||
}
|
||||
}
|
@@ -2,9 +2,14 @@
|
||||
|
||||
namespace Intervention\Image\Drivers\Imagick\Modifiers;
|
||||
|
||||
use Intervention\Image\Drivers\DriverModifier;
|
||||
use Intervention\Image\Drivers\Imagick\FontProcessor;
|
||||
use Imagick;
|
||||
use ImagickDraw;
|
||||
use ImagickPixel;
|
||||
use Intervention\Image\Drivers\AbstractTextModifier;
|
||||
use Intervention\Image\Exceptions\FontException;
|
||||
use Intervention\Image\Geometry\Point;
|
||||
use Intervention\Image\Geometry\Polygon;
|
||||
use Intervention\Image\Geometry\Rectangle;
|
||||
use Intervention\Image\Interfaces\FontInterface;
|
||||
use Intervention\Image\Interfaces\ImageInterface;
|
||||
|
||||
@@ -13,18 +18,17 @@ use Intervention\Image\Interfaces\ImageInterface;
|
||||
* @property string $text
|
||||
* @property FontInterface $font
|
||||
*/
|
||||
class TextModifier extends DriverModifier
|
||||
class TextModifier extends AbstractTextModifier
|
||||
{
|
||||
public function apply(ImageInterface $image): ImageInterface
|
||||
{
|
||||
$processor = $this->fontProcessor();
|
||||
$lines = $processor->alignedTextBlock($this->position, $this->text);
|
||||
$lines = $this->alignedTextBlock($this->position, $this->text);
|
||||
|
||||
$color = $this->driver()->colorProcessor($image->colorspace())->colorToNative(
|
||||
$this->driver()->handleInput($this->font->color())
|
||||
);
|
||||
|
||||
$draw = $processor->toImagickDraw($color);
|
||||
$draw = $this->toImagickDraw($color);
|
||||
|
||||
foreach ($image as $frame) {
|
||||
foreach ($lines as $line) {
|
||||
@@ -41,8 +45,46 @@ class TextModifier extends DriverModifier
|
||||
return $image;
|
||||
}
|
||||
|
||||
private function fontProcessor(): FontProcessor
|
||||
/**
|
||||
* Calculate box size of current font
|
||||
*
|
||||
* @return Polygon
|
||||
*/
|
||||
protected function boxSize(string $text): Polygon
|
||||
{
|
||||
return $this->driver()->fontProcessor($this->font);
|
||||
// no text - no box size
|
||||
if (mb_strlen($text) === 0) {
|
||||
return (new Rectangle(0, 0));
|
||||
}
|
||||
|
||||
$draw = $this->toImagickDraw();
|
||||
$draw->setStrokeAntialias(true);
|
||||
$draw->setTextAntialias(true);
|
||||
$dimensions = (new Imagick())->queryFontMetrics($draw, $text);
|
||||
|
||||
return (new Rectangle(
|
||||
intval(round($dimensions['textWidth'])),
|
||||
intval(round($dimensions['ascender'] + $dimensions['descender'])),
|
||||
));
|
||||
}
|
||||
|
||||
private function toImagickDraw(?ImagickPixel $color = null): ImagickDraw
|
||||
{
|
||||
if (!$this->font->hasFilename()) {
|
||||
throw new FontException('No font file specified.');
|
||||
}
|
||||
|
||||
$draw = new ImagickDraw();
|
||||
$draw->setStrokeAntialias(true);
|
||||
$draw->setTextAntialias(true);
|
||||
$draw->setFont($this->font->filename());
|
||||
$draw->setFontSize($this->font->size());
|
||||
$draw->setTextAlignment(Imagick::ALIGN_LEFT);
|
||||
|
||||
if ($color) {
|
||||
$draw->setFillColor($color);
|
||||
}
|
||||
|
||||
return $draw;
|
||||
}
|
||||
}
|
||||
|
@@ -9,5 +9,4 @@ interface DriverInterface
|
||||
public function createImage(int $width, int $height): ImageInterface;
|
||||
public function handleInput(mixed $input): ImageInterface|ColorInterface;
|
||||
public function colorProcessor(ColorspaceInterface $colorspace): ColorProcessorInterface;
|
||||
public function fontProcessor(FontInterface $font): FontProcessorInterface;
|
||||
}
|
||||
|
@@ -1,17 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Interfaces;
|
||||
|
||||
use Intervention\Image\Geometry\Point;
|
||||
use Intervention\Image\Geometry\Polygon;
|
||||
use Intervention\Image\Typography\TextBlock;
|
||||
|
||||
interface FontProcessorInterface
|
||||
{
|
||||
public function leadingInPixels(): int;
|
||||
public function fontSizeInPixels(): int;
|
||||
public function capHeight(): int;
|
||||
public function boxSize(string $text): Polygon;
|
||||
public function alignedTextBlock(Point $position, string $text): TextBlock;
|
||||
public function boundingBox(TextBlock $block, Point $pivot = null): Polygon;
|
||||
}
|
@@ -1,64 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Tests\Drivers;
|
||||
|
||||
use Intervention\Image\Drivers\AbstractFontProcessor;
|
||||
use Intervention\Image\Geometry\Point;
|
||||
use Intervention\Image\Geometry\Polygon;
|
||||
use Intervention\Image\Geometry\Rectangle;
|
||||
use Intervention\Image\Interfaces\FontInterface;
|
||||
use Intervention\Image\Tests\TestCase;
|
||||
use Intervention\Image\Typography\Font;
|
||||
use Intervention\Image\Typography\TextBlock;
|
||||
use Mockery;
|
||||
|
||||
class AbstractFontProcessorTest extends TestCase
|
||||
{
|
||||
private function getMock(FontInterface $font)
|
||||
{
|
||||
// create mock
|
||||
$mock = Mockery::mock(AbstractFontProcessor::class, [$font])
|
||||
->shouldAllowMockingProtectedMethods()
|
||||
->makePartial();
|
||||
|
||||
$mock->shouldReceive('boxSize')->with('Hy')->andReturn(new Rectangle(123, 456));
|
||||
$mock->shouldReceive('boxSize')->with('T')->andReturn(new Rectangle(12, 34));
|
||||
$mock->shouldReceive('boxSize')->with('foobar')->andReturn(new Rectangle(4, 8));
|
||||
|
||||
return $mock;
|
||||
}
|
||||
|
||||
public function testLeadingInPixels(): void
|
||||
{
|
||||
$mock = $this->getMock((new Font())->setLineHeight(2));
|
||||
$this->assertEquals(912, $mock->leadingInPixels());
|
||||
}
|
||||
|
||||
public function testCapHeight(): void
|
||||
{
|
||||
$mock = $this->getMock((new Font())->setLineHeight(2));
|
||||
$this->assertEquals(34, $mock->capHeight());
|
||||
}
|
||||
|
||||
public function testFontSizeInPixels(): void
|
||||
{
|
||||
$mock = $this->getMock((new Font())->setLineHeight(2));
|
||||
$this->assertEquals(456, $mock->fontSizeInPixels());
|
||||
}
|
||||
|
||||
public function testAlignedTextBlock(): void
|
||||
{
|
||||
$mock = $this->getMock((new Font())->setLineHeight(2));
|
||||
$block = $mock->alignedTextBlock(new Point(0, 0), 'foobar');
|
||||
$this->assertInstanceOf(TextBlock::class, $block);
|
||||
}
|
||||
|
||||
public function testBoundingBox(): void
|
||||
{
|
||||
$mock = $this->getMock((new Font())->setLineHeight(2));
|
||||
$box = $mock->boundingBox(new TextBlock('foobar'));
|
||||
$this->assertInstanceOf(Polygon::class, $box);
|
||||
$this->assertEquals(4, $box->width());
|
||||
$this->assertEquals(34, $box->height());
|
||||
}
|
||||
}
|
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Intervention\Image\Tests\Drivers\Gd;
|
||||
|
||||
use Intervention\Image\Drivers\Gd\FontProcessor;
|
||||
use Intervention\Image\Geometry\Polygon;
|
||||
use Intervention\Image\Tests\TestCase;
|
||||
use Intervention\Image\Typography\Font;
|
||||
|
||||
class FontProcessorTest extends TestCase
|
||||
{
|
||||
public function testBoxSize(): void
|
||||
{
|
||||
$processor = new FontProcessor(new Font());
|
||||
$result = $processor->boxSize('test');
|
||||
$this->assertInstanceOf(Polygon::class, $result);
|
||||
$this->assertEquals(20, $result->width());
|
||||
$this->assertEquals(8, $result->height());
|
||||
}
|
||||
|
||||
public function testAdjustedSize(): void
|
||||
{
|
||||
$processor = new FontProcessor((new Font())->setSize(100));
|
||||
$this->assertEquals(75, $processor->adjustedSize());
|
||||
}
|
||||
|
||||
public function testGetGdFont(): void
|
||||
{
|
||||
$processor = new FontProcessor(new Font());
|
||||
$this->assertEquals(1, $processor->getGdFont());
|
||||
|
||||
$processor = new FontProcessor((new Font())->setFilename(100));
|
||||
$this->assertEquals(100, $processor->getGdFont());
|
||||
|
||||
$processor = new FontProcessor((new Font())->setFilename('foo'));
|
||||
$this->assertEquals(1, $processor->getGdFont());
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user