1
0
mirror of https://github.com/Intervention/image.git synced 2025-08-22 21:42:53 +02:00

Refactor font processing

This commit is contained in:
Oliver Vogel
2023-11-26 10:31:23 +01:00
parent 5eb3eb5427
commit 60f2bed543
11 changed files with 136 additions and 301 deletions

View File

@@ -6,15 +6,15 @@ use Intervention\Image\Geometry\Point;
use Intervention\Image\Geometry\Polygon; use Intervention\Image\Geometry\Polygon;
use Intervention\Image\Geometry\Rectangle; use Intervention\Image\Geometry\Rectangle;
use Intervention\Image\Interfaces\FontInterface; use Intervention\Image\Interfaces\FontInterface;
use Intervention\Image\Interfaces\FontProcessorInterface;
use Intervention\Image\Typography\Line;
use Intervention\Image\Typography\TextBlock; 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 public function leadingInPixels(): int
{ {

View File

@@ -7,8 +7,6 @@ use Intervention\Image\Image;
use Intervention\Image\Interfaces\ColorInterface; use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorProcessorInterface; use Intervention\Image\Interfaces\ColorProcessorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface; use Intervention\Image\Interfaces\ColorspaceInterface;
use Intervention\Image\Interfaces\FontInterface;
use Intervention\Image\Interfaces\FontProcessorInterface;
use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Interfaces\ImageInterface;
class Driver extends AbstractDriver class Driver extends AbstractDriver
@@ -45,9 +43,4 @@ class Driver extends AbstractDriver
{ {
return new ColorProcessor($colorspace); return new ColorProcessor($colorspace);
} }
public function fontProcessor(FontInterface $font): FontProcessorInterface
{
return new FontProcessor($font);
}
} }

View File

@@ -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;
}
}
}

View File

@@ -2,10 +2,11 @@
namespace Intervention\Image\Drivers\Gd\Modifiers; namespace Intervention\Image\Drivers\Gd\Modifiers;
use Intervention\Image\Drivers\DriverModifier; use Intervention\Image\Drivers\AbstractTextModifier;
use Intervention\Image\Drivers\Gd\FontProcessor;
use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Interfaces\ImageInterface;
use Intervention\Image\Geometry\Point; use Intervention\Image\Geometry\Point;
use Intervention\Image\Geometry\Polygon;
use Intervention\Image\Geometry\Rectangle;
use Intervention\Image\Interfaces\FontInterface; use Intervention\Image\Interfaces\FontInterface;
/** /**
@@ -13,12 +14,11 @@ use Intervention\Image\Interfaces\FontInterface;
* @property string $text * @property string $text
* @property FontInterface $font * @property FontInterface $font
*/ */
class TextModifier extends DriverModifier class TextModifier extends AbstractTextModifier
{ {
public function apply(ImageInterface $image): ImageInterface public function apply(ImageInterface $image): ImageInterface
{ {
$processor = $this->fontProcessor(); $lines = $this->alignedTextBlock($this->position, $this->text);
$lines = $processor->alignedTextBlock($this->position, $this->text);
$color = $this->driver()->colorProcessor($image->colorspace())->colorToNative( $color = $this->driver()->colorProcessor($image->colorspace())->colorToNative(
$this->driver()->handleInput($this->font->color()) $this->driver()->handleInput($this->font->color())
@@ -29,7 +29,7 @@ class TextModifier extends DriverModifier
foreach ($lines as $line) { foreach ($lines as $line) {
imagettftext( imagettftext(
$frame->native(), $frame->native(),
$processor->adjustedSize(), $this->adjustedSize(),
$this->font->angle() * -1, $this->font->angle() * -1,
$line->position()->x(), $line->position()->x(),
$line->position()->y(), $line->position()->y(),
@@ -42,7 +42,7 @@ class TextModifier extends DriverModifier
foreach ($lines as $line) { foreach ($lines as $line) {
imagestring( imagestring(
$frame->native(), $frame->native(),
$processor->getGdFont(), $this->getGdFont(),
$line->position()->x(), $line->position()->x(),
$line->position()->y(), $line->position()->y(),
$line, $line,
@@ -55,8 +55,79 @@ class TextModifier extends DriverModifier
return $image; 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;
}
} }
} }

View File

@@ -9,8 +9,6 @@ use Intervention\Image\Image;
use Intervention\Image\Interfaces\ColorInterface; use Intervention\Image\Interfaces\ColorInterface;
use Intervention\Image\Interfaces\ColorProcessorInterface; use Intervention\Image\Interfaces\ColorProcessorInterface;
use Intervention\Image\Interfaces\ColorspaceInterface; use Intervention\Image\Interfaces\ColorspaceInterface;
use Intervention\Image\Interfaces\FontInterface;
use Intervention\Image\Interfaces\FontProcessorInterface;
use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Interfaces\ImageInterface;
class Driver extends AbstractDriver class Driver extends AbstractDriver
@@ -43,9 +41,4 @@ class Driver extends AbstractDriver
{ {
return new ColorProcessor($colorspace); return new ColorProcessor($colorspace);
} }
public function fontProcessor(FontInterface $font): FontProcessorInterface
{
return new FontProcessor($font);
}
} }

View File

@@ -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;
}
}

View File

@@ -2,9 +2,14 @@
namespace Intervention\Image\Drivers\Imagick\Modifiers; namespace Intervention\Image\Drivers\Imagick\Modifiers;
use Intervention\Image\Drivers\DriverModifier; use Imagick;
use Intervention\Image\Drivers\Imagick\FontProcessor; use ImagickDraw;
use ImagickPixel;
use Intervention\Image\Drivers\AbstractTextModifier;
use Intervention\Image\Exceptions\FontException;
use Intervention\Image\Geometry\Point; use Intervention\Image\Geometry\Point;
use Intervention\Image\Geometry\Polygon;
use Intervention\Image\Geometry\Rectangle;
use Intervention\Image\Interfaces\FontInterface; use Intervention\Image\Interfaces\FontInterface;
use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Interfaces\ImageInterface;
@@ -13,18 +18,17 @@ use Intervention\Image\Interfaces\ImageInterface;
* @property string $text * @property string $text
* @property FontInterface $font * @property FontInterface $font
*/ */
class TextModifier extends DriverModifier class TextModifier extends AbstractTextModifier
{ {
public function apply(ImageInterface $image): ImageInterface public function apply(ImageInterface $image): ImageInterface
{ {
$processor = $this->fontProcessor(); $lines = $this->alignedTextBlock($this->position, $this->text);
$lines = $processor->alignedTextBlock($this->position, $this->text);
$color = $this->driver()->colorProcessor($image->colorspace())->colorToNative( $color = $this->driver()->colorProcessor($image->colorspace())->colorToNative(
$this->driver()->handleInput($this->font->color()) $this->driver()->handleInput($this->font->color())
); );
$draw = $processor->toImagickDraw($color); $draw = $this->toImagickDraw($color);
foreach ($image as $frame) { foreach ($image as $frame) {
foreach ($lines as $line) { foreach ($lines as $line) {
@@ -41,8 +45,46 @@ class TextModifier extends DriverModifier
return $image; 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;
} }
} }

View File

@@ -9,5 +9,4 @@ interface DriverInterface
public function createImage(int $width, int $height): ImageInterface; public function createImage(int $width, int $height): ImageInterface;
public function handleInput(mixed $input): ImageInterface|ColorInterface; public function handleInput(mixed $input): ImageInterface|ColorInterface;
public function colorProcessor(ColorspaceInterface $colorspace): ColorProcessorInterface; public function colorProcessor(ColorspaceInterface $colorspace): ColorProcessorInterface;
public function fontProcessor(FontInterface $font): FontProcessorInterface;
} }

View File

@@ -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;
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}