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.
|