Share |

martedì 3 agosto 2010

PHP: Creare grafici ed immagini on line

In questo articolo tecnico presenterò il prototipo funzionante di un mio piccolo framework PHP utile per la creazione di immagini e grafici da inserire nelle proprie applicazioni, web o client che siano.

Tale framework, allo stato attuale, è in grado di eseguire il rendering di grafici a torta(in inglese PieChart) ed istogrammi verticali ed orizzontali.

Per avere un'idea di ciò che questa classe può generare è possibile dare uno sguardo a questi grafici PHP di esempio.

Noterai per prima cosa che sono presenti sia grafici a torte sia istogrammi, poi che i valori espressi negli istogrammi si auto-ridimensionano automaticamente e che i possibili colori delle etichette, delle barre e delle fette di torta sono praticamente infiniti.

Introduzione alla classe Picasso

La suddetta classe, che è il cuore del framework in questione, è in grado di generare immagini in molti formati tra i quali GIF, PNG e JPG/JPEG. Questo è possibile perchè Picasso utilizza i metodi esposti dalle librerie grafiche GDLib.

Effettivamente, la classe incapsula alcuni metodi delle suddette librerie ed esegue una serie di calcoli per normalizzare le dimensioni delle barre, per distribuire i vari oggetti nello spazio, per calcolare l'ampiezza degli angoli delle fette di torta e le dimensioni dell'etichette.

I metodi delle GDLib che vengono incapsulati ed utilizzati all'interno dei membri della classe Picasso sono:

  • imagestring(image,font,x,y,text,lineColor): aggiunge del testo orizzontale all'immagine;
  • imagestringup(image,font,x,y,text,lineColor): aggiunge del testo verticale all'immagine;
  • imagerectangle(image,x,y,X,Y,lineColor): disegna un rettangolo nell'immagine;
  • imagefilledrectangle(image,x,y,X,Y,lineColor): disegna un rettangolo colorato nell'immagine;
  • imagefilledarc(image,x,y,width,height,start,end,lineColor,IMG_ARC_PIE): disegna una fetta di torta colorata nell'immagine;

Prima di esporre ed analizzare il codice sorgente della classe è necessario spiegare il funzionamento del framework.

Il framework e le classi accessorie

Per funzionare esso ha bisogno, oltre alla classe Picasso, di un'altra classe per rappresentare le barre degli istogrammi, o le fette di pie charts, e di una pagina per gestire i valori da rappresentare nei grafici, istanziare l'oggetto Picasso ed inviare al client le immagini generate.

Grazie a questo framework, per scatenare la creazione delle immagini e dei grafici in una pagina HTML basterà utilizzare un semplice tag IMG come il seguente:

<img src="imageViewer.php?
  width=150&height=150&red=255&green=255&blu=255&
  type=pie&
  items=2&
  label1=IndPres&value1=100&r1=0&g1=225&b1=0&
  label2=IndNonPres&value2=0&r2=255&g2=0&b2=0" />

I dati in querystring

Che significato hanno i parametri utilizzati in querystring per il trasporto dei dati?

  • width ed height indicano le dimensioni, in pixel, dell'immagine da creare;
  • red,green e blu sono le percentuali, in formato RGB, per determinarne il colore di sfondo dell'immagine;
  • type indica che tipo di grafico si vuole ottenere, in questo caso una torta;
  • items indica quanti valori si vogliono rappresentare, in questo caso 2;
  • label1,value1,r1,g1,b1 sono le proprietà del primo dato da rappresentare graficamente, e si incrementano per ogni dato successivo;

Il gestore delle richieste: imageViewer.php

Entriamo nel vivo del codice analizzando cosa fa lo script PHP presente in imageViewer.php. Come al solito spiattello il codice sorgente commentando abbondantemente i passaggi chiave nel codice stesso:

<?php

include_once 'CPicasso.php';
include_once 'CBar.php';

//Valori di default per l'immagine
$w = 500;
$h = 300;
$red = 200;
$green = 100;
$blu = 300;

//Ricezione dei parametri in querystring
if($_GET['width'])
  $w = $_GET['width'];

if($_GET['height'])
  $h = $_GET['height'];

if($_GET['red'])
  $red = $_GET['red'];

if($_GET['green'])
  $green = $_GET['green'];

if($_GET['blu'])
  $blu = $_GET['blu'];

/**
 * Creo una istanza della classe Picasso:
 * il costruttore della classe riceve in input:
 *   1)La larghezza che deve avere l'immagine;
 *   2)L'altezza che deve avere l'immagine;
 *   3)La percentuale di Rosso che deve avere lo sfondo;  
 *   4)La percentuale di Verde che deve avere lo sfondo;  
 *   5)La percentuale di Blue che deve avere lo sfondo;  
 */
$img = new Picasso($w,$h,$red,$green,$blu);

//Ricevo il numero di items da rappresentare graficamente
if($_GET['items']){
  $items = $_GET['items'];

  //Array di Bar che Picasso disegnerà nel grafico
  $barArr = array();
   
  for($i=0;$i<$items; $i++){
    $param = $i+1;
    $lbl = "label$param";
    $value = "value$param";
    $r = "r$param";
    $g = "g$param";
    $b = "b$param";
  
    /**
     * Istanzio un oggetto Bar per ogni items ricevuto in querystring
     * il costruttore della classe riceve in input:
     *   1)L'etichetta che deve indicare l'elemento;
     *   2)Il valore che deve esprimere l'elemento;
     *   3)La percentuale di Rosso che deve avere lo sfondo;  
     *   4)La percentuale di Verde che deve avere lo sfondo;  
     *   5)La percentuale di Blue che deve avere lo sfondo;
     */
    $barArr[] = 
      new Bar($_GET[$lbl],$_GET[$value],$_GET[$r],$_GET[$g],$_GET[$b]);
  }

  /**
   * Gestione della tipologia di grafico da creare:
   * type = "hbars" -> istogramma con barre orizzontali.
   */
  if($_GET['type'] && $_GET['type'] == 'hbars')
    $img->addHorizontalBars($barArr);

  //type = "vbars" -> istogramma con barre verticali
  else if($_GET['type'] && $_GET['type'] == 'vbars')
    $img->addVerticalBars($barArr);

  //type = "pie" -> PieChart
  else if($_GET['type'] && $_GET['type'] == 'pie')
    $img->addPieChart($barArr);
}

/**
 * Setto il content-type per quello che voglio 
 * inviare al browser cliente: in questo caso
 * si tratta di un'immagine PNG.
 */
header('Content-Type: image/png');

//Invio l'immagine al client
imagepng($img->getImage());

//Distruggo l'immagine
unset($img);

?>

Riassumendo: imageViewer.php crea un array di istanze della classe Bar che l'oggetto Picasso successivamente disegnerà nel grafico scelto.

Inoltre, analizzando il codice dello script PHP presente in imageViewer.php, si è potuto vedere con che facilità è possibile scegliere quali tipologie di grafici far disegnare dalla classe Picasso, in particolare:

  • Istogrammi con barre verticali;
  • Istogrammi con barre orizzontali;
  • PieChart, ovvero grafici a Torta;

In realtà sarà dimostrato, successivamente analizzandone il codice, che la classe Picasso espone altri metodi per disegnare rettangoli colorati e con bordo, testo e archi di circonferenze.

Passiamo ora all'analisi della classe Bar, ovvero il componente di model che rappresenta una barra, una fetta di torta oppure un semplice rettangolo:

La classe Bar, ovvero un item del grafico

<?php
/**
 * Rappresenta un oggetto, o item, rappresentabile 
 * all'interno di qualunque tipologia di grafico
 * generato dal componente Picasso.
 */
class Bar{
  //Proprietà Private
  private $label = 'label';
  private $value = 0;
  private $r = 0;
  private $g = 0;
  private $b = 0;

  /**
   * Costruttore:
   * Riceve: etichetta,valore,%rosso,%verde,%blue
   */
  public function __construct($lbl,$value,$r,$g,$b){
    $this->value = $value;
    $this->label = $lbl;
    $this->r = $r;
    $this->g = $g;
    $this->b = $b;
  }

  /**
   * Metodi Setter & Getter Pubblici
   */
  public function getLabel(){
    return $this->label;
  }

  public function getValue(){
    return $this->value;
  }

  public function getRed(){
    return $this->r;
  }

  public function getGreen(){
    return $this->g;
  }

  public function getBlu(){
    return $this->b;
  }
}
?>

Il componente è molto semplice e non ha bisogno di alcun commento supplementare a ciò che è già presente nel codice sorgente, in ambito Java definirei questa classe un semplice bean.

Ciò che è importante è che Bar rappresenta la tipologia utilizzata da Picasso per effettuare il rendering grafico di barre e fette di torta all'interno dei propri grafici.

Ora, manca solo l'esposizione e l'analisi del codice sorgente base del nocciolo che
caratterizza il mio framework: ovvero la classe Picasso.

<?php

class Picasso{
  /** Dimensione del Margine tra ogni Barra */
  const BARS_MARGIN = 4;

  /** Margine interno dell'immagine */
  const IMAGE_MARGIN = 4;

  /** Dimensione del font di default */
  const DEFAULT_FONT_SIZE = 6;

  /**
   * Coefficiente di default per ricavare
   * il colore del testo in base a quello del
   * proprio contenitore 
   */
  const DEFAULT_FONT_COLOR_COEFF = 4;

  /**  Immagine Padre di tutti i grafici */
  private $image = null;

  /** Larghezza dell'immagine Padre */
  private $width = 0; 

  /** Altezza dell'immagine Padre */
  private $height = 0;

  /** Colore di sfondo dell'immagine Padre */
  private $bgColor = '';

  /**
   * Costruttore:
   * Riceve: larghezza,altezza,%rosso,%verde,%blue
   */
  function __construct($w,$h,$red,$green,$blu){
    $this->width = $w;
    $this->height = $h;
    $this->image = imagecreate($w,$h);
    $this->bgColor = $this->setColor($red,$green,$blu);
  }

  /** Aggiunge del testo alle coordinate x,y */
  public function addText($font,$x,$y,$text
        ,$linered,$linegreen,$lineblu){
    $lineColor = $this->setColor($linered,$linegreen,$lineblu);
    imagestring($this->image,$font,$x,$y,$text,$lineColor);
   }

  /** Aggiunge del testo verticale alle coordinate x,y */
  public function addVerticalText($font,$x,$y,$text
        ,$linered,$linegreen,$lineblu){
    $lineColor = $this->setColor($linered,$linegreen,$lineblu);
    imagestringup($this->image,$font,$x,$y,$text,$lineColor);
  }

  /**
   * Crea un rettangolo colorato con l'angolo 
   * alto-sinistro alle coordinate x,y
   * e con l'angolo basso-destro alle coordinate X,Y
   */
  public function addFilledRectangle($x,$y,$X,$Y
        ,$linered,$linegreen,$lineblu){
    $lineColor = $this->setColor($linered,$linegreen,$lineblu);
    imagefilledrectangle($this->image,$x,$y,$X,$Y,$lineColor);
  }

  /**
   * Crea i bordi di un rettangolo con l'angolo 
   * alto-sinistro alle coordinate x,y
   * e con l'angolo basso-destro alle coordinate X,Y
   */
  public function addEmptyRectangle($x,$y,$X,$Y
        ,$linered,$linegreen,$lineblu){
    $lineColor = $this->setColor($linered,$linegreen,$lineblu);
    imagerectangle($this->image,$x,$y,$X,$Y,$lineColor);
  }

  /**
   * Crea una fetta di Torta colorata 
   * con centro alle coordinate x,y
   * con raggio di lunghezza width, 
   * con un angolo rappresentato da start e end
   */  
  public function addFilledPieceOfPie($x,$y,$width,$height
        ,$start,$end,$red,$green,$blu){
    $lineColor = $this->setColor($red,$green,$blu);
    imagefilledarc ($this->image,$x,$y,$width,$height
      ,$start,$end,$lineColor, IMG_ARC_PIE);
  }

  /**
   * Crea una grafico a barre orizzontali:
   * riceve un array di barre con cui 
   * riempire il grafico (class CBar)
   */
  public function addHorizontalBars($barArray){
    /**
     * Coefficiente per la rappresentazione delle barre:
     * Se le barre sono piu' lunghe della dimensione dell'immagine
     * $scala viene incrementato fino a che tutte le barre possano
     * essere disegnate nell'immagine.
     */
    $scala = $this->normalizeBarsLength($barArray,$this->width);

    $nBar = count($barArray);

    //Altezza delle barre
    $h = $this->calculateBarsDimension($this->height,$nBar);

    $tempHeight = Picasso::BARS_MARGIN;
    $newY = $h + $tempHeight;

    foreach($barArray as $bar){
      try{
        $this->addFilledRectangle(Picasso::IMAGE_MARGIN
            ,$tempHeight,($bar->getValue()/$scala),$newY
            ,$bar->getRed(),$bar->getGreen(),$bar->getBlu());

        $fontSize = $this->calculateHorizontalFontSize($h);
        $this->addText($fontSize
            ,(Picasso::IMAGE_MARGIN*2)
            , $this->centerHorizontalText($fontSize,$h,$newY)
            ,$bar->getLabel() . ": " . $bar->getValue()
            ,$bar->getRed()/Picasso::DEFAULT_FONT_COLOR_COEFF
            ,$bar->getGreen()/Picasso::DEFAULT_FONT_COLOR_COEFF
            ,$bar->getBlu()/Picasso::DEFAULT_FONT_COLOR_COEFF);

        $tempHeight = $tempHeight + Picasso::BARS_MARGIN + $h;
        $newY = $tempHeight + $h;
      }
      catch (Exception $e){
        die('<br />Eccezzione: ' 
            . $e->getMessage() . '<br />');
      }
    }
  }
  
  /**
   * Crea una grafico a barre verticali:
   * riceve un array di barre con cui riempire 
   * il grafico (class CBar).
   */
  public function addVerticalBars($barArray){
    /**
     * Coefficiente per la rappresentazione delle barre:
     * Se le barre sono piu' lunghe della dimensione dell'immagine
     * $scala viene incrementato fino a che tutte le barre possano
     * essere disegnate nell'immagine.
     */
    $scala = $this->normalizeBarsLength($barArray,$this->height);

    $nBar = count($barArray);

    //larghezza delle barre
    $h = $this->calculateBarsDimension($this->width,$nBar);

    $tempWidth = Picasso::BARS_MARGIN;
    $newX = $h + $tempWidth;

    foreach($barArray as $bar){
      try{
        $this->addFilledRectangle($tempWidth
            ,($this->height - Picasso::IMAGE_MARGIN 
                - $bar->getValue()/$scala)
            ,$newX
            ,($this->height - Picasso::IMAGE_MARGIN)
            ,$bar->getRed(),$bar->getGreen(),$bar->getBlu());

        $text = $bar->getLabel()."(".$bar->getValue().")";
        $fontSize = $this->calculateVBarFontSize($h,$text);

        if($fontSize < 1){
          /**
           * Dimensione dello Spazio che rimane vuoto 
           * tra l'immagine e la barra
           */
          $emptySpace = $this->height - Picasso::IMAGE_MARGIN 
              - $bar->getValue()/$scala;

          /**
           * Ricalcolo la dimensione del font 
           * in base all'altezza della barra
           */
          $fontSize = $this->calculateVBarFontSize(
              $this->height - $emptySpace,$text);
          $this->addVerticalText($fontSize
              ,$this->centerVerticalVBarText
                  ($fontSize,$h,$tempWidth,$text)
              ,($this->height - Picasso::IMAGE_MARGIN 
                  - imagefontwidth($fontSize))
              ,$text 
              ,$bar->getRed()/Picasso::DEFAULT_FONT_COLOR_COEFF
              ,$bar->getGreen()/Picasso::DEFAULT_FONT_COLOR_COEFF
              ,$bar->getBlu()/Picasso::DEFAULT_FONT_COLOR_COEFF);
        }
        else{
          $this->addText($fontSize
              ,$this->centerVBarText($fontSize,$h,$tempWidth,$text)
              ,($this->height - Picasso::IMAGE_MARGIN 
                  - imagefontheight($fontSize))
              ,$text 
              ,$bar->getRed()/Picasso::DEFAULT_FONT_COLOR_COEFF
              ,$bar->getGreen()/Picasso::DEFAULT_FONT_COLOR_COEFF
              ,$bar->getBlu()/Picasso::DEFAULT_FONT_COLOR_COEFF);
        }
        $tempWidth = $tempWidth + Picasso::BARS_MARGIN + $h;
        $newX = $tempWidth + $h; 
      }
      catch (Exception $e){
        die('<br />Eccezzione: '
            . $e->getMessage() . '<br />');
      }
    }
  }

  function addPieChart($barArray){
    $coeff = $this->getPieCoeff($barArray);
    $raggio = (imagesx($this->image) <= imagesy($this->image))
        ? imagesx($this->image) : imagesy($this->image);
    $raggio = $raggio - Picasso::IMAGE_MARGIN*2;

    $x = imagesx($this->image)/2;
    $y = imagesy($this->image)/2;
    $start = 0;
    $end = 0;

    foreach($barArray as $bar){
      $end += $coeff * $bar->getValue(); 
      $this->addFilledPieceOfPie($x,$y,$raggio,$raggio
          , $start, $end, $bar->getRed()
          , $bar->getGreen(),$bar->getBlu());

      $start = $end;
    }
  }

  /** Restituisce l'immagine Padre. */
  public function getImage(){
    return $this->image;
  }

  /**
   * Calcola lo spessore di ogni barra dividendo 
   * l'altezza o la larghezza dell'immagine
   * per il numero di barre considerando uno spazio 
   * di 4px per distanziarle.
   * 
   * Spessore: imgHeight = 4 + nBar(x + 4);
   *  ==> x = (imgHeight - 4 - 4*nBar)/nBar; 
   */ 
  private function calculateBarsDimension($dimensionSide,$barsNumber){
    return ($dimensionSide - Picasso::BARS_MARGIN - 
        (Picasso::BARS_MARGIN*$barsNumber))/$barsNumber;
  }

  /** 
   * Restituisce un Colore da usare per l'immagine 
   * o un oggetto creato nell'immagine 
   */
  private function setColor($red,$green,$blu){
    return imagecolorallocate($this->image,$red,$green,$blu);
  }

  /** Calcola il punto per centrare il testo nell'immagine */
  private function centerHorizontalText($font,$dimension,$position){
    $fHeight = imagefontheight($font);
    return $position - ($dimension/2 + $fHeight/2);
  }

  /** 
   * Calcola il punto per centrare un testo orizzontale 
   * in una Vertical Bar.
   */
  private function centerVBarText($font,$dimension,$position,$txt){
    $fWidth = imagefontwidth($font);
    return $position + ($dimension/2 - ($fWidth * strlen($txt))/2);
  }

  /**
   * Calcola il punto per centrare un testo verticale 
   * in una Vertical Bar 
   */
  private function centerVerticalVBarText
        ($font,$dimension,$position,$txt){
    $fWidth = imagefontheight($font);
    return $position + 
        ($dimension/2 - ($fWidth * strlen($txt[1]))/2);
  }

  /**
   * Calcola il font da utilizzare in base alla dimensione 
   * del lato da considerare nell'immagine
   */
  private function calculateHorizontalFontSize($dimension){
    $font = Picasso::DEFAULT_FONT_SIZE;

    while(imagefontheight($font) > 
          ($dimension - Picasso::BARS_MARGIN) 
          && $font >0){
      $font--;
    }
    return $font;
  }

  /**
   * Calcola la dimensione del font da utilizzare 
   * nelle barre verticali in base alla dimensione 
   * del lato da considerare nell'immagine e alla 
   * lunghezza del testo.
   */ 
  private function calculateVBarFontSize($dimension,$txt){
    $font = Picasso::DEFAULT_FONT_SIZE;

    while((imagefontwidth($font)*strlen($txt) > 
          $dimension - Picasso::BARS_MARGIN) 
          && $font >0){
      $font--;
    }
    return $font;
  }

  /**
   * Trova il coefficiente con cui ridurre in scala 
   * la lunghezza delle barre nei grafici 
   * a barre orizzontali e verticali.
   */
  private function normalizeBarsLength($arr,$dimension){
    $coeff = 1;

    //Rapresenta il valore piu' grande da rappresentare
    $maxLength = 0;
    $coeffIng = 1;

    foreach($arr as $bar){
      $temp = $bar->getValue();
      $maxLength = ($temp > $maxLength) 
          ? $temp : $maxLength; 

      while( ($temp/$coeff) >= 
          ($dimension - Picasso::IMAGE_MARGIN)){
        $coeff++;
      }
    }

    //Se $coeff == 1 allora provo ad ingrandire la scala
    if($coeff == 1){
      $temp = $coeffIng + 1;
      
      while( ($maxLength*$temp) < 
          ($dimension - Picasso::IMAGE_MARGIN)){
        $coeffIng++;
        $temp = $coeffIng + 1;
      }

      $coeff = 1/$coeffIng;
    }

    return $coeff;
  }

  /**
   * restituisce il coefficiente per trasformare il valore
   * di ogni oggetto Bar in una fetta di torta
   */
  function getPieCoeff($arr){
    $tot = 0;

    foreach($arr as $bar){
      $tot += $bar->getValue();
    }

    return 360/$tot;
  }
}

?>

La classe è un po' complessa, sopratutto nelle fasi di calcolo per determinare le dimensioni e le distanze degli oggetti, per il resto il modo in cui vengono create le immagini e come sono posizionate nell'immagine padre risulta essere molto semplice e trasparente.

Come premesso, questo framework è un prototipo funzionante dal quale poter facilmente creare grafici sempre più complessi e raffinati. Direi però che è una buona base da cui iniziare.

Alla prossima,
MA.

0 commenti:

Posta un commento

Non ti è chiaro qualcosa?
No problem, posta il tuo dubbio ;)

..... e ricordati di firmarlo!