MokaByte Numero 35  - Novembre  99
Un sistema grafico 
Client/Server in Java
di 
Giovanni Puliti
Come realizzare un sistema di manipolazione immagini Client Server per la gestione del formato jpg e gif
In collaborazione con Computer Programming


Dopo molti articoli dedicati alle caratteristiche di Java, alle nuove API introdotte di recente, questo mese vediamo di mettere in pratica quanto visto fino ad oggi, con un  esempio pratico.Vediamo come utilizzare Java per la creazione di una struttura di   gestione immagini C/S: è questo un esercizio utile sia perché fa luce su alcuni aspetti  non sempre molto chiari a tutti, sia perché risponde ai molti interrogativi che sorgono a chi si avvicina a Java per la prima volta.

Introduzione
Questo mese, invece di parlare di qualche recente ed interessante innovazione tecnologica di Java, di una nuova API o tecnica di programmazione, vorrei proporre un esempio che mostri come si possa utilizzare la tecnologia Java per risolvere un problema pratico che mi è capitato di dover affrontare. Anche se il caso specifico non è detto interessi a molti, con le dovute modifiche e trasposizioni potrà essere adattato anche ad altri casi più o meno simili.
Prima di procedere vediamo quindi il problema in esame: mi era stato chiesto di realizzare una applet che permettesse di effettuare alcune semplici di manipolazioni grafiche su immagini caricate dal server via HTTP.
L’applet doveva lavorare nel contesto di un sito web piuttosto complesso. Il tipo di browser che avrebbe utilizzato l’utente finale sarebbe stato Internet Explorer versione 4.xx (anche se doveva essere garantita la compatibilità con Netscape 4.xx).
L’applet inoltre, dopo tutte le modifiche effettuate dall’utente, doveva anche salvare l’immagine finale in un formato che fosse gif o jpg (quale dei due non era inizialmente un requisito importante).
Per quanto riguarda le modifiche, inizialmente dovevano essere disponibili solo due funzioni, il ritaglio (crop) ed il ridimensionamento (scale). Questo il caso, vediamo quindi cosa comporti dover realizzare una applet di questo tipo.
 

 

Figura 1 Il percorso dei dati (in questo caso un’immagine) dal cliente al server, in una tipica struttura client/server implementata in Java

Le problematiche da risolvere
Ecco in sequenza i vari punti che richiedono di essere affrontati con una attenzione particolare.
In nessuno di questi casi si tratta di problemi insormontabili e possono essere risolti in più modi: la difficoltà sta proprio nel capire quale possa essere la soluzione migliore in funzione delle esigenze e dei vincoli posti nel progetto.

    Caricamento immagine: questo passaggio è piuttosto semplice e non riserva particolari sorprese. L’immagine deve essere richiesta direttamente al web server (quindi non aperta direttamente come file sul file system). Si tenga presente che per motivi di sicurezza questa operazione è permessa dal Security Manager del browser solo se si cerca di ottenere un’immagine che risiede sullo stesso host (stesso IP) dove è in esecuzione il web server, cioè dall’host dal quale è stata scaricata l’applet. Ad esempio per poter caricare e poi visualizzare l’immagine possiamo scrivere
MediaTracker tracker= new MediaTracker(this);
Img = getImage(UrlImg,ImageFileNameOrig);
tracker.addImage(Img,0);
tracker.waitForID(0);
L’utilizzo del mediatracker, permette di attendere che il caricamento dell’immagine prima di visualizzarla (praticamente si tratta di una specie di loader in grado di caricare contemporaneamente più immagini, si memorizzarle in un vettore, e di eventualmente bloccare il corso del programma fino a che tutte le immagini non siano state scaricate).
Per chi volesse approfondire tali aspetti, rimando al corso sulla gestione della grafica che si è tenuto su MokaByte ([MBColors]).
Per la visualizzazione dell’immagine all’interno del metodo paint possiamo scrivere
g.drawImage(Img,X,Y,this);
dove il riferimento a this serve per specificare l’observer da utilizzare (in questo caso l’applet stessa). Anche in questo caso maggiori dettagli si possono trovare in [MBColors].
Un aspetto piuttosto importante è legato alla modalità di utilizzo della applet all’interno del sito.
Nel caso in esame l’applet doveva essere utilizzata più volte nell’arco della stessa sessione di lavoro: un cgi si preoccupava di impaginare al volo il codice html contenente l’applet, in modo che ogni volta questa (leggendo dai parametri di configurazione direttamente in html) visualizzasse una immagine differente.
Ovviamente lo stesso risultato lo si sarebbe potuto ottenere facendo leggere tali informazioni dinamiche (il nome del file .gif o .jpg appunto) da un file di configurazione il cui contenuto lo si sarebbe variato in funzione del caso specifico. Ora se questa soluzione può essere forse più elegante, ha l’inconveniente di una maggiore complessità: si pensi solo alle problematiche (non eccessive, ma pur sempre evitabili) derivanti dal dover riferire un file di testo che agli occhi della applet risulta essere remoto. Se invece si ricavano i parametri direttamente dalla pagina html, ci si possono dimenticare tutte queste problematiche.
Ad esempio possiamo ricavare il valore di un parametro grazie al metodo getParam
UrlImg = new URL(getParameter("url_image"));
Si tenga presente pero’ che se tipicamente l’invocazione del getParam si effettua nel metodo init(), in questo caso tale soluzione non sempre darebbe i risultati voluti: infatti la init() viene effettuata solo la prima volta che una applet viene caricata in memoria (almeno in teoria). Non tutti i browser però seguono lo stesso schema per il ciclo di vita dell’applet, o almeno non ne possiamo avere la garanzia (specie con IExplorer, che segue una filosofia tutta sua).
Per questo per essere sicuri che ad ogni caricamento di una pagina html contenente tale applet, si possa visualizzare una immagine differente, ho preferito mettere il caricamento dell’immagine nella Applet.start() e non nella init().
    Manipolazione immagine: prima di passare a considerare quali sono le operazioni necessarie per modificare l’immagine, si tenga presente che, immediatamente dopo l’operazione di caricamento, non abbiamo più a che fare con un file gif, jpg o altro, ma con un oggetto di tipo java.awt.Image.
    Tale classe contiene l’immagine codificata come semplice matrice di pixel, cioè senza nessun particolare sistema di codifica o compressione, come invece avviene all’interno del file. Questa osservazione seppur banale sarà utile in seguito. Fatta questa premessa, per effettuare le due operazioni richieste si può operare in maniera molto semplice. Per lo scale possiamo utilizzare il metodo getScaledInstance(), che di fatto permette di modificare l’immagine in funzione delle nuove dimensioni, restiuendo una nuova immagine partendo dalla originale. Ad esempio
Image newImg= Img.getScaledInstance(w, h, RescalingAlg);
dove RescalingAlg indica l’algoritmo da utilizzare per l’operazione di zoom. Per tale parametro si può utilizzare un algoritmo veloce a bassa qualità (java.awt.Image.SCALE_FAST), un algoritmo che viceversa è un po’ più lento ma opera ad risoluzione maggiore (java.awt.Image.SCALE_SMOOTH), oppure uno che è una via di mezzo (java.awt.Image.SCALE_DEFAULT).
L’operazione di crop invece richiede qualche conoscenza in più: a partire dal JDK 1.1, per quanto riguarda la gestione della grafica sono state introdotte alcune novità, fra cui quella dei filtri. Un filtro è un oggetto che opera come un canale di comunicazione nel quale l’immagine passa subendo determinate manipolazioni. Nel nostro caso l’oggetto java.awt.image. CropImageFilter effettua proprio il tipo di modifica che serve nel nostro caso. Ad esempio

CropImageFilter Crop =new CropImageFilter(X, Y, W, H);
ImageProducer IP = new FilteredImageSource(Img.getSource(), CropFilter);
Image newImg = createImage(IP);

dove il metodo createImage fa parte della classe java.awt.Component (quindi disponibile all’interno di una applet che deriva da Component). Le variabili StartX, StartY, W, H definiscono il rettangolo con cui effettuare il ritaglio dell’immagine.
Come si può notare la nuova immagine viene creata per mezzo di un oggetto detto ImageProducer. Questo componente fa parte della modalità con cui viene gestita la grafica in Java (detta Produce/Consumer): al solito su [MBColors] si trovano tutti gli approfondimenti necessari per comprendere a pieno tali operazioni.
Nei file Imager.java e pnlImage.java è riportato un esempio sintetico ma completo di come utilizzare sia lo zoom che il crop e tutte le altre cose che vedremo in seguito.
A questo punto, dopo una serie di operazioni scale/crop, si può supporre che l’utente abbia ottenuto l’immagine voluta. Se così non fosse possiamo pensare di utilizzare una seconda variabile di tipo Image per effettuare un revert all’immagine originale.

    Codifica, Salvataggio, scrittura su file.
    Questi tre aspetti, strettamente legati fra loro, sono stati risolti in maniera piuttosto elegante, grazie all’ausilio di alcune risorse che ho trovato in rete. Per prima cosa consideriamo il problema della codifica: come detto in precedenza, l’oggetto Image che contiene l’immagine non è altro che una raccolta di pixels messi in sequenza come in una matrice. Se si volesse salvare l’immagine dovremmo quindi implementare in Java un qualche algoritmo che effettui la codifica in gif o jpg, cosa questa possibile ma non facile e non alla portata di tutti. L’esempio riportato nel file Converter.java mostra come sia possibile effettuare trasformazioni da una Image a vettore di pixel e viceversa. Fortunatamente esistono delle classi Java liberamente scaricabili dall’indirizzo http://www.acme.com/java/ (il sito sicuramente noto a lavora in Java da un po’ di tempo), classi che partendo da un oggetto Image ne effettuano la codifica sia in formato gif che jpg, redirigendo il tutto in uno stream di output (quindi eventualmente su file). Purtroppo tali classi si sono mostrate non utilizzabili: al momento della stesura dell’articolo infatti, la classe per la codifica jpg non funzionava ancora, mentre quella per il gif, pur funzionando perfettamente, genera errore (giustamente) se si prova a codificare una Image composta da un numero di colori maggiore di 256 (limte massimo per il formato gif). Non avendo garanzia della provenienza delle immagini, della loro qualità, ed anzi proprio per il fatto che il sistema doveva funzionare sia con immagini con un numero limitato di colori che ad alta risoluzione, non si è potuto utilizzare tale package. Inizialmente ho pensato di risolvere questo problema semplicemente effettuando una conversione dell’Image a 256 colori: questa soluzione, per quanto di fatto introduce una limitazione intrinseca, non da grossi problemi, dato che nella maggior parte dei casi 256 colori sono sufficienti. Girellando in rete però ho trovato alcune classi che lavorano con il formato jpg e che funzionano egregiamente, per cui ho deciso di seguire questa strada. Nella versione finale che ho realizzato ho utilizzato come codifica finale sempre e solamente il jpg (anche se il file di origine era un gif), ma niente vieta di effettuare opportuni controlli (o sul nome file origine, o sul numero di colori) ed adottare la codifica più adatta. Per chi volesse utilizzare le classi per la codifica in jpg, tali classi si possono prelevare liberamente (dopo una registrazione) dal sito www.obrador.com. Per la codifica della immagine in jpg ad esempio si può prima scrivere
JPGEncoder ge= new JPGEncoder (image, quality, os);
ge.encode();
dove os è l’outputstream dove si vuole inviare il file creato, mentre il parametro quality serve per specificare il grado di compressione (inversamente proporzionale alla qualità) con il quale creare l’immagine. Con l’invocazione del metodo JPGEncoder.encode() invece si effettua l’invio vero e proprio
A questo punto si può prendere in considerazione il problema del salvataggio: come ormai dovrebbe essere noto ai più, per precise problematiche legate alla sicurezza, una applet non può effettuare operazioni di scrittura ne in locale ne in remoto.
Una possibile soluzione a questo problema potrebbe essere quella di utilizzare applet firmate o basate sul modello della Security del JDK 1.2.
Ho ritenuto non adottabili nessuna delle due soluzioni: la prima troppo complessa e non user friendly (il sistema doveva essere utilizzato anche da utenti non esperti che potevano non gradire messaggi tipo "attenzione applet fidata, è giusta la firma digitale?"); la seconda non applicabile perché richiedeva l’utilizzo del Java Plug-in, soluzione che si è cercato di evitare fino all’ultimo.
L’ultima possibile soluzione è quella di implementare una piccola struttura client server, tramite la quale effettuare il salvataggio sulla parte server, e non direttamente dalla applet.
Più precisamente si può pensare quindi di realizzare un piccolo demone TCP che resti in ascolto su una determinata porta. L’applet invierà quindi l’immagine codificata in uno stream al server, e questo redige tale flusso di dati su file.

Server
Per quanto riguarda l’implementazione del server Java, si può utilizzare la classi ServerSocket e Socket, che si utilizzano tipicamente in coppia quando si deve realizzare una struttura di questo tipo. La prima serve per implementare il demone vero e proprio: resta in ascolto su una porta xx (si tenga presente che se il s.o. è Unix, un generico programma che debba utilizzare una porta sotto la 1000 deve essere avviato con i diritti di amministratore), e tutte le volte che riceve una richiesta di connessione crea un oggetto di tipo Socket per servire tale client.
Ad esempio nel codice che segue possiamo vedere come implementare tale meccanismo

ServerSocket serverSocket = new ServerSocket(porta);
for (;;) {
 System.out.println("Aspetto una connessione... ");
 Socket socket = serverSocket.accept();
 System.out.println("New socket " + socket);
}
Adesso, disponendo del canale di comunicazione tramite la classe Socket possiamo utilizzare gli stream che essa ci offre per l’invio e la lettura di dati
DataInputStream is = new 
        DataInputStream(socket.getInputStream());
DataOutputStream os = new
        DataOutputStream(socket.getOutputStream());
In particolare lo stream is sarà quello da dove arriveranno i dati relativi all’immagine, ed inviati dall’applet, mentre os può essere utilizzato per comunicare alla applet eventuali stati di errore.
I file ServerThread.java TCPParallelServer costituiscono un un esempio completo di server tcp con il quale realizzare un demone molto semplice e facile da far funzionare.

 

Un server alternativo
In alternativa alla soluzione applicazione lato server qui proposta, si potrebbe pensare di utilizzare un servlet in esecuzione su un web server abilitato.
Questa soluzione sicuramente ha il vantaggio di non richiedere di doversi preoccupare di installare e mandare in esecuzione una applicazione a se stante, dato che tutto rimane inglobato in un motore sempre presente ormai, su ogni host aziendale e non.
Dal punto di vista strettamente formale però, l’utilizzo dei servlet, per quanto non sia da scartare, è forse un po’ inutile in questo caso, dato che la comunicazione applet/servlet dovrebbe avvenire sempre a livello tcp, e quindi utilizzando un protocollo ad un livello inferiore rispetto all’http.
 

 

Conclusione: a che serve tutto ciò?
Bene, ora che abbiamo analizzato le varie parti che compongono il progetto, vorrei aggiungere un breve commento finale.
L’esempio che ho trattato in questo articolo non ha affatto la pretesa di essere di livello avanzato, e sicuramente alcuni lo troveranno piuttosto semplice, e non particolarmente innovativo.
L’obiettivo finale di questo articolo era invece quello di mettere di fronte chi sta studiando Java da un po’, con quella che è la realtà lavorativa con la quale poi si deve fare i conti abbastanza spesso.
Il punto fondamentale è comprendere che non sempre la tecnologia più avanzata possa essere utilizzabile o possa offrire la soluzione migliore.
Ad esempio utilizzando le Java 2D API, probabilmente tutto il problema della gestione dei vari formati grafici lo si sarebbe potuto risolvere in maniera più elegante e semplice, ma questo comporta l’utilizzo di api non supportate dai vari browser in commercio, e non sempre è possibile imporre un plug-in (per giunta un ActiveX) di 9Mb al cliente finale.
Spesso accade inoltre di dover realizzare una qualche struttura in modo anche da seguire quello che il cliente impone (o per motivi politici, o sulla base della tecnologia che utilizza in azienda): ad esempio è noto come il supporto di Netscape allo standard di Java sia più completo e che la gestione della memoria e della grafica sia più attendibile rispetto al concorrente, ma è anche vero che non sempre si possano imporre scelte di questo tipo.
Infine si tenga sempre presente che, almeno nel caso di soluzioni Internet oriented, per quanto possa essere preferibile una certa scelta piuttosto che un’altra, è sempre bene adottare quello che implementa lo standard a pieno, almeno in fase di test.
Ad esempio nell’ambito dei servlet, il JavaWebServer per quanto non sia stabile e performante come ad esempio Apache, è attualmente il Web server che segue a pieno la specifica delle Servlet API, per cui non riserva sorprese.
Infine la raccomandazione che vi avranno fatto sempre nella vostra carriera di studenti o professionisti: cercare di utilizzare sempre la soluzione più semplice e standard. In Java questo precetto vale ancora di più e si aggiunge a quello che dice di non reinventare ogni volta la ruota, sempre che se ne abbia a disposizione una più semplice e funzionalmente equivalente.
 

Bibliografia

    [MBColors] Corso di gestione della grafica in Java MokaByte n° 2, 0ttobre 1996 e successivi (http://www.mokabyte.it/1996/11)
  • [MBNegozio] "Il negozio virtuale" Un sistema Client server fatto in Java
  • [ACME] Il sito dove reperire le classi per la codifica gif e tanto altro ancora http://www.acme.com
  • [Obrador] Il sito dove reperire le classi per la codifica jpg http://www.obrador.com.

  
 

MokaByte rivista web su Java

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