La
Image I/O API
La
Image I/O API è la libreria di riferimento per
la lettura e scrittura di immagini in una molteplicità
di formati. Il supporto originale di Java per
queste funzioni era di fatto inesistente, limitandosi
alla capacità di leggere i formati GIF, PNG e
JPEG mediante le classi MediaTracker , Image ed ImageIcon.
Di fatto, pur non avendo effettivi limiti alle dimensioni
di immagini che possono essere manipolate, queste classi
sono state progettate sostanzialmente per la manipolazione
di icone o piccole bitmap da utilizzare nel contesto
di una interfaccia grafica. L'uso di queste classi è
semplicissimo (in particolare ImageIcon si carica in
un passaggio da una URL) come dev'essere il caricamento
di un'icona da disegnare, per esempio, in un pulsante.
Questo spiega anche perché il supporto dei formati
era originariamente limitato a quelli citati.
Con
la JSR-15, Image I/O Framework Specification,
i cui lavori iniziarono nel lontano 1999 per concludersi
nel 2002, fu definita una specifica più evoluta,
dedicata alla lettura e scrittura di immagini digitali
vere e proprie, con una piattaforma basata su plug-in
per estendere il numero di formati disponibili, e con
tutte le operazioni al corredo necessarie per la gestione
di metadati (con metadati si intende un insieme di proprietà
associate ad un'immagine digitale un esempio
comune di metadati è costituito dalla specifica
EXIF, che permette alle macchine fotografiche digitali
di registrare dentro ogni fotografia le informazioni
relative al diaframma, alla velocità di otturatore,
eccetera).
Il
risultato concreto della JSR-15 è stata la Image
I/O API, introdotta con il JDK 1.4. Parallelamente ad
essa il JDK 1.4 ha introdotto in Java2D tutta una serie
di classi per l'imaging più avanzate delle originali
Image ed ImageIcon, in particolare la BufferedImage.
Mentre Image sostanzialmente permetteva poco più
che il rendering ed il semplice accesso ai pixel, BufferedImage
contiene il supporto per operazioni più avanzate
come il filtraggio, la modifica dei colori, il ridimensionamento
e trasformazioni geometriche. In aggiunta a tutto ciò
esiste un'estensione di Java2D, nota con il nome di
JAI (Java Advanced API for Imaging) che fornisce strumenti
di un ulteriore livello di sofisticazione, particolarmente
mirati alla manipolazioni di immagini tanto grandi da
non essere agevolmente contenute in memoria.
Non è semplicissimo districarsi in tutte queste
classi e nelle loro interazioni, per cui in questi primi
articoli non prenderemo in considerazione JAI, che verrà
studiata nelle prossime puntate.
La
Image I/O API, dal punto di vista del programmatore
che la usa, è molto semplice. Per caricare un'immagine
in memoria basta uno statement:
BufferedImage
image = ImageIO.read(new File(MyPhoto.JPG));
Per
memorizzarla su disco ad esempio in formato PNG:
ImageIO.write(image,
png, new File(MyPhoto.PNG));
Dietro
le quinte accadono cose abbastanza complesse, come ad
esempio la ricerca di un plugin adatto al caricamento
dell'immagine, la cui scelta viene effettuata automaticamente
analizzando il file, utilizzando un registro generale
che può essere esteso a volontà. L'estensione
avviene semplicemente aggiungendo nel classpath i jar
che contengono i plugin che ci interessano.
Image
I/O può essere interrogato per sapere quali sono
i formati riconosciuti. Ad esempio, il seguente codice:
String
readFormats[] = ImageIO.getReaderMIMETypes();
String writeFormats[] = ImageIO.getWriterMIMETypes();
ci
dice quali sono i tipi MIME che siamo in grado di leggere
e scrivere.
Image
I/O può essere usato in modo molto più
complesso. Ad esempio il codice seguente restituisce
le dimensioni di un'immagine JPEG senza caricarla in
memoria:
ImageReader
ir = (ImageReader)(ImageIO.getImageReadersByFormatName("JPEG").next());
ir.setInput(ImageIO.createImageInputStream(new FileInputStream(...)));
int width = ir.getWidth(0);
int height = ir.getHeight(0);
int tWidth = ir.getThumbnailWidth(0, 0);
int tHeight = ir.getThumbnailHeight(0, 0);
Le
ultime due righe leggono le dimensioni di un thumbnail,
cioé una piccola immagine di preview che, ad
esempio, quasi tutte le macchine fotografiche includono
nei file generati per facilitare il lavoro del software
di navigazione. Gli indici usati nei vari metodi ci
fanno capire che la API è in grado di gestire
immagini multiple contenute nello stesso file
questo non capita quasi mai nelle foto digitali, ma
è una cosa frequente in altri contesti, come
ad esempio i fax multipagina codificati in TIFF.
L'idea
alla base di questa serie di articoli è di analizzare
piuttosto in dettaglio Image I/O vedendo come sia possibile
realizzare un nuovo plugin, cioè un estensione
in grado di manipolare formati non supportati dal JDK.
Faremo riferimento a jrawio, un progetto opensource
realizzato da chi sta scrivendo in grado di leggere
i formati Camera Raw generati da vari modelli di macchine
fotografiche digitali (Nikon, Canon, Sony, Pentax, Minolta)
e il formato Adobe Digital Negative. Questo plugin non
implementa operazioni di scrittura e quindi ci concentreremo
solo sulla parte relativa alla lettura.
Il
plugin è stato recentemente ospitato su java.net
come progetto-figlio di imageio, che è il contenitore
ufficiale di Sun per tutte le API relative all'I/O di
immagini digitali. Alla url http://jrawio.dev.java.net
potete trovare documentazione e scaricare i sorgenti.
Se
a questo punto non sapete cosa sono i formati Camera
Raw (che chiameremo Raw per brevità)...
ve lo spieghiamo noi.
I
formati Camera Raw
Praticamente
tutte le fotocamere digitali esistenti al mondo sono
in grado di generare file JPEG pronti per l'uso (stampa
e/o pubblicazione sul web). Eventuali settaggi di preferenze
(intensità dei colori, contrasto, ecc...) vengono
impostati sul corpo macchina e l'elaborazione avviene
direttamente a carico del microprocessore integrato.
Questo è il modo più ovvio di fornire
il risultato finale a chi scatta fotografie senza pensarci
su troppo (come dicono gli inglesi, point and
shoot).
Con
un qualsiasi programma di fotoritocco si può
effettuare un editing ulteriore, come pure una correzione
di eventuali errori di esposizione. Tuttavia il formato
JPEG ci pone pesanti limitazioni: prima di tutto usa
uno schema di compressione lossy, cioè l'immagine
salvata su disco non è identica all'originale,
ma alcuni dettagli vengono persi per ridurre l'ingombro
in bytes, e in secondo luogo prevede solo 8 bit per
pixel per canale cioè è un formato
a 24 bit per pixel. Sembrano tanti, ma i due problemi
messi insieme fanno sì che ogni ritocco ed ogni
salvataggio degradano l'immagine, fino a farla diventare
scadente.
Questo
fenomeno non è tollerabile dai fotografi professionisti,
i quali fanno sicuramente meno errori sul campo dei
dilettanti, ma comunque vogliono un'ampia possibilità
di intervenire in sede di fotoritocco per esprimere
la propria creatività. Questo d'altronde avveniva
già con la foto analogica, in quanto nella camera
oscura il negativo permetteva vari modi di essere sviluppato.
In
linea di principio il problema sarebbe risolvibile utilizzando
un formato con uno schema di compressione senza perdita:
ad esempio il TIFF o il lossless JPG, che oltretutto
permettono di lavorare a 16-bit per pixel (più
che sufficienti dal momento che le fotocamere campionano
generalmente a 12-14 bit per pixel).
NOTA:
quando si parla di campionamento su un sensore digitale
bisognerebbe parlare di photosite e non di pixel, ma
in questo articolo non ci formalizzeremo troppo.
C'è
però un altro problema. Per ragioni di progettazione,
i sensori digitali non producono un'immagine già
pronta per essere visualizzata. Producono invece una
serie di dati crudi o raw (ed ecco spiegata l'origine
del termine Camera Raw) che devono essere
postprocessati, ad esempio per l'eliminazione del rumore,
oppure per calibrare il livello del nero (che non necessariamente
corrisponde allo zero, a causa di fenomeni come correnti
di origine termica, eccetera).
Ma
le complicazioni non sono finite. Il numero di pixel
in un sensore digitale per fotocamere di medio livello
in su varia dai 6 ai 12 milioni (lasciamo perdere macchine
estremamente particolari che arrivano a 50 megapixel
e costano come un'auto di lusso!). Siccome ogni pixel
è composto da tre canali, rosso-verde-blu, questo
vorrebbe dire realizzare sensori con un numero di fotodiodi
che varia da 18 a 36 milioni. Oggi questo non è
possibile a costi ragionevoli. Per questo motivo, un
certo Bayer, ricercatore della Kodak, ha inventato il
cosiddetto filtro di Bayer, o Bayer Array,
che consiste in una scacchiera di materiale translucente
di colori rosso, verde e blu come in figura:
Ne esistono diverse configurazioni (quella in figura
è una GRBG), tutte con due sensori verdi, uno
rosso ed uno blu.
NOTA:
esistono anche Bayer Array non RGB o a quattro colori
diversi, ma sono poco comuni.
L'idea
è che ogni pixel campiona un solo canale e non
tutti e tre i colori. In questo modo si può far
sì che una fotocamera da 10 megapixel contenga
effettivamente 10 milioni di sensori e non 30, permettendo
il taglio dei costi di produzione.
NOTA:
la Foveon ha messo in commercio un sensore in grado
di campionare tutti e tre i canali per ogni pixel, ma
non sta avendo molto successo.
Ovviamente
però si perde dell'informazione: questa deve
essere ricostruita via software con un'interpolazione
(operazione detta demosaicing). Tutte
queste elaborazioni possono essere benissimo compiute
a bordo della fotocamera, perché ormai i processori
imbarcati sono abbastanza potenti. I produttori di fotocamere,
tuttavia, si sono resi conto che è molto meglio
delegare queste operazioni al software di visualizzazione
che installiamo sul PC, perché questo può
evolvere in modo più sofisticato ed è
più semplice da realizzare che non il firmware
di bordo. Per esempio, un algoritmo di demosaicing inventato
di recente può migliorare la qualità di
fotografie scattate con una vecchia fotocamera. Inoltre
in questo modo si apre la possibilità di avere
diversi software in competizione.
NOTA:
questo in realtà pare che non sia un desiderio
dei produttori. Per chi è interessato a questo
problema si veda la OpenRAW initiative (www.openraw.org).
In
definitiva, se abilitate la modalità ad alta
qualità su una fotocamera di medio alto livello
(recentemente anche alcuni modelli compatti hanno iniziato
ad offrire questa possibilità) quello che vi
ritrovate su disco altro non è che la copia non
elaborata dei dati letti dal sensore. Nei metadati vengono
inseriti, oltre le informazioni usuali, anche i settaggi
impostati dal fotografo (ad esempio incremento della
saturazione, vari livelli di contrasto, eccetera). Questi
verranno onorati dal programma di visualizzazione ogni
volta che aprite la foto. Il vantaggio è che
potete completamente cambiare idea sui settaggi al momento
dello scatto e rielaborare la foto senza aggiungere
rumore di quantizzazione (praticamente inevitabile quando
si elaborano dati digitali), perché ogni volta
si riparte dall'originale. Qualcuno dice che i formati
Raw sono di fatto una specie di negativo digitale,
in quanto può essere elaborato più volte
in modi diversi.
Struttura
dei formati RAW
A
questo punto la situazione diventa caotica, perché
ogni produttore si è inventato un suo formato
proprietario (tra i più comuni NEF di Nikon,
CRW e CR2 di Canon, SRF di Sony, PEF di Pentax, MRW
di Minolta questi sono i formati supportati da
jrawio al momento di scrivere questo articolo). Oltretutto,
salvo rarissime eccezioni, i formati non sono documentati
ufficialmente, anche se si trova qualche spunto e soprattuto
codice funzionante opensource, scritto da smanettoni,
che può fornire come riferimento (è impossibile
non citare a questo punto il pioniere della decodifica
dei formati Raw, Dave Coffin con il suo programma dcraw).
La
maggior parte dei formati sono variazioni più
o meno fantasiose del formato TIFF di Adobe, quindi
fortunatamente è possibile riusare buona parte
del codice. A questo punto è opportuno ricordare
i concetti base che descrivono questo formato, rimandando
i dettagli alla documentazione specializzata. I componenti
sono: un header, alcune directory di dati, tags, uno
o più thumbnail e i dati raster:
- l'header
è una breve sequenza di byte memorizzati all'inizio
del file. Contiene un magic cookie, cioè
una manciata di byte che contengono un particolare
valore, la cui presenza è un indicatore del
formato utilizzato. L'header contiene anche l'informazione
sull'ordinamento dei byte (basso-alto o alto-basso),
la versione del protocollo ed il puntatore alla prossima
sezione di dati.
- Un
tag è un pezzo di informazione elementare identificato
da un codice numerico univoco (codice del tag) che
definisce il significato dell'informazione (ad esempio,
se trovate il codice 256, il valore associato è
la larghezza dell'immagine). Le informazioni elementari
possono essere interi di vari formati, da 8 a 32 bit,
numeri floating point oppure numeri razionali, cioè
coppie di interi a 32 bit che fungono da numeratore
e denominatore.
- una
directory (lo standard TIFF le designa con l'acronimo
IFD) è un insieme di tag relazionati tra loro.
In ogni file in generale ci sono più directory:
una descrive l'immagine principale, altre descrivono
i thumbnail, se presenti, i dati EXIF sono sempre
codificati in una directory a parte. Il cosiddetto
makernote è un'ulteriore blocco
di dati codificato in modo proprietario.
- I
dati raster sono la bitmap dell'immagine. Possono
essere memorizzati in tanti modi diversi: riga per
riga (organizzazione a strip), oppure in tasselli
(organizzazione a tile). Degli opportuni tag forniscono
le informazioni relative.
Le
informazioni contenute nell'header puntano alla prima
directory. Ogni directory poi può contenere un
ulteriore puntatore ad altre directory dello stesso
livello, oppure puntatori a sottodirectory. In generale
possiamo immaginarci un albero di directory un po' come
un (semplice) file system. I puntatori non sono altro
che l'offset all'interno del file della struttura puntata.
I
formati Raw seguono questo schema salvo alcune eccezioni
(completamente proprietarie), introducendo però
delle varianti: qualche byte extra nello header, qualche
byte extra all'inizio degli IFD, e se per tutti gli
IFD viene usata una codifica standard dei tag, il makernote
è diverso da produttore a produttore. In certi
casi i puntatori sono tutti sfasati di un certo offset,
in alcuni casi le informazioni sono offuscate, evidentemente
per confondere le idee a chi prova ad analizzare questi
formati (Peraltro con poco successo, dal momento che
Dave Coffin dimostra di essere capace di decodificare
un nuovo formato entro un paio di settimane). Inoltre
i dati raster possono essere compressi con schemi proprietari
e con dettagli fantasiosi.
NOTA:
praticamente tutti gli articoli che trovate su internet
parlano di dati criptati, ma io mi ostino ad usare il
termine offuscati (in inglese direi mangled).
Se è vero che vengono usati degli algoritmi crittografici
(peraltro molto blandi) con tanto di chiavi, queste
sono contenute all'interno del file, in chiaro. Ma ogni
cifratura che si rispetti si assicura di non trasmettere
in chiaro il valore della chiave
Ora
ne sappiamo abbastanza per passare al design.
L'interfaccia
ImageInputStream
Sappiamo
bene che tutte le operazioni di input effettuate in
Java passano attraverso un DataInputStream, che
fornisce le primitive di lettura di singoli byte (ad
esempio read()) e di tipi primitivi (ad esempio readInt(),
readShort(), readFloat(), eccetera). Image I/O definisce
un particolare tipo di stream, ImageInputStream (ed
ImageInputStreamImpl che ne è un'implementazione
di default), che oltre alle operazioni fondamentali
è in grado di leggere singoli bit (con il metodo
readBits()). Questo si rivela molto utile dal momento
che nel maggior parte dei casi il caricamento dei dati
raster avviene leggendo un numero variabile di bit,
raramente multiplo di 8 o 16.
Viste
le specifiche descritte precedentemente, è necessario
estendere ImageInputStream con un paio di nuove funzionalità:
- la
capacità di usare un offset 'sfalsato';
- la
capacità di deoffuscare alcune sezioni del
file.
Per
questo scopo usiamo il pattern Chain Of Responsibility
(CoR), che è d'altronde ampiamente usato dal
runtime Java proprio con gli stream. In pratica
implementiamo una nuova classe RAWImageInputStream che
estende ImageInputStreamImpl , delega l'implementazione
dei propri metodi ad un altro ImageInputStream ed ne
aggiunge alcuni:
public
void setBaseOffset (long baseOffset);
public long getBaseOffset();
L'implementazione
del metodo seek() viene ridefinita in questo modo:
public
void seek (long pos) throws IOException{
delegate.seek(pos + baseOffset);
}
La
deoffuscatura del file, essendo dipendente dal formato,
verrà implementata analogamente con eventuali
sottoclassi ad hoc (se guardate il codice della versione
attuale di jrawio (1.0.RC3), vedrete che RAWImageInputStream
fornisce anche implementazioni ottimizzate di readBits()
per alcuni casi particolari. L'esempio è didatticamente
utile, in quanto è proprio in questo modo che
bisogna intervenire se si vogliono eseguire ottimizzazioni.
Tuttavia le ottimizzazioni che erano significative un
paio di anni fa oggi sono praticamente impercettibili,
grazie al fatto che Sun ha migliorato le performance
di Image I/O. Pertanto questa parte verrà presto
eliminata.). Altri due metodi implementano due modalità
di lettura che saranno utili in molti casi nella lettura
dei dati raster:
public
void setSkipZeroAfterFF (boolean enabled);
public int readComplementedBits (int bitsToGet);
Curiosamente
alcuni formati inseriscono un inutile byte a zero, che
va ignorato, dopo ogni byte valorizzato a 0xff. La lettura
di bit complementati è parimenti comune in certe
forme di compressione.
Classi
AbstractTag, Directory, TagRegistry, TIFFTag, IFD
Non
c'è molto di significativo su queste classi che
rappresentano i componenti dei metadati. AbstractTag
contiene sostanzialmente vari metodi getXXX() per ogni
tipo primitivo che permettono di accedere ai valori
contenuti. TIFFTag ne è una sottoclasse concreta
che implementa la lettura da stream secondo le specifiche
TIFF. Directory è un insieme di tag con metodi
per aggiungere e cercare un tag dato il suo codice.
Una directory è di fatto il nodo di un albero,
dal momento che, come già detto, la specifica
TIFF permette di includere più directory all'interno
dello stesso file. Essa contiene quindi metodi per accedere
ad altre IFD sullo stesso livello gerarchico o su quello
inferiore:
public
Iterator subDirectories()
public Directory getNextDirectory();
IFD
è una sottoclasse concreta che implementa la
lettura di una directory da stream secondo le specifiche
TIFF. Infine TagRegistry è il descrittore di
una famiglia di tag secondo una certa specifica. Di
fatto questa classe assegna ad ogni codice di tag un
tipo ed una descrizione. Diverse istanze rappresentano
la famiglia di tag TIFF, EXIF, varie estensioni, e in
particolare i makernote di ogni formato. Ne parleremo
più in dettaglio nella prossima puntata, ma vale
la pena anticipare che alcune di queste classi vengono
generate automaticamente a partire da una descrizione
dei codici di tag in formato XML
La classe ImageReaderSpi
ImageReaderSpi
(dove Spi sta per Service Provider Interface) è
un descrittore di un plugin. Al suo interno contiene
varie informazioni, come il nome del plugin, il formato
MIME supportato, i nomi dei formati di metadata supportati
(ne parliamo più avanti); inoltre il metodo:
public
boolean canDecodeInput (Object source);
viene
richiamato dal runtime passando come parametro l'oggetto
da cui ci accingiamo a leggere l'immagine per chiederci
se il nostro plugin è in grado di leggerla.
Una
prima generica sottoclasse RAWImageReaderSpi si preoccupa
di specificare tutte le proprietà comuni ai formati
RAW che intendiamo gestire e implementa canDecodeInput()
in modo che richiami direttamente un nuovo metodo che
usa un RAWImageInputStream:
protected
abstract boolean canDecodeInput (RAWImageInputStream
iis);
Inoltre,
dal momento che la verifica di capacità a decodificare
richiede spesso di leggere varie parti del file, a partire
dall'header, a cavallo dell'invocazione di canDecodeInput()
viene salvata l'attuale posizione corrente di lettura
all'interno dello stream.
La
classe IIOMetadata
IIOMetadata
rappresenta il contenitore dei metadati associati ad
un'immagine. Se per quanto riguarda i formati Raw siamo
fortunati dal momento che la maggioranza di essi, essendo
basati sullo standard TIFF, condividono la stessa struttura,
in generale diversi formati di file possono contenere
dati strutturati in modo molto diverso. Questo è
un problema perché questa classe deve permettere
l'accesso a tutti i metadati (e anche la manipolazione
nel caso si implementi un ImageWriter oltre che un ImageReader).
Per tagliare la testa al toro ed evitare di definire
un'interfaccia troppo complessa, Sun ha pensato che
in generale i metadati di un'immagine possono essere
rappresentati da XML. Così il metodo più
importante di questa classe è:
public
abstract Node getAsTree(String formatName);
e
restituisce un nodo DOM che rappresenta i metadati contenuti
nel file. È possibile fornire diverse rappresentazioni
alternative, che il programmatore seleziona attraverso
il parametro formatName. Ecco un esempio di metadati
in XML:
<?xml
version="1.0" encoding="UTF-8"?>
<it_tidalwave_imageio_tiff_image_1.0>
<TIFFIFD name="IFD0" start="8"
end="229">
<TIFFField number="256" name="ImageWidth">
<TIFFLongs>
<TIFFLong value="3040"/> </TIFFLongs>
</TIFFField>
<TIFFField number="257" name="ImageLength">
<TIFFLongs>
<TIFFLong value="2024"/>
</TIFFLongs>
</TIFFField>
...
La
classe TIFFMetadataSupport altro non fa che implementare
il codice in grado di trasformare una struttura di IFD
nell'opportuno DOM.
La
classe RasterReader
RasterReader
è la classe deputata a caricare in memoria i
dati raster del file. Per prima cosa vanno valorizzate
una serie di proprietà che descrivono il formato
raster:
public
void setWidth (int width)
public void setHeight (int height)
public void setCFAPattern (byte[] cfaPattern)
public void setBitsPerSample (int bitsPerSample)
public void setTileWidth (int tileWidth)
public void setTileHeight (int tileHeight)
public void setTilesAcross (int tilesAcross)
public void setTilesDown (int tilesDown)
public void setTileOffsets (int[] tileOffsets)
public void setRasterOffset (long rasterOffset)
public void setStripByteCount (int stripByteCount)
public void setCompression (int compression)
public void setLinearizationTable (int[] linearizationTable)
Il
CFA pattern altro non è che la descrizione del
Bayer Array (ad esempio la sequenza 0 1 1 2 indica un
filtro RGGB). La LinearizationTable è una tabella
di lookup che va usata in certi casi, indicizzandola
con i valori letti dal raster. Per esempio, il formato
Nikon NEF compressed memorizza nel raster valori da
9.5 bit e usa una tabella per espanderli a 12 bit, con
un campionamento più fitto per i valori che rappresentano
bassa luminosità.
NOTA:
CFA sta per Color Filter Array, perché
per essere pignoli il termine Bayer Array è limitato
ai filtri RGB
I
raster possono essere compressi o non compressi, e questo
lo stabilisce il metodo:
protected
boolean isCompressedRaster();
Per
i due casi esistono due metodi diversi che implementano
il caricamento dei dati:
protected
void loadUncompressedRaster (RAWImageInputStream iis,
WritableRaster raster, RAWImageReaderSupport ir);
protected void loadCompressedRaster (RAWImageInputStream
iis, WritableRaster raster, RAWImageReaderSupport ir)
;
Il
primo metodo contiene un'implementazione universale,
dal momento che un formato non compresso è semplicemente
la sequenza di pacchetti di bit, a partire da quello
che rappresenta il pixel in alto a sinistra, spostandosi
poi verso sinistra e poi riga per riga. Il secondo metodo
è vuoto, e andrà implementato dalle sottoclassi
per gli specifici schemi di compressione di ciascun
vendor.
Dall'esterno
si invoca semplicemente;
public
WritableRaster loadRaster (RAWImageInputStream iis,
RAWImageReaderSupport ir);
che
in base al risultato di isCompressed() attiva la lettura
senza o con compressione.
La
classe ImageReader
A
questo punto dovremmo finalmente implementare una opportuna
sottoclasse di ImageReader, che poi è il prodotto
finale che vogliamo fornire al programmatore. Ma ci
pare di aver messo già abbastanza carne al fuoco
per questa puntata.
Nella
prossima vedremo come una struttura basata sul polimormismo
ci renderà possibile implementare plugin per
sei formati Raw diversi, tutti basati su TIFF, scrivendo
relativamente poco codice.
|