Skip to content

Commit 61e449c

Browse files
authored
Fix TextModifier position bug (#74)
1 parent ea2e023 commit 61e449c

File tree

4 files changed

+254
-129
lines changed

4 files changed

+254
-129
lines changed

src/FontProcessor.php

+47-16
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44

55
namespace Intervention\Image\Drivers\Vips;
66

7+
use Intervention\Image\Colors\Rgb\Color;
78
use Intervention\Image\Drivers\AbstractFontProcessor;
89
use Intervention\Image\Exceptions\FontException;
910
use Intervention\Image\Exceptions\RuntimeException;
1011
use Intervention\Image\Geometry\Rectangle;
1112
use Intervention\Image\Interfaces\ColorInterface;
1213
use Intervention\Image\Interfaces\FontInterface;
1314
use Intervention\Image\Interfaces\SizeInterface;
15+
use Jcupitt\Vips\Align;
1416
use Jcupitt\Vips\Image as VipsImage;
17+
use Jcupitt\Vips\TextWrap;
1518

1619
class FontProcessor extends AbstractFontProcessor
1720
{
@@ -37,29 +40,57 @@ public function boxSize(string $text, FontInterface $font): SizeInterface
3740
}
3841

3942
/**
40-
* Create vips text object according to given parameters
43+
* Return renderable text/font combination in the specified colour as an vips image
4144
*
4245
* @param string $text
4346
* @param FontInterface $font
44-
* @param null|ColorInterface $color
45-
* @throws RuntimeException
47+
* @param ColorInterface $color
4648
* @throws FontException
49+
* @throws RuntimeException
4750
* @return VipsImage
4851
*/
49-
public function textToVipsImage(string $text, FontInterface $font, ?ColorInterface $color = null): VipsImage
50-
{
51-
// VipsImage::text() can only handle certain characters as HTML entities
52-
$text = htmlentities($text);
52+
public function textToVipsImage(
53+
string $text,
54+
FontInterface $font,
55+
ColorInterface $color = new Color(0, 0, 0),
56+
): VipsImage {
57+
return VipsImage::text(
58+
'<span ' . $this->pangoAttributes($font, $color) . '>' . htmlentities($text) . '</span>',
59+
[
60+
'fontfile' => $font->filename(),
61+
'font' => TrueTypeFont::fromPath($font->filename())->familyName() . ' ' . $font->size(),
62+
'dpi' => 72,
63+
'rgba' => true,
64+
'width' => $font->wrapWidth(),
65+
'wrap' => TextWrap::WORD,
66+
'align' => match ($font->alignment()) {
67+
'center',
68+
'middle' => Align::CENTRE,
69+
'right' => Align::HIGH,
70+
default => Align::LOW,
71+
},
72+
'spacing' => 0
73+
]
74+
);
75+
}
5376

54-
if (!is_null($color)) {
55-
$text = '<span foreground="' . $color->toHex('#') . '">' . $text . '</span>';
56-
}
77+
/**
78+
* Return a pango markup attribute string based on the given font and color values
79+
*
80+
* @param FontInterface $font
81+
* @param ColorInterface $color
82+
* @return string
83+
*/
84+
private function pangoAttributes(FontInterface $font, ColorInterface $color): string
85+
{
86+
$pango_attributes = [
87+
'line_height' => (string) $font->lineHeight() / 1.62,
88+
'foreground' => $color->toHex('#'),
89+
];
5790

58-
return VipsImage::text($text, [
59-
'fontfile' => $font->filename(),
60-
'font' => TrueTypeFont::fromPath($font->filename())->familyName() . ' ' . $font->size(),
61-
'dpi' => 72,
62-
'rgba' => true,
63-
]);
91+
// format pango attributes
92+
return join(' ', array_map(function ($value, $key): string {
93+
return $key . '="' . $value . '"';
94+
}, $pango_attributes, array_keys($pango_attributes)));
6495
}
6596
}

src/Modifiers/TextModifier.php

+141-59
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@
77
use Intervention\Image\Drivers\Vips\Core;
88
use Intervention\Image\Drivers\Vips\FontProcessor;
99
use Intervention\Image\Exceptions\RuntimeException;
10+
use Intervention\Image\Geometry\Factories\CircleFactory;
11+
use Intervention\Image\Geometry\Rectangle;
1012
use Intervention\Image\Interfaces\FrameInterface;
1113
use Intervention\Image\Interfaces\ImageInterface;
14+
use Intervention\Image\Interfaces\PointInterface;
1215
use Intervention\Image\Interfaces\SpecializedInterface;
1316
use Intervention\Image\Modifiers\TextModifier as GenericTextModifier;
17+
use Intervention\Image\Typography\TextBlock;
1418
use Jcupitt\Vips\BlendMode;
1519
use Jcupitt\Vips\Image as VipsImage;
1620
use Jcupitt\Vips\Exception as VipsException;
@@ -26,87 +30,123 @@ class TextModifier extends GenericTextModifier implements SpecializedInterface
2630
*/
2731
public function apply(ImageInterface $image): ImageInterface
2832
{
33+
$textBlock = new TextBlock($this->text);
2934
$fontProcessor = new FontProcessor();
35+
36+
// decode text color
3037
$color = $this->driver()->handleInput($this->font->color());
31-
$strokeColor = $this->driver()->handleInput($this->font->strokeColor());
32-
$lines = $fontProcessor->textBlock($this->text, $this->font, $this->position);
3338

34-
foreach ($lines as $line) {
35-
// build vips image from text
36-
$text = $fontProcessor->textToVipsImage((string) $line, $this->font, $color);
39+
// build vips image with text
40+
$textBlockImage = $fontProcessor->textToVipsImage($this->text, $this->font, $color);
41+
42+
// calculate block position
43+
$blockSize = $this->blockSize($textBlockImage);
44+
45+
// calculate baseline
46+
$capImage = $fontProcessor->textToVipsImage('T', $this->font);
47+
$baseline = $capImage->height + $capImage->yoffset;
48+
49+
// adjust block size
50+
switch ($this->font->valignment()) {
51+
case 'top':
52+
$blockSize->movePointsY($baseline * -1);
53+
$blockSize->movePointsY($textBlockImage->yoffset);
54+
$blockSize->movePointsY($capImage->height);
55+
break;
56+
57+
case 'bottom':
58+
$lastLineImage = $fontProcessor->textToVipsImage((string) $textBlock->last(), $this->font);
59+
$blockSize->movePointsY($lastLineImage->height);
60+
$blockSize->movePointsY($baseline * -1);
61+
$blockSize->movePointsY($lastLineImage->yoffset);
62+
break;
63+
}
3764

38-
// original line height from vips image before rotation
39-
$height = $text->height;
65+
// apply rotation
66+
$blockSize->rotate($this->font->angle());
4067

41-
// apply rotation
42-
$text = $this->maybeRotateText($text);
68+
// extract block position
69+
$blockPosition = clone $blockSize->last();
4370

44-
if ($this->font->hasStrokeEffect()) {
45-
// build stroke text image if applicable
46-
$stroke = $fontProcessor->textToVipsImage((string) $line, $this->font, $strokeColor);
71+
// apply text rotation if necessary
72+
$textBlockImage = $this->maybeRotateText($textBlockImage);
4773

48-
// original line height from vips image before rotation
49-
$strokeHeight = $stroke->height;
74+
// apply rotation offset to block position
75+
if ($this->font->angle() != 0) {
76+
$blockPosition->move(
77+
$textBlockImage->xoffset * -1,
78+
$textBlockImage->yoffset * -1
79+
);
80+
}
5081

51-
// apply rotation for stroke effect
52-
$stroke = $this->maybeRotateText($stroke);
53-
}
82+
if ($this->font->hasStrokeEffect()) {
83+
// decode stroke color
84+
$strokeColor = $this->driver()->handleInput($this->font->strokeColor());
5485

55-
if (!$image->isAnimated()) {
56-
$modified = $image->core()->first();
86+
// build stroke text image if applicable
87+
$stroke = $fontProcessor->textToVipsImage($this->text, $this->font, $strokeColor);
88+
89+
// apply rotation for stroke effect
90+
$stroke = $this->maybeRotateText($stroke);
91+
}
92+
93+
if (!$image->isAnimated()) {
94+
$modified = $image->core()->first();
95+
96+
if (isset($stroke)) {
97+
// draw stroke effect with offsets
98+
foreach ($this->strokeOffsets($this->font) as $offset) {
99+
$modified = $this->placeTextOnFrame(
100+
$stroke,
101+
$modified,
102+
$blockPosition->x() - $offset->x(),
103+
$blockPosition->y() - $offset->y()
104+
);
105+
}
106+
}
57107

58-
if (isset($stroke) && isset($strokeHeight)) {
108+
// place text image on original image
109+
$modified = $this->placeTextOnFrame(
110+
$textBlockImage,
111+
$modified,
112+
$blockPosition->x(),
113+
$blockPosition->y()
114+
);
115+
116+
$modified = $modified->native();
117+
} else {
118+
$frames = [];
119+
foreach ($image as $frame) {
120+
$modifiedFrame = $frame;
121+
122+
if (isset($stroke)) {
59123
// draw stroke effect with offsets
60124
foreach ($this->strokeOffsets($this->font) as $offset) {
61-
$modified = $this->placeTextOnFrame(
125+
$modifiedFrame = $this->placeTextOnFrame(
62126
$stroke,
63-
$modified,
64-
$line->position()->x() - $offset->x(),
65-
$line->position()->y() - $strokeHeight - $offset->y(),
127+
$modifiedFrame,
128+
$blockPosition->x() - $offset->x(),
129+
$blockPosition->y() - $offset->y()
66130
);
67131
}
68132
}
69133

70134
// place text image on original image
71-
$modified = $this->placeTextOnFrame(
72-
$text,
73-
$modified,
74-
$line->position()->x(),
75-
$line->position()->y() - $height,
135+
$modifiedFrame = $this->placeTextOnFrame(
136+
$textBlockImage,
137+
$modifiedFrame,
138+
$blockPosition->x(),
139+
$blockPosition->y()
76140
);
77141

78-
$modified = $modified->native();
79-
} else {
80-
$frames = [];
81-
foreach ($image as $frame) {
82-
$modifiedFrame = $frame;
83-
if (isset($stroke) && isset($strokeHeight)) {
84-
// draw stroke effect with offsets
85-
foreach ($this->strokeOffsets($this->font) as $offset) {
86-
$modifiedFrame = $this->placeTextOnFrame(
87-
$stroke,
88-
$modifiedFrame,
89-
$line->position()->x() - $offset->x(),
90-
$line->position()->y() - $strokeHeight - $offset->y(),
91-
);
92-
}
93-
}
94-
// place text image on original image
95-
$modifiedFrame = $this->placeTextOnFrame(
96-
$text,
97-
$modifiedFrame,
98-
$line->position()->x(),
99-
$line->position()->y() - $height,
100-
);
101-
102-
$frames[] = $modifiedFrame;
103-
}
104-
105-
$modified = Core::replaceFrames($image->core()->native(), $frames);
142+
$frames[] = $modifiedFrame;
106143
}
107-
$image->core()->setNative($modified);
144+
145+
$modified = Core::replaceFrames($image->core()->native(), $frames);
108146
}
109147

148+
$image->core()->setNative($modified);
149+
110150
return $image;
111151
}
112152

@@ -128,12 +168,27 @@ private function placeTextOnFrame(VipsImage $text, FrameInterface $frame, int $x
128168
return $frame;
129169
}
130170

171+
/**
172+
* Build size from given vips image
173+
*
174+
* @param VipsImage $blockImage
175+
* @return Rectangle
176+
*/
177+
private function blockSize(VipsImage $blockImage): Rectangle
178+
{
179+
$imageSize = new Rectangle($blockImage->width, $blockImage->height, $this->position);
180+
$imageSize->align($this->font->alignment());
181+
$imageSize->valign($this->font->valignment());
182+
183+
return $imageSize;
184+
}
185+
131186
/**
132187
* Maybe rotate text image according to current font angle
133188
*
134189
* @param VipsImage $text
135-
* @return VipsImage
136190
* @throws VipsException
191+
* @return VipsImage
137192
*/
138193
private function maybeRotateText(VipsImage $text): VipsImage
139194
{
@@ -145,4 +200,31 @@ private function maybeRotateText(VipsImage $text): VipsImage
145200
default => $text->similarity(['angle' => $this->font->angle()]),
146201
};
147202
}
203+
204+
/** @phpstan-ignore method.unused */
205+
private function debugPos(ImageInterface $image, PointInterface $position, Rectangle $size): void
206+
{
207+
// draw pos
208+
// @phpstan-ignore missingType.checkedException
209+
$image->drawCircle($position->x(), $position->y(), function (CircleFactory $circle): void {
210+
$circle->diameter(8);
211+
$circle->background('red');
212+
});
213+
214+
// draw points of size
215+
foreach (array_chunk($size->toArray(), 2) as $point) {
216+
// @phpstan-ignore missingType.checkedException
217+
$image->drawCircle($point[0], $point[1], function (CircleFactory $circle): void {
218+
$circle->diameter(12);
219+
$circle->border('green');
220+
});
221+
}
222+
223+
// draw size's pivot
224+
// @phpstan-ignore missingType.checkedException
225+
$image->drawCircle($size->pivot()->x(), $size->pivot()->y(), function (CircleFactory $circle): void {
226+
$circle->diameter(20);
227+
$circle->border('blue');
228+
});
229+
}
148230
}

0 commit comments

Comments
 (0)