diff options
author | Kore Nordmann <github@kore-nordmann.de> | 2008-05-03 16:36:57 +0000 |
---|---|---|
committer | Kore Nordmann <github@kore-nordmann.de> | 2008-05-03 16:36:57 +0000 |
commit | 5d4947a0cb1ba3bd7e4097b3da9b772dd16d4094 (patch) | |
tree | 088ede950f85e6af9aca1902bcc34a1a463e7d37 /src/driver | |
parent | 92c7fc36f830d168e02cd26729d7f099bddce707 (diff) | |
download | zetacomponents-graph-5d4947a0cb1ba3bd7e4097b3da9b772dd16d4094.zip zetacomponents-graph-5d4947a0cb1ba3bd7e4097b3da9b772dd16d4094.tar.gz |
- Implemented feature #10957: Embed glyphs for exact SVG font width estimation
# Lots of binary diffs, because font-family names are now enclosed in ' in SVG
# files.
Diffstat (limited to 'src/driver')
-rw-r--r-- | src/driver/svg.php | 51 | ||||
-rw-r--r-- | src/driver/svg_font.php | 285 |
2 files changed, 322 insertions, 14 deletions
diff --git a/src/driver/svg.php b/src/driver/svg.php index d49ab45..9a814f1 100644 --- a/src/driver/svg.php +++ b/src/driver/svg.php @@ -100,6 +100,13 @@ class ezcGraphSvgDriver extends ezcGraphDriver protected $elementID = 0; /** + * Font storage for SVG font glyphs and kernings. + * + * @var ezcGraphSvgFont + */ + protected $font = null; + + /** * Constructor * * @param array $options Default option array @@ -110,6 +117,7 @@ class ezcGraphSvgDriver extends ezcGraphDriver { ezcBase::checkDependency( 'Graph', ezcBase::DEP_PHP_EXTENSION, 'dom' ); $this->options = new ezcGraphSvgDriverOptions( $options ); + $this->font = new ezcGraphSvgFont(); } /** @@ -136,7 +144,6 @@ class ezcGraphSvgDriver extends ezcGraphDriver if ( $this->options->templateDocument !== false ) { -// @TODO: Add $this->dom->format $this->dom->load( $this->options->templateDocument ); $this->defs = $this->dom->getElementsByTagName( 'defs' )->item( 0 ); @@ -450,12 +457,25 @@ class ezcGraphSvgDriver extends ezcGraphDriver */ protected function getTextBoundings( $size, ezcGraphFontOptions $font, $text ) { - return new ezcGraphBoundings( - 0, - 0, - $this->getTextWidth( $text, $size ), - $size - ); + if ( $font->type === ezcGraph::SVG_FONT ) + { + return new ezcGraphBoundings( + 0, + 0, + $this->font->calculateStringWidth( $font->path, $text ) * $size, + $size + ); + } + else + { + // If we didn't get a SVG font, continue guessing the font width. + return new ezcGraphBoundings( + 0, + 0, + $this->getTextWidth( $text, $size ), + $size + ); + } } /** @@ -655,7 +675,7 @@ class ezcGraphSvgDriver extends ezcGraphDriver foreach ( $text['text'] as $line ) { $string = implode( ' ', $line ); - if ( ( $strWidth = $this->getTextWidth( $string, $size ) ) > $width ) + if ( ( $strWidth = $this->getTextBoundings( $size, $text['font'], $string )->width ) > $width ) { $width = $strWidth; } @@ -767,13 +787,13 @@ class ezcGraphSvgDriver extends ezcGraphDriver break; case ( $text['align'] & ezcGraph::RIGHT ): $position = new ezcGraphCoordinate( - $text['position']->x + ( $text['width'] - $this->getTextWidth( $string, $size ) ), + $text['position']->x + ( $text['width'] - $this->getTextBoundings( $size, $text['font'], $string )->width ), $text['position']->y + $yOffset ); break; case ( $text['align'] & ezcGraph::CENTER ): $position = new ezcGraphCoordinate( - $text['position']->x + ( ( $text['width'] - $this->getTextWidth( $string, $size ) ) / 2 ), + $text['position']->x + ( ( $text['width'] - $this->getTextBoundings( $size, $text['font'], $string )->width ) / 2 ), $text['position']->y + $yOffset ); break; @@ -785,12 +805,12 @@ class ezcGraphSvgDriver extends ezcGraphDriver $textNode = $this->dom->createElement( 'text', $this->encode( $string ) ); $textNode->setAttribute( 'id', $text['id'] . '_shadow' ); $textNode->setAttribute( 'x', sprintf( '%.4F', $position->x + $this->options->graphOffset->x + $text['font']->textShadowOffset ) ); - $textNode->setAttribute( 'text-length', sprintf( '%.4Fpx', $this->getTextWidth( $string, $size ) ) ); + $textNode->setAttribute( 'text-length', sprintf( '%.4Fpx', $this->getTextBoundings( $size, $text['font'], $string )->width ) ); $textNode->setAttribute( 'y', sprintf( '%.4F', $position->y + $this->options->graphOffset->y + $text['font']->textShadowOffset ) ); $textNode->setAttribute( 'style', sprintf( - 'font-size: %dpx; font-family: %s; fill: #%02x%02x%02x; fill-opacity: %.2F; stroke: none;', + 'font-size: %dpx; font-family: \'%s\'; fill: #%02x%02x%02x; fill-opacity: %.2F; stroke: none;', $size, $text['font']->name, $text['font']->textShadowColor->red, @@ -806,12 +826,12 @@ class ezcGraphSvgDriver extends ezcGraphDriver $textNode = $this->dom->createElement( 'text', $this->encode( $string ) ); $textNode->setAttribute( 'id', $text['id'] . '_text' ); $textNode->setAttribute( 'x', sprintf( '%.4F', $position->x + $this->options->graphOffset->x ) ); - $textNode->setAttribute( 'text-length', sprintf( '%.4Fpx', $this->getTextWidth( $string, $size ) ) ); + $textNode->setAttribute( 'text-length', sprintf( '%.4Fpx', $this->getTextBoundings( $size, $text['font'], $string )->width ) ); $textNode->setAttribute( 'y', sprintf( '%.4F', $position->y + $this->options->graphOffset->y ) ); $textNode->setAttribute( 'style', sprintf( - 'font-size: %dpx; font-family: %s; fill: #%02x%02x%02x; fill-opacity: %.2F; stroke: none;', + 'font-size: %dpx; font-family: \'%s\'; fill: #%02x%02x%02x; fill-opacity: %.2F; stroke: none;', $size, $text['font']->name, $text['font']->color->red, @@ -1186,6 +1206,9 @@ class ezcGraphSvgDriver extends ezcGraphDriver { $this->createDocument(); $this->drawAllTexts(); + + // Embed used glyphs + $this->font->addFontToDocument( $this->dom ); $this->dom->save( $file ); } } diff --git a/src/driver/svg_font.php b/src/driver/svg_font.php new file mode 100644 index 0000000..341fa48 --- /dev/null +++ b/src/driver/svg_font.php @@ -0,0 +1,285 @@ +<?php +/** + * File containing the ezcGraphSVGDriver class + * + * @package Graph + * @version //autogentag// + * @copyright Copyright ( C ) 2005-2008 eZ systems as. All rights reserved. + * @author Freddie Witherden + * @license http://ez.no/licenses/new_bsd New BSD License + */ + +/** + * Helper class, offering requrired calculation basics and font metrics to use + * SVG fonts with the SVG driver. + * + * You may convert any ttf font into a SVG font using the `ttf2svg` bianry from + * the batik package. Depending on the distribution it may only be available as + * `batik-ttf2svg-<version>`. + * + * Usage: + * <code> + * $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.' ) + * ); + * </code> + * + * @version //autogentag// + * @package Graph + * @mainclass + */ +class ezcGraphSvgFont +{ + /** + * Units per EM + * + * @var float + */ + protected $unitsPerEm; + + /** + * Used glyphs + * + * @var array + */ + protected $usedGlyphs = 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 ) + { + $fontPath = realpath( $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 <glyph> associated with $char. + * + * @param string $fontPath + * @param string $char + * @return SimpleXMLElement + */ + protected function getGlyph( $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 ( $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 $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 ); + } + } +} + +?> |