diff --git a/src/Calculator/Font/DejaVuSans.svg b/src/Calculator/Font/DejaVuSans.svg
new file mode 100644
index 0000000..984b0ac
--- /dev/null
+++ b/src/Calculator/Font/DejaVuSans.svg
@@ -0,0 +1,251 @@
+
+
+
diff --git a/src/Calculator/SvgFont.php b/src/Calculator/SvgFont.php
new file mode 100644
index 0000000..662bd8d
--- /dev/null
+++ b/src/Calculator/SvgFont.php
@@ -0,0 +1,130 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace PUGX\Poser\Calculator;
+
+/**
+ * @author Anton Komarev
+ */
+class SvgFont
+{
+ private const UNICODE_CODE_POINT_M_LOWERCASE = 109;
+
+ private array $glyphs;
+
+ private int $unitsPerEm;
+
+ private int $glyphSpacingAdvX;
+
+ private int $missingGlyphAdvX;
+
+ public function __construct(
+ array $glyphs = [],
+ int $unitsPerEm = 0,
+ int $glyphSpacingAdvX = 0,
+ int $missingGlyphAdvX = 0
+ ) {
+ $this->glyphs = $glyphs;
+ $this->unitsPerEm = $unitsPerEm;
+ $this->glyphSpacingAdvX = $glyphSpacingAdvX;
+ $this->missingGlyphAdvX = $missingGlyphAdvX;
+ }
+
+ /**
+ * Takes path to SVG font (local path) and processes its XML
+ * to get path representation of every character and additional
+ * font parameters.
+ */
+ public static function fromFile(
+ string $filePath
+ ): self {
+ $xml = new \XMLReader();
+ $xml->open($filePath);
+
+ $glyphs = [];
+
+ while ($xml->read()) {
+ if (\XMLReader::ELEMENT !== $xml->nodeType) {
+ continue;
+ }
+
+ if ('font' === $xml->name) {
+ $defaultHorizAdvX = (int) $xml->getAttribute('horiz-adv-x');
+ }
+
+ if ('font-face' === $xml->name) {
+ $unitsPerEm = (int) $xml->getAttribute('units-per-em');
+ }
+
+ if ('missing-glyph' === $xml->name) {
+ $missingGlyphHorizAdvX = (int) $xml->getAttribute('horiz-adv-x');
+ }
+
+ if ('glyph' === $xml->name) {
+ $unicode = $xml->getAttribute('unicode');
+
+ if (isset($unicode)) {
+ $codePoints = TextUnicodeConverter::convertTextToCodePoints($unicode);
+
+ if (isset($codePoints[0])) {
+ $codePoint = $codePoints[0];
+
+ $glyphs[$codePoint] = new \stdClass();
+
+ $glyphHorizAdvX = $xml->getAttribute('horiz-adv-x');
+
+ if (empty($glyphHorizAdvX)) {
+ $glyphs[$codePoint]->horizAdvX = $defaultHorizAdvX ?? 0;
+ } else {
+ $glyphs[$codePoint]->horizAdvX = (int) $glyphHorizAdvX;
+ }
+
+ $glyphs[$codePoint]->d = $xml->getAttribute('d');
+
+ if (self::UNICODE_CODE_POINT_M_LOWERCASE === $codePoint) {
+ $glyphSpacingHorizAdvX = $glyphs[$codePoint]->horizAdvX;
+ }
+ }
+ }
+ }
+ }
+
+ return new self(
+ $glyphs,
+ $unitsPerEm ?? 0,
+ $glyphSpacingHorizAdvX ?? 0,
+ $missingGlyphHorizAdvX ?? 0,
+ );
+ }
+
+ public function computeWidth(
+ int $codePoint,
+ int $size,
+ float $glyphSpacing = 0.0
+ ): float {
+ $size /= $this->unitsPerEm;
+
+ $glyphAdvX = $this->getGlyphAdvX($codePoint);
+
+ $glyphWidth = $glyphAdvX * $size;
+ $glyphSpacingWidth = $this->glyphSpacingAdvX * $glyphSpacing * $size;
+
+ return $glyphWidth + $glyphSpacingWidth;
+ }
+
+ private function getGlyphAdvX(
+ int $codePoint
+ ): int {
+ return isset($this->glyphs[$codePoint])
+ ? $this->glyphs[$codePoint]->horizAdvX
+ : $this->missingGlyphAdvX;
+ }
+}
diff --git a/src/Calculator/SvgTextSizeCalculator.php b/src/Calculator/SvgTextSizeCalculator.php
new file mode 100644
index 0000000..da18561
--- /dev/null
+++ b/src/Calculator/SvgTextSizeCalculator.php
@@ -0,0 +1,49 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace PUGX\Poser\Calculator;
+
+/**
+ * @author Anton Komarev
+ */
+class SvgTextSizeCalculator implements TextSizeCalculatorInterface
+{
+ private const SHIELD_PADDING = 12;
+
+ private const UNICODE_CODE_POINT_LINE_FEED = 10;
+
+ /**
+ * Calculate the width of the text box.
+ */
+ public function calculateWidth(string $text, int $size = self::TEXT_SIZE): float
+ {
+ $font = SvgFont::fromFile(__DIR__ . '/Font/DejaVuSans.svg');
+
+ $textUnicode = TextUnicodeConverter::convertTextToCodePoints($text);
+
+ $width = 0;
+ $lineWidth = 0;
+
+ foreach ($textUnicode as $unicodeCodePoint) {
+ if (self::UNICODE_CODE_POINT_LINE_FEED === $unicodeCodePoint) {
+ $width = \max($width, $lineWidth);
+ $lineWidth = 0;
+ continue;
+ }
+
+ $lineWidth += $font->computeWidth($unicodeCodePoint, $size);
+ }
+
+ $width = \max($width, $lineWidth);
+
+ return \round($width + self::SHIELD_PADDING, 1);
+ }
+}
diff --git a/src/Calculator/TextUnicodeConverter.php b/src/Calculator/TextUnicodeConverter.php
new file mode 100644
index 0000000..ef8d8ea
--- /dev/null
+++ b/src/Calculator/TextUnicodeConverter.php
@@ -0,0 +1,55 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace PUGX\Poser\Calculator;
+
+/**
+ * @author Anton Komarev
+ */
+class TextUnicodeConverter
+{
+ /**
+ * Converts UTF-8 encoded string and returns unicode code points for every character.
+ */
+ public static function convertTextToCodePoints(
+ string $string
+ ): array {
+ $codePoints = [];
+ $values = [];
+ $lookingFor = 1;
+
+ for ($i = 0, $iMax = \strlen($string); $i < $iMax; ++$i) {
+ $thisValue = \ord($string[$i]);
+
+ if ($thisValue < 128) {
+ $codePoints[] = $thisValue;
+ } else {
+ if (0 === \count($values)) {
+ $lookingFor = ($thisValue < 224) ? 2 : 3;
+ }
+
+ $values[] = $thisValue;
+
+ if (\count($values) === $lookingFor) {
+ $number = (3 === $lookingFor)
+ ? (($values[0] % 16) * 4096) + (($values[1] % 64) * 64) + ($values[2] % 64)
+ : (($values[0] % 32) * 64) + ($values[1] % 64);
+
+ $codePoints[] = $number;
+ $values = [];
+ $lookingFor = 1;
+ }
+ }
+ }
+
+ return $codePoints;
+ }
+}