Witam. W tym artykule przedstawiam korzystanie z unitu Zlib.
Unit Zlib służy do pakowania danych algorytmem Deflate, co pozwala zaoszczędzić miejsce na dysku zajmowane przez dane używane przez nasz program. Pakowanie przebiega dość szybko, szczególnie w nowszych wersjach unitu, a rozpakowywanie-bardzo szybko. Algorytm nie należy do wydajnych, nie można porównywać rozmiaru spakowanych danych do rozmiaru spakowanych tych samych danych np. WinRarem, jednakże obsługa Zliba jest bardzo prosta, nie musimy szukać dodatkowych bibliotek, instalować komponentów, wszystko mamy na miejscu. Jeżeli chcemy korzystać z unitu Zlib, musi dodać go do sekcji uses. Jest on dołączony standardowo do Delphi, jednakże zalecam pobrać najnowszą wersję ze strony:
http://www.base2ti.com/zlib.htm
gdyż unit został nieco rozbudowany a algorytm kompresji przyspieszony. W tym artykule używam wersji 1.2.3, a w razie czego podam co należy zmienić aby przykłady działały także w starszej wersji, to znaczy tej, która jest standardowo dołączona do Delphi (dodam tylko, że pracuję w Delphi 7). Do ściągnięcia są przykłady dla obu wersji, starszej i nowszej, w jednym archiwum.
Obsługa nowszej wersji.
Jeżeli nie masz zamiaru korzystać z nowszej wersji Zlib’a, opuść ten paragraf.
Po ściągnięciu nowej wersji Zlib’a, wypakuj go i nazwij folder ‘Zlib’, następnie przenieś go do folderu z Delphi. Uruchom Delphi’aka, wybierz Menu->Tools->Environment Options, następnie kliknij na zakładkę Library i w miejscu LibraryPath na samym końcu dopisz:
;$(DELPHI)\Zlib\
Możesz od teraz korzystać z nowego unitu, który w wersji 1.2.3 nazywa się ZlibEx, a w wersji dołączonej standardowo do Delphi-Zlib.
Opisy klasy TZCompressionStream
Do pakowania danych używamy strumienia klasy TZCompressionStream (w starszej wersji: TCompressionStream), która jest pochodną od TCustomZStream (poprzednio TCustomZlibStream). TZCompressionStream jest strumieniem tylko i wyłącznie do zapisu. Kompresuje on dane, wpisując je do strumienia, który podamy podczas wywoływania konstruktora strumienia pakującego. Musimy jednak sami zatroszczyć się o wcześniejsze utworzenie strumienia docelowego oraz zwolnienie go, gdy skończymy na nim pracę.
Próba czytania z obiektu TZCompressionStream wywoła wyjątek typu ECompressionError z wiadomością „Invalid Stream Operation”. To samo dotyczy próby użycia funkcji Seek (służy ona do przesuwania się po pliku). Jednakże, użycie funkcji Seek z parametrami Offset = 0 oraz Origin = soFromCurrent zwróci liczbę bajtów już wpisanych do strumienia.
Do dyspozycji mamy jeszcze właściwość CompressionRate (typu Single), która oznacza, jakim procentem oryginalnej danej jest jej spakowana wersja, np. jeżeli plik zajmował przed kompresją 100KB, a po kompresji zajmuje 10KB, to współczynnik kompresji wyniesie 10(%).
Do utworzenia nowego strumienia mamy do dyspozycji dwa konstruktory (w starszej wersji-tylko jeden). W artykule przedstawię tylko ten wspólny dla starszej i nowszej wersji. Jako pierwszy parametr podajemy strumień, do którego wpisywane będą skompresowane dane. Natomiast drugi parametr oznacza rodzaj kompresji (w nowej bibliotece jest to domyślnie zcDefault). W starszej wersji biblioteki, parametry zamienione są ze sobą miejscami:
1 2 3 4 5 6 7 8 9 |
// nowsza wersja: constructor Create(dest: TStream; compressionLevel: TZCompressionLevel = zcDefault); overload; // starsza wersja constructor Create(CompressionLevel: TCompressionLevel; Dest: TStream); |
Do wyboru mamy 4 rodzaje kompresji (w nawiasie podałem nazwę ze starej biblioteki):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Nazwa Działanie zcDefault (clDefault) Dane są kompresowane w standardowy sposób. zcFastest (clFastest) Dane kompresowane są z maksymalną prędkością (przez co rozmiar skompresowanych danych jest nieco większy) zcMax (clMax) Dane są maksymalnie kompresowane, co zwiększa czas kompresji. zcNone (clNone) Dane nie są kompresowane, zostają wpisane do strumienia w takiej postaci, w jakiej je podaliśmy. |
Wywołując konstruktor, musimy podać strumień, do którego wpisywane będą kompresowane dane. Posłużymy się strumieniem klasy TFileStream. Aby skompresować dane, używamy funkcji Write, podając w parametrach źródło danych oraz ich rozmiar. Funkcja ta w rezultacie zwraca ilość bajtów wypisanych danych.
Podczas wpisywania, gdy zapełni się bufor wyjścia strumienia, zawartość bufora zostanie wpisana do zewnętrznego strumienia (podanego podczas tworzenia strumienie kompresującego). Po wpisaniu, uruchomiane jest zdarzenie OnProgress. Możemy w nim np. poinformować użytkownika postęp kompresji danych.
Uff…mnóstwo teorii 😉 To co wyżej napisałem może się wydawać trudne/niezrozumiałe, ale zapewniam, że korzystanie ze Zlib’a jest banalnie proste. Czas na praktykę! Czas na
Kompresowanie danych.
Zacznijmy od zdefiniowania strumienia kompresującego oraz strumienia, do którego wpisywane będą skompresowane dane. Niech będą one obiektami globalnymi:
1 2 3 4 5 6 7 8 9 |
var Form1 : TForm1 ; CompressStr : TZCompressionStream ; // w starszej wersji unitu: TCompressionStream OutStr : TFileStream ; // do tego strumienia będą wpisywane skompresowane dane Mapa : array [1..1000] of array [1..1000] of Integer ; // dla przykładu spakujemy tę tablicę, wypełniając ją ‘abstrakcyjnymi’ danymi |
Mamy już nasze strumienie, teraz dodaj dwa przyciski na formę. Zdarzenie OnClick pierwszego z nich powinno wyglądać następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
procedure TForm1.Button1Click(Sender: TObject); var i , j : Integer ; begin for i := Low(Mapa) to High(Mapa) do for j := Low(Mapa[Low(Mapa)]) to High(Mapa[High(Mapa)]) do Mapa[j] := i * j ; // przykładową wartością dla każdego elementu niech będzie iloczyn i oraz j end; |
Skompresujmy coś wreszcie!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
procedure TForm1.Button2Click(Sender: TObject); begin { tworzymy nowy strumień, podajemy w parametrach: ścieżkę(ExtractFilePath zwraca ścieżkę do podanego pliku, a Application.ExeName zawiera nazwę aplikacji, dzięki temu nowy plik zawsze będzie tworzony w folderze z naszym programem) wraz z nazwą pliku oraz rodzajem trybu otwarcia pliku } OutStr := TFileStream.Create(ExtractFilePath(Application.ExeName) + 'ExCompress.map', fmCreate) ; { tworzymy nowy strumień kompresujący, podajemy w pierwszym parametrze strumień, do którego wpisane zostąne skompresowane dane oraz rodzaj kompresji; jeżeli używasz starszej wersji zliba, to ta linijka powinna wyglądac nastepująco: CompressStr := TCompressionStream.Create(clDefault , OutStr) ; } CompressStr := TZCompressionStream.Create(OutStr , zcDefault) ; // i nareszcie-kompresujemy podaną daną-czyli Mapa CompressStr.Write(Mapa , SizeOf(Mapa)) ; // po kompresji, OutStr zawiera skompresowane dane (wpisane są one do pliku). { może się zdarzyć, że spakowany plik nie zajmie całego bufora, co nie spowoduje uruchomienia OnProgress, dlatego na wszelki wypadek należy wywołać ją jeszcze raz na koniec } ZlibCompressionProgress(nil) ; // zwalniamy pamięć CompressStr.Free ; OutStr.Free ; end; |
Uruchom program, kliknij najpierw na pierwszy, a potem na drugi przycisk. Po chwili kompresja dobiegnie końca.
W tym przypadku, nieskompresowane dane zajmują 3,81MB – dużo…po kompresji z parametrem zcDefault – 2,54MB. Nadmienię tylko, że ten sam plik spakowany WinRar’em zajmuje 105KB.
Spróbuj poeksperymentować, inaczej inicjalizując tablicę Mapa oraz zmieniając sposoby kompresji na zcMax lub zcFastest, a aby się przekonać, ile dana zajmuje w nieskompresowanej postaci, użyj zcNone, lub po prostu – SizeOf. W przypadku zcMax, róznica nie będzie wielka, jeśli w ogóle widoczna, a spowolni widocznie proces kompresji, natomiast użycie parametru zcFastest zwiększy się zauważalnie rozmiar pliku, ale także da się odczuć szybsze przyspieszenie kompresji.
Jak widać, kompresowanie jest łatwe. Dodamy teraz pasek postępu, informujący nas ile % już skompresowano. Poza tym, wypiszemy, o ile % skompresowane dane są mniejsze od danych nieskompresowanych.
Dodaj na formę dwie etykiety (label) oraz pasek postępu (progress bar). W sekcji public formy wpisz deklarację metody, która zostanie przypisana do zdarzenia OnProgress strumienia kompresującego:
1 2 3 4 5 6 7 |
public procedure ZlibCompressionProgress(Sender : TObject) ; { Public declarations } end; |
Dodaj definicję:
1 2 3 4 5 6 7 8 9 10 11 |
procedure TForm1.ZlibCompressionProgress(Sender: TObject); begin ProgressBar.Position := Round((CompressStr.Position / SizeOf(Mapa)) * 100) ; Label1.Caption := 'Postęp: ' + IntToStr(ProgressBar.Position) + '%' ; Application.ProcessMessages ; end; |
Najpierw obliczamy, jakim % rozmiaru mapy jest pozycja (w bajtach) strumienia kompresującego w Mapie. Zaokrąglamy wynik. W drugiej linijce wpisujemy do etykiety procentowe wykonanie zadania. Następnie wywołujemy funkcję ProcessMessages, przez którą aplikacja nie sprawia wrażenia zawieszonej. Dzięki niej napis na etykiecie uaktualniany jest ‘na bieżąco’.
Pozostało już tylko zmodyfikować procedurę OnClick drugiego przycisku:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
procedure TForm1.Button2Click(Sender: TObject); begin OutStr := TFileStream.Create(ExtractFilePath(Application.ExeName) + 'ExCompress.map', fmCreate) ; CompressStr := TZCompressionStream.Create(OutStr , zcDefault) ; CompressStr.OnProgress := ZlibCompressionProgress ; // przypisujemy metodę obsługi zdarzenia OnProgress CompressStr.Write(Mapa , SizeOf(Mapa)) ; { CompressionRate informuje nas, jakim % rozmiaru pliku nieskompresowanego jest rozmiar pliku skompresowanego, więc aby dowiedzieć się, o ile skomresowany plik jest mniejszy, musimy od 100 odjąć wartość CompressionRate; używająć FormatFloat, formatujemy wynik do 5 miejsc po przecinku } Label2.Caption := 'Plik został pomniejszony o: ' + FormatFloat('0.00000' , 100 - CompressStr.CompressionRate) + '%' ; CompressStr.Free ; OutStr.Free ; end; |
Kompresowanie mamy opanowane, poznajmy zatem
Opis klasy TZDecompressionStream.
Jak nietrudno się domyślić, strumienia tej klasy (w starszej wersji klasa ta nosi nazwę: TDecompressionStream) używać będziemy do rozpakowywania danych. Strumień ten służy jedynie do odczytu, a próba wpisu danych spowoduje wywołanie wyjątku. Możliwe jest przesuwanie się w strumieniu do przodu, ale nie do tyłu. W klasie TZDecompressionStream (co logiczne) nie ma właściwości CompressionRate. Poza tym, możesz wywołać funkcję Seek z parametrem Offset = 0 i Origin = soFromBeginning. W efekcie, strumień wyjściowy zostanie zresetowany, przesuwając go na początek. W przeciwnym razie, możesz jako Offset podać dodatni argument typu Longint i Origin = soFromCurrent lub soFromBeginning, co spowoduje przesunięcie się zewnętrznego strumienia do przodu, dekompresując dane, po których się przesunąłeś.
Dalej nie będą opisywał, ponieważ TZDecompressionStream nie różnie się znacznie od TZCompressionStream. Po więcej inforamcji zajrzyj do fragmentu o opisie klasy kompresującej.
Zajmiemy się teraz używaniem nowo poznanej klasy.
Dekompresowanie danych.
Dodajmy nową definicję globalnego obiektu TDecompressionStream:
1 2 3 4 5 6 7 8 9 10 11 |
var Form1 : TForm1 ; CompressStr : TZCompressionStream ; // w starszej wersji unitu: TCompressionStream DecompressStr : TZDecompressionStream ; // w starszej wersji unitu: TDecompressionStream OutStr : TFileStream ; // do tego strumienia będą wpisywane skompresowane/zdekompresowane dane Mapa : array [1..1000] of array [1..1000] of Integer ; // dla przykładu spakujemy tę tablicę, wypełniając ją ‘abstrakcyjnymi’ danymi |
Dodaj na formę nowy przycisk. Jego procedura OnClick powinna wyglądać następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
procedure TForm1.Button3Click(Sender: TObject); begin // jeżeli plik nie istnieje, opuszczamy procedurę if not FileExists(ExtractFilePath(Application.ExeName) + 'ExCompress.map') then begin ShowMessage('Plik nie istnieje.') ; Exit ; end ; // tworzy strumień, do którego zostaną wpisane rozpakowane dane OutStr := TFileStream.Create(ExtractFilePath(Application.ExeName) + 'ExCompress.map', fmOpenRead) ; // tworzymy strumień rozpakowujący; w parametrze konstruktora podajemy zewnętrzny strumień DecompressStr := TZDecompressionStream.Create(OutStr) ; // metoda ZlibDecompressionProgress będzie nas informować o postępie rozpakowywania DecompressStr.OnProgress := ZlibDecompressionProgress ; // rozpakowujemy dane, które zostaną wpisane do Mapa DecompressStr.Read(Mapa , SizeOf(Mapa)) ; ZlibDecompressionProgress(nil) ; DecompressStr.Free ; OutStr.Free ; end; |
Dodaj także metodę ZlibDecompressionProgress. Deklaracja:
1 2 3 4 5 6 7 8 9 |
public procedure ZlibCompressionProgress(Sender : TObject) ; procedure ZlibDecompressionProgress(Sender : TObject) ; { Public declarations } end; |
oraz definicja:
1 2 3 4 5 6 7 8 9 10 11 |
procedure TForm1.ZlibDecompressionProgress(Sender: TObject); begin ProgressBar.Position := Round((DecompressStr.Position / SizeOf(Mapa)) * 100) ; Label1.Caption := 'Postęp: ' + IntToStr(ProgressBar.Position) + '%' ; Application.ProcessMessages ; end; |
Rozpakowywanie trwa dużo szybciej od pakowania. Załączam przykładowe kody źródłowe, jeden, pracujący na starszej wersji oraz drugi, wykorzystujący nowszą wersję Zlib’a.
Oficjalna strona Zlib’a: http://www.zlib.net/
Na tym zakończę artykuł. Dzięki świeżo nabytej wiedzy możesz zacząć wykorzystywać Zlib’a w swoich aplikacjach, co pozwoli na zmniejszenie rozmiaru danych przez nią wykorzystywanych. W razie uwag/spostrzeżeń/problemów/sugestii itd. – napisz co i jak na forum, w dziale poświęconym serwisowi lub wyślij mi PW.