`. * * Usage: * * $font = new ezcGraphSvgFont(); * var_dump( * $font->calculateStringWidth( '../tests/data/font.svg', 'Just a test string.' ), * $font->calculateStringWidth( '../tests/data/font2.svg', 'Just a test string.' ) * ); * * * @version 1.4beta1 * @package Graph * @mainclass */ class ezcGraphSvgFont { /** * Units per EM * * @var float */ protected $unitsPerEm; /** * Used glyphs * * @var array */ protected $usedGlyphs = array(); /** * Cache for glyph size to save XPath lookups. * * @var array */ protected $glyphCache = array(); /** * Used kernings * * @var array */ protected $usedKerns = array(); /** * Path to font * * @var string */ protected $fonts = array(); /** * Initialize SVG font * * Loads the SVG font $filename. This should be the path to the file * generated by ttf2svg. * * Returns the (normlized) name of the initilized font. * * @param string $fontPath * @return string */ protected function initializeFont( $fontPath ) { if ( isset( $this->fonts[$fontPath] ) ) { return $fontPath; } // Check for existance of font file if ( !is_file( $fontPath ) || !is_readable( $fontPath ) ) { throw new ezcBaseFileNotFoundException( $fontPath ); } $this->fonts[$fontPath] = simplexml_load_file( $fontPath )->defs->font; // SimpleXML requires us to register a namespace for XPath to work $this->fonts[$fontPath]->registerXPathNamespace( 'svg', 'http://www.w3.org/2000/svg' ); // Extract the number of units per Em $this->unitsPerEm[$fontPath] = (int) $this->fonts[$fontPath]->{'font-face'}['units-per-em']; return $fontPath; } /** * Get name of font * * Get the name of the given font, by extracting its font family from the * SVG font file. * * @param string $fontPath * @return string */ public static function getFontName( $fontPath ) { $font = simplexml_load_file( $fontPath )->defs->font; // SimpleXML requires us to register a namespace for XPath to work $font->registerXPathNamespace( 'svg', 'http://www.w3.org/2000/svg' ); // Extract the font family name return (string) $font->{'font-face'}['font-family']; } /** * XPath has no standard means of escaping ' and ", with the only solution * being to delimit your string with the opposite type of quote. ( And if * your string contains both concat( ) it ). * * This method will correctly delimit $char with the appropriate quote type * so that it can be used in an XPath expression. * * @param string $char * @return string */ protected static function xpathEscape( $char ) { return "'" . str_replace( array( '\'', '\\' ), array( '\\\'', '\\\\' ), $char ) . "'"; } /** * Returns the associated with $char. * * @param string $fontPath * @param string $char * @return float */ protected function getGlyph( $fontPath, $char ) { // Check if glyphwidth has already been calculated. if ( isset( $this->glyphCache[$fontPath][$char] ) ) { return $this->glyphCache[$fontPath][$char]; } $matches = $this->fonts[$fontPath]->xpath( $query = "glyph[@unicode=" . self::xpathEscape( $char ) . "]" ); if ( count( $matches ) === 0 ) { // Just ignore missing glyphs. The client will still render them // using a default font. We try to estimate some width by using a // common other character. return $this->glyphCache[$fontPath][$char] = ( $char === 'o' ? false : $this->getGlyph( $fontPath, 'o' ) ); } $glyph = $matches[0]; if ( !in_array( $glyph, $this->usedGlyphs ) ) { $this->usedGlyphs[$fontPath][] = $glyph; } // There should only ever be one match return $this->glyphCache[$fontPath][$char] = $glyph; } /** * Returns the amount of kerning to apply for glyphs $g1 and $g2. If no * valid kerning pair can be found 0 is returned. * * @param string $fontPath * @param SimpleXMLElement $g1 * @param SimpleXMLElement $g2 * @return int */ public function getKerning( $fontPath, SimpleXMLElement $glyph1, SimpleXMLElement $glyph2 ) { // Get the glyph names $g1Name = self::xpathEscape( ( string ) $glyph1['glyph-name'] ); $g2Name = self::xpathEscape( ( string ) $glyph2['glyph-name'] ); // Get the unicode character names $g1Uni = self::xpathEscape( ( string ) $glyph1['unicode'] ); $g2Uni = self::xpathEscape( ( string ) $glyph2['unicode'] ); // Search for kerning pairs $pair = $this->fonts[$fontPath]->xpath( "svg:hkern[( @g1=$g1Name and @g2=$g2Name ) or ( @u1=$g1Uni and @g2=$g2Uni )]" ); // If we found anything return it if ( count( $pair ) ) { if ( !in_array( $pair[0], $this->usedKerns ) ) { $this->usedKerns[$fontPath][] = $pair[0]; } return ( int ) $pair[0]['k']; } else { return 0; } } /** * Calculates the width of $string in the current font in Em's. * * @param string $fontPath * @param string $string * @return float */ public function calculateStringWidth( $fontPath, $string ) { // Ensure font is properly initilized $fontPath = $this->initializeFont( $fontPath ); $strlen = strlen( $string ); $prevCharInfo = null; $length = 0; // @TODO: Add UTF-8 support here - iterating over the bytes does not // really help. for ( $i = 0; $i < $strlen; ++$i ) { // Find the font information for the character $charInfo = $this->getGlyph( $fontPath, $string[$i] ); // Handle missing glyphs if ( $charInfo === false ) { $prevCharInfo = null; $length .= .5 * $this->unitsPerEm[$fontPath]; continue; } // Add the horizontal advance for the character to the length $length += (float) $charInfo['horiz-adv-x']; // If we are not the first character, look for kerning pairs if ( $prevCharInfo !== null ) { // Apply kerning (if any) $length -= $this->getKerning( $fontPath, $prevCharInfo, $charInfo ); } $prevCharInfo = clone $charInfo; } // Divide by _unitsPerEm to get the length in Em return (float) $length / $this->unitsPerEm[$fontPath]; } /** * Add font definitions to SVG document * * Add the SVG font definition paths for all used glyphs and kernings to * the given SVG document. * * @param DOMDocument $document * @return void */ public function addFontToDocument( DOMDocument $document ) { $defs = $document->getElementsByTagName( 'defs' )->item( 0 ); $fontNr = 0; foreach ( $this->fonts as $path => $definition ) { // Just import complete font for now. // @TODO: Only import used characters. $font = dom_import_simplexml( $definition ); $font = $document->importNode( $font, true ); $font->setAttribute( 'id', 'Font' . ++$fontNr ); $defs->appendChild( $font ); } } } ?>