diff --git a/common/source/java/ch/systemsx/cisd/common/compression/file/CompressionWorker.java b/common/source/java/ch/systemsx/cisd/common/compression/file/CompressionWorker.java
new file mode 100644
index 0000000000000000000000000000000000000000..cb83f7c7387067a0cb5ff73785d8f60ba9160902
--- /dev/null
+++ b/common/source/java/ch/systemsx/cisd/common/compression/file/CompressionWorker.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2008 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.common.compression.file;
+
+import java.io.File;
+
+import java.util.Collection;
+import java.util.Queue;
+
+import org.apache.log4j.Logger;
+
+import ch.systemsx.cisd.common.exceptions.Status;
+import ch.systemsx.cisd.common.exceptions.StatusFlag;
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+
+/**
+ * A worker {@link Runnable} for (image) compression.
+ * 
+ * @author Bernd Rinn
+ */
+class CompressionWorker implements Runnable
+{
+
+    // @Private
+    static final int MAX_RETRY_OF_FAILED_COMPRESSIONS = 3;
+
+    // @Private
+    static final String COMPRESSING_MSG_TEMPLATE = "Compressing '%s'.";
+
+    // @Private
+    static final String EXCEPTION_COMPRESSING_MSG_TEMPLATE = "Exceptional condition when trying to compress '%s'.";
+    
+    // @Private
+    static final String INTERRPTED_MSG = "Thread has been interrupted - exiting worker.";
+
+    // @Private
+    static final String EXITING_MSG = "No more files to compress - exiting worker.";
+
+    private final static Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION, CompressionWorker.class);
+
+    private final Queue<File> workerQueue;
+    
+    private final Collection<FailureRecord> failures;
+
+    private final ICompressionMethod compressor;
+
+    CompressionWorker(Queue<File> incommingQueue, Collection<FailureRecord> failures, ICompressionMethod compressor)
+    {
+        assert incommingQueue != null;
+        assert failures != null;
+        assert compressor != null;
+
+        this.workerQueue = incommingQueue;
+        this.failures = failures;
+        this.compressor = compressor;
+    }
+
+    public void run()
+    {
+        do
+        {
+            if (Thread.currentThread().isInterrupted())
+            {
+                if (operationLog.isInfoEnabled())
+                {
+                    operationLog.info(INTERRPTED_MSG);
+                }
+                return;
+            }
+            final File fileToCompressOrNull = workerQueue.poll();
+            if (fileToCompressOrNull == null)
+            {
+                operationLog.info(EXITING_MSG);
+                return;
+            }
+            if (operationLog.isDebugEnabled())
+            {
+                operationLog.debug(String.format(COMPRESSING_MSG_TEMPLATE, fileToCompressOrNull));
+            }
+            Status status = null;
+            int count = 0;
+            do {
+                try
+                {
+                    status = compressor.compress(fileToCompressOrNull);
+                } catch (Throwable th)
+                {
+                    operationLog.error(String.format(EXCEPTION_COMPRESSING_MSG_TEMPLATE, fileToCompressOrNull), th);
+                    failures.add(new FailureRecord(fileToCompressOrNull, th));
+                    status = null;
+                    break;
+                }
+            } while (StatusFlag.RETRIABLE_ERROR.equals(status.getFlag()) && ++count < MAX_RETRY_OF_FAILED_COMPRESSIONS);
+            if (status != null && Status.OK.equals(status) == false)
+            {
+                failures.add(new FailureRecord(fileToCompressOrNull, status));
+            }
+        } while (true);
+    }
+
+}
diff --git a/common/source/java/ch/systemsx/cisd/common/compression/file/Compressor.java b/common/source/java/ch/systemsx/cisd/common/compression/file/Compressor.java
new file mode 100644
index 0000000000000000000000000000000000000000..dcdf93abd38148e17210f2e75485e1e12d71fd0a
--- /dev/null
+++ b/common/source/java/ch/systemsx/cisd/common/compression/file/Compressor.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2008 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.common.compression.file;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Queue;
+import java.util.concurrent.ArrayBlockingQueue;
+
+import org.apache.log4j.Logger;
+
+import ch.systemsx.cisd.common.logging.Log4jSimpleLogger;
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+import ch.systemsx.cisd.common.logging.LogInitializer;
+import ch.systemsx.cisd.common.utilities.FileUtilities;
+import ch.systemsx.cisd.common.utilities.ISelfTestable;
+
+/**
+ * The base class for file compression.
+ * 
+ * @author Bernd Rinn
+ */
+public class Compressor
+{
+
+    static
+    {
+        LogInitializer.init();
+    }
+
+    private static final Logger machineLog = LogFactory.getLogger(LogCategory.MACHINE, Compressor.class);
+
+    private static final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION, Compressor.class);
+
+    private static final int NUMBER_OF_WORKERS = Runtime.getRuntime().availableProcessors();
+
+    private static Queue<File> fillWorkerQueueOrExit(File directory, final FileFilter filter)
+    {
+        final File[] filesToCompressOrNull =
+                FileUtilities.tryListFiles(directory, filter, new Log4jSimpleLogger(machineLog));
+        if (filesToCompressOrNull == null)
+        {
+            System.err.printf("Path '%s' is not a directory.\n", directory.getPath());
+            System.exit(1);
+        }
+        if (filesToCompressOrNull.length == 0)
+        {
+            System.out.println("No files to compress.");
+            System.exit(0);
+        }
+        if (operationLog.isInfoEnabled())
+        {
+            operationLog.info(String.format("Found %d files to compress.", filesToCompressOrNull.length));
+        }
+        return new ArrayBlockingQueue<File>(filesToCompressOrNull.length, false, Arrays.asList(filesToCompressOrNull));
+    }
+
+    private static void startUpWorkerThreads(Queue<File> workerQueue,
+            Collection<FailureRecord> failed, ICompressionMethod compressor)
+    {
+        for (int i = 0; i < NUMBER_OF_WORKERS; ++i)
+        {
+            new Thread(new CompressionWorker(workerQueue, failed, compressor), "Compressor " + i).start();
+        }
+        if (operationLog.isInfoEnabled())
+        {
+            operationLog.info(String.format("Started up %d worker threads.", NUMBER_OF_WORKERS));
+        }
+    }
+
+    public static Collection<FailureRecord> start(String directoryName,
+            ICompressionMethod compressionMethod)
+    {
+        if (compressionMethod instanceof ISelfTestable)
+        {
+            ((ISelfTestable) compressionMethod).check();
+        }
+        final Queue<File> workerQueue = fillWorkerQueueOrExit(new File(directoryName), compressionMethod);
+        final Collection<FailureRecord> failed =
+                Collections.synchronizedCollection(new ArrayList<FailureRecord>());
+        startUpWorkerThreads(workerQueue, failed, compressionMethod);
+        return failed;
+    }
+}
diff --git a/common/source/java/ch/systemsx/cisd/common/compression/file/FailureRecord.java b/common/source/java/ch/systemsx/cisd/common/compression/file/FailureRecord.java
new file mode 100644
index 0000000000000000000000000000000000000000..36d5108825e6e5f15bd75d499e892e080bd231de
--- /dev/null
+++ b/common/source/java/ch/systemsx/cisd/common/compression/file/FailureRecord.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2008 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.common.compression.file;
+
+import java.io.File;
+
+import ch.systemsx.cisd.common.exceptions.Status;
+import ch.systemsx.cisd.common.exceptions.StatusFlag;
+
+/**
+ * A class that holds the information about a compression failure.
+ * 
+ * @author Bernd Rinn
+ */
+public class FailureRecord
+{
+    private final File failedFile;
+
+    private final Status failureStatus;
+
+    private final Throwable throwableOrNull;
+
+    FailureRecord(File failedFile, Status failureStatus)
+    {
+        this.failedFile = failedFile;
+        this.failureStatus = failureStatus;
+        this.throwableOrNull = null;
+    }
+
+    FailureRecord(File failedFile, Throwable throwableOrNull)
+    {
+        this.failedFile = failedFile;
+        this.failureStatus =
+                new Status(StatusFlag.FATAL_ERROR, "Exceptional condition: "
+                        + throwableOrNull.getClass().getSimpleName());
+        this.throwableOrNull = throwableOrNull;
+    }
+
+    /**
+     * Returns the file that caused the failure.
+     */
+    public final File getFailedFile()
+    {
+        return failedFile;
+    }
+
+    /**
+     * Returns the {@link Status} of the failure. Can have a {@link StatusFlag} of {@link StatusFlag#RETRIABLE_ERROR} if
+     * retrying the operation did not help.
+     */
+    public final Status getFailureStatus()
+    {
+        return failureStatus;
+    }
+
+    /**
+     * Returns the {@link Throwable}, if any has occurred in the compression method.
+     */
+    public final Throwable tryGetThrowable()
+    {
+        return throwableOrNull;
+    }
+}
\ No newline at end of file
diff --git a/common/source/java/ch/systemsx/cisd/common/compression/file/ICompressionMethod.java b/common/source/java/ch/systemsx/cisd/common/compression/file/ICompressionMethod.java
new file mode 100644
index 0000000000000000000000000000000000000000..fb4b1a1212552a76dd1328204a67dc3a1eeb1ec0
--- /dev/null
+++ b/common/source/java/ch/systemsx/cisd/common/compression/file/ICompressionMethod.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2008 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.common.compression.file;
+
+import java.io.File;
+import java.io.FileFilter;
+
+import ch.systemsx.cisd.common.exceptions.Status;
+
+/**
+ * A role that compresses a file. A compression method may only be suitable for some files, thus it is also a
+ * {@link FileFilter}.
+ * 
+ * @author Bernd Rinn
+ */
+public interface ICompressionMethod extends FileFilter
+{
+
+    /**
+     * Compress the <var>fileToCompress</var>
+     *  
+     * @return {@link Status#OK} if the operation was successful, a status indicating the kind of problem otherwise.
+     */
+    public Status compress(File fileToCompress);
+
+}
diff --git a/common/source/java/ch/systemsx/cisd/common/compression/file/InPlaceCompressionMethod.java b/common/source/java/ch/systemsx/cisd/common/compression/file/InPlaceCompressionMethod.java
new file mode 100644
index 0000000000000000000000000000000000000000..65cdedf9fefbea0115f8d8ff1387e73f1f8c4e87
--- /dev/null
+++ b/common/source/java/ch/systemsx/cisd/common/compression/file/InPlaceCompressionMethod.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2008 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.common.compression.file;
+
+import java.io.File;
+import java.util.List;
+
+import org.apache.log4j.Logger;
+
+import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
+import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
+import ch.systemsx.cisd.common.exceptions.Status;
+import ch.systemsx.cisd.common.exceptions.StatusFlag;
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+import ch.systemsx.cisd.common.utilities.ISelfTestable;
+import ch.systemsx.cisd.common.utilities.ProcessExecutionHelper;
+
+/**
+ * An {@link ICompressionMethod} that performs in-place compression of a bulk of files by means of calling an external
+ * compression program and running it per file in an external process.
+ * 
+ * @author Bernd Rinn
+ */
+public abstract class InPlaceCompressionMethod implements ICompressionMethod, ISelfTestable
+{
+
+    private static final String INPROGRESS_MARKER = ".COMPRESSION_IN_PROGRESS_";
+
+    private static final String COMPRESSED_MARKER = ".COMPRESSED_";
+
+    protected static final Logger machineLog =
+            LogFactory.getLogger(LogCategory.MACHINE, InPlaceCompressionMethod.class);
+
+    protected static final Logger operationLog =
+            LogFactory.getLogger(LogCategory.OPERATION, InPlaceCompressionMethod.class);
+
+    private File prefixInProgress(File file)
+    {
+        assert file != null;
+
+        return new File(file.getParent(), INPROGRESS_MARKER + file.getName());
+    }
+
+    private File prefixCompressed(File file)
+    {
+        assert file != null;
+
+        return new File(file.getParent(), COMPRESSED_MARKER + file.getName());
+    }
+
+    private File tryRemovePrefix(File file)
+    {
+        assert file != null;
+
+        final String name = file.getName();
+        if (name.startsWith(INPROGRESS_MARKER))
+        {
+            return new File(file.getParent(), name.substring(INPROGRESS_MARKER.length()));
+        } else if (name.startsWith(COMPRESSED_MARKER))
+        {
+            return new File(file.getParent(), name.substring(COMPRESSED_MARKER.length()));
+        } else
+        {
+            return null;
+        }
+    }
+
+    private boolean isCompressedFile(File fileToCompress)
+    {
+        return fileToCompress.getName().startsWith(COMPRESSED_MARKER);
+    }
+
+    private boolean isInProgressFile(File fileToCompress)
+    {
+        return fileToCompress.getName().startsWith(INPROGRESS_MARKER);
+    }
+
+    private Status createStatusAndLog(String msgTemplate, Object... params)
+    {
+        final String msg = String.format(msgTemplate, params);
+        operationLog.error(msg);
+        return new Status(StatusFlag.FATAL_ERROR, msg);
+    }
+
+    /**
+     * Creates the command line of the external program to call in order to perform the compression.
+     */
+    protected abstract List<String> createCommandLine(File fileToCompress, File inProgressFile);
+
+    /**
+     * Returns the file extensions of files that this compression method can compress.
+     * <p>
+     * All extensions need to be returned in lower case.
+     */
+    protected abstract List<String> getAcceptedExtensions();
+
+    /**
+     * Perform any check necessary to see whether the external program that has been found is suitable for the
+     * compression task (e.g. program version).
+     */
+    public abstract void check() throws EnvironmentFailureException, ConfigurationFailureException;
+
+    public boolean accept(File pathname)
+    {
+        if (pathname.isFile() == false)
+        {
+            return false;
+        }
+        final String name = pathname.getName().toLowerCase();
+        for (String extension : getAcceptedExtensions())
+        {
+            if (name.endsWith(extension))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public Status compress(File fileToCompress)
+    {
+        assert fileToCompress != null;
+
+        // Clean up
+        if (isInProgressFile(fileToCompress))
+        {
+            final boolean ok = fileToCompress.delete();
+            if (ok)
+            {
+                operationLog.warn(String.format("Clean up: deleting left-over file '%s'", fileToCompress
+                        .getAbsolutePath()));
+                return Status.OK;
+            } else
+            {
+                return createStatusAndLog("Clean up: Unable to delete left-over file '%s'", fileToCompress
+                        .getAbsolutePath());
+            }
+        }
+        if (isCompressedFile(fileToCompress))
+        {
+            final File originalFile = tryRemovePrefix(fileToCompress);
+            assert originalFile != null;
+            if (originalFile.exists())
+            {
+                final boolean ok = originalFile.delete();
+                if (ok == false)
+                {
+                    return createStatusAndLog("Clean up: Unable to delete uncompressed file '%s'", originalFile);
+                }
+            }
+            if (fileToCompress.renameTo(originalFile))
+            {
+                return Status.OK;
+            } else
+            {
+                return createStatusAndLog("Renaming compressed file '%s' to original name '%s' failed.",
+                        fileToCompress, originalFile);
+            }
+        }
+        final File inProgressFile = prefixInProgress(fileToCompress);
+        final File compressionFinishedFile = prefixCompressed(fileToCompress);
+        final boolean runOK =
+                ProcessExecutionHelper.runAndLog(createCommandLine(fileToCompress, inProgressFile), operationLog,
+                        machineLog);
+        if (runOK == false)
+        {
+            return createStatusAndLog("Unable to compress '%s'.", fileToCompress.getAbsolutePath());
+        }
+        final boolean firstRenameOK = inProgressFile.renameTo(compressionFinishedFile);
+        if (firstRenameOK == false)
+        {
+            return createStatusAndLog("Unable to rename '%s' to '%s'.", inProgressFile.getAbsolutePath(),
+                    compressionFinishedFile.getAbsolutePath());
+        }
+        final boolean removalOfOriginalOK = fileToCompress.delete();
+        if (removalOfOriginalOK == false)
+        {
+            return createStatusAndLog("Unable to delete original file '%s'", fileToCompress.getAbsolutePath());
+        }
+        final boolean secondRenameOK = compressionFinishedFile.renameTo(fileToCompress);
+        if (secondRenameOK == false)
+        {
+            return createStatusAndLog("Unable to rename '%s' to '%s'.", compressionFinishedFile.getAbsolutePath(),
+                    fileToCompress.getAbsolutePath());
+        }
+        return Status.OK;
+    }
+
+}
\ No newline at end of file
diff --git a/common/source/java/ch/systemsx/cisd/common/compression/tiff/TiffCompressor.java b/common/source/java/ch/systemsx/cisd/common/compression/tiff/TiffCompressor.java
new file mode 100644
index 0000000000000000000000000000000000000000..b6cb1f7f24e5c912ed5ec4747b2370e65d797e7f
--- /dev/null
+++ b/common/source/java/ch/systemsx/cisd/common/compression/tiff/TiffCompressor.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2008 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.common.compression.tiff;
+
+import java.util.Collection;
+
+import ch.systemsx.cisd.common.compression.file.Compressor;
+import ch.systemsx.cisd.common.compression.file.FailureRecord;
+
+/**
+ * The main class for tiff file compression.
+ * 
+ * @author Bernd Rinn
+ */
+public class TiffCompressor extends Compressor
+{
+
+    public static void main(String[] args)
+    {
+        if (args.length != 1)
+        {
+            System.err.println("Syntax: TiffCompressor <directory>");
+            System.exit(1);
+        }
+        final Collection<FailureRecord> failed = start(args[0], new TiffZipCompressionMethod());
+        if (failed.size() > 0)
+        {
+            System.err.println("The following files could not bee successfully compressed:");
+        }
+        for (FailureRecord r : failed)
+        {
+            System.err.printf("%s (%s)\n", r.getFailedFile().getName(), r.getFailureStatus().getMessage());
+        }
+    }
+
+}
diff --git a/common/source/java/ch/systemsx/cisd/common/compression/tiff/TiffZipCompressionMethod.java b/common/source/java/ch/systemsx/cisd/common/compression/tiff/TiffZipCompressionMethod.java
new file mode 100644
index 0000000000000000000000000000000000000000..cef175f430065609ddb9e9844115c3f1ab7b3130
--- /dev/null
+++ b/common/source/java/ch/systemsx/cisd/common/compression/tiff/TiffZipCompressionMethod.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2008 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.common.compression.tiff;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+
+
+import ch.systemsx.cisd.common.compression.file.InPlaceCompressionMethod;
+import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
+import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
+import ch.systemsx.cisd.common.logging.LogInitializer;
+import ch.systemsx.cisd.common.utilities.ProcessExecutionHelper;
+import ch.systemsx.cisd.common.utilities.OSUtilities;
+import ch.systemsx.cisd.common.utilities.ProcessResult;
+
+/**
+ * A compression method for TIFF files using the ImageMagick <code>convert</code> utility with compression
+ * <code>Zip</code>.
+ * 
+ * @author Bernd Rinn
+ */
+public class TiffZipCompressionMethod extends InPlaceCompressionMethod
+{
+
+    final static File convertExecutable = OSUtilities.findExecutable("convert");
+
+    private static String getImageMagickVersion(String convertExecutableToCheck)
+    {
+        final long TIME_TO_WAIT_FOR_COMPLETION = 2 * 1000;
+        final ProcessResult result =
+                ProcessExecutionHelper.run(Arrays.asList(convertExecutableToCheck, "--version"),
+                        TIME_TO_WAIT_FOR_COMPLETION, operationLog, machineLog);
+        result.log();
+        final String versionString = extractImageMagickVersion(result.getProcessOutput().get(0));
+        return versionString;
+    }
+
+    private static String extractImageMagickVersion(String imageMagickVersionLine)
+    {
+        if (imageMagickVersionLine.startsWith("Version: ImageMagick") == false)
+        {
+            return null;
+        } else
+        {
+            final String[] versionStringParts = imageMagickVersionLine.split("\\s+");
+            if (versionStringParts.length < 3)
+            {
+                return null;
+            }
+            return versionStringParts[2];
+        }
+    }
+
+    @Override
+    protected List<String> createCommandLine(File fileToCompress, File inProgressFile)
+    {
+        assert convertExecutable != null;
+        assert fileToCompress != null;
+        assert fileToCompress.isFile();
+        assert inProgressFile != null;
+        assert inProgressFile.exists() == false;
+    
+        final List<String> parameters =
+                Arrays.asList(convertExecutable.getAbsolutePath(), fileToCompress.getAbsolutePath(), "-compress",
+                        "Zip", inProgressFile.getAbsolutePath());
+        return parameters;
+    }
+
+    @Override
+    protected List<String> getAcceptedExtensions()
+    {
+        return Arrays.asList(".tif", ".tiff");
+    }
+    
+    @Override
+    public void check() throws EnvironmentFailureException, ConfigurationFailureException
+    {
+        if (convertExecutable == null)
+        {
+            throw new ConfigurationFailureException("Cannot find executable of the convert utility.");
+        }
+        final String imageMagickVersionOrNull = getImageMagickVersion(convertExecutable.getAbsolutePath());
+        if (imageMagickVersionOrNull == null)
+        {
+            throw new ConfigurationFailureException("Invalid convert utility");
+        }
+        String[] imageMagickVersionParts = imageMagickVersionOrNull.split("\\.");
+        if (imageMagickVersionParts.length != 3)
+        {
+            throw new ConfigurationFailureException("Invalid convert utility");
+        }
+        final int imageMagickMajorVersion = Integer.parseInt(imageMagickVersionParts[0]);
+        final int imageMagickMinorVersion = Integer.parseInt(imageMagickVersionParts[1]);
+        if (imageMagickMajorVersion < 6 || imageMagickMinorVersion < 2)
+        {
+            throw ConfigurationFailureException.fromTemplate(
+                    "Convert utility is too old (expected: v6.2 or newer, found: v%s)", imageMagickVersionOrNull);
+        }
+        if (machineLog.isInfoEnabled())
+        {
+            machineLog.info(String.format("Using convert executable '%s', ImageMagick version %s", convertExecutable,
+                    imageMagickVersionOrNull));
+        }
+    }
+
+    public static void main(String[] args)
+    {
+        LogInitializer.init();
+        final TiffZipCompressionMethod compressor = new TiffZipCompressionMethod();
+        compressor.check();
+    }
+
+}
diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/compression/file/CompresionWorkerTest.java b/common/sourceTest/java/ch/systemsx/cisd/common/compression/file/CompresionWorkerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..12a7600b941632cc9f40084aa950a992c80eda45
--- /dev/null
+++ b/common/sourceTest/java/ch/systemsx/cisd/common/compression/file/CompresionWorkerTest.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright 2008 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.common.compression.file;
+
+import static org.testng.AssertJUnit.*;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Queue;
+
+import org.apache.log4j.Level;
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.jmock.api.Invocation;
+import org.jmock.lib.action.CustomAction;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import ch.systemsx.cisd.common.exceptions.Status;
+import ch.systemsx.cisd.common.exceptions.StatusFlag;
+import ch.systemsx.cisd.common.logging.BufferedAppender;
+
+/**
+ * Test cases for the {@link CompressionWorker}.
+ * 
+ * @author Bernd Rinn
+ */
+public class CompresionWorkerTest
+{
+
+    private Mockery context;
+
+    private Queue<File> queue;
+
+    private Collection<FailureRecord> failed;
+
+    private ICompressionMethod compressionMethod;
+
+    private CompressionWorker worker;
+
+    private BufferedAppender logRecorder;
+
+    @SuppressWarnings("unchecked")
+    private Queue<File> createFileQueue()
+    {
+        return context.mock(Queue.class);
+    }
+
+    @BeforeMethod
+    public void setUp()
+    {
+        context = new Mockery();
+        queue = createFileQueue();
+        failed = new ArrayList<FailureRecord>();
+        compressionMethod = context.mock(ICompressionMethod.class);
+        logRecorder = new BufferedAppender("%-5p %c - %m%n", Level.DEBUG);
+        worker = new CompressionWorker(queue, failed, compressionMethod);
+    }
+
+    @AfterMethod
+    public void tearDown()
+    {
+        logRecorder.reset();
+        // To following line of code should also be called at the end of each test method.
+        // Otherwise one do not known which test failed.
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testCompressionWorkerImmediateExit()
+    {
+        context.checking(new Expectations()
+            {
+                {
+                    one(queue).poll();
+                    will(returnValue(null));
+                }
+            });
+        worker.run();
+        assertTrue(logRecorder.getLogContent().indexOf(CompressionWorker.EXITING_MSG) >= 0);
+        assertEquals(0, failed.size());
+    }
+
+    @Test
+    public void testCompressionWorkerHappyCase()
+    {
+        final File[] files = new File[]
+            { new File("a"), new File("b"), new File("c") };
+        context.checking(new Expectations()
+            {
+                {
+                    for (File f : files)
+                    {
+                        one(queue).poll();
+                        will(returnValue(f));
+                        one(compressionMethod).compress(f);
+                        will(returnValue(Status.OK));
+                    }
+                    one(queue).poll();
+                    will(returnValue(null));
+                }
+            });
+        worker.run();
+        assertTrue(logRecorder.getLogContent().indexOf(CompressionWorker.EXITING_MSG) >= 0);
+        assertEquals(0, failed.size());
+    }
+
+    @Test
+    public void testCompressionWorkerWithRetriableFailure()
+    {
+        final String faultyFile = "b";
+        final Status faultyStatus = new Status(StatusFlag.RETRIABLE_ERROR, "some problem");
+        final File[] files = new File[]
+            { new File("a"), new File(faultyFile), new File("c") };
+        context.checking(new Expectations()
+            {
+                {
+                    for (File f : files)
+                    {
+                        one(queue).poll();
+                        will(returnValue(f));
+                        one(compressionMethod).compress(f);
+                        if (faultyFile.equals(f.getName()))
+                        {
+                            will(returnValue(faultyStatus));
+                            one(compressionMethod).compress(f);
+                            will(returnValue(Status.OK));
+                        } else
+                        {
+                            will(returnValue(Status.OK));
+                        }
+                    }
+                    one(queue).poll();
+                    will(returnValue(null));
+                }
+            });
+        worker.run();
+        assertEquals(0, failed.size());
+        assertTrue(logRecorder.getLogContent().indexOf(CompressionWorker.EXITING_MSG) >= 0);
+    }
+
+    @Test
+    public void testCompressionWorkerWithRetriableFailureFinallyFailed()
+    {
+        final String faultyFile = "b";
+        final Status faultyStatus = new Status(StatusFlag.RETRIABLE_ERROR, "some problem");
+        final File[] files = new File[]
+            { new File("a"), new File(faultyFile), new File("c") };
+        context.checking(new Expectations()
+            {
+                {
+                    for (File f : files)
+                    {
+                        one(queue).poll();
+                        will(returnValue(f));
+                        if (faultyFile.equals(f.getName()))
+                        {
+                            for (int i = 0; i < CompressionWorker.MAX_RETRY_OF_FAILED_COMPRESSIONS; ++i)
+                            {
+                                one(compressionMethod).compress(f);
+                                will(returnValue(faultyStatus));
+                            }
+                        } else
+                        {
+                            one(compressionMethod).compress(f);
+                            will(returnValue(Status.OK));
+                        }
+                    }
+                    one(queue).poll();
+                    will(returnValue(null));
+                }
+            });
+        worker.run();
+        assertEquals(1, failed.size());
+        final FailureRecord record = failed.iterator().next();
+        assertEquals(faultyFile, record.getFailedFile().getName());
+        assertEquals(StatusFlag.RETRIABLE_ERROR, record.getFailureStatus().getFlag());
+        assertEquals(faultyStatus.getMessage(), record.getFailureStatus().getMessage());
+        assertTrue(logRecorder.getLogContent().indexOf(CompressionWorker.EXITING_MSG) >= 0);
+    }
+
+    @Test
+    public void testCompressionWorkerWithFatalFailure()
+    {
+        final String faultyFile = "b";
+        final Status fatalStatus = new Status(StatusFlag.FATAL_ERROR, "some problem");
+        final File[] files = new File[]
+            { new File("a"), new File(faultyFile), new File("c") };
+        context.checking(new Expectations()
+            {
+                {
+                    for (File f : files)
+                    {
+                        one(queue).poll();
+                        will(returnValue(f));
+                        one(compressionMethod).compress(f);
+                        if (faultyFile.equals(f.getName()))
+                        {
+                            will(returnValue(fatalStatus));
+                        } else
+                        {
+                            will(returnValue(Status.OK));
+                        }
+                    }
+                    one(queue).poll();
+                    will(returnValue(null));
+                }
+            });
+        worker.run();
+        assertEquals(1, failed.size());
+        final FailureRecord record = failed.iterator().next();
+        assertEquals(faultyFile, record.getFailedFile().getName());
+        assertEquals(StatusFlag.FATAL_ERROR, record.getFailureStatus().getFlag());
+        assertEquals(fatalStatus.getMessage(), record.getFailureStatus().getMessage());
+        assertTrue(logRecorder.getLogContent().indexOf(CompressionWorker.EXITING_MSG) >= 0);
+    }
+
+    @Test
+    public void testCompressionWorkerInterrupted()
+    {
+        final File[] files = new File[]
+            { new File("a"), new File("b"), new File("c") };
+        context.checking(new Expectations()
+            {
+                {
+                    for (File f : files)
+                    {
+                        one(queue).poll();
+                        will(returnValue(f));
+                        one(compressionMethod).compress(f);
+                        if (f.getName().equals("c"))
+                        {
+                            will(new CustomAction("Interrupt Thread")
+                                {
+                                    public Object invoke(Invocation invocation) throws Throwable
+                                    {
+                                        Thread.currentThread().interrupt();
+                                        return Status.OK;
+                                    }
+                                });
+                        } else
+                        {
+                            will(returnValue(Status.OK));
+                        }
+                    }
+                }
+            });
+        worker.run();
+        assertTrue(logRecorder.getLogContent().indexOf(CompressionWorker.INTERRPTED_MSG) >= 0);
+    }
+
+    private final class FakedException extends RuntimeException
+    {
+
+        private static final long serialVersionUID = 1L;
+    }
+
+    @Test
+    public void testCompressionWorkerCompressorThrowsException()
+    {
+        final String faultyFile = "b";
+        final File[] files = new File[]
+            { new File("a"), new File(faultyFile), new File("c") };
+        final FakedException ex = new FakedException();
+        context.checking(new Expectations()
+            {
+                {
+                    for (File f : files)
+                    {
+                        one(queue).poll();
+                        will(returnValue(f));
+                        one(compressionMethod).compress(f);
+                        if (f.getName().equals(faultyFile))
+                        {
+                            will(new CustomAction("Throws Exception")
+                                {
+                                    public Object invoke(Invocation invocation) throws Throwable
+                                    {
+                                        throw ex;
+                                    }
+                                });
+                        } else
+                        {
+                            will(returnValue(Status.OK));
+                        }
+                    }
+                    one(queue).poll();
+                    will(returnValue(null));
+                }
+            });
+        worker.run();
+        assertEquals(1, failed.size());
+        final FailureRecord record = failed.iterator().next();
+        assertEquals(faultyFile, record.getFailedFile().getName());
+        assertEquals(StatusFlag.FATAL_ERROR, record.getFailureStatus().getFlag());
+        assertEquals("Exceptional condition: " + FakedException.class.getSimpleName(), record.getFailureStatus()
+                .getMessage());
+        assertEquals(ex, record.tryGetThrowable());
+        assertTrue(logRecorder.getLogContent().indexOf(
+                String.format(CompressionWorker.EXCEPTION_COMPRESSING_MSG_TEMPLATE, faultyFile)) >= 0);
+    }
+
+}