Skip to content

Commit 0a6af5c

Browse files
committed
Fix TextModifier position bug
1 parent ea2e023 commit 0a6af5c

File tree

3 files changed

+259
-125
lines changed

3 files changed

+259
-125
lines changed

src/FontProcessor.php

+60-16
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
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;
1517

1618
class FontProcessor extends AbstractFontProcessor
@@ -37,29 +39,71 @@ public function boxSize(string $text, FontInterface $font): SizeInterface
3739
}
3840

3941
/**
40-
* Create vips text object according to given parameters
42+
* Return renderable text/font combination in the specified colour as an vips image
4143
*
4244
* @param string $text
4345
* @param FontInterface $font
44-
* @param null|ColorInterface $color
45-
* @throws RuntimeException
46+
* @param ColorInterface $color
4647
* @throws FontException
48+
* @throws RuntimeException
4749
* @return VipsImage
4850
*/
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);
51+
public function textToVipsImage(
52+
string $text,
53+
FontInterface $font,
54+
ColorInterface $color = new Color(0, 0, 0),
55+
): VipsImage {
56+
// TODO: implement line spacing
5357

54-
if (!is_null($color)) {
55-
$text = '<span foreground="' . $color->toHex('#') . '">' . $text . '</span>';
56-
}
58+
// @font size 24:
59+
// ---------------
60+
// 1 -> -15
61+
// 1.25 -> -10
62+
// 2 -> 7
63+
// 3 -> 18
64+
65+
// @font size 80:
66+
// ---------------
67+
// 1 -> -35
68+
// 1.25 -> -30
69+
// 2 -> 35
70+
// 3 -> 110
5771

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-
]);
72+
// @font size 100:
73+
// ---------------
74+
// 1 -> -45
75+
// 1.25 -> -35
76+
// 2 -> -10
77+
// 3 -> 55
78+
79+
// @font size 120:
80+
// ---------------
81+
// 1 -> -55
82+
// 1.25 -> -30
83+
// 2 -> 35
84+
// 3 -> 110
85+
86+
// leading 168
87+
88+
// 1 point (computer) 1.3333333333 pixel (X)
89+
// typicall like font size times 1.2.
90+
91+
return VipsImage::text(
92+
'<span foreground="' . $color->toHex('#') . '">' . htmlentities($text) . '</span>',
93+
[
94+
'fontfile' => $font->filename(),
95+
'font' => TrueTypeFont::fromPath($font->filename())->familyName() . ' ' . $font->size(),
96+
'dpi' => 72,
97+
'rgba' => true,
98+
'width' => $font->wrapWidth(),
99+
'align' => match ($font->alignment()) {
100+
'center',
101+
'middle' => Align::CENTRE,
102+
'right' => Align::HIGH,
103+
default => Align::LOW,
104+
},
105+
'spacing' => 0 // add value as pixel to each line
106+
]
107+
);
64108
}
65109
}

src/Modifiers/TextModifier.php

+144-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,125 @@ 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());
85+
86+
// build stroke text image if applicable
87+
$stroke = $fontProcessor->textToVipsImage($this->text, $this->font, $strokeColor);
5488

55-
if (!$image->isAnimated()) {
56-
$modified = $image->core()->first();
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+
150+
$this->debugPos($image, $blockPosition, $blockSize);
151+
110152
return $image;
111153
}
112154

@@ -128,12 +170,27 @@ private function placeTextOnFrame(VipsImage $text, FrameInterface $frame, int $x
128170
return $frame;
129171
}
130172

173+
/**
174+
* Build size from given vips image
175+
*
176+
* @param VipsImage $blockImage
177+
* @return Rectangle
178+
*/
179+
private function blockSize(VipsImage $blockImage): Rectangle
180+
{
181+
$imageSize = new Rectangle($blockImage->width, $blockImage->height, $this->position);
182+
$imageSize->align($this->font->alignment());
183+
$imageSize->valign($this->font->valignment());
184+
185+
return $imageSize;
186+
}
187+
131188
/**
132189
* Maybe rotate text image according to current font angle
133190
*
134191
* @param VipsImage $text
135-
* @return VipsImage
136192
* @throws VipsException
193+
* @return VipsImage
137194
*/
138195
private function maybeRotateText(VipsImage $text): VipsImage
139196
{
@@ -145,4 +202,32 @@ private function maybeRotateText(VipsImage $text): VipsImage
145202
default => $text->similarity(['angle' => $this->font->angle()]),
146203
};
147204
}
205+
206+
/**
207+
* Draw debug information for given position and given rectangle size
208+
*
209+
* @throws RuntimeException
210+
*/
211+
private function debugPos(ImageInterface $image, PointInterface $position, Rectangle $size): void
212+
{
213+
// draw pos
214+
$image->drawCircle($position->x(), $position->y(), function (CircleFactory $circle): void {
215+
$circle->diameter(8);
216+
$circle->background('red');
217+
});
218+
219+
// draw points of size
220+
foreach (array_chunk($size->toArray(), 2) as $point) {
221+
$image->drawCircle($point[0], $point[1], function (CircleFactory $circle): void {
222+
$circle->diameter(12);
223+
$circle->border('green');
224+
});
225+
}
226+
227+
// draw size's pivot
228+
$image->drawCircle($size->pivot()->x(), $size->pivot()->y(), function (CircleFactory $circle): void {
229+
$circle->diameter(20);
230+
$circle->border('blue');
231+
});
232+
}
148233
}

0 commit comments

Comments
 (0)