MokaByte Numero 23  -  Ottobre 98

 
 
 
 
 
 
 
 

di 
Stefano Fornari

Visualizzare immagini 
BMP in Java

.
 
 


Con poco lavoro è possibile estendere le capacità di un browser: vediamo come


Java supporta in modo nativo i formati grafici utilizzati su Web: GIF e JPEG. Estendere l'AWT con nuovi formati è tuttavia un'operazione piuttosto semplice che applicheremo alle immagini bitmap di OS/2 e Windows



 
Per un programmatore abituato a districarsi tra varie librerie di fornitori diversi, regolarmente incompatibili tra loro, è sicuramente sorprendente scoprire il metodo semplice e immediato messo a disposizione da Java per caricare e visualizzare immagini in formato GIF e JPEG. Stupisce, tuttavia, anche l'assenza del formato grafico più diffuso (almeno prima dell'avvento di Internet) nel regno dei PC: le immagini bitmap. Il presente articolo si propone di colmare questo vuoto estendendo l'AWT (Abstract Windowing Toolkit, vale a dire il set di classi Java dedicato all'interfaccia utente) in modo da supportare immagini di formati diversi e creando un'applet in grado di visualizzare bitmap. Verrà anche brevemente mostrato un semplice visualizzatore di immagini che fa uso delle classi descritte di seguito.

Immagini e applet

Java fornisce un supporto nativo per immagini GIF e JPEG grazie alle funzionalità dei package java.applet, java.awt e java.awt.Image. Ogni immagine è rappresentata da un'istanza della classe java.awt.Image e può essere caricata con i metodi getImage() di java.awt.Toolkit e java.applet.Applet.  Il caricamento può avvenire a partire dal nome del file o indicando un URL. Naturalmente, nel caso di applicazioni Java, è possibile utilizzare entrambi i metodi, mentre nel caso delle applet bisognerà adeguarsi ai vincoli a cui tali classi sono sottoposte. Per motivi di prestazioni, e grazie alla natura multithreaded di Java, getImage() termina immediatamente, anche se l'immagine non è stata caricata completamente. 

miaImg = getImage(URL);
L'istruzione di sopra è utilizzabile solo in un'applet, mentre in un'applicazione Java si usa:
miaImg = Toolkit.getDefaultToolkit().
 
 

getImage(fileOrURL);

È necessario, quindi, verificare lo stato di caricamento delle immagini. Esistono due modi per farlo: utilizzare i servizi della classe java.awt.MediaTracker o reimplementare il metodo imageUpdate() dell'interfaccia java.awt.image.ImageObserver. L'AWT segue la seconda strada implementando in java.awt.Component l'interfaccia java.awt.image.ImageObserver e, quindi, fornendo la possibilità agli elementi della GUI di visualizzare l'immagine legata a un oggetto Image man mano che i dati si rendono disponibili. er disegnare l'immagine vera e propria si utilizza (generalmente nei metodi paint() o update()) il metodo drawImage() di java.awt.Graphics:
g.drawImage(miaImage, 0, 0, this);
L'ultimo parametro specifica proprio l'ImageObserver destinatario dei dati dell'immagine. Non sempre è necessario occuparsi dei dettagli del caricamento dell'immagine, ma spesso è sufficiente disegnare l'immagine una volta che questa è stata letta completamente. In questi casi si usa il MediaTracker. Basta creare un'istanza della classe java.awt.MediaTracker e dirgli di tenere traccia di una o più immagini di cui si vuole, all'occorrenza, controllare lo stato. 

Generalmente in un'applet il MediaTracker viene creato e inizializzato nel metodo init():

public void init() {
  tracker = new MediaTracker(this);
  img = getImage(getDocumentBase(), "img.gif");
  tracker.addImage(img, 0);
}
Quindi, durante l'esecuzione dell'applet, si chiede al MediaTracker di attendere fino al completo caricamento dell'immagine, per poi ordinare il ridisegno dell'area dell'applet:
public void run() {
 try {
  tracker.waitForAll();
 } catch (InterruptedException ie) {
 return;
}
repaint();
}
L'immagine viene visualizzata nel metodo paint:
public void paint(Graphics g) {
 if ((tracker.statusAll(false) & MediaTracker.ERRORED) != 0) {
   g.setColor(Color.red);
   g.fillRect(0, 0, size().width,size().height);
   return;
 }
g.drawImage(img, 0, 0, this);
}
Tutto ciò è valido per i tipi di immagine trattati direttamente dalla AWT, cioè i formati GIF e JPEG. Nei prossimi paragrafi si vedrà dettagliatamente come l'AWT gestisce le immagini e quindi come estenderla per supportare il formato BMP. 

Nuovi formati grafici in Java

La gestione delle immagini grafiche nell'AWT è basata su un modello produttore-consumatore. Si definisce Image Producer un oggetto in grado di interpretare i dati di un'immagine in un formato specifico e di trasformarli in una rappresentazione interna univoca. Questa vede l'immagine come una matrice bidimensionale di NxM pixel alla quale corrisponde un array di NxM valori che rappresentano il colore dei singoli pixel. Il modo con cui viene codificato un valore di colore è definito dal Color Model e, generalmente, corrisponde con la codifica RGB, nella quale a ogni pixel vengono assegnati tre byte contenenti i valori delle componenti di rosso, verde e blu della tinta da rappresentare. I dati così trasformati possono essere forniti a un Image Consumer che può elaborarli o, semplicemente, visualizzarli. In questo modello il consumer deve essere in grado di elaborare i dati man mano che il producer glieli fornisce. Il ciclo di produzione è basato su chiamate, da parte dell'Image Producer, di metodi callback implementati nell'Image Consumer.

Ecco come tecnicamente l'AWT realizza tutto ciò. AWT formalizza il "comportamento" degli Image Producer e Image Consumer attraverso le interfacce java.awt.Image.ImageProducer e java.awt.Image.ImageConsumer. Gli oggetti che vogliono ricevere i dati di un'immagine devono implementare l'interfaccia ImageConsumer e ridefinire il comportamento dei metodi callback setColorModel(), setDimensions(), setHints(), setPixel() che servono per ricevere rispettivamente il modello di colori, le dimensioni, le modalità di produzione e i dati relativi ai pixel dell'immagine. Il metodo imageComplete() del consumer viene infine invocato dal producer per segnalare il completamento dell'operazione. Probabilmente ci si chiederà come il producer venga a conoscenza degli oggetti consumer che esso deve alimentare. La risposta è nell'implementazione dell'interfaccia ImageProducer. A tal fine, infatti, questa impone di ridefinire i metodi addConsumer(), isConsumer(), removeConsumer(). Il metodo startProduction(), invece, comanda all'ImageProducer di iniziare la generazione dei dati. L'interfaccia java.awt.Image.ImageProducer definisce un ultimo metodo, requestTopDownLeftRightResend(), che permette a un ImageConsumer di richiedere che il produttore fornisca nuovamente parte dei dati dell'immagine (seguendo l'ordine alto-basso, sinistra-destra). Se il produttore non supporta questa funzionalità può fornire un'implementazione vuota del metodo. Per estendere Java con un nuovo formato grafico è sufficiente predisporre una classe che implementi l'interfaccia ImageProducer e sfruttare il metodo:

public Image createImage(ImageProducer producer)
di java.awt.Component. Come esempio si realizzerà un producer di immagini bitmap scaricate dalla rete o lette da un file. Naturalmente per farlo bisogna conoscere il formato dei file BMP, argomento del prossimo paragrafo.

Il formato BMP

Il formato BMP è quello privilegiato dai sistemi operativi OS/2 e Windows. Nell'ambito dei due sistemi vi sono alcune differenze che, come si vedrà in seguito, sono facilmente aggirabili. Un file bitmap può contenere immagini a 1, 4, 8, 24 bit per pixel, in formato compresso o non compresso. Nel primo caso possono essere utilizzati più schemi di compressione, basati principalmente su algoritmi run-length encoding (RLE). Le classi descritte in questo articolo leggono solo file bitmap non compressi.
 
Fig 1: struttura di un file BMP

La figura 1  mostra la struttura di un file BMP. Si nota un header della dimensione di 14 byte (detto File Info Header), seguito da una seconda intestazione contenente le informazioni sull'immagine (detta Bitmap Info Header). La dimensione di questo secondo blocco di dati è specificata nel primo long (4 byte) della struttura e corrisponde a 64 byte nel caso dei bitmap di OS/2 e a 40 byte per il formato DIB (Device-Independent Bitmap) di Windows. Dal punto di vista del codice presentato nell'articolo questa è l'unica differenza significativa tra i due tipi di file. Alle strutture di descrizione seguono i blocchi di dati che rappresentano l'immagine. Nel caso di immagini con palette (per esempio a 4 e 8 bit per pixel) il primo blocco che segue gli header è la palette dei colori, vale a dire un array di valori RGB. I pixel dell'immagine faranno riferimento agli indici di questa tabella per indicare il loro colore. Se l'immagine è a 24-bit la palette dei colori non esiste e i dati che seguono indicano direttamente il colore dei singoli pixel attraverso la usuale notazione RGB. I dati dell'immagine sono memorizzati riga per riga, a partire dall'ultima fino alla prima (cioè i primi dati che si incontrano sono quelli relativi all'ultima riga della figura). I dati di ogni linea sono allineati a multipli di 4 byte. I primi due byte di un file BMP ne indicano il formato. Possono assumere i seguenti valori:

  • 0x4142 ('BA') per le bitmap multiple;
  • 0x4D42 ('BM') per le bitmap singole;
  • 0x4349 ('IC') per le icone 1 bit per pixel;
  • 0x4540 ('PT') per i puntatori del mouse 1 bit per pixel;
  • 0x4943 ('CI') per le icone a colori;
  • 0xc043 ('CP') per i puntatori a colori.
In ambiente Windows i DIB corrispondono alla variante BM, mentre le altre varianti non hanno significato. In ambiente OS/2, invece, i file BA possono contenere più bitmap, generalmente la prima device-independent e le altre specifiche per determinati device. Inoltre, il sistema operativo IBM utilizza lo stesso formato di file anche per le icone e per i puntatori del mouse. 

Visualizzare un'immagine BMP in Java

Il codice di seguito descritto lo trovate integralmente sul dischetto allegato alla rivista o al sito Ftp di Infomedia. è costituito dai seguenti file:

  • BMP.java: applet che visualizza un file BMP scaricato da una connessione web;
  • BMPInfoHeader.java: incapsula le informazioni del Bitmap Info Header;
  • BMPProducer.java: implementazione di ImageProducer per i file BMP;
  • UnknownBitmapException.java: indica un formato sconosciuto;
  • Viewer.java: una semplice applicazione Java in grado di visualizzare immagini GIF, JPEG e BMP.
  • Ciò che garantisce l'estendibilità dell'AWT a nuovi formati grafici è il già citato metodo 
    public Image createImage
    di java.awt.Component, grazie al quale è possibile creare un oggetto Image a partire da una qualsiasi sorgente, basta avere un adeguato ImageProducer. Come esempio, si realizzerà un ImageProducer in grado di decodificare un file BMP. La classe fondamentale è BMPProducer.java che implementa l'interfaccia ImageProducer ed è dotata di un solo costruttore che riceve in input un oggetto java.net.URL dal quale leggere la bitmap:
    public BMPProducer(URL url) {
      super();
      vConsumatori = new Vector();
      urlBitmap = url;
    }
    I metodi di ImageProducer addConsumer(), isConsumer(), removeConsumer() si limitano a gestire il vettore degli ImageConsumer che vogliono ricevere i dati dell'immagine:
    public synchronized void addConsumer(ImageConsumer ic) {
      if (vConsumatori.contains(ic)) {
       return;
      }
      vConsumatori.addElement(ic);
    }

    public synchronized boolean isConsumer(ImageConsumer ic) {
      return vConsumatori.contains(ic);
    }

    public synchronized void removeConsumer(ImageConsumer ic) {
     vConsumatori.removeElement(ic);
    }

    Un ImageConsumer richiede l'inizio della produzione dei dati invocando il metodo startProduction() dell'ImageProducer. Questo riceve in input l'ImageConsumer che esegue la richiesta e gestisce la logica di alimentazione dei dati richiamando i metodi callback visti in precedenza. La prima cosa che startProduction() deve fare è registrare il consumer:
    addConsumer(ic);
    quindi, se necessario, legge l'immagine dalla URL specificata nel costruttore:
    try {
      synchronized (this) {
       if (fLetta == false) {
        leggiBitmap(urlBitmap.openStream());
        fLetta = true;
       }
      }
    }
    catch (Exception e) {
     ic.imageComplete(ImageConsumer.IMAGEERROR);
     removeConsumer(ic);
     return;
    }
    Una volta letta l'immagine si hanno tutte le informazioni necessarie per alimentare i consumer; per prima cosa viene impostato il ColorModel
    ic.setColorModel(ColorModel.getRGBdefault());
    poi si forniscono le dimensioni dell'immagine
    ic.setDimensions(bmpInfo.iWidth,bmpInfo.iHeight);
    e i dati veri e propri
    ic.setHints(ImageConsumer.COMPLETESCANLINES | ImageConsumer.SINGLEPASS | 
                ImageConsumer.SINGLEFRAME);
    ic.setPixels(0, 0, bmpInfo.iWidth-1, bmpInfo.iHeight-1,
         ColorModel.getRGBdefault(), aiDatiImg,0, bmpInfo.iWidth);
    Si noti che ogni chiamata a setPixels() deve essere preceduta da una setHints() in modo da comunicare al consumer il formato dei dati nel buffer. L'interfaccia ImageConsumer definisce varie costanti per specificare le modalità di produzione dei dati; tra queste spicca la possibilità di produrre intere sequenze animate, cosa che per semplicità è stata ignorata. Dopo aver fornito tutti i pixel si comunica il completamento dell'operazione:
    ic.imageComplete(ImageConsumer.STATICIMAGEDONE);
    La lettura dell'immagine BMP vera e propria viene eseguita dal metodo leggiBitmap() in BMPProducer.java. Come si può osservare dal listato 1 questo metodo legge la bitmap da uno stream di dati e può generare due tipi di eccezioni: java.io.IOException nel caso di errori di lettura, mentre l'eccezione UnknownBitmapException è definita appositamente per indicare un'anomalia nel formato del file. 
    void leggiBitmap(InputStream is)

        throws IOException, UnknownBitmapException {
                DataInputStream dis = new DataInputStream(is);
                byte[] ab = new byte[4];
                short sTipo = dis.readShort();
                if (sTipo != 0x424D) {
                        dis.close();
                        throw new UnknownBitmapException();
               }
               dis.read(ab);   // OS/2 : dimensione del bitmap file header
                               // Windows : dimensione del file
               dis.read(ab);   // Hotspot (x e y)
               dis.read(ab);   // Offeset dati bitmap
               dis.read(ab);   // Dimensioni del bitmap info header
               // Leggo il bitmap info header
               byte[] abBIH = new byte[ab2int(ab)];
               abBIH[0] = ab[0]; abBIH[1] = ab[1];
               abBIH[2] = ab[2]; abBIH[4] = ab[3];
               dis.read(abBIH,4,abBIH.length-4);
               bmpInfo = new BMPInfoHeader(abBIH);
               bmpInfo.iWidth = bmpInfo.iWidth;
               bmpInfo.iHeight = bmpInfo.iHeight;

               if (bmpInfo.iBitPixel == 24) {
                       // ...
               } 
               else if (bmpInfo.iBitPixel == 8) {
               // Leggo la palette
                int aiPalette[] = new int[bmpInfo.iColori];
                byte abPalette[] = new byte[bmpInfo.iColori*4];
                dis.readFully(abPalette);
                int iIndex = 0;
                for(n=0; n<bmpInfo.iColori; ++n, iIndex += 4) {
                 aiPalette[n] =  (255&0xff)<<24 | 
                     (((int)abPalette[iIndex+2]&0xff)<<16) | 
                     ((int)abPalette[iIndex+1]&0xff)<<8)    |
                     ((int)abPalette[iIndex]&0xff);
                }
                int iPad = 
                  (bmpInfo.iDimensione / bmpInfo.iHeight) -  bmpInfo.iWidth;
                   aiDatiImg =  new int[bmpInfo.iWidth*bmpInfo.iHeight];
                byte[] abDatiImg = 
                   new byte[(bmpInfo.iWidth+iPad)*bmpInfo.iHeight];
                dis.readFully(abDatiImg);
                iIndex = 0;
                int n = 0;
                for(int j=0; j<bmpInfo.iHeight; ++j) {
                  for (int i=0; i<bmpInfo.iWidth; ++i) {
                    n = bmpInfo.iWidth*(bmpInfo.iHeight-j-1)+i;
                    aiDatiImg[n] = aiPalette[((int)abDatiImg[iIndex]&0xff)];
                    ++iIndex;
                  }       // next i
                  iIndex += iPad;
                }       // next j
                } else if (bmpInfo.iBitPixel == 4) {
                // ...

                }
                else {
                  dis.close();
                  throw new UnknownBitmapException();
                }
                dis.close();
        }       // leggiBitmap

    Listato 1:  il metodo leggiBitmap() 

    La prima verifica da compiere è sui due byte che indicano il formato del file. Quindi segue la lettura del File Info Header e della dimensione del Bitmap Info Header. In base a questo dato viene letto l'intero Bitmap Info Header e viene creata un'istanza di BMPInfoHeader. Quest'ultima classe è definita in BMPInfoHeader.java e incapsula le informazioni contenute nel Bitmap Info Header. Non ha alcun metodo e possiede un solo costruttore il quale decodifica il blocco di dati in ingresso (passati come un array di byte) inizializzando opportunamente i campi della classe (vedi il listato  seguente).

    public class BMPInfoHeader {
      public int   iPiani, // piani di colore
      iBitPixel,           // bit per pixel
      iCompressione,       // indicatore di compressione
      iDimensione,         // dimensione del  blocco di dati dell'immagine
      iPPMX,           // pixel per metro asse X
      iPPMY,           // pixel per metro asse Y
      iColori,            // n§ di colori utilizzati
      iImportanti,      // n§ di colori importanti
      iWidth,            // larghezza immagine
      iHeight,           // altezza immagine
      iDimRGB;        // n§ di byte utilizzati per contenere un  valore RGB

    public BMPInfoHeader(byte[] abBIH) {
     // ... decodifica del Bitmap Info Header  in abBIH ...
     }

    }

    Le informazioni aggiuntive del formato bitmap di OS/2 vengono ignorate. A questo punto l'algoritmo si comporta in modo diverso in funzione del tipo di immagine. In generale, comunque, viene letta la palette dei colori (se l'immagine non è a 24 bit per pixel) e vengono letti i valori dei pixel che compongono l'immagine secondo le regole di memorizzazione descritte nel paragrafo precedente.

    Visualizzare l'immagine in un'applet

    Buona parte del codice necessario per realizzare l'applet desiderata è quello descritto nel paragrafo "Immagini e applet". Infatti l'unica cosa che è necessario compiere è quella di ridefinire il metodo getImage() per utilizzare createImage() con il nuovo ImageProducer:

    public Image getImage(URL urlBase, String name) {
      String stURL = urlBase.toString();
      int p = stURL.lastIndexOf('/');
      if (p >= 0) {
        stURL = stURL.substring(0, p+1)+name;
      }

    Image imgRet = null;
    try {
    imgRet = createImage(new BMPProducer
    (new URL(stURL)));
    } catch (Exception e) {}
    return imgRet;
    }

    Naturalmente si vorrà poter specificare il nome dell'immagine da visualizzare direttamente all'interno della pagina HTML come parametro del tag <APPLET ...></APPLET>. Ciò è possibile utilizzando il tag <PARAM ...> come segue
    <APPLET CODE="BMP.class" WIDTH=100 HEIGHT=100>
    <PARAM NAME="immagine" VALUE="unaimmagine.bmp">
    </APPLET>
    dove si è definito un nuovo parametro "immagine" che sarà fornito dal browser all'applet BMP.class. Per sfruttare questo tipo di passaggio di informazioni tra la pagina HTML e l'applet Java è sufficiente utilizzare il metodo getParameter() di java.applet.Applet. La chiamata a getImage() quindi diventa:
    getImage(getDocumentBase(),

     getParameter("immagine"));

    dove "immagine" è il parametro specificato nell'attributo NAME del tag <PARAM ...>. La figura 2 mostra l'applet BMP.class all'opera in una pagina caricata con Netscape Navigator per OS/2.
     
    Fig 2: l'applet BMP.class eseguita in Netscape Navigator per OS/2

     

    Un semplice browser di immagini

    In figura 3 è possibile vedere in azione il nuovo ImageProducer all'interno di un'applicazione Java. 
     
    Fig 3: un semplice browser di immagini GIF, JPEG e BMP eseguito come applicazione Java sulla Work Place Shell di OS/2 struttura di un file BMP

    Si tratta di un semplice browser di immagini in grado di visualizzare GIF, JPG e BMP. Il funzionamento del programma è molto semplice. La finestra è costituita da due Panel, uno per la scelta del file da visualizzare (a sinistra) e l'altro per il disegno dell'immagine (a destra). Quando un file viene selezionato (facendo doppio click nella lista dei file) viene determinato il tipo di immagine (in base all'estensione del nome del file); se questa è un GIF o un JPG viene utilizzato il metodo getImage() del Toolkit di default, mentre se si carica un file BMP viene impiegato BMPProducer:

    if (stNome.toUpperCase().endsWith(".BMP")) {
      try {
        URL url = new URL("file", "localhost", hostNome);
        img = createImage(new BMPProducer(url));
      } 
      catch (MalformedURLException mue) {
        img = null;
      }
    } else {
    img = Toolkit.getDefaultToolkit().
    getImage(stNome);
    }
    Una volta caricata, l'immagine viene disegnata attraverso il metodo paint().

    Conclusioni

    Nel corso dell'articolo siamo partiti dalle nozioni di base necessarie per visualizzare un'immagine e per decodificare un file BMP, per poi approfondire le classi che stanno alla base della produzione e visualizzazione delle immagini. Questo ha permesso la realizzazione di un'applet e di una applicazione in grado di visualizzare bitmap OS/2 e Windows. È importante notare che l'approccio seguito è riproducibile per qualsiasi tipo di formato grafico; è stato scelto il formato BMP perché è molto diffuso e abbastanza conosciuto, ma nulla vieta di realizzare degli ImageProducer per i formati grafici più disparati. 


     
     
     
     

    MokaByte Web  1998 - www.mokabyte.it 
    MokaByte ricerca nuovi collaboratori. Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it