diff --git a/src/main/java/azgracompress/compression/CompressorDecompressorBase.java b/src/main/java/azgracompress/compression/CompressorDecompressorBase.java
index 603c1cd5c4ba9f8c48fc4b08373abc4da49e1266..89b5fd76724196f662c358b72b86a1062357ee9c 100644
--- a/src/main/java/azgracompress/compression/CompressorDecompressorBase.java
+++ b/src/main/java/azgracompress/compression/CompressorDecompressorBase.java
@@ -1,5 +1,7 @@
 package azgracompress.compression;
 
+import azgracompress.compression.listeners.IProgressListener;
+import azgracompress.compression.listeners.IStatusListener;
 import azgracompress.io.InputData;
 import azgracompress.compression.exception.ImageCompressionException;
 import azgracompress.huffman.Huffman;
@@ -15,9 +17,52 @@ public abstract class CompressorDecompressorBase {
     protected final CompressionOptions options;
     private final int codebookSize;
 
+    private IStatusListener statusListener = null;
+    private IProgressListener progressListener = null;
+
     public CompressorDecompressorBase(CompressionOptions options) {
         this.options = options;
         this.codebookSize = (int) Math.pow(2, this.options.getBitsPerCodebookIndex());
+        // Default status listener, which can be override by setStatusListener.
+    }
+
+    public void setStatusListener(IStatusListener listener) {
+        this.statusListener = listener;
+    }
+
+    public void setProgressListener(IProgressListener listener) {
+        this.progressListener = listener;
+    }
+
+    protected IProgressListener getProgressListener() {
+        return progressListener;
+    }
+
+    protected IStatusListener getStatusListener() {
+        return statusListener;
+    }
+
+    protected void reportStatusToListeners(final String status) {
+        if (this.statusListener != null) {
+            this.statusListener.sendMessage(status);
+        }
+    }
+
+    protected void reportStatusToListeners(final String format, final Object... args) {
+        reportStatusToListeners(String.format(format, args));
+    }
+
+    protected void reportProgressListeners(final int index, final int finalIndex, final String message) {
+        if (this.progressListener != null) {
+            this.progressListener.sendProgress(message, index, finalIndex);
+        }
+    }
+
+    protected void reportProgressListeners(final int index,
+                                           final int finalIndex,
+                                           final String message,
+                                           final Object... args) {
+        reportProgressListeners(index, finalIndex, String.format(message, args));
     }
 
     protected int[] createHuffmanSymbols(final int codebookSize) {
@@ -76,28 +121,12 @@ public abstract class CompressorDecompressorBase {
     }
 
 
-    protected void Log(final String message) {
+    private void defaultLog(final String message) {
         if (options.isVerbose()) {
             System.out.println(message);
         }
     }
 
-    protected void Log(final String format, final Object... args) {
-        if (options.isVerbose()) {
-            System.out.println(String.format(format, args));
-        }
-    }
-
-    protected void DebugLog(final String message) {
-        System.out.println(message);
-    }
-
-    protected void LogError(final String message) {
-        if (options.isVerbose()) {
-            System.err.println(message);
-        }
-    }
-
     /**
      * Get index of the middle plane.
      *
diff --git a/src/main/java/azgracompress/compression/IImageCompressor.java b/src/main/java/azgracompress/compression/IImageCompressor.java
index be9d499576b1de60a8e24bfd86f0580a1d82d8bb..c2e6ee2ec27198996c18c7f16eaac15167b5d3a8 100644
--- a/src/main/java/azgracompress/compression/IImageCompressor.java
+++ b/src/main/java/azgracompress/compression/IImageCompressor.java
@@ -4,7 +4,7 @@ import azgracompress.compression.exception.ImageCompressionException;
 
 import java.io.DataOutputStream;
 
-public interface IImageCompressor {
+public interface IImageCompressor extends IListenable {
 
     /**
      * Compress the image planes.
diff --git a/src/main/java/azgracompress/compression/IImageDecompressor.java b/src/main/java/azgracompress/compression/IImageDecompressor.java
index da1f871184339758abd3d6a8fe47aa5e0ed7646c..7e2d35cabca3759ea4efc65fdbe9252b48409e87 100644
--- a/src/main/java/azgracompress/compression/IImageDecompressor.java
+++ b/src/main/java/azgracompress/compression/IImageDecompressor.java
@@ -6,7 +6,7 @@ import azgracompress.fileformat.QCMPFileHeader;
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
 
-public interface IImageDecompressor {
+public interface IImageDecompressor extends IListenable {
     /**
      * Get correct size of data block.
      *
@@ -38,5 +38,4 @@ public interface IImageDecompressor {
     void decompressToBuffer(DataInputStream compressedStream,
                             short[][] buffer,
                             final QCMPFileHeader header) throws ImageDecompressionException;
-
 }
diff --git a/src/main/java/azgracompress/compression/IListenable.java b/src/main/java/azgracompress/compression/IListenable.java
new file mode 100644
index 0000000000000000000000000000000000000000..4b17fe615b3f006b1d37efc3e250b45a450e3d03
--- /dev/null
+++ b/src/main/java/azgracompress/compression/IListenable.java
@@ -0,0 +1,20 @@
+package azgracompress.compression;
+
+import azgracompress.compression.listeners.IProgressListener;
+import azgracompress.compression.listeners.IStatusListener;
+
+public interface IListenable {
+    /**
+     * Set status listener. Status messages are reported to this listener.
+     *
+     * @param listener Status listener.
+     */
+    void setStatusListener(IStatusListener listener);
+
+    /**
+     * Set progress listener. Status messages with progress information are reported to this listener.
+     *
+     * @param listener Progress listener.
+     */
+    void setProgressListener(IProgressListener listener);
+}
diff --git a/src/main/java/azgracompress/compression/ImageCompressor.java b/src/main/java/azgracompress/compression/ImageCompressor.java
index ed65191b96868516a51eaccc370d56cdb42eae2d..4c0dbe2558442ee3caaff72c9788b3f4202160f3 100644
--- a/src/main/java/azgracompress/compression/ImageCompressor.java
+++ b/src/main/java/azgracompress/compression/ImageCompressor.java
@@ -2,6 +2,8 @@ package azgracompress.compression;
 
 import azgracompress.cli.ParsedCliOptions;
 import azgracompress.compression.exception.ImageCompressionException;
+import azgracompress.compression.listeners.IProgressListener;
+import azgracompress.compression.listeners.IStatusListener;
 import azgracompress.fileformat.QCMPFileHeader;
 
 import java.io.*;
@@ -19,19 +21,32 @@ public class ImageCompressor extends CompressorDecompressorBase {
      * @return Correct implementation of image compressor or null if configuration is not valid.
      */
     private IImageCompressor getImageCompressor() {
+        IImageCompressor compressor = null;
         switch (options.getQuantizationType()) {
-            case Scalar: {
-                return new SQImageCompressor(options);
-            }
+            case Scalar:
+                compressor = new SQImageCompressor(options);
+                break;
             case Vector1D:
-            case Vector2D: {
-                return new VQImageCompressor(options);
-            }
+            case Vector2D:
+
+                compressor = new VQImageCompressor(options);
+                break;
             case Vector3D:
             case Invalid:
             default:
                 return null;
         }
+
+        // Forward listeners to image compressor.
+        final IStatusListener parentStatusListener = getStatusListener();
+        if (parentStatusListener != null)
+            compressor.setStatusListener(parentStatusListener);
+
+        final IProgressListener parentProgressListener = getProgressListener();
+        if (parentProgressListener != null)
+            compressor.setProgressListener(parentProgressListener);
+
+        return compressor;
     }
 
     private void reportCompressionRatio(final QCMPFileHeader header, final int written) {
@@ -41,7 +56,7 @@ public class ImageCompressor extends CompressorDecompressorBase {
     }
 
     public boolean trainAndSaveCodebook() {
-        Log("=== Training codebook ===");
+        reportStatusToListeners("=== Training codebook ===");
         IImageCompressor imageCompressor = getImageCompressor();
         if (imageCompressor == null) {
             return false;
diff --git a/src/main/java/azgracompress/compression/ImageDecompressor.java b/src/main/java/azgracompress/compression/ImageDecompressor.java
index 9961b31f79b62d05d3479a1da20806ff2339faaf..7062f8d809cca9d3af573f15ce1d7227a5a05703 100644
--- a/src/main/java/azgracompress/compression/ImageDecompressor.java
+++ b/src/main/java/azgracompress/compression/ImageDecompressor.java
@@ -1,6 +1,8 @@
 package azgracompress.compression;
 
 import azgracompress.compression.exception.ImageDecompressionException;
+import azgracompress.compression.listeners.IProgressListener;
+import azgracompress.compression.listeners.IStatusListener;
 import azgracompress.data.ImageU16Dataset;
 import azgracompress.fileformat.QCMPFileHeader;
 import azgracompress.utilities.Stopwatch;
@@ -38,17 +40,31 @@ public class ImageDecompressor extends CompressorDecompressorBase {
      * @return Correct implementation of image decompressor.
      */
     private IImageDecompressor getImageDecompressor(final QCMPFileHeader header) {
+        IImageDecompressor decompressor = null;
         switch (header.getQuantizationType()) {
             case Scalar:
-                return new SQImageDecompressor(options);
+                decompressor = new SQImageDecompressor(options);
+                break;
             case Vector1D:
             case Vector2D:
-                return new VQImageDecompressor(options);
+                decompressor = new VQImageDecompressor(options);
+                break;
             case Vector3D:
             case Invalid:
             default:
                 return null;
         }
+
+        // Forward listeners to image decompressor.
+        final IStatusListener parentStatusListener = getStatusListener();
+        if (parentStatusListener != null)
+            decompressor.setStatusListener(parentStatusListener);
+
+        final IProgressListener parentProgressListener = getProgressListener();
+        if (parentProgressListener != null)
+            decompressor.setProgressListener(parentProgressListener);
+
+        return decompressor;
     }
 
     /**
@@ -203,7 +219,7 @@ public class ImageDecompressor extends CompressorDecompressorBase {
         final double seconds = decompressionStopwatch.totalElapsedSeconds();
         final double MBSize = ((double) decompressedFileSize / 1000.0) / 1000.0;
         final double MBPerSec = MBSize / seconds;
-        Log("Decompression speed: %.4f MB/s", MBPerSec);
+        reportStatusToListeners("Decompression speed: %.4f MB/s", MBPerSec);
 
         return true;
     }
@@ -213,7 +229,7 @@ public class ImageDecompressor extends CompressorDecompressorBase {
         final long dataSize = fileSize - header.getHeaderSize();
         final long expectedDataSize = imageDecompressor.getExpectedDataSize(header);
         if (dataSize != expectedDataSize) {
-            LogError("Invalid file size.");
+            reportStatusToListeners("Invalid file size.");
             return false;
         }
         return true;
diff --git a/src/main/java/azgracompress/compression/SQImageCompressor.java b/src/main/java/azgracompress/compression/SQImageCompressor.java
index 7a3b067ed5ebb7bcf9df5b1391c4f55faddbb81e..2796178b19129c09da551c1f43bc7c3cd094f476 100644
--- a/src/main/java/azgracompress/compression/SQImageCompressor.java
+++ b/src/main/java/azgracompress/compression/SQImageCompressor.java
@@ -60,7 +60,7 @@ public class SQImageCompressor extends CompressorDecompressorBase implements IIm
             throw new ImageCompressionException("Unable to write codebook to compress stream.", ioEx);
         }
         if (options.isVerbose()) {
-            Log("Wrote quantization values to compressed stream.");
+            reportStatusToListeners("Wrote quantization values to compressed stream.");
         }
     }
 
@@ -106,12 +106,12 @@ public class SQImageCompressor extends CompressorDecompressorBase implements IIm
         Huffman huffman = null;
         final int[] huffmanSymbols = createHuffmanSymbols(getCodebookSize());
         if (options.getCodebookType() == CompressionOptions.CodebookType.Global) {
-            Log("Loading codebook from cache file.");
+            reportStatusToListeners("Loading codebook from cache file.");
 
             quantizer = loadQuantizerFromCache();
             huffman = createHuffmanCoder(huffmanSymbols, quantizer.getCodebook().getSymbolFrequencies());
 
-            Log("Cached quantizer with huffman coder created.");
+            reportStatusToListeners("Cached quantizer with huffman coder created.");
             writeCodebookToOutputStream(quantizer, compressStream);
         } else if (options.getCodebookType() == CompressionOptions.CodebookType.MiddlePlane) {
             stopwatch.restart();
@@ -124,13 +124,13 @@ public class SQImageCompressor extends CompressorDecompressorBase implements IIm
                 throw new ImageCompressionException("Unable to load middle plane data.", ex);
             }
 
-            Log(String.format("Training scalar quantizer from middle plane %d.", middlePlaneIndex));
+            reportStatusToListeners(String.format("Training scalar quantizer from middle plane %d.", middlePlaneIndex));
             quantizer = trainScalarQuantizerFromData(middlePlane.getData());
             huffman = createHuffmanCoder(huffmanSymbols, quantizer.getCodebook().getSymbolFrequencies());
 
             stopwatch.stop();
             writeCodebookToOutputStream(quantizer, compressStream);
-            Log("Middle plane codebook with huffman coder created in: " + stopwatch.getElapsedTimeString());
+            reportStatusToListeners("Middle plane codebook with huffman coder created in: " + stopwatch.getElapsedTimeString());
         }
 
         final int[] planeIndices = getPlaneIndicesForCompression();
@@ -138,7 +138,7 @@ public class SQImageCompressor extends CompressorDecompressorBase implements IIm
         int planeCounter = 0;
         for (final int planeIndex : planeIndices) {
             stopwatch.restart();
-            Log(String.format("Loading plane %d.", planeIndex));
+            reportStatusToListeners(String.format("Loading plane %d.", planeIndex));
 
             ImageU16 plane = null;
 
@@ -150,7 +150,7 @@ public class SQImageCompressor extends CompressorDecompressorBase implements IIm
             }
 
             if (!hasGeneralQuantizer) {
-                Log(String.format("Training scalar quantizer from plane %d.", planeIndex));
+                reportStatusToListeners(String.format("Training scalar quantizer from plane %d.", planeIndex));
                 quantizer = trainScalarQuantizerFromData(plane.getData());
                 writeCodebookToOutputStream(quantizer, compressStream);
 
@@ -161,14 +161,14 @@ public class SQImageCompressor extends CompressorDecompressorBase implements IIm
             assert (quantizer != null) : "Scalar Quantizer wasn't initialized.";
             assert (huffman != null) : "Huffman wasn't initialized.";
 
-            Log("Compressing plane...");
+            reportStatusToListeners("Compressing plane...");
             final int[] indices = quantizer.quantizeIntoIndices(plane.getData(), 1);
 
             planeDataSizes[planeCounter++] = writeHuffmanEncodedIndices(compressStream, huffman, indices);
 
             stopwatch.stop();
-            Log("Plane time: " + stopwatch.getElapsedTimeString());
-            Log(String.format("Finished processing of plane %d", planeIndex));
+            reportStatusToListeners("Plane time: " + stopwatch.getElapsedTimeString());
+            reportStatusToListeners(String.format("Finished processing of plane %d", planeIndex));
         }
         return planeDataSizes;
     }
@@ -185,13 +185,13 @@ public class SQImageCompressor extends CompressorDecompressorBase implements IIm
 
         if (inputDataInfo.isPlaneIndexSet()) {
             try {
-                Log("Loading single plane data.");
+                reportStatusToListeners("Loading single plane data.");
                 trainData = planeLoader.loadPlaneU16(inputDataInfo.getPlaneIndex()).getData();
             } catch (IOException e) {
                 throw new ImageCompressionException("Failed to load plane data.", e);
             }
         } else if (inputDataInfo.isPlaneRangeSet()) {
-            Log("Loading plane range data.");
+            reportStatusToListeners("Loading plane range data.");
             final int[] planes = getPlaneIndicesForCompression();
             try {
                 trainData = planeLoader.loadPlanesU16Data(planes);
@@ -200,7 +200,7 @@ public class SQImageCompressor extends CompressorDecompressorBase implements IIm
                 throw new ImageCompressionException("Failed to load plane range data.", e);
             }
         } else {
-            Log("Loading all planes data.");
+            reportStatusToListeners("Loading all planes data.");
             try {
                 trainData = planeLoader.loadAllPlanesU16Data();
             } catch (IOException e) {
@@ -217,18 +217,18 @@ public class SQImageCompressor extends CompressorDecompressorBase implements IIm
         LloydMaxU16ScalarQuantization lloydMax = new LloydMaxU16ScalarQuantization(trainData,
                 getCodebookSize(),
                 options.getWorkerCount());
-        Log("Starting LloydMax training.");
+        reportStatusToListeners("Starting LloydMax training.");
         lloydMax.train(options.isVerbose());
         final SQCodebook codebook = lloydMax.getCodebook();
-        Log("Finished LloydMax training.");
+        reportStatusToListeners("Finished LloydMax training.");
 
-        Log(String.format("Saving cache file to %s", options.getOutputFilePath()));
+        reportStatusToListeners(String.format("Saving cache file to %s", options.getOutputFilePath()));
         QuantizationCacheManager cacheManager = new QuantizationCacheManager(options.getCodebookCacheFolder());
         try {
             cacheManager.saveCodebook(options.getInputDataInfo().getCacheFileName(), codebook);
         } catch (IOException e) {
             throw new ImageCompressionException("Unable to write cache.", e);
         }
-        Log("Operation completed.");
+        reportStatusToListeners("Operation completed.");
     }
 }
diff --git a/src/main/java/azgracompress/compression/SQImageDecompressor.java b/src/main/java/azgracompress/compression/SQImageDecompressor.java
index 6c6ea59eb979234454ade33305ca810036f608b6..fefc9b0b1bc9c77803e59b2b9a31e06011b993c9 100644
--- a/src/main/java/azgracompress/compression/SQImageDecompressor.java
+++ b/src/main/java/azgracompress/compression/SQImageDecompressor.java
@@ -70,7 +70,7 @@ public class SQImageDecompressor extends CompressorDecompressorBase implements I
         Huffman huffman = null;
         if (!header.isCodebookPerPlane()) {
             // There is only one codebook.
-            Log("Loading single codebook and huffman coder.");
+            reportStatusToListeners("Loading single codebook and huffman coder.");
             codebook = readScalarQuantizationValues(compressedStream, codebookSize);
             huffman = createHuffmanCoder(huffmanSymbols, codebook.getSymbolFrequencies());
         }
@@ -79,13 +79,13 @@ public class SQImageDecompressor extends CompressorDecompressorBase implements I
         for (int planeIndex = 0; planeIndex < planeCountForDecompression; planeIndex++) {
             stopwatch.restart();
             if (header.isCodebookPerPlane()) {
-                Log("Loading plane codebook...");
+                reportStatusToListeners("Loading plane codebook...");
                 codebook = readScalarQuantizationValues(compressedStream, codebookSize);
                 huffman = createHuffmanCoder(huffmanSymbols, codebook.getSymbolFrequencies());
             }
             assert (codebook != null && huffman != null);
 
-            Log(String.format("Decompressing plane %d...", planeIndex));
+            reportStatusToListeners(String.format("Decompressing plane %d...", planeIndex));
             byte[] decompressedPlaneData = null;
             final int planeDataSize = (int) header.getPlaneDataSizes()[planeIndex];
             try (InBitStream inBitStream = new InBitStream(compressedStream,
@@ -120,7 +120,7 @@ public class SQImageDecompressor extends CompressorDecompressorBase implements I
             }
 
             stopwatch.stop();
-            Log(String.format("Decompressed plane %d in %s.", planeIndex, stopwatch.getElapsedTimeString()));
+            reportStatusToListeners(String.format("Decompressed plane %d in %s.", planeIndex, stopwatch.getElapsedTimeString()));
         }
     }
 
@@ -141,6 +141,7 @@ public class SQImageDecompressor extends CompressorDecompressorBase implements I
         }
 
         for (int planeIndex = 0; planeIndex < planeCountForDecompression; planeIndex++) {
+            reportProgressListeners(planeIndex, planeCountForDecompression, "Decompressing plane %d", planeIndex);
             if (header.isCodebookPerPlane()) {
                 codebook = readScalarQuantizationValues(compressedStream, codebookSize);
                 huffman = createHuffmanCoder(huffmanSymbols, codebook.getSymbolFrequencies());
diff --git a/src/main/java/azgracompress/compression/VQImageCompressor.java b/src/main/java/azgracompress/compression/VQImageCompressor.java
index d94984ca218d9262c006f50ea468e90f0acbfe0c..0f5d6b79d2b9de0b413858ded2609d05c2e44127 100644
--- a/src/main/java/azgracompress/compression/VQImageCompressor.java
+++ b/src/main/java/azgracompress/compression/VQImageCompressor.java
@@ -61,7 +61,7 @@ public class VQImageCompressor extends CompressorDecompressorBase implements IIm
             throw new ImageCompressionException("Unable to write codebook to compress stream.", ioEx);
         }
         if (options.isVerbose()) {
-            Log("Wrote quantization vectors to compressed stream.");
+            reportStatusToListeners("Wrote quantization vectors to compressed stream.");
         }
     }
 
@@ -112,10 +112,10 @@ public class VQImageCompressor extends CompressorDecompressorBase implements IIm
         Huffman huffman = null;
 
         if (options.getCodebookType() == CompressionOptions.CodebookType.Global) {
-            Log("Loading codebook from cache file.");
+            reportStatusToListeners("Loading codebook from cache file.");
             quantizer = loadQuantizerFromCache();
             huffman = createHuffmanCoder(huffmanSymbols, quantizer.getFrequencies());
-            Log("Cached quantizer with huffman coder created.");
+            reportStatusToListeners("Cached quantizer with huffman coder created.");
             writeQuantizerToCompressStream(quantizer, compressStream);
         } else if (options.getCodebookType() == CompressionOptions.CodebookType.MiddlePlane) {
             stopwatch.restart();
@@ -129,13 +129,13 @@ public class VQImageCompressor extends CompressorDecompressorBase implements IIm
                 throw new ImageCompressionException("Unable to load reference plane data.", ex);
             }
 
-            Log(String.format("Training vector quantizer from middle plane %d.", middlePlaneIndex));
+            reportStatusToListeners(String.format("Training vector quantizer from middle plane %d.", middlePlaneIndex));
             final int[][] refPlaneVectors = middlePlane.toQuantizationVectors(options.getVectorDimension());
             quantizer = trainVectorQuantizerFromPlaneVectors(refPlaneVectors);
             huffman = createHuffmanCoder(huffmanSymbols, quantizer.getFrequencies());
             writeQuantizerToCompressStream(quantizer, compressStream);
             stopwatch.stop();
-            Log("Middle plane codebook created in: " + stopwatch.getElapsedTimeString());
+            reportStatusToListeners("Middle plane codebook created in: " + stopwatch.getElapsedTimeString());
         }
 
         final int[] planeIndices = getPlaneIndicesForCompression();
@@ -144,7 +144,7 @@ public class VQImageCompressor extends CompressorDecompressorBase implements IIm
 
         for (final int planeIndex : planeIndices) {
             stopwatch.restart();
-            Log(String.format("Loading plane %d.", planeIndex));
+            reportStatusToListeners(String.format("Loading plane %d.", planeIndex));
 
             ImageU16 plane = null;
             try {
@@ -157,23 +157,23 @@ public class VQImageCompressor extends CompressorDecompressorBase implements IIm
             final int[][] planeVectors = plane.toQuantizationVectors(options.getVectorDimension());
 
             if (!hasGeneralQuantizer) {
-                Log(String.format("Training vector quantizer from plane %d.", planeIndex));
+                reportStatusToListeners(String.format("Training vector quantizer from plane %d.", planeIndex));
                 quantizer = trainVectorQuantizerFromPlaneVectors(planeVectors);
                 huffman = createHuffmanCoder(huffmanSymbols, quantizer.getFrequencies());
                 writeQuantizerToCompressStream(quantizer, compressStream);
-                Log("Wrote plane codebook.");
+                reportStatusToListeners("Wrote plane codebook.");
             }
 
             assert (quantizer != null);
 
-            Log("Compressing plane...");
+            reportStatusToListeners("Compressing plane...");
             final int[] indices = quantizer.quantizeIntoIndices(planeVectors, options.getWorkerCount());
 
             planeDataSizes[planeCounter++] = writeHuffmanEncodedIndices(compressStream, huffman, indices);
 
             stopwatch.stop();
-            Log("Plane time: " + stopwatch.getElapsedTimeString());
-            Log(String.format("Finished processing of plane %d.", planeIndex));
+            reportStatusToListeners("Plane time: " + stopwatch.getElapsedTimeString());
+            reportStatusToListeners(String.format("Finished processing of plane %d.", planeIndex));
         }
         return planeDataSizes;
     }
@@ -206,7 +206,7 @@ public class VQImageCompressor extends CompressorDecompressorBase implements IIm
         Stopwatch s = new Stopwatch();
         s.start();
         if (inputDataInfo.isPlaneIndexSet()) {
-            Log("VQ: Loading single plane data.");
+            reportStatusToListeners("VQ: Loading single plane data.");
             try {
 
                 trainData = loadPlaneQuantizationVectors(planeLoader, inputDataInfo.getPlaneIndex());
@@ -214,7 +214,7 @@ public class VQImageCompressor extends CompressorDecompressorBase implements IIm
                 throw new ImageCompressionException("Failed to load plane data.", e);
             }
         } else {
-            Log(inputDataInfo.isPlaneRangeSet() ? "VQ: Loading plane range data." : "VQ: Loading all planes data.");
+            reportStatusToListeners(inputDataInfo.isPlaneRangeSet() ? "VQ: Loading plane range data." : "VQ: Loading all planes data.");
             final int[] planeIndices = getPlaneIndicesForCompression();
 
             final int chunkCountPerPlane = Chunk2D.calculateRequiredChunkCountPerPlane(
@@ -244,7 +244,7 @@ public class VQImageCompressor extends CompressorDecompressorBase implements IIm
             }
         }
         s.stop();
-        Log("Quantization vector load took: " + s.getElapsedTimeString());
+        reportStatusToListeners("Quantization vector load took: " + s.getElapsedTimeString());
         return trainData;
     }
 
@@ -256,19 +256,19 @@ public class VQImageCompressor extends CompressorDecompressorBase implements IIm
                 getCodebookSize(),
                 options.getWorkerCount(),
                 options.getVectorDimension().toV3i());
-        Log("Starting LBG optimization.");
+        reportStatusToListeners("Starting LBG optimization.");
         LBGResult lbgResult = vqInitializer.findOptimalCodebook(options.isVerbose());
-        Log("Learned the optimal codebook.");
+        reportStatusToListeners("Learned the optimal codebook.");
 
 
-        Log("Saving cache file to %s", options.getOutputFilePath());
+        reportStatusToListeners("Saving cache file to %s", options.getOutputFilePath());
         QuantizationCacheManager cacheManager = new QuantizationCacheManager(options.getCodebookCacheFolder());
         try {
             cacheManager.saveCodebook(options.getInputDataInfo().getCacheFileName(), lbgResult.getCodebook());
         } catch (IOException e) {
             throw new ImageCompressionException("Unable to write VQ cache.", e);
         }
-        Log("Operation completed.");
+        reportStatusToListeners("Operation completed.");
     }
 
 
diff --git a/src/main/java/azgracompress/compression/VQImageDecompressor.java b/src/main/java/azgracompress/compression/VQImageDecompressor.java
index 168513c50b2c3e08c7f5ed23344a87c9bdaff2fc..1e1d61e6727b34ceb77ab388fc47276b8c2e44b2 100644
--- a/src/main/java/azgracompress/compression/VQImageDecompressor.java
+++ b/src/main/java/azgracompress/compression/VQImageDecompressor.java
@@ -117,7 +117,7 @@ public class VQImageDecompressor extends CompressorDecompressorBase implements I
         Huffman huffman = null;
         if (!header.isCodebookPerPlane()) {
             // There is only one codebook.
-            Log("Loading codebook from cache...");
+            reportStatusToListeners("Loading codebook from cache...");
             codebook = readCodebook(compressedStream, codebookSize, vectorSize);
             huffman = createHuffmanCoder(huffmanSymbols, codebook.getVectorFrequencies());
         }
@@ -126,13 +126,13 @@ public class VQImageDecompressor extends CompressorDecompressorBase implements I
         for (int planeIndex = 0; planeIndex < planeCountForDecompression; planeIndex++) {
             stopwatch.restart();
             if (header.isCodebookPerPlane()) {
-                Log("Loading plane codebook...");
+                reportStatusToListeners("Loading plane codebook...");
                 codebook = readCodebook(compressedStream, codebookSize, vectorSize);
                 huffman = createHuffmanCoder(huffmanSymbols, codebook.getVectorFrequencies());
             }
             assert (codebook != null && huffman != null);
 
-            Log(String.format("Decompressing plane %d...", planeIndex));
+            reportStatusToListeners(String.format("Decompressing plane %d...", planeIndex));
 
             byte[] decompressedPlaneData = null;
 
@@ -176,7 +176,7 @@ public class VQImageDecompressor extends CompressorDecompressorBase implements I
             }
 
             stopwatch.stop();
-            Log(String.format("Decompressed plane %d in %s.", planeIndex, stopwatch.getElapsedTimeString()));
+            reportStatusToListeners(String.format("Decompressed plane %d in %s.", planeIndex, stopwatch.getElapsedTimeString()));
         }
     }
 
diff --git a/src/main/java/azgracompress/compression/listeners/IProgressListener.java b/src/main/java/azgracompress/compression/listeners/IProgressListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..3c9c68828f96533f777bf0f3dd9332c1c94d0dde
--- /dev/null
+++ b/src/main/java/azgracompress/compression/listeners/IProgressListener.java
@@ -0,0 +1,5 @@
+package azgracompress.compression.listeners;
+
+public interface IProgressListener {
+    void sendProgress(final String message, final int index, final int finalIndex);
+}
diff --git a/src/main/java/azgracompress/compression/listeners/IStatusListener.java b/src/main/java/azgracompress/compression/listeners/IStatusListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..bd549e4c5de18abd2d9a262d806460139dcca410
--- /dev/null
+++ b/src/main/java/azgracompress/compression/listeners/IStatusListener.java
@@ -0,0 +1,5 @@
+package azgracompress.compression.listeners;
+
+public interface IStatusListener {
+    void sendMessage(final String message);
+}