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..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; + } +}