* $graph = new ezcGraphPieChart(); * $graph->background->color = '#FFFFFFFF'; * $graph->title = 'Access statistics'; * $graph->legend = false; * * $graph->data['Access statistics'] = new ezcGraphArrayDataSet( array( * 'Mozilla' => 19113, * 'Explorer' => 10917, * 'Opera' => 1464, * 'Safari' => 652, * 'Konqueror' => 474, * ) ); * * $graph->renderer = new ezcGraphRenderer3d(); * $graph->renderer->options->pieChartShadowSize = 10; * $graph->renderer->options->pieChartGleam = .5; * $graph->renderer->options->dataBorder = false; * $graph->renderer->options->pieChartHeight = 16; * $graph->renderer->options->legendSymbolGleam = .5; * * // Use cairo driver * $graph->driver = new ezcGraphCairoDriver(); * * $graph->render( 400, 200, 'tutorial_driver_cairo.png' ); * * * @version //autogentag// * @package Graph * @mainclass */ class ezcGraphCairoDriver extends ezcGraphDriver { /** * Surface for cairo * * @var resource */ protected $surface; /** * Current cairo context. * * @var resource */ protected $context; /** * List of strings to draw * array ( array( * 'text' => array( 'strings' ), * 'options' => ezcGraphFontOptions, * ) * * @var array */ protected $strings = array(); /** * Constructor * * @param array $options Default option array * @return void * @ignore */ public function __construct( array $options = array() ) { ezcBase::checkDependency( 'Graph', ezcBase::DEP_PHP_EXTENSION, 'cairo_wrapper' ); $this->options = new ezcGraphCairoDriverOptions( $options ); } /** * Initilize cairo surface * * Initilize cairo surface from values provided in the options object, if * is has not been already initlized. * * @return void */ protected function initiliazeSurface() { // Immediatly exit, if surface already exists if ( $this->surface !== null ) { return; } $this->surface = cairo_image_surface_create( CAIRO_FORMAT_ARGB32, $this->options->width, $this->options->height ); $this->context = cairo_create( $this->surface ); cairo_set_line_width( $this->context, 1 ); } /** * Get SVG style definition * * Returns a string with SVG style definitions created from color, * fillstatus and line thickness. * * @param ezcGraphColor $color Color * @param mixed $filled Filled * @param float $thickness Line thickness. * @return string Formatstring */ protected function getStyle( ezcGraphColor $color, $filled = true, $thickness = 1. ) { switch ( true ) { case $color instanceof ezcGraphLinearGradient: $pattern = cairo_pattern_create_linear( $color->startPoint->x, $color->startPoint->y, $color->endPoint->x, $color->endPoint->y ); cairo_pattern_add_color_stop_rgba ( $pattern, 0, $color->startColor->red / 255, $color->startColor->green / 255, $color->startColor->blue / 255, 1 - $color->startColor->alpha / 255 ); cairo_pattern_add_color_stop_rgba ( $pattern, 1, $color->endColor->red / 255, $color->endColor->green / 255, $color->endColor->blue / 255, 1 - $color->endColor->alpha / 255 ); cairo_set_source( $this->context, $pattern ); cairo_fill( $this->context ); break; case $color instanceof ezcGraphRadialGradient: $pattern = cairo_pattern_create_radial( 0, 0, 0, 0, 0, 1 ); cairo_pattern_add_color_stop_rgba ( $pattern, 0, $color->startColor->red / 255, $color->startColor->green / 255, $color->startColor->blue / 255, 1 - $color->startColor->alpha / 255 ); cairo_pattern_add_color_stop_rgba ( $pattern, 1, $color->endColor->red / 255, $color->endColor->green / 255, $color->endColor->blue / 255, 1 - $color->endColor->alpha / 255 ); // Scale pattern, and move it to the correct position $matrix = cairo_matrix_multiply( $move = cairo_matrix_create_translate( -$color->center->x, -$color->center->y ), $scale = cairo_matrix_create_scale( 1 / $color->width, 1 / $color->height ) ); cairo_pattern_set_matrix( $pattern, $matrix ); cairo_set_source( $this->context, $pattern ); cairo_fill( $this->context ); break; default: cairo_set_source_rgba( $this->context, $color->red / 255, $color->green / 255, $color->blue / 255, 1 - $color->alpha / 255 ); break; } // Set line width cairo_set_line_width( $this->context, $thickness ); // Set requested fill state for context if ( $filled ) { cairo_fill_preserve( $this->context ); } } /** * Draws a single polygon. * * @param array $points Point array * @param ezcGraphColor $color Polygon color * @param mixed $filled Filled * @param float $thickness Line thickness * @return void */ public function drawPolygon( array $points, ezcGraphColor $color, $filled = true, $thickness = 1. ) { $this->initiliazeSurface(); $path = cairo_new_path( $this->context ); $lastPoint = end( $points ); cairo_move_to( $this->context, $lastPoint->x, $lastPoint->y ); foreach ( $points as $point ) { cairo_line_to( $this->context, $point->x, $point->y ); } cairo_close_path( $this->context ); $this->getStyle( $color, $filled, $thickness ); cairo_stroke( $this->context ); return $points; } /** * Draws a line * * @param ezcGraphCoordinate $start Start point * @param ezcGraphCoordinate $end End point * @param ezcGraphColor $color Line color * @param float $thickness Line thickness * @return void */ public function drawLine( ezcGraphCoordinate $start, ezcGraphCoordinate $end, ezcGraphColor $color, $thickness = 1. ) { $this->initiliazeSurface(); $path = cairo_new_path( $this->context ); cairo_move_to( $this->context, $start->x, $start->y ); cairo_line_to( $this->context, $end->x, $end->y ); $this->getStyle( $color, false, $thickness ); cairo_stroke( $this->context ); return array( $start, $end ); } /** * Returns boundings of text depending on the available font extension * * @param float $size Textsize * @param ezcGraphFontOptions $font Font * @param string $text Text * @return ezcGraphBoundings Boundings of text */ protected function getTextBoundings( $size, ezcGraphFontOptions $font, $text ) { cairo_select_font_face( $this->context, $font->name, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL ); cairo_set_font_size( $this->context, $size ); $extents = cairo_text_extents( $this->context, $text ); return new ezcGraphBoundings( 0, 0, $extents['width'], $extents['height'] ); } /** * Writes text in a box of desired size * * @param string $string Text * @param ezcGraphCoordinate $position Top left position * @param float $width Width of text box * @param float $height Height of text box * @param int $align Alignement of text * @param ezcGraphRotation $rotation * @return void */ public function drawTextBox( $string, ezcGraphCoordinate $position, $width, $height, $align, ezcGraphRotation $rotation = null ) { $this->initiliazeSurface(); $padding = $this->options->font->padding + ( $this->options->font->border !== false ? $this->options->font->borderWidth : 0 ); $width -= $padding * 2; $height -= $padding * 2; $textPosition = new ezcGraphCoordinate( $position->x + $padding, $position->y + $padding ); // Try to get a font size for the text to fit into the box $maxSize = min( $height, $this->options->font->maxFontSize ); $result = false; for ( $size = $maxSize; $size >= $this->options->font->minFontSize; ) { $result = $this->testFitStringInTextBox( $string, $position, $width, $height, $size ); if ( is_array( $result ) ) { break; } $size = ( ( $newsize = $size * ( $result ) ) >= $size ? $size - 1 : floor( $newsize ) ); } if ( !is_array( $result ) ) { if ( ( $height >= $this->options->font->minFontSize ) && ( $this->options->autoShortenString ) ) { $result = $this->tryFitShortenedString( $string, $position, $width, $height, $size = $this->options->font->minFontSize ); } else { throw new ezcGraphFontRenderingException( $string, $this->options->font->minFontSize, $width, $height ); } } $this->options->font->minimalUsedFont = $size; $this->strings[] = array( 'text' => $result, 'position' => $textPosition, 'width' => $width, 'height' => $height, 'align' => $align, 'font' => $this->options->font, 'rotation' => $rotation, ); return array( clone $position, new ezcGraphCoordinate( $position->x + $width, $position->y ), new ezcGraphCoordinate( $position->x + $width, $position->y + $height ), new ezcGraphCoordinate( $position->x, $position->y + $height ), ); } /** * Render text depending of font type and available font extensions * * @param string $id * @param string $text * @param string $font * @param ezcGraphColor $color * @param ezcGraphCoordinate $position * @param float $size * @param float $rotation * @return void */ protected function renderText( $text, $font, ezcGraphColor $color, ezcGraphCoordinate $position, $size, $rotation = null ) { cairo_select_font_face( $this->context, $font, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL ); cairo_set_font_size( $this->context, $size ); // Store current state of context cairo_save( $this->context ); cairo_move_to( $this->context, 0, 0 ); if ( $rotation !== null ) { // Move to the center cairo_translate( $this->context, $rotation->getCenter()->x, $rotation->getCenter()->y ); // Rotate around text center cairo_rotate( $this->context, deg2rad( $rotation->getRotation() ) ); // Center the text cairo_translate( $this->context, $position->x - $rotation->getCenter()->x, $position->y - $rotation->getCenter()->y - $size * .15 ); } else { cairo_translate( $this->context, $position->x, $position->y - $size * .15 ); } cairo_new_path( $this->context ); $this->getStyle( $color, true ); cairo_show_text( $this->context, $text ); cairo_stroke( $this->context ); // Restore state of context cairo_restore( $this->context ); } /** * Draw all collected texts * * The texts are collected and their maximum possible font size is * calculated. This function finally draws the texts on the image, this * delayed drawing has two reasons: * * 1) This way the text strings are always on top of the image, what * results in better readable texts * 2) The maximum possible font size can be calculated for a set of texts * with the same font configuration. Strings belonging to one chart * element normally have the same font configuration, so that all texts * belonging to one element will have the same font size. * * @access protected * @return void */ protected function drawAllTexts() { $this->initiliazeSurface(); foreach ( $this->strings as $text ) { $size = $text['font']->minimalUsedFont; $completeHeight = count( $text['text'] ) * $size + ( count( $text['text'] ) - 1 ) * $this->options->lineSpacing; // Calculate y offset for vertical alignement switch ( true ) { case ( $text['align'] & ezcGraph::BOTTOM ): $yOffset = $text['height'] - $completeHeight; break; case ( $text['align'] & ezcGraph::MIDDLE ): $yOffset = ( $text['height'] - $completeHeight ) / 2; break; case ( $text['align'] & ezcGraph::TOP ): default: $yOffset = 0; break; } $padding = $text['font']->padding + $text['font']->borderWidth / 2; if ( $this->options->font->minimizeBorder === true ) { // Calculate maximum width of text rows $width = false; foreach ( $text['text'] as $line ) { $string = implode( ' ', $line ); $boundings = $this->getTextBoundings( $size, $text['font'], $string ); if ( ( $width === false) || ( $boundings->width > $width ) ) { $width = $boundings->width; } } switch ( true ) { case ( $text['align'] & ezcGraph::CENTER ): $xOffset = ( $text['width'] - $width ) / 2; break; case ( $text['align'] & ezcGraph::RIGHT ): $xOffset = $text['width'] - $width; break; case ( $text['align'] & ezcGraph::LEFT ): default: $xOffset = 0; break; } $borderPolygonArray = array( new ezcGraphCoordinate( $text['position']->x - $padding + $xOffset, $text['position']->y - $padding + $yOffset ), new ezcGraphCoordinate( $text['position']->x + $padding * 2 + $xOffset + $width, $text['position']->y - $padding + $yOffset ), new ezcGraphCoordinate( $text['position']->x + $padding * 2 + $xOffset + $width, $text['position']->y + $padding * 2 + $yOffset + $completeHeight ), new ezcGraphCoordinate( $text['position']->x - $padding + $xOffset, $text['position']->y + $padding * 2 + $yOffset + $completeHeight ), ); } else { $borderPolygonArray = array( new ezcGraphCoordinate( $text['position']->x - $padding, $text['position']->y - $padding ), new ezcGraphCoordinate( $text['position']->x + $padding * 2 + $text['width'], $text['position']->y - $padding ), new ezcGraphCoordinate( $text['position']->x + $padding * 2 + $text['width'], $text['position']->y + $padding * 2 + $text['height'] ), new ezcGraphCoordinate( $text['position']->x - $padding, $text['position']->y + $padding * 2 + $text['height'] ), ); } if ( $text['rotation'] !== null ) { foreach ( $borderPolygonArray as $nr => $point ) { $borderPolygonArray[$nr] = $text['rotation']->transformCoordinate( $point ); } } if ( $text['font']->background !== false ) { $this->drawPolygon( $borderPolygonArray, $text['font']->background, true ); } if ( $text['font']->border !== false ) { $this->drawPolygon( $borderPolygonArray, $text['font']->border, false, $text['font']->borderWidth ); } // Render text with evaluated font size $completeString = ''; foreach ( $text['text'] as $line ) { $string = implode( ' ', $line ); $completeString .= $string; $boundings = $this->getTextBoundings( $size, $text['font'], $string ); $text['position']->y += $size; switch ( true ) { case ( $text['align'] & ezcGraph::LEFT ): $position = new ezcGraphCoordinate( $text['position']->x, $text['position']->y + $yOffset ); break; case ( $text['align'] & ezcGraph::RIGHT ): $position = new ezcGraphCoordinate( $text['position']->x + ( $text['width'] - $boundings->width ), $text['position']->y + $yOffset ); break; case ( $text['align'] & ezcGraph::CENTER ): $position = new ezcGraphCoordinate( $text['position']->x + ( ( $text['width'] - $boundings->width ) / 2 ), $text['position']->y + $yOffset ); break; } // Optionally draw text shadow if ( $text['font']->textShadow === true ) { $this->renderText( $string, $text['font']->name, $text['font']->textShadowColor, new ezcGraphCoordinate( $position->x + $text['font']->textShadowOffset, $position->y + $text['font']->textShadowOffset ), $size, $text['rotation'] ); } // Finally draw text $this->renderText( $string, $text['font']->name, $text['font']->color, $position, $size, $text['rotation'] ); $text['position']->y += $size * $this->options->lineSpacing; } } } /** * Draws a sector of cirlce * * @param ezcGraphCoordinate $center Center of circle * @param mixed $width Width * @param mixed $height Height * @param mixed $startAngle Start angle of circle sector * @param mixed $endAngle End angle of circle sector * @param ezcGraphColor $color Color * @param mixed $filled Filled; * @return void */ public function drawCircleSector( ezcGraphCoordinate $center, $width, $height, $startAngle, $endAngle, ezcGraphColor $color, $filled = true ) { $this->initiliazeSurface(); // Normalize angles if ( $startAngle > $endAngle ) { $tmp = $startAngle; $startAngle = $endAngle; $endAngle = $tmp; } cairo_save( $this->context ); // Draw circular arc path $path = cairo_new_path( $this->context ); cairo_translate( $this->context, $center->x, $center->y ); cairo_scale( $this->context, 1, $height / $width ); cairo_move_to( $this->context, 0, 0 ); cairo_arc( $this->context, 0., 0., $width / 2, deg2rad( $startAngle ), deg2rad( $endAngle ) ); cairo_line_to( $this->context, 0, 0 ); cairo_restore( $this->context ); $this->getStyle( $color, $filled ); cairo_stroke( $this->context ); // Create polygon array to return $polygonArray = array( $center ); for ( $angle = $startAngle; $angle < $endAngle; $angle += $this->options->imageMapResolution ) { $polygonArray[] = new ezcGraphCoordinate( $center->x + ( ( cos( deg2rad( $angle ) ) * $width ) / 2 ), $center->y + ( ( sin( deg2rad( $angle ) ) * $height ) / 2 ) ); } $polygonArray[] = new ezcGraphCoordinate( $center->x + ( ( cos( deg2rad( $endAngle ) ) * $width ) / 2 ), $center->y + ( ( sin( deg2rad( $endAngle ) ) * $height ) / 2 ) ); return $polygonArray; } /** * Draws a circular arc consisting of several minor steps on the bounding * lines. * * @param ezcGraphCoordinate $center * @param mixed $width * @param mixed $height * @param mixed $size * @param mixed $startAngle * @param mixed $endAngle * @param ezcGraphColor $color * @param bool $filled * @return string Element id */ protected function simulateCircularArc( ezcGraphCoordinate $center, $width, $height, $size, $startAngle, $endAngle, ezcGraphColor $color, $filled ) { for ( $tmpAngle = min( ceil ( $startAngle / 180 ) * 180, $endAngle ); $tmpAngle <= $endAngle; $tmpAngle = min( ceil ( $startAngle / 180 + 1 ) * 180, $endAngle ) ) { $path = cairo_new_path( $this->context ); cairo_move_to( $this->context, $center->x + cos( deg2rad( $startAngle ) ) * $width / 2, $center->y + sin( deg2rad( $startAngle ) ) * $height / 2 ); // @TODO: Use cairo_curve_to() for( $angle = $startAngle; $angle <= $tmpAngle; $angle = min( $angle + $this->options->circleResolution, $tmpAngle ) ) { cairo_line_to( $this->context, $center->x + cos( deg2rad( $angle ) ) * $width / 2, $center->y + sin( deg2rad( $angle ) ) * $height / 2 + $size ); if ( $angle === $tmpAngle ) { break; } } for( $angle = $tmpAngle; $angle >= $startAngle; $angle = max( $angle - $this->options->circleResolution, $startAngle ) ) { cairo_line_to( $this->context, $center->x + cos( deg2rad( $angle ) ) * $width / 2, $center->y + sin( deg2rad( $angle ) ) * $height / 2 ); if ( $angle === $startAngle ) { break; } } cairo_close_path( $this->context ); $this->getStyle( $color, $filled ); cairo_stroke( $this->context ); $startAngle = $tmpAngle; if ( $tmpAngle === $endAngle ) { break; } } } /** * Draws a circular arc * * @param ezcGraphCoordinate $center Center of ellipse * @param integer $width Width of ellipse * @param integer $height Height of ellipse * @param integer $size Height of border * @param float $startAngle Starting angle of circle sector * @param float $endAngle Ending angle of circle sector * @param ezcGraphColor $color Color of Border * @param bool $filled * @return void */ public function drawCircularArc( ezcGraphCoordinate $center, $width, $height, $size, $startAngle, $endAngle, ezcGraphColor $color, $filled = true ) { $this->initiliazeSurface(); // Normalize angles if ( $startAngle > $endAngle ) { $tmp = $startAngle; $startAngle = $endAngle; $endAngle = $tmp; } $this->simulateCircularArc( $center, $width, $height, $size, $startAngle, $endAngle, $color, $filled ); if ( ( $this->options->shadeCircularArc !== false ) && $filled ) { $gradient = new ezcGraphLinearGradient( new ezcGraphCoordinate( $center->x - $width, $center->y ), new ezcGraphCoordinate( $center->x + $width, $center->y ), ezcGraphColor::fromHex( '#FFFFFF' )->transparent( $this->options->shadeCircularArc * 1.5 ), ezcGraphColor::fromHex( '#000000' )->transparent( $this->options->shadeCircularArc * 1.5 ) ); $this->simulateCircularArc( $center, $width, $height, $size, $startAngle, $endAngle, $gradient, $filled ); } // Create polygon array to return $polygonArray = array(); for ( $angle = $startAngle; $angle < $endAngle; $angle += $this->options->imageMapResolution ) { $polygonArray[] = new ezcGraphCoordinate( $center->x + ( ( cos( deg2rad( $angle ) ) * $width ) / 2 ), $center->y + ( ( sin( deg2rad( $angle ) ) * $height ) / 2 ) ); } $polygonArray[] = new ezcGraphCoordinate( $center->x + ( ( cos( deg2rad( $endAngle ) ) * $width ) / 2 ), $center->y + ( ( sin( deg2rad( $endAngle ) ) * $height ) / 2 ) ); for ( $angle = $endAngle; $angle > $startAngle; $angle -= $this->options->imageMapResolution ) { $polygonArray[] = new ezcGraphCoordinate( $center->x + ( ( cos( deg2rad( $angle ) ) * $width ) / 2 ) + $size, $center->y + ( ( sin( deg2rad( $angle ) ) * $height ) / 2 ) ); } $polygonArray[] = new ezcGraphCoordinate( $center->x + ( ( cos( deg2rad( $startAngle ) ) * $width ) / 2 ) + $size, $center->y + ( ( sin( deg2rad( $startAngle ) ) * $height ) / 2 ) ); return $polygonArray; } /** * Draw circle * * @param ezcGraphCoordinate $center Center of ellipse * @param mixed $width Width of ellipse * @param mixed $height height of ellipse * @param ezcGraphColor $color Color * @param mixed $filled Filled * @return void */ public function drawCircle( ezcGraphCoordinate $center, $width, $height, ezcGraphColor $color, $filled = true ) { $this->initiliazeSurface(); cairo_save( $this->context ); // Draw circular arc path $path = cairo_new_path( $this->context ); cairo_translate( $this->context, $center->x, $center->y ); cairo_scale( $this->context, 1, $height / $width ); cairo_arc( $this->context, 0., 0., $width / 2, 0, 2 * M_PI ); cairo_restore( $this->context ); $this->getStyle( $color, $filled ); cairo_stroke( $this->context ); // Create polygon array to return $polygonArray = array(); for ( $angle = 0; $angle < ( 2 * M_PI ); $angle += deg2rad( $this->options->imageMapResolution ) ) { $polygonArray[] = new ezcGraphCoordinate( $center->x + ( ( cos( $angle ) * $width ) / 2 ), $center->y + ( ( sin( $angle ) * $height ) / 2 ) ); } return $polygonArray; } /** * Draw an image * * The image will be inlined in the SVG document using data URL scheme. For * this the mime type and base64 encoded file content will be merged to * URL. * * @param mixed $file Image file * @param ezcGraphCoordinate $position Top left position * @param mixed $width Width of image in destination image * @param mixed $height Height of image in destination image * @return void */ public function drawImage( $file, ezcGraphCoordinate $position, $width, $height ) { $this->initiliazeSurface(); // Ensure given bitmap is a PNG image $data = getimagesize( $file ); if ( $data[2] !== IMAGETYPE_PNG ) { throw new Exception( 'Cairo only has support for PNGs.' ); } // Create new surface from given bitmap $imageSurface = cairo_image_surface_create_from_png( $file ); // Create pattern from source image to be able to transform it $pattern = cairo_pattern_create_for_surface( $imageSurface ); // Scale pattern to defined dimensions and move it to its destination position $matrix = cairo_matrix_multiply( $move = cairo_matrix_create_translate( -$position->x, -$position->y ), $scale = cairo_matrix_create_scale( $data[0] / $width, $data[1] / $height ) ); cairo_pattern_set_matrix( $pattern, $matrix ); // Merge surfaces cairo_set_source( $this->context, $pattern ); cairo_rectangle( $this->context, $position->x, $position->y, $width, $height ); cairo_fill( $this->context ); } /** * Return mime type for current image format * * @return string */ public function getMimeType() { return 'image/png'; } /** * Render image directly to output * * The method renders the image directly to the standard output. You * normally do not want to use this function, because it makes it harder * to proper cache the generated graphs. * * @return void */ public function renderToOutput() { $this->drawAllTexts(); header( 'Content-Type: ' . $this->getMimeType() ); // Write to tmp file, echo and remove tmp file again. $fileName = tempnam( '/tmp', 'ezc' ); // cairo_surface_write_to_png( $this->surface, $file ); cairo_surface_write_to_png( $this->surface, $fileName ); $contents = file_get_contents( $fileName ); unlink( $fileName ); // Directly echo contents echo $contents; } /** * Finally save image * * @param string $file Destination filename * @return void */ public function render( $file ) { $this->drawAllTexts(); cairo_surface_write_to_png( $this->surface, $file ); } /** * Get resource of rendered result * * Return the resource of the rendered result. You should not use this * method before you called either renderToOutput() or render(), as the * image may not be completely rendered until then. * * This method returns an array, containing the surface and the context in * a structure like: * * array( * 'surface' => resource, * 'context' => resource, * ) * * * @return array */ public function getResource() { return array( 'surface' => $this->surface, 'context' => $this->context, ); } } ?>