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 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 2003 by Bitstream Inc All Rights ReservedCopyright c 2006 by Tavmjong Bah All Rights ReservedDejaVu changes are in public domain +Foundry : DejaVu fonts team +Foundry URL : httpdejavusourceforgenet + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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; + } +}