Per motivi di studio, e non perchè vorrò usarla
praticamente (ma mai dire mai), ho provato e riprovato la classe GZipStream del FX2.0 per
comprimere flussi di dati. Ho avuto diverse difficoltà, perchè ritengo che
l'Intellisense un po' impreciso: l'unico vero aiuto l'ho avuto dalla pagina con
un po' di codice sorgente che dimostra veramente come utilizzare i metodi di
GZipStream. Questa classe, in combinata con DeflateStream, permette di
comprimere e decomprimere qualsiasi array di bytes,
indipendentemente dalla loro provenienza: altri files, contenuti di TextBox,
interi DataSet, e così via.
Ho scritto un'utility che mostra la bontà dell'algoritmo di compressione, che
ho trovato molto variabile, nel senso che ha un ratio di compressione del 90% in
alcuni casi, in altri aumenta del 50% la dimensione del file originale. Beh,
insomma, c'è da sapere che esiste e, se volete sapere come funziona, continuate
a leggere questo post.
Introduzione
Dunque, in questo post vedremo come
comprimere uno stream. In fase di creazione di un oggetto GZipStream, occorre specificare almeno due parametri di
fondamentale importanza:
- lo stream di output su cui vogliamo scrivere i bytes compressi
(ad
esempio, un MemoryStream o un FileStream)
- la modalità con cui vogliamo operare, tratta dall'enum CompressionMode
Quindi, per esempio, possiamo dichiarare un oggetto GZipStream in questo
modo:
FileStream file = new FileStream("C:\\Compresso.dat", System.IO.FileMode.Create);
GZipStream zip = new GZipStream(file, System.IO.Compression.CompressionMode.Compress);
Così facendo, qualsiasi cosa venga scritta sullo stream zip
viene prima compressa, e poi scritta effettivamente. La classe GZipStream espone
un metodo Write che accetta in ingresso
un byte[] che rappresenta la source di dati che vogliamo comprimere e
scrivere su file. Ad esempio, possiamo fare quanto segue:
FileStream readingFile = new FileStream(txtFilename.Text, FileMode.Open);
byte[] bufferInput = new byte[readingFile.Length];
readingFile.Read(bufferInput, 0, (int)readingFile.Length);
Con le tre righe di codice qui sopra abbiamo 1) aperto uno
stream per leggere un qualsiasi file, 2) inizializzato un
byte[] adatto a contenere l'intero file e 3) riempito
effettivamente il byte[] con lo stream. A questo punto, possiamo dare in pasto
all'oggetto GZipStream l'array bufferInput.
Tale array verrà compresso e scritto sullo stream di output che abbiamo creato
nel primo riquadro di codice:
using (GZipStream zip = new GZipStream(compressed, CompressionMode.Compress, true))
{
zip.Write(bufferInput, 0, bufferInput.Length);
ratio = 100 - (((double)compressed.Length / (double)readingFile.Length) * 100);
}
Fondamentalmente, ci sono due metodi diversi per scrivere sullo stream
puntato dall'istanza di GZipStream. Il primo metodo consiste
nel chiamare il metodo Write come abbiamo fatto qui
sopra, che però accetta come parametro di input solo un byte[]: a noi in questo
caso va più che bene, ma se avessimo qualcosa di diverso?
Esiste in questo caso un secondo metodo che consiste nello
sfruttare il polimorfismo di .NET: la classe GZipStream eredita da Stream. Se un qualsiasi
oggetto .NET può scrivere qualcosa su uno Stream, allora può anche scrivere su
GZipStream e di conseguenza essere compresso. Mi spiego meglio. Supponiamo di
avere un DataSet popolato con DataTable, DataRow e così via. Possiamo chiamare
il metodo WriteXml che, guarda caso, può essere indirizzato verso uno Stream. Se
questo Stream fosse uno GZipStream, l'XML prodotto verrebbe compresso in modo
trasparente, senza alcuno sforzo da parte nostra:
System.Data.DataSet ds = new System.Data.DataSet("mioDataSet");
// Riempio il dataset con i miei dati
ds.WriteXml(zip);
Oppure, ancora, potremmo serializzare in modo compresso, con poche linee di
codice:
FileStream file = new FileStream("C:\\Compresso.dat", System.IO.FileMode.Create);
Compression.GZipStream zip = new GZipStream(file, CompressionMode.Compress);
XmlSerializer ser = new XmlSerializer(typeof(string));
ser.Serialize(zip, "hello
world!");
file.Close();
zip.Close();
Con l'esempio qui sopra, abbiamo semplicemente 1) aperto uno
stream in scrittura 2) istanziato un oggetto GZipStream che
vada a scrivere su questo stream di output 3) istanziato un
semplice XmlSerializer per le stringhe 4) effettuato una
serializzazione della stringa "hello world!" sullo stream compresso, che
corrisponde in chiaro al seguente XML:
<?xml version="1.0"?>
<string>hello
world!</string>
Molto semplice, vero? Dopo aver eseguito queste linee di
codice, ci ritroviamo su HD un file C:\Compresso.dat che contiene la nostra
serializzazione, ma in formato compresso (e non human-readable).
E la DeflateStream?
FX2.0 ci mette a disposizione anche
la classe DeflateStream. Funziona
esattamente allo stesso modo della GZipStream descritta in questo post, solo che
applica un algoritmo diverso per comprimere e decomprimere. Di conseguenza,
eredita anch'essa dalla classe Stream e possiamo applicare tutti i criteri e le
logiche che abbiamo visto.
E la decompressione?
Lo dico onestamente: ho trovato
macchinoso l'utilizzo di queste classi. Con la decompressione infatti sto ancora
litigando. In breve, è sufficiente dichiarare un oggetto GZipStream, ma con il parametro
CompressionMode.Decompress. Qual'è il vero problema? Lo dico velocemente, perchè
il blog non è il luogo adatto per fare e porre domande: per decomprimere, devo
utilizzare il metodo Read, specificando un array di
bytes su cui dovrò andare a scrivere i dati decompressi. Quindi:
zip.Read(bufferOutput, 0, bufferOutput.Length);
Il vero problema con cui mi sto scontrando è che io non so a priori la
dimensione da dare a bufferOutput, perchè non posso sapere a
priori quanto sarà grande uno stream di bytes che compresso occupa 128KB. Lo stesso esempio tratto da MSDN fa una cosa "strana":
dichiara un array di bytes grande come quello compresso + 100: non capisco,
perchè non è detto - come ho detto prima - che la compressione funzioni a
dovere. Provate a far macinare alla GZipStream un file JPG e vedrete. Beh,
insomma, ho ancora molti dubbi sulla questione.