summaryrefslogtreecommitdiffstats
path: root/src/driver
diff options
context:
space:
mode:
authorKore Nordmann <github@kore-nordmann.de>2008-05-03 16:36:57 +0000
committerKore Nordmann <github@kore-nordmann.de>2008-05-03 16:36:57 +0000
commit5d4947a0cb1ba3bd7e4097b3da9b772dd16d4094 (patch)
tree088ede950f85e6af9aca1902bcc34a1a463e7d37 /src/driver
parent92c7fc36f830d168e02cd26729d7f099bddce707 (diff)
downloadzetacomponents-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.php51
-rw-r--r--src/driver/svg_font.php285
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 );
+ }
+ }
+}
+
+?>
OpenPOWER on IntegriCloud