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..ae266d5
--- /dev/null
+++ b/src/Calculator/SvgFont.php
@@ -0,0 +1,135 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace PUGX\Poser\Calculator;
+
+use XMLReader;
+
+/**
+ * @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 = [];
+ $defaultHorizAdvX = 0;
+ $unitsPerEm = 0;
+ $glyphSpacingHorizAdvX = 0;
+ $missingGlyphHorizAdvX = 0;
+
+ while ($xml->read()) {
+ if ($xml->nodeType !== XMLReader::ELEMENT) {
+ continue;
+ }
+
+ if ($xml->name === 'font') {
+ $defaultHorizAdvX = intval($xml->getAttribute('horiz-adv-x'));
+ }
+
+ if ($xml->name === 'font-face') {
+ $unitsPerEm = intval($xml->getAttribute('units-per-em'));
+ }
+
+ if ($xml->name === 'missing-glyph') {
+ $missingGlyphHorizAdvX = intval($xml->getAttribute('horiz-adv-x'));
+ }
+
+ if ($xml->name === 'glyph') {
+ $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;
+ } else {
+ $glyphs[$codePoint]->horizAdvX = intval($glyphHorizAdvX);
+ }
+
+ $glyphs[$codePoint]->d = $xml->getAttribute('d');
+
+ if ($codePoint === self::UNICODE_CODE_POINT_M_LOWERCASE) {
+ $glyphSpacingHorizAdvX = $glyphs[$codePoint]->horizAdvX;
+ }
+ }
+ }
+ }
+ }
+
+ return new self(
+ $glyphs,
+ $unitsPerEm,
+ $glyphSpacingHorizAdvX,
+ $missingGlyphHorizAdvX,
+ );
+ }
+
+ public function computeWidth(
+ int $codePoint,
+ int $size,
+ float $glyphSpacing = 0.0
+ ): float {
+ $size = $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..5df2408
--- /dev/null
+++ b/src/Calculator/SvgTextSizeCalculator.php
@@ -0,0 +1,51 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace PUGX\Poser\Calculator;
+
+use PUGX\Poser\Calculator\TextSizeCalculatorInterface;
+
+/**
+ * @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 ($unicodeCodePoint === self::UNICODE_CODE_POINT_LINE_FEED) {
+ $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..9dc19e1
--- /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
+ */
+final 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 (count($values) === 0) {
+ $lookingFor = ($thisValue < 224) ? 2 : 3;
+ }
+
+ $values[] = $thisValue;
+
+ if (count($values) === $lookingFor) {
+ $number = ($lookingFor === 3)
+ ? (($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;
+ }
+}