diff --git a/MANIFEST.in b/MANIFEST.in
index 3b102a792910980e64bed97ee7cd1ee6ee361b2b..d4fcb40cffc78eb9cf58bbbc67a131386a4cf8d0 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -2,3 +2,4 @@ include LICENSE README.rst bootstrap.py buildout.cfg
 recursive-include doc conf.py *.rst
 recursive-include xbob *.cpp *.h
 recursive-include xbob/io/test/data *.*
+recursive-include xbob/io/fonts *.txt *.ttf
diff --git a/setup.py b/setup.py
index 29c175f336cac1b2ef732e75f63cdfe97a53ef37..b071fb27b3e22ea35ad79f486a02dde3477b74e1 100644
--- a/setup.py
+++ b/setup.py
@@ -113,6 +113,12 @@ setup(
         ),
       ],
 
+    entry_points={
+      'console_scripts': [
+        'xbob_video_test.py = xbob.io.script.video_test:main',
+        ],
+      },
+
     classifiers = [
       'Development Status :: 3 - Alpha',
       'Intended Audience :: Developers',
diff --git a/xbob/io/__init__.py b/xbob/io/__init__.py
index 8d04c8504d57a16981af06a5f9d767c43080f5d3..8a062569adcbcb8df36e57fdb9a2f4071d622653 100644
--- a/xbob/io/__init__.py
+++ b/xbob/io/__init__.py
@@ -1,8 +1,178 @@
-from ._library import __version__, __api_version__, file
+from ._library import __version__, __api_version__, File
+
+import os
+
+def create_directories_save(directory, dryrun=False):
+  """Creates a directory if it does not exists, with concurrent access support.
+  This function will also create any parent directories that might be required.
+  If the dryrun option is selected, it does not actually create the directory,
+  but just writes the (Linux) command that would have been executed.
+
+  Parameters:
+
+  directory
+    The directory that you want to create.
+
+  dryrun
+    Only write the command, but do not execute it.
+  """
+  try:
+    if dryrun:
+      print("[dry-run] mkdir -p '%s'" % directory)
+    else:
+      if directory and not os.path.exists(directory): os.makedirs(directory)
+
+  except OSError as exc: # Python >2.5
+    import errno
+    if exc.errno != errno.EEXIST:
+      raise
+
+
+def load(inputs):
+  """Loads the contents of a file, an iterable of files, or an iterable of
+  :py:class:`bob.io.File`'s into a :py:class:`numpy.ndarray`.
+
+  Parameters:
+
+  inputs
+
+    This might represent several different entities:
+
+    1. The name of a file (full path) from where to load the data. In this
+       case, this assumes that the file contains an array and returns a loaded
+       numpy ndarray.
+    2. An iterable of filenames to be loaded in memory. In this case, this
+       would assume that each file contains a single 1D sample or a set of 1D
+       samples, load them in memory and concatenate them into a single and
+       returned 2D numpy ndarray.
+    3. An iterable of :py:class:`bob.io.File`. In this case, this would assume
+       that each :py:class:`bob.io.File` contains a single 1D sample or a set
+       of 1D samples, load them in memory if required and concatenate them into
+       a single and returned 2D numpy ndarray.
+    4. An iterable with mixed filenames and :py:class:`bob.io.File`. In this
+       case, this would returned a 2D :py:class:`numpy.ndarray`, as described
+       by points 2 and 3 above.
+  """
+
+  from collections import Iterable
+  import numpy
+  from .utils import is_string
+  if is_string(inputs):
+    return File(inputs, 'r').read()
+  elif isinstance(inputs, Iterable):
+    retval = []
+    for obj in inputs:
+      if is_string(obj):
+        retval.append(load(obj))
+      elif isinstance(obj, File):
+        retval.append(obj.read())
+      else:
+        raise TypeError("Iterable contains an object which is not a filename nor a bob.io.File.")
+    return numpy.vstack(retval)
+  else:
+    raise TypeError("Unexpected input object. This function is expecting a filename, or an iterable of filenames and/or bob.io.File's")
+
+def merge(filenames):
+  """Converts an iterable of filenames into an iterable over read-only
+  bob.io.File's.
+
+  Parameters:
+
+  filenames
+
+    This might represent:
+
+    1. A single filename. In this case, an iterable with a single
+       :py:class:`bob.io.File` is returned.
+    2. An iterable of filenames to be converted into an iterable of
+       :py:class:`bob.io.File`'s.
+  """
+
+  from collections import Iterable
+  from .utils import is_string
+  if is_string(filenames):
+    return [File(filenames, 'r')]
+  elif isinstance(filenames, Iterable):
+    return [File(k, 'r') for k in filenames]
+  else:
+    raise TypeError("Unexpected input object. This function is expecting an iterable of filenames.")
+
+def save(array, filename, create_directories = False):
+  """Saves the contents of an array-like object to file.
+
+  Effectively, this is the same as creating a :py:class:`bob.io.File` object
+  with the mode flag set to `w` (write with truncation) and calling
+  :py:meth:`bob.io.File.write` passing `array` as parameter.
+
+  Parameters:
+
+  array
+    The array-like object to be saved on the file
+
+  filename
+    The name of the file where you need the contents saved to
+
+  create_directories
+    Automatically generate the directories if required
+  """
+  # create directory if not existent yet
+  if create_directories:
+    create_directories_save(os.path.dirname(filename))
+
+  return File(filename, 'w').write(array)
+
+# Just to make it homogenous with the C++ API
+write = save
+
+def append(array, filename):
+  """Appends the contents of an array-like object to file.
+
+  Effectively, this is the same as creating a :py:class:`bob.io.File` object
+  with the mode flag set to `a` (append) and calling
+  :py:meth:`bob.io.File.append` passing `array` as parameter.
+
+  Parameters:
+
+  array
+    The array-like object to be saved on the file
+
+  filename
+    The name of the file where you need the contents saved to
+  """
+  return File(filename, 'a').append(array)
+
+def peek(filename):
+  """Returns the type of array (frame or sample) saved in the given file.
+
+  Effectively, this is the same as creating a :py:class:`bob.io.File` object
+  with the mode flag set to `r` (read-only) and returning
+  :py:attr:`bob.io.File.type`.
+
+  Parameters:
+
+  filename
+    The name of the file to peek information from
+  """
+  return File(filename, 'r').type
+
+def peek_all(filename):
+  """Returns the type of array (for full readouts) saved in the given file.
+
+  Effectively, this is the same as creating a :py:class:`bob.io.File` object
+  with the mode flag set to `r` (read-only) and returning
+  :py:attr:`bob.io.File.type_all`.
+
+  Parameters:
+
+  filename
+    The name of the file to peek information from
+  """
+  return File(filename, 'r').type_all
+
+# Keeps compatibility with the previously existing API
+open = File
 
 def get_include():
   """Returns the directory containing the C/C++ API include directives"""
 
   return __import__('pkg_resources').resource_filename(__name__, 'include')
-
-__all__ = []
diff --git a/xbob/io/bobskin.cpp b/xbob/io/bobskin.cpp
index bcd91acc2b091ccdbe01218b573a0914a6bcbe65..27bca8161bb90023fdcfbbd03c06e708b62257f9 100644
--- a/xbob/io/bobskin.cpp
+++ b/xbob/io/bobskin.cpp
@@ -5,46 +5,145 @@
  * @brief Implementation of our bobskin class
  */
 
-#include <numpy/arrayobject.h>
+#include <bobskin.h>
 #include <stdexcept>
 
-bobskin::bobskin(PyObject* array, bob::core::array::ElementType& eltype) {
+bobskin::bobskin(PyObject* array, bob::core::array::ElementType eltype) {
 
   if (!PyArray_CheckExact(array)) {
-    PyErr_SetString(PyExc_TypeError, "input object to bobskin constructor is not a numpy.ndarray");
-    throw std::runtime_error();
+    PyErr_SetString(PyExc_TypeError, "input object to bobskin constructor is not (exactly) a numpy.ndarray");
+    throw std::runtime_error("error is already set");
   }
 
-  m_type.set(eltype, PyArray_NDIM(array), PyArray_DIMS(array),
-      PyArray_STRIDES(array));
+  m_type.set<npy_intp>(eltype, PyArray_NDIM((PyArrayObject*)array), 
+      PyArray_DIMS((PyArrayObject*)array),
+      PyArray_STRIDES((PyArrayObject*)array));
 
-  m_ptr = PyArray_DATA(array);
+  m_ptr = PyArray_DATA((PyArrayObject*)array);
 
 }
 
+static bob::core::array::ElementType signed_integer_type(int bits) {
+  switch(bits) {
+    case 8:
+      return bob::core::array::t_int8;
+    case 16:
+      return bob::core::array::t_int16;
+    case 32:
+      return bob::core::array::t_int32;
+    case 64:
+      return bob::core::array::t_int64;
+    default:
+      PyErr_Format(PyExc_TypeError, "unsupported signed integer element type with %d bits", bits);
+  }
+  return bob::core::array::t_unknown;
+}
+
+static bob::core::array::ElementType unsigned_integer_type(int bits) {
+  switch(bits) {
+    case 8:
+      return bob::core::array::t_uint8;
+    case 16:
+      return bob::core::array::t_uint16;
+    case 32:
+      return bob::core::array::t_uint32;
+    case 64:
+      return bob::core::array::t_uint64;
+    default:
+      PyErr_Format(PyExc_TypeError, "unsupported unsigned signed integer element type with %d bits", bits);
+  }
+  return bob::core::array::t_unknown;
+}
+
+static bob::core::array::ElementType num_to_type (int num) {
+  switch(num) {
+    case NPY_BOOL:
+      return bob::core::array::t_bool;
+
+    //signed integers
+    case NPY_BYTE:
+      return signed_integer_type(NPY_BITSOF_CHAR);
+    case NPY_SHORT:
+      return signed_integer_type(NPY_BITSOF_SHORT);
+    case NPY_INT:
+      return signed_integer_type(NPY_BITSOF_INT);
+    case NPY_LONG:
+      return signed_integer_type(NPY_BITSOF_LONG);
+    case NPY_LONGLONG:
+      return signed_integer_type(NPY_BITSOF_LONGLONG);
+
+    //unsigned integers
+    case NPY_UBYTE:
+      return unsigned_integer_type(NPY_BITSOF_CHAR);
+    case NPY_USHORT:
+      return unsigned_integer_type(NPY_BITSOF_SHORT);
+    case NPY_UINT:
+      return unsigned_integer_type(NPY_BITSOF_INT);
+    case NPY_ULONG:
+      return unsigned_integer_type(NPY_BITSOF_LONG);
+    case NPY_ULONGLONG:
+      return unsigned_integer_type(NPY_BITSOF_LONGLONG);
+
+    //floats
+    case NPY_FLOAT32:
+      return bob::core::array::t_float32;
+    case NPY_FLOAT64:
+      return bob::core::array::t_float64;
+#ifdef NPY_FLOAT128
+    case NPY_FLOAT128:
+      return bob::core::array::t_float128;
+#endif
+
+    //complex
+    case NPY_COMPLEX64:
+      return bob::core::array::t_complex64;
+    case NPY_COMPLEX128:
+      return bob::core::array::t_complex128;
+#ifdef NPY_COMPLEX256
+    case NPY_COMPLEX256:
+      return bob::core::array::t_complex256;
+#endif
+
+    default:
+      PyErr_Format(PyExc_TypeError, "unsupported NumPy element type (%d)", num);
+  }
+
+  return bob::core::array::t_unknown;
+}
+
+bobskin::bobskin(PyBlitzArrayObject* array) {
+  bob::core::array::ElementType eltype = num_to_type(array->type_num);
+  if (eltype == bob::core::array::t_unknown) {
+    throw std::runtime_error("error is already set");
+  }
+  m_type.set<Py_ssize_t>(num_to_type(array->type_num), array->ndim,
+      array->shape, array->stride);
+  m_ptr = array->data;
+}
+
 bobskin::~bobskin() { }
 
 void bobskin::set(const interface&) {
   PyErr_SetString(PyExc_NotImplementedError, "setting C++ bobskin with (const interface&) is not implemented - DEBUG ME!");
-  throw std::runtime_error();
+  throw std::runtime_error("error is already set");
 }
 
-void bobskin::set(boost::shared_ptr<interface> other);
+void bobskin::set(boost::shared_ptr<interface>) {
   PyErr_SetString(PyExc_NotImplementedError, "setting C++ bobskin with (boost::shared_ptr<interface>) is not implemented - DEBUG ME!");
-  throw std::runtime_error();
+  throw std::runtime_error("error is already set");
 }
 
-void bobskin::set (const bob::core::array::typeinfo& req) {
+void bobskin::set (const bob::core::array::typeinfo&) {
   PyErr_SetString(PyExc_NotImplementedError, "setting C++ bobskin with (const typeinfo&) implemented - DEBUG ME!");
-  throw std::runtime_error();
+  throw std::runtime_error("error is already set");
 }
 
 boost::shared_ptr<void> bobskin::owner() {
   PyErr_SetString(PyExc_NotImplementedError, "acquiring non-const owner from C++ bobskin is not implemented - DEBUG ME!");
-  throw std::runtime_error();
+  throw std::runtime_error("error is already set");
 }
 
 boost::shared_ptr<const void> bobskin::owner() const {
   PyErr_SetString(PyExc_NotImplementedError, "acquiring const owner from C++ bobskin is not implemented - DEBUG ME!");
-  throw std::runtime_error();
+  throw std::runtime_error("error is already set");
 }
diff --git a/xbob/io/bobskin.h b/xbob/io/bobskin.h
index 4b38b4e18c824df1d4d6f7158d0223f657049777..f1afcf16ddac2ebcc972130424838c27542f5c59 100644
--- a/xbob/io/bobskin.h
+++ b/xbob/io/bobskin.h
@@ -6,9 +6,14 @@
  * functionality.
  */
 
-#include <Python.h>
 #include <bob/core/array.h>
 
+extern "C" {
+#include <Python.h>
+#include <blitz.array/capi.h>
+}
+
+
 /**
  * Wraps a PyArrayObject such that we can access it from bob::io
  */
@@ -21,6 +26,11 @@ class bobskin: public bob::core::array::interface {
      */
     bobskin(PyObject* array, bob::core::array::ElementType eltype);
 
+    /**
+     * @brief Builds a new array an array like object
+     */
+    bobskin(PyBlitzArrayObject* array);
+
     /**
      * @brief By default, the interface is never freed. You must override 
      * this method to do something special for your class type.
diff --git a/xbob/io/file.cpp b/xbob/io/file.cpp
index db89459714e9da96298201b21ff6cd134cb08fce..4187cc3632a3a99fda30881ab52e8e605933ec0a 100644
--- a/xbob/io/file.cpp
+++ b/xbob/io/file.cpp
@@ -10,6 +10,7 @@
 #include <bob/io/CodecRegistry.h>
 #include <bob/io/utils.h>
 #include <numpy/arrayobject.h>
+#include <blitz.array/capi.h>
 #include <stdexcept>
 
 #include <bobskin.h>
@@ -46,10 +47,10 @@ static int PyBobIoFile_Init(PyBobIoFileObject* self, PyObject *args, PyObject* k
   char* mode = 0;
   int mode_len = 0;
   char* pretend_extension = 0;
-  if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss|s", kwlist, &filename,
+  if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss#|s", kwlist, &filename,
         &mode, &mode_len, &pretend_extension)) return -1;
 
-  if (mode_len != 1 || !(mode[0] != 'r' && mode[0] != 'w' && mode[0] != 'a')) {
+  if (mode_len != 1 || !(mode[0] == 'r' || mode[0] == 'w' || mode[0] == 'a')) {
     PyErr_Format(PyExc_ValueError, "file open mode string should have 1 element and be either 'r' (read), 'w' (write) or 'a' (append)");
     return -1;
   }
@@ -215,33 +216,38 @@ int PyBobIo_AsTypenum (bob::core::array::ElementType type) {
 static PyObject* PyBobIoFile_GetItem (PyBobIoFileObject* self, Py_ssize_t i) {
 
   if (i < 0 || i >= self->f->size()) {
-    PyErr_SetString(PyExc_IndexError, "file index out of range");
+    PyErr_Format(PyExc_IndexError, "file index out of range - `%s' only contains %" PY_FORMAT_SIZE_T "d object(s)", self->f->filename().c_str(), self->f->size());
     return 0;
   }
 
   const bob::core::array::typeinfo& info = self->f->type();
 
   npy_intp shape[NPY_MAXDIMS];
-  for (i=0; i<info.nd; ++i) shape[i] = info.shape[i];
+  for (int k=0; k<info.nd; ++k) shape[k] = info.shape[k];
 
   int type_num = PyBobIo_AsTypenum(info.dtype);
   if (type_num == NPY_NOTYPE) return 0; ///< failure
 
   PyObject* retval = PyArray_SimpleNew(info.nd, shape, type_num);
-  bobskin skin(retval, info.dtype);
+  if (!retval) return 0;
 
   try {
+    bobskin skin(retval, info.dtype);
     self->f->read(skin, i);
   }
   catch (std::runtime_error& e) {
+    if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught std::runtime_error while reading object #%" PY_FORMAT_SIZE_T "d from file `%s': %s", i, self->f->filename().c_str(), e.what());
+    Py_DECREF(retval);
     return 0;
   }
   catch (std::exception& e) {
-    PyErr_Format(PyExc_RuntimeError, "caught std::exception while reading file `%s': %s", self->f->filename().c_str(), e.what());
+    if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught std::exception while reading object #%" PY_FORMAT_SIZE_T "d from file `%s': %s", i, self->f->filename().c_str(), e.what());
+    Py_DECREF(retval);
     return 0;
   }
   catch (...) {
-    PyErr_Format(PyExc_RuntimeError, "caught unknown while reading file `%s'", self->f->filename().c_str());
+    if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught unknown exception while reading object #%" PY_FORMAT_SIZE_T "d from file `%s'", i, self->f->filename().c_str());
+    Py_DECREF(retval);
     return 0;
   }
 
@@ -257,6 +263,220 @@ static PySequenceMethods PyBobIoFile_Sequence = {
     0 /* slice */
 };
 
+static PyObject* PyBobIoFile_Read(PyBobIoFileObject* self, PyObject *args, PyObject* kwds) {
+
+  /* Parses input arguments in a single shot */
+  static const char* const_kwlist[] = {"index", 0};
+  static char** kwlist = const_cast<char**>(const_kwlist);
+
+  Py_ssize_t i = PY_SSIZE_T_MIN;
+  if (!PyArg_ParseTupleAndKeywords(args, kwds, "|n", kwlist, &i)) return 0;
+
+  if (i != PY_SSIZE_T_MIN) {
+
+    // reads a specific object inside the file
+
+    if (i < 0) i += self->f->size();
+
+    if (i < 0 || i >= self->f->size()) {
+      PyErr_Format(PyExc_IndexError, "file index out of range - `%s' only contains %" PY_FORMAT_SIZE_T "d object(s)", self->f->filename().c_str(), self->f->size());
+      return 0;
+    }
+
+    return PyBobIoFile_GetItem(self, i);
+
+  }
+
+  // reads the whole file in a single shot
+
+  const bob::core::array::typeinfo& info = self->f->type_all();
+
+  npy_intp shape[NPY_MAXDIMS];
+  for (int k=0; k<info.nd; ++k) shape[k] = info.shape[k];
+
+  int type_num = PyBobIo_AsTypenum(info.dtype);
+  if (type_num == NPY_NOTYPE) return 0; ///< failure
+
+  PyObject* retval = PyArray_SimpleNew(info.nd, shape, type_num);
+  if (!retval) return 0;
+
+  try {
+    bobskin skin(retval, info.dtype);
+    self->f->read_all(skin);
+  }
+  catch (std::runtime_error& e) {
+    if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught std::runtime_error while reading all contents of file `%s': %s", self->f->filename().c_str(), e.what());
+    Py_DECREF(retval);
+    return 0;
+  }
+  catch (std::exception& e) {
+    if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught std::exception while reading all contents of file `%s': %s", self->f->filename().c_str(), e.what());
+    Py_DECREF(retval);
+    return 0;
+  }
+  catch (...) {
+    if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught unknown while reading all contents of file `%s'", self->f->filename().c_str());
+    Py_DECREF(retval);
+    return 0;
+  }
+
+  return retval;
+
+}
+
+PyDoc_STRVAR(s_read_str, "read");
+PyDoc_STRVAR(s_read_doc,
+"read([index]) -> numpy.ndarray\n\
+\n\
+Reads a specific object in the file, or the whole file.\n\
+\n\
+Parameters:\n\
+\n\
+index\n\
+  [int|long, optional] The index to the object one wishes\n\
+  to retrieve from the file. Negative indexing is supported.\n\
+  If not given, impliess retrieval of the whole file contents.\n\
+\n\
+This method reads data from the file. If you specified an\n\
+index, it reads just the object indicated by the index, as\n\
+you would do using the ``[]`` operator. If an index is\n\
+not specified, reads the whole contents of the file into a\n\
+:py:class:`numpy.ndarray`.\n\
+"
+);
+
+static PyObject* PyBobIoFile_Write(PyBobIoFileObject* self, PyObject *args, PyObject* kwds) {
+  
+  /* Parses input arguments in a single shot */
+  static const char* const_kwlist[] = {"array", 0};
+  static char** kwlist = const_cast<char**>(const_kwlist);
+
+  PyBlitzArrayObject* bz = 0;
+  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&", kwlist, &PyBlitzArray_Converter, &bz)) return 0;
+
+  try {
+    bobskin skin(bz);
+    self->f->write(skin);
+  }
+  catch (std::runtime_error& e) {
+    if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught std::runtime_error while writing to file `%s': %s", self->f->filename().c_str(), e.what());
+    return 0;
+  }
+  catch (std::exception& e) {
+    if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught std::exception while writing to file `%s': %s", self->f->filename().c_str(), e.what());
+    return 0;
+  }
+  catch (...) {
+    if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught unknown while writing to file `%s'", self->f->filename().c_str());
+    return 0;
+  }
+
+  Py_RETURN_NONE;
+
+}
+
+PyDoc_STRVAR(s_write_str, "write");
+PyDoc_STRVAR(s_write_doc,
+"write(array) -> None\n\
+\n\
+Writes the contents of an object to the file.\n\
+\n\
+Parameters:\n\
+\n\
+array\n\
+  [array] The array to be written into the file. It can be a\n\
+  numpy, a blitz array or any other object which can be\n\
+  converted to either of them, as long as the number of\n\
+  dimensions and scalar type are supported by\n\
+  :py:class:`blitz.array`.\n\
+\n\
+This method writes data to the file. It acts like the\n\
+given array is the only piece of data that will ever be written\n\
+to such a file. No more data appending may happen after a call to\n\
+this method.\n\
+"
+);
+
+static PyObject* PyBobIoFile_Append(PyBobIoFileObject* self, PyObject *args, PyObject* kwds) {
+  
+  /* Parses input arguments in a single shot */
+  static const char* const_kwlist[] = {"array", 0};
+  static char** kwlist = const_cast<char**>(const_kwlist);
+
+  PyBlitzArrayObject* bz = 0;
+  if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&", kwlist, &PyBlitzArray_Converter, &bz)) return 0;
+  Py_ssize_t pos = -1;
+
+  try {
+    bobskin skin(bz);
+    pos = self->f->append(skin);
+  }
+  catch (std::runtime_error& e) {
+    if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught std::runtime_error while appending to file `%s': %s", self->f->filename().c_str(), e.what());
+    return 0;
+  }
+  catch (std::exception& e) {
+    if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught std::exception while appending to file `%s': %s", self->f->filename().c_str(), e.what());
+    return 0;
+  }
+  catch (...) {
+    if (!PyErr_Occurred()) PyErr_Format(PyExc_RuntimeError, "caught unknown while appending to file `%s'", self->f->filename().c_str());
+    return 0;
+  }
+
+# if PY_VERSION_HEX >= 0x03000000
+  return PyLong_FromSsize_t(pos);
+# else
+  return PyInt_FromSsize_t(pos);
+# endif
+
+}
+
+PyDoc_STRVAR(s_append_str, "append");
+PyDoc_STRVAR(s_append_doc,
+"append(array) -> int\n\
+\n\
+Adds the contents of an object to the file.\n\
+\n\
+Parameters:\n\
+\n\
+array\n\
+  [array] The array to be added into the file. It can be a\n\
+  numpy, a blitz array or any other object which can be\n\
+  converted to either of them, as long as the number of\n\
+  dimensions and scalar type are supported by\n\
+  :py:class:`blitz.array`.\n\
+\n\
+This method appends data to the file. If the file does not\n\
+exist, creates a new file, else, makes sure that the inserted\n\
+array respects the previously set file structure.\n\
+\n\
+Returns the current position of the newly written array.\n\
+"
+);
+
+static PyMethodDef PyBobIoFile_Methods[] = {
+    {
+      s_read_str,
+      (PyCFunction)PyBobIoFile_Read,
+      METH_VARARGS|METH_KEYWORDS,
+      s_read_doc,
+    },
+    {
+      s_write_str,
+      (PyCFunction)PyBobIoFile_Write,
+      METH_VARARGS|METH_KEYWORDS,
+      s_write_doc,
+    },
+    {
+      s_append_str,
+      (PyCFunction)PyBobIoFile_Append,
+      METH_VARARGS|METH_KEYWORDS,
+      s_append_doc,
+    },
+    {0}  /* Sentinel */
+};
+
 PyTypeObject PyBobIoFile_Type = {
     PyObject_HEAD_INIT(0)
     0,                                          /*ob_size*/
@@ -286,7 +506,7 @@ PyTypeObject PyBobIoFile_Type = {
     0,		                                      /* tp_weaklistoffset */
     0,		                                      /* tp_iter */
     0,		                                      /* tp_iternext */
-    0, //PyBobIoFile_methods,                               /* tp_methods */
+    PyBobIoFile_Methods,                        /* tp_methods */
     0,                                          /* tp_members */
     PyBobIoFile_getseters,                      /* tp_getset */
     0,                                          /* tp_base */
@@ -300,28 +520,6 @@ PyTypeObject PyBobIoFile_Type = {
 };
 
 /**
-static object file_read_all(bob::io::File& f) {
-  bob::python::py_array a(f.type_all());
-  f.read_all(a);
-  return a.pyobject(); //shallow copy
-}
-
-static object file_read(bob::io::File& f, size_t index) {
-  bob::python::py_array a(f.type_all());
-  f.read(a, index);
-  return a.pyobject(); //shallow copy
-}
-
-static void file_write(bob::io::File& f, object array) {
-  bob::python::py_array a(array, object());
-  f.write(a);
-}
-
-static void file_append(bob::io::File& f, object array) {
-  bob::python::py_array a(array, object());
-  f.append(a);
-}
-
 static dict extensions() {
   typedef std::map<std::string, std::string> map_type;
   dict retval;
@@ -338,18 +536,6 @@ void bind_io_file() {
 
     .add_property("type", make_function(&bob::io::File::type, return_value_policy<copy_const_reference>()), "Typing information to load the file as an Arrayset")
 
-    .def("read", &file_read_all, (arg("self")), "Reads the whole contents of the file into a NumPy ndarray")
-    .def("write", &file_write, (arg("self"), arg("array")), "Writes an array into the file, truncating it first")
-
-    .def("__len__", &bob::io::File::size, (arg("self")), "Size of the file if it is supposed to be read as a set of arrays instead of performing a single read")
-
-    .def("read", &file_read, (arg("self"), arg("index")), "Reads a single array from the file considering it to be an arrayset list")
-
-    .def("__getitem__", &file_read, (arg("self"), arg("index")), "Reads a single array from the file considering it to be an arrayset list")
-
-    .def("append", &file_append, (arg("self"), arg("array")), "Appends an array to a file. Compatibility requirements may be enforced.")
-    ;
-
   def("extensions", &extensions, "Returns a dictionary containing all extensions and descriptions currently stored on the global codec registry");
 
 }
diff --git a/xbob/io/fonts/bold.ttf b/xbob/io/fonts/bold.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..8f5a1d3f1edd49cf7a630887f2f61dfe6ebd9fc2
Binary files /dev/null and b/xbob/io/fonts/bold.ttf differ
diff --git a/xbob/io/fonts/bold_italic.ttf b/xbob/io/fonts/bold_italic.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..916e9b351d985581d728574bb8f4766e64f7ffde
Binary files /dev/null and b/xbob/io/fonts/bold_italic.ttf differ
diff --git a/xbob/io/fonts/font_license.txt b/xbob/io/fonts/font_license.txt
new file mode 100644
index 0000000000000000000000000000000000000000..69399804ca6c71fd61ed0ae47535baad3bb55e79
--- /dev/null
+++ b/xbob/io/fonts/font_license.txt
@@ -0,0 +1,97 @@
+Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
+Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below)
+
+Bitstream Vera Fonts Copyright
+------------------------------
+
+Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is
+a trademark of Bitstream, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of the fonts accompanying this license ("Fonts") and associated
+documentation files (the "Font Software"), to reproduce and distribute the
+Font Software, including without limitation the rights to use, copy, merge,
+publish, distribute, and/or sell copies of the Font Software, and to permit
+persons to whom the Font Software is furnished to do so, subject to the
+following conditions:
+
+The above copyright and trademark notices and this permission notice shall
+be included in all copies of one or more of the Font Software typefaces.
+
+The Font Software may be modified, altered, or added to, and in particular
+the designs of glyphs or characters in the Fonts may be modified and
+additional glyphs or characters may be added to the Fonts, only if the fonts
+are renamed to names not containing either the words "Bitstream" or the word
+"Vera".
+
+This License becomes null and void to the extent applicable to Fonts or Font
+Software that has been modified and is distributed under the "Bitstream
+Vera" names.
+
+The Font Software may be sold as part of a larger software package but no
+copy of one or more of the Font Software typefaces may be sold by itself.
+
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
+TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
+FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
+ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
+THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
+FONT SOFTWARE.
+
+Except as contained in this notice, the names of Gnome, the Gnome
+Foundation, and Bitstream Inc., shall not be used in advertising or
+otherwise to promote the sale, use or other dealings in this Font Software
+without prior written authorization from the Gnome Foundation or Bitstream
+Inc., respectively. For further information, contact: fonts at gnome dot
+org. 
+
+Arev Fonts Copyright
+------------------------------
+
+Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the fonts accompanying this license ("Fonts") and
+associated documentation files (the "Font Software"), to reproduce
+and distribute the modifications to the Bitstream Vera Font Software,
+including without limitation the rights to use, copy, merge, publish,
+distribute, and/or sell copies of the Font Software, and to permit
+persons to whom the Font Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright and trademark notices and this permission notice
+shall be included in all copies of one or more of the Font Software
+typefaces.
+
+The Font Software may be modified, altered, or added to, and in
+particular the designs of glyphs or characters in the Fonts may be
+modified and additional glyphs or characters may be added to the
+Fonts, only if the fonts are renamed to names not containing either
+the words "Tavmjong Bah" or the word "Arev".
+
+This License becomes null and void to the extent applicable to Fonts
+or Font Software that has been modified and is distributed under the 
+"Tavmjong Bah Arev" names.
+
+The Font Software may be sold as part of a larger software package but
+no copy of one or more of the Font Software typefaces may be sold by
+itself.
+
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL
+TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
+
+Except as contained in this notice, the name of Tavmjong Bah shall not
+be used in advertising or otherwise to promote the sale, use or other
+dealings in this Font Software without prior written authorization
+from Tavmjong Bah. For further information, contact: tavmjong @ free
+. fr.
\ No newline at end of file
diff --git a/xbob/io/fonts/italic.ttf b/xbob/io/fonts/italic.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..7b8b787525dd555b0c0144d0a8db84f9f317e268
Binary files /dev/null and b/xbob/io/fonts/italic.ttf differ
diff --git a/xbob/io/fonts/regular.ttf b/xbob/io/fonts/regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..ea0bfd8c276fd20bc0f121bf28c9facbda430905
Binary files /dev/null and b/xbob/io/fonts/regular.ttf differ
diff --git a/xbob/io/include/xbob.io/api.h b/xbob/io/include/xbob.io/api.h
index b20aca3fbdb5764013254ca7d20f3699c9e171a8..64f54cfe1cbb4ef11bce8823917c37a2da7c15bc 100644
--- a/xbob/io/include/xbob.io/api.h
+++ b/xbob/io/include/xbob.io/api.h
@@ -12,7 +12,10 @@
 #include <bob/io/File.h>
 #include <boost/preprocessor/stringize.hpp>
 #include <boost/shared_ptr.hpp>
+
+extern "C" {
 #include <Python.h>
+}
 
 #define XBOB_IO_MODULE_PREFIX xbob.io
 #define XBOB_IO_MODULE_NAME _library
@@ -90,7 +93,7 @@ typedef struct {
 #  if defined(PY_ARRAY_UNIQUE_SYMBOL)
 #    define XBOB_IO_MAKE_API_NAME_INNER(a) XBOB_IO_ ## a
 #    define XBOB_IO_MAKE_API_NAME(a) XBOB_IO_MAKE_API_NAME_INNER(a)
-#    define PyBlitzArray_API XBOB_IO_MAKE_API_NAME(PY_ARRAY_UNIQUE_SYMBOL)
+#    define PyXbobIo_API XBOB_IO_MAKE_API_NAME(PY_ARRAY_UNIQUE_SYMBOL)
 #  endif
 
 #  if defined(NO_IMPORT_ARRAY)
@@ -121,7 +124,7 @@ typedef struct {
    * I/O generic bindings *
    ************************/
 
-# define PyBobIo_AsTypenum (*(PyBobIo_AsTypenum_RET (*)PyBobIo_AsTypenum_PROTO) PyBlitzArray_API[PyBobIo_AsTypenum_NUM])
+# define PyBobIo_AsTypenum (*(PyBobIo_AsTypenum_RET (*)PyBobIo_AsTypenum_PROTO) PyXbobIo_API[PyBobIo_AsTypenum_NUM])
 
   /**
    * Returns -1 on error, 0 on success. PyCapsule_Import will set an exception
diff --git a/xbob/io/main.cpp b/xbob/io/main.cpp
index e420b670092eab3fa00b391d8f27b76ead297ef5..b5cccefff89679b9c1a9cb7c218e8c25bb68453c 100644
--- a/xbob/io/main.cpp
+++ b/xbob/io/main.cpp
@@ -7,13 +7,53 @@
 
 #define XBOB_IO_MODULE
 #include <xbob.io/api.h>
+#include <bob/io/CodecRegistry.h>
 
 #ifdef NO_IMPORT_ARRAY
 #undef NO_IMPORT_ARRAY
 #endif
 #include <blitz.array/capi.h>
 
+static PyObject* PyBobIo_Extensions(PyObject*) {
+
+  typedef std::map<std::string, std::string> map_type;
+  const map_type& table = bob::io::CodecRegistry::getExtensions();
+
+  PyObject* retval = PyDict_New();
+  if (!retval) return 0;
+
+  for (auto it=table.begin(); it!=table.end(); ++it) {
+#   if PY_VERSION_HEX >= 0x03000000
+    PyObject* value = PyString_FromString(it->second.c_str());
+#   else
+    PyObject* value = PyUnicode_FromString(it->second.c_str());
+#   endif
+    if (!value) {
+      Py_DECREF(retval);
+      return 0;
+    }
+    PyDict_SetItemString(retval, it->first.c_str(), value);
+    Py_DECREF(value);
+  }
+  return retval;
+
+}
+
+PyDoc_STRVAR(s_extensions_str, "extensions");
+PyDoc_STRVAR(s_extensions_doc,
+"as_blitz(x) -> dict\n\
+\n\
+Returns a dictionary containing all extensions and descriptions\n\
+currently stored on the global codec registry\n\
+");
+
 static PyMethodDef module_methods[] = {
+    {
+      s_extensions_str,
+      (PyCFunction)PyBobIo_Extensions,
+      METH_NOARGS,
+      s_extensions_doc,
+    },
     {0}  /* Sentinel */
 };
 
@@ -38,7 +78,7 @@ PyMODINIT_FUNC ENTRY_FUNCTION(XBOB_IO_MODULE_NAME) (void) {
 
   /* register the types to python */
   Py_INCREF(&PyBobIoFile_Type);
-  PyModule_AddObject(m, "file", (PyObject *)&PyBobIoFile_Type);
+  PyModule_AddObject(m, "File", (PyObject *)&PyBobIoFile_Type);
 
   static void* PyXbobIo_API[PyXbobIo_API_pointers];
 
@@ -60,7 +100,7 @@ PyMODINIT_FUNC ENTRY_FUNCTION(XBOB_IO_MODULE_NAME) (void) {
    * I/O generic bindings *
    ************************/
   
-  PyBlitzArray_API[PyBobIo_AsTypenum_NUM] = (void *)PyBobIo_AsTypenum;
+  PyXbobIo_API[PyBobIo_AsTypenum_NUM] = (void *)PyBobIo_AsTypenum;
 
   /* imports the NumPy C-API */
   import_array();
diff --git a/xbob/io/script/__init__.py b/xbob/io/script/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/xbob/io/script/video_test.py b/xbob/io/script/video_test.py
new file mode 100644
index 0000000000000000000000000000000000000000..f5a9e56aba9582090e816310a321f9a8d70dfd41
--- /dev/null
+++ b/xbob/io/script/video_test.py
@@ -0,0 +1,497 @@
+#!/usr/bin/env python
+# vim: set fileencoding=utf-8 :
+# Andre Anjos <andre.dos.anjos@gmail.com>
+# Thu 14 Mar 17:53:16 2013
+
+"""This program can run manual tests using any video codec available in Bob. It
+can report standard distortion figures and build video test sequences for
+manual inspection. It tries to help identifying problems with:
+
+  1. Color distortion
+  2. Frame skipping or delay
+  3. Encoding or decoding quality
+  4. User test (with a user provided video sample)
+
+You can parameterize the program with the type of file, (FFmpeg) codec and a
+few other parameters. The program then generates artificial input signals to
+test for each of the parameters above.
+"""
+
+import os
+import sys
+import argparse
+import numpy
+
+# internal
+from .. import supported_video_codecs, available_video_codecs, supported_videowriter_formats, available_videowriter_formats
+from .. import utils, create_directories_save
+from ... import version
+from .. import save as save_to_file
+from ...test import utils as test_utils
+from .. import test as io_test
+
+CODECS = supported_video_codecs()
+ALL_CODECS = available_video_codecs()
+
+def list_codecs(*args, **kwargs):
+  retval = """\
+  Supported Codecs:
+  -----------------\n"""
+
+  for k in sorted(CODECS.keys()):
+    retval += ("  %-20s  %s\n" % (k, CODECS[k]['long_name']))[:80]
+
+  return retval[:-1]
+
+def list_all_codecs(*args, **kwargs):
+  retval = """\
+  Available Codecs:
+  -----------------\n"""
+
+  for k in sorted(ALL_CODECS.keys()):
+    retval += ("  %-20s  %s\n" % (k, ALL_CODECS[k]['long_name']))[:80]
+
+  return retval[:-1]
+
+FORMATS = supported_videowriter_formats()
+ALL_FORMATS = available_videowriter_formats()
+
+def list_formats(*args, **kwargs):
+
+  retval = """\
+  Supported Formats:
+  ------------------\n"""
+
+  for k in sorted(FORMATS.keys()):
+    retval += ("  %-20s  %s\n" % (k, FORMATS[k]['long_name']))[:80]
+
+  return retval[:-1]
+
+def list_all_formats(*args, **kwargs):
+
+  retval = """\
+  Available Formats:
+  ------------------\n"""
+
+  for k in sorted(ALL_FORMATS.keys()):
+    retval += ("  %-20s  %s\n" % (k, ALL_FORMATS[k]['long_name']))[:80]
+
+  return retval[:-1]
+
+__epilog__ = """Example usage:
+
+1. Check for color distortion using H.264 codec in a .mov video container:
+
+  $ %(prog)s --format='mov' --codec='h264' color
+
+2. Check for frame skipping using MJPEG codec in an .avi video container:
+
+  $ %(prog)s --format='avi' --codec='mjpeg' frameskip
+
+3. Check for encoding/decoding quality using a FFV1 codec in a '.flv' video
+container (not supported - note the usage of the '--force' flag):
+
+  $ %(prog)s --force --format='flv' --codec='ffv1' noise
+
+4. To run-only the user-video test and provide a test video:
+
+  $ %(prog)s --format='mov' --user-video=test_sample.avi user
+
+5. To list all available codecs:
+
+  $ %(prog)s --list-codecs
+
+6. To list all available formats:
+
+  $ %(prog)s --list-formats
+
+7. Run all tests for all **supported** codecs and formats:
+
+  $ %(prog)s
+""" % {
+    'prog': os.path.basename(sys.argv[0]),
+    }
+
+def user_video(original, max_frames, format, codec, filename):
+  """Returns distortion patterns for a set of frames with moving colors.
+
+  Keyword parameters:
+
+  original
+    The name (path) to the original user file that will be used for the test
+
+  max_frames
+    The maximum number of frames to read from user input
+
+  format
+    The string that identifies the format to be used for the output file
+
+  codec
+    The codec to be used for the output file
+
+  filename
+    The name (path) of the file to use for encoding the test
+  """
+
+  from .. import VideoReader, VideoWriter
+  vreader = VideoReader(original, check=True)
+  orig = vreader[:max_frames]
+
+  # rounding frame rate - some older codecs do not accept random frame rates
+  framerate = vreader.frame_rate
+  if codec in ('mpegvideo', 'mpeg1video', 'mpeg2video'):
+    import math
+    framerate = math.ceil(vreader.frame_rate)
+
+  vwriter = VideoWriter(filename, vreader.height, vreader.width,
+      framerate, codec=codec, format=format, check=False)
+  for k in orig: vwriter.append(k)
+  del vwriter
+  return orig, framerate, VideoReader(filename, check=False)
+
+def summarize(function, shape, framerate, format, codec, output=None):
+  """Summarizes distortion patterns for a given set of video settings and
+  for a given input function.
+
+  Keyword parameters:
+
+  shape (int, int, int)
+    The length (number of frames), height and width for the generated sequence
+
+  format
+    The string that identifies the format to be used for the output file
+
+  codec
+    The codec to be used for the output file
+
+  output
+    If set, the video is not created on the temporary directory, but it is
+    saved on the advised location. This must be a filename.
+
+  Returns a single a single string summarizing the distortion results
+  """
+
+  length, height, width = shape
+
+  if output:
+    fname = output
+  else:
+    fname = test_utils.temporary_filename(suffix='.%s' % format)
+
+  retval = "did not run"
+
+  try:
+    # Width and height should be powers of 2 as the encoded image is going
+    # to be approximated to the closest one, would not not be the case.
+    # In this case, the encoding is subject to more noise as the filtered,
+    # final image that is encoded will contain added noise on the extra
+    # borders.
+    orig, framerate, encoded = function(shape, framerate, format, codec, fname)
+
+    tmp = []
+    for k, of in enumerate(orig):
+      tmp.append(abs(of.astype('float64')-encoded[k].astype('float64')).sum())
+    size = numpy.prod(orig[0].shape)
+    S = sum(tmp)/size
+    M = S/len(tmp)
+    Min = min(tmp)/size
+    Max = max(tmp)/size
+    ArgMin = tmp.index(min(tmp))
+    ArgMax = tmp.index(max(tmp))
+    retval = "%.3f min=%.3f@%d max=%.3f@%d" % (M, Min, ArgMin, Max, ArgMax)
+    if abs(encoded.frame_rate - framerate) > 0.01:
+      retval += " !FR(%g)" % abs(encoded.frame_rate - framerate)
+    if len(encoded) != len(orig):
+      retval += " !LEN(%d)" % len(encoded)
+
+  finally:
+
+    if os.path.exists(fname) and output is None: os.unlink(fname)
+
+  if output:
+    return retval, orig, encoded
+  else:
+    return retval
+
+def detail(function, shape, framerate, format, codec, outdir):
+  """Summarizes distortion patterns for a given set of video settings and
+  for a given input function.
+
+  Keyword parameters:
+
+  shape (int, int, int)
+    The length (number of frames), height and width for the generated sequence
+
+  format
+    The string that identifies the format to be used for the output file
+
+  codec
+    The codec to be used for the output file
+
+  outdir
+    We will save all analysis for this sequence on the given output directory.
+
+  Returns a single a single string summarizing the distortion results.
+  """
+
+  length, height, width = shape
+
+  text_format = "%%0%dd" % len(str(length-1))
+
+  output = os.path.join(outdir, "video." + format)
+  retval, orig, encoded = summarize(function, shape, framerate,
+      format, codec, output)
+
+  length, _, height, width = orig.shape
+
+  # save original, reloaded and difference images on output directories
+  for i, orig_frame in enumerate(orig):
+    out = numpy.ndarray((3, height, 3*width), dtype='uint8')
+    out[:,:,:width] = orig_frame
+    out[:,:,width:(2*width)] = encoded[i]
+    diff = abs(encoded[i].astype('int64')-orig_frame.astype('int64'))
+    diff[diff>0] = 255 #binary output
+    out[:,:,(2*width):] = diff.astype('uint8')
+    save_to_file(out, os.path.join(outdir, 'frame-' + (text_format%i) + '.png'))
+
+  return retval
+
+def main(user_input=None):
+
+  parser = argparse.ArgumentParser(description=__doc__, epilog=__epilog__,
+      formatter_class=argparse.RawDescriptionHelpFormatter)
+
+  name = os.path.basename(os.path.splitext(sys.argv[0])[0])
+  version_info = 'Video Encoding/Decoding Test Tool v%s (%s)' % (version, name)
+  parser.add_argument('-V', '--version', action='version', version=version_info)
+
+  test_choices = [
+      'color',
+      'frameskip',
+      'noise',
+      'user',
+      ]
+
+  parser.add_argument("test", metavar='TEST', type=str, nargs='*',
+      default=test_choices, help="The name of the test or tests you want to run. Choose between `%s'. If none given, run through all." % ('|'.join(test_choices)))
+
+  supported_codecs = sorted(CODECS.keys())
+  available_codecs = sorted(ALL_CODECS.keys())
+
+  parser.add_argument("-c", "--codec", metavar='CODEC', type=str, nargs='*',
+      default=supported_codecs, choices=available_codecs, help="The name of the codec you want to test with. For a list of available codecs, look below. If none given, run through all.")
+  parser.add_argument("--list-codecs", action="store_true", default=False,
+      help="List all supported codecs and exits")
+  parser.add_argument("--list-all-codecs", action="store_true", default=False,
+      help="List all available codecs and exits")
+
+  supported_formats = sorted(FORMATS.keys())
+  available_formats = sorted(ALL_FORMATS.keys())
+  parser.add_argument("-f", "--format", metavar='FORMAT', type=str, nargs='*',
+      default=supported_formats, choices=available_formats, help="The name of the format you want to test with. For a list of available formats, look below. If none given, run through all.")
+  parser.add_argument("--list-formats", action="store_true", default=False,
+      help="List all supported formats and exits")
+  parser.add_argument("--list-all-formats", action="store_true", default=False,
+      help="List all available formats and exits")
+
+  parser.add_argument("-F", "--force", action="store_true", default=False,
+      help="Force command execution (possibly generating an error) even if the format or the combination of format+codec is not supported. This flag is needed in case you need to test new formats or combinations of formats and codecs which are unsupported by the build")
+
+  parser.add_argument("-t", "--height", metavar="INT", type=int,
+      default=128, help="Height of the test video (defaults to %(default)s pixels). Note this number has to be even.")
+
+  parser.add_argument("-w", "--width", metavar="INT", type=int,
+      default=128, help="Width of the test video (defaults to %(default)s pixels). Note this number has to be even.")
+
+  parser.add_argument("-l", "--length", metavar="INT", type=int,
+      default=30, help="Length of the test sequence (defaults to %(default)s frames). The longer, the more accurate the test becomes.")
+
+  parser.add_argument("-r", "--framerate", metavar="FLOAT", type=float,
+      default=30., help="Framerate to be used on the test videos (defaults to %(default)s Hz).")
+
+  parser.add_argument("-o", "--output", type=str,
+      help="If set, then videos created for the tests are stored on the given directory. By default this option is empty and videos are created on a temporary directory and deleted after tests are done. If you set it, we also produced detailed output analysis for manual inspection.")
+
+  parser.add_argument("-u", "--user-video", type=str, metavar="PATH",
+      help="Set the path to the user video that will be used for distortion tests (if not set use default test video)")
+
+  parser.add_argument("-n", "--user-frames", type=int, default=10, metavar="INT", help="Set the number of maximum frames to read from the user video (reads %(default)s by default)")
+
+  args = parser.parse_args(args=user_input)
+
+  # manual check because of argparse limitation
+  for t in args.test:
+    if t not in test_choices:
+      parser.error("invalid test choice: '%s' (choose from %s)" % \
+          (t, ", ".join(["'%s'" % k for k in test_choices])))
+
+  if not args.test: args.test = test_choices
+
+  if args.list_codecs:
+    print(list_codecs())
+    sys.exit(0)
+
+  if args.list_all_codecs:
+    print(list_all_codecs())
+    sys.exit(0)
+
+  if args.list_formats:
+    print(list_formats())
+    sys.exit(0)
+
+  if args.list_all_formats:
+    print(list_all_formats())
+    sys.exit(0)
+
+  if 'user' in args.test and args.user_video is None:
+    # in this case, take our standard video test
+    args.user_video = test_utils.datafile('test.mov', io_test.__name__)
+
+  def wrap_user_function(shape, framerate, format, codec, filename):
+    return user_video(args.user_video, args.user_frames, format, codec, filename)
+
+  # mapping between test name and function
+  test_function = {
+      'color': (utils.color_distortion, 'C'),
+      'frameskip': (utils.frameskip_detection, 'S'),
+      'noise': (utils.quality_degradation, 'N'),
+      'user': (wrap_user_function, 'U'),
+      }
+
+  # result table
+  table = {}
+
+  # report results in a readable way
+  print(version_info)
+  print("Settings:")
+  print("  Width    : %d pixels" % args.width)
+  print("  Height   : %d pixels" % args.height)
+  print("  Length   : %d frames" % args.length)
+  print("  Framerate: %f Hz"     % args.framerate)
+
+  print("Legend:")
+  for k, (f, code) in test_function.items():
+    print("  %s: %s test" % (code, k.capitalize()))
+
+  sys.stdout.write("Running %d test(s)..." %
+      (len(args.test)*len(args.format)*len(args.codec)))
+  sys.stdout.flush()
+
+  # run tests
+  need_notes = False
+  for test in args.test:
+    test_table = table.setdefault(test, {})
+    f, code = test_function[test]
+
+    for format in args.format:
+      format_table = test_table.setdefault(format, {})
+
+      for codec in args.codec:
+
+        # cautionary settings
+        notes = ""
+        if format not in FORMATS:
+          if args.force:
+            notes += "[!F] "
+            need_notes = True
+
+          else:
+            sys.stdout.write(code)
+            sys.stdout.flush()
+            format_table[codec] = "unsupported format"
+            continue
+
+        else:
+          if codec not in FORMATS[format]['supported_codecs']:
+            if args.force:
+              notes += "[!F+C] "
+              need_notes = True
+
+            else:
+              sys.stdout.write(code)
+              sys.stdout.flush()
+              format_table[codec] = "format+codec unsupported"
+              continue
+
+        if args.output:
+
+          size = '%dx%dx%d@%gHz' % (args.length, args.height, args.width,
+              args.framerate)
+          outdir = os.path.join(args.output, test, codec, size, format)
+          create_directories_save(outdir)
+
+          try:
+            result = detail(f, (args.length, args.height, args.width),
+                args.framerate, format, codec, outdir)
+            sys.stdout.write(code)
+            sys.stdout.flush()
+          except Exception as e:
+            result = str(e)
+            sys.stdout.write(code)
+            sys.stdout.flush()
+          finally:
+            format_table[codec] = notes + result
+
+        else:
+
+          try:
+            result = summarize(f, (args.length, args.height, args.width),
+                args.framerate, format, codec)
+            sys.stdout.write(code)
+            sys.stdout.flush()
+          except Exception as e:
+            result = str(e)
+            sys.stdout.write(code)
+            sys.stdout.flush()
+          finally:
+            format_table[codec] = notes + result
+
+  sys.stdout.write("\n")
+  sys.stdout.flush()
+
+  # builds a nicely organized dynamically sized table
+  test_size   = max([len(k) for k in args.test] + [len('test')])
+  fmt_size    = max([len(k) for k in args.format] + [len('fmt')])
+  codec_size  = max([len(k) for k in args.codec] + [len('codec')])
+  figure_size = 79 - (test_size + 3 + fmt_size + 3 + codec_size + 3 + 2)
+  if figure_size <= 0: figure_size = 40
+
+  test_cover   = (test_size + 2) * '='
+  fmt_cover    = (fmt_size + 2) * '='
+  codec_cover  = (codec_size + 2) * '='
+  figure_cover = (figure_size + 2) * '='
+
+  sep  = test_cover + ' ' + fmt_cover + ' ' + codec_cover + ' ' + figure_cover
+  line = " %s   %s   %s   %s"
+
+  print("")
+  print(sep)
+  print(line % (
+      'test'.ljust(test_size),
+      'fmt'.center(fmt_size),
+      'codec'.center(codec_size),
+      'figure (lower means better quality)'.ljust(figure_size),
+      ))
+  print(sep)
+
+  for test in sorted(table.keys()):
+    test_table = table[test]
+    for format in sorted(test_table.keys()):
+      format_table = test_table[format]
+      for codec in sorted(format_table.keys()):
+        figure = format_table[codec]
+        print(line % (
+            test.ljust(test_size),
+            format.center(fmt_size),
+            codec.ljust(codec_size),
+            figure.ljust(figure_size),
+            ))
+
+  print(sep)
+
+  # only printed if unsupported combinations of formats and codecs are used
+  if need_notes:
+    print("")
+    print("Notes:")
+    print("  [!F] Format is available, but not supported by this build")
+    print("  [!F+C] Format is supported, but not in combination with this codec")
diff --git a/xbob/io/test/test_examples.py b/xbob/io/test/test_examples.py
deleted file mode 100644
index 3b1afdb4d88297ef542d57cf387612a4ae4a28c8..0000000000000000000000000000000000000000
--- a/xbob/io/test/test_examples.py
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/usr/bin/env python
-# vim: set fileencoding=utf-8 :
-# Andre Anjos <andre.anjos@idiap.ch>
-# Tue 21 Aug 2012 13:20:38 CEST
-
-"""Tests various examples for bob.io
-"""
-
-from ...test import utils
-
-@utils.ffmpeg_found()
-def test_video2frame():
-
-  movie = utils.datafile('test.mov', __name__)
-
-  from ..example.video2frame import main
-  cmdline = ['--self-test', movie]
-  assert main(cmdline) == 0
diff --git a/xbob/io/test/test_file.py b/xbob/io/test/test_file.py
index a5934f255176c62c3b4c45c611bffa72a4e4f198..643bc7c02f35c2d5cd1bbcb03c8fb0bd8cc7afb5 100644
--- a/xbob/io/test/test_file.py
+++ b/xbob/io/test/test_file.py
@@ -4,18 +4,6 @@
 # Wed Nov 16 13:27:15 2011 +0100
 #
 # Copyright (C) 2011-2013 Idiap Research Institute, Martigny, Switzerland
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, version 3 of the License.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 """A combined test for all built-in types of Array/interaction in
 python.
@@ -27,7 +15,7 @@ import numpy
 import nose.tools
 
 from .. import load, write, File
-from ...test import utils as testutils
+from . import utils as testutils
 
 def transcode(filename):
   """Runs a complete transcoding test, to and from the binary format."""
diff --git a/xbob/io/test/test_image.py b/xbob/io/test/test_image.py
index 4810b37e1817086fecddafd93dc1aefcc5d934b5..5d3dc242c62e2d98054447212b0ea91964921739 100644
--- a/xbob/io/test/test_image.py
+++ b/xbob/io/test/test_image.py
@@ -20,11 +20,7 @@
 """Runs some image tests
 """
 
-import os
-import sys
-import numpy
-
-from ...test import utils as testutils
+from . import utils as testutils
 
 # These are some global parameters for the test.
 PNG_INDEXED_COLOR = testutils.datafile('img_indexed_color.png', __name__)
diff --git a/xbob/io/test/test_video.py b/xbob/io/test/test_video.py
index 5b640c16312696d67bc0ccfcd9590526de27b90c..65991b00f6ef5cdbcaa9d07563dc3873485b150a 100644
--- a/xbob/io/test/test_video.py
+++ b/xbob/io/test/test_video.py
@@ -23,8 +23,8 @@
 import os
 import sys
 import numpy
-from ...test import utils as testutils
-from .. import supported_videowriter_formats
+from . import utils as testutils
+from .._library import supported_videowriter_formats
 from ..utils import color_distortion, frameskip_detection, quality_degradation
 
 # These are some global parameters for the test.
diff --git a/xbob/io/test/utils.py b/xbob/io/test/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..34f103ced60ae27a56c8bba549f70d9159f861bf
--- /dev/null
+++ b/xbob/io/test/utils.py
@@ -0,0 +1,147 @@
+#!/usr/bin/env python
+# vim: set fileencoding=utf-8 :
+# Andre Anjos <andre.anjos@idiap.ch>
+# Thu Feb  7 09:58:22 2013
+
+"""Re-usable decorators and utilities for xbob test code
+"""
+
+import os
+import functools
+import nose.plugins.skip
+from distutils.version import StrictVersion as SV
+
+def datafile(f, module=None, path='data'):
+  """Returns the test file on the "data" subdirectory of the current module.
+
+  Keyword attributes
+
+  f: str
+    This is the filename of the file you want to retrieve. Something like
+    ``'movie.avi'``.
+
+  package: string, optional
+    This is the python-style package name of the module you want to retrieve
+    the data from. This should be something like ``xbob.io.test``, but you
+    normally refer it using the ``__name__`` property of the module you want to
+    find the path relative to.
+
+  path: str, optional
+    This is the subdirectory where the datafile will be taken from inside the
+    module. Normally (the default) ``data``. It can be set to ``None`` if it
+    should be taken from the module path root (where the ``__init__.py`` file
+    sits).
+
+  Returns the full path of the file.
+  """
+
+  resource = __name__ if module is None else module
+  final_path = f if path is None else os.path.join(path, f)
+  return __import__('pkg_resources').resource_filename(resource, final_path)
+
+def temporary_filename(prefix='bobtest_', suffix='.hdf5'):
+  """Generates a temporary filename to be used in tests"""
+
+  (fd, name) = __import__('tempfile').mkstemp(suffix, prefix)
+  os.close(fd)
+  os.unlink(name)
+  return name
+
+# Here is a table of ffmpeg versions against libavcodec, libavformat and
+# libavutil versions
+ffmpeg_versions = {
+    '0.5':  [ SV('52.20.0'),   SV('52.31.0'),   SV('49.15.0')   ],
+    '0.6':  [ SV('52.72.2'),   SV('52.64.2'),   SV('50.15.1')   ],
+    '0.7':  [ SV('52.122.0'),  SV('52.110.0'),  SV('50.43.0')   ],
+    '0.8':  [ SV('53.7.0'),    SV('53.4.0'),    SV('51.9.1')    ],
+    '0.9':  [ SV('53.42.0'),   SV('53.24.0'),   SV('51.32.0')   ],
+    '0.10': [ SV('53.60.100'), SV('53.31.100'), SV('51.34.101') ],
+    '0.11': [ SV('54.23.100'), SV('54.6.100'),  SV('51.54.100') ],
+    '1.0':  [ SV('54.59.100'), SV('54.29.104'), SV('51.73.101') ],
+    '1.1':  [ SV('54.86.100'), SV('54.59.106'), SV('52.13.100') ],
+    }
+
+def ffmpeg_version_lessthan(v):
+  '''Returns true if the version of ffmpeg compiled-in is at least the version
+  indicated as a string parameter.'''
+
+  from ..io._io import version
+  avcodec_inst= SV(version['FFmpeg']['avcodec'])
+  avcodec_req = ffmpeg_versions[v][0]
+  return avcodec_inst < avcodec_req
+
+def ffmpeg_found(version_geq=None):
+  '''Decorator to check if a codec is available before enabling a test
+
+  To use this, decorate your test routine with something like:
+
+  .. code-block:: python
+
+    @ffmpeg_found()
+
+  You can pass an optional string to require that the FFMpeg version installed
+  is greater or equal that version identifier. For example:
+
+  .. code-block:: python
+
+    @ffmpeg_found('0.10') #requires at least version 0.10
+
+  Versions you can test for are set in the ``ffmpeg_versions`` dictionary in
+  this module.
+  '''
+
+  def test_wrapper(test):
+
+    @functools.wraps(test)
+    def wrapper(*args, **kwargs):
+      try:
+        from ..io._io import version
+        avcodec_inst= SV(version['FFmpeg']['avcodec'])
+        avformat_inst= SV(version['FFmpeg']['avformat'])
+        avutil_inst= SV(version['FFmpeg']['avutil'])
+        if version_geq is not None:
+          avcodec_req,avformat_req,avutil_req = ffmpeg_versions[version_geq]
+          if avcodec_inst < avcodec_req:
+            raise nose.plugins.skip.SkipTest('FFMpeg/libav version installed (%s) is smaller than required for this test (%s)' % (version['FFmpeg']['ffmpeg'], version_geq))
+        return test(*args, **kwargs)
+      except KeyError:
+        raise nose.plugins.skip.SkipTest('FFMpeg was not available at compile time')
+
+    return wrapper
+
+  return test_wrapper
+
+def codec_available(codec):
+  '''Decorator to check if a codec is available before enabling a test'''
+
+  def test_wrapper(test):
+
+    @functools.wraps(test)
+    def wrapper(*args, **kwargs):
+      from ..io import supported_video_codecs
+      d = supported_video_codecs()
+      if codec in d and d[codec]['encode'] and d[codec]['decode']:
+        return test(*args, **kwargs)
+      else:
+        raise nose.plugins.skip.SkipTest('A functional codec for "%s" is not installed with FFmpeg' % codec)
+
+    return wrapper
+
+  return test_wrapper
+
+def extension_available(extension):
+  '''Decorator to check if a extension is available before enabling a test'''
+
+  def test_wrapper(test):
+
+    @functools.wraps(test)
+    def wrapper(*args, **kwargs):
+      from .._library import extensions
+      if extension in extensions():
+        return test(*args, **kwargs)
+      else:
+        raise nose.plugins.skip.SkipTest('Extension to handle "%s" files was not available at compile time' % extension)
+
+    return wrapper
+
+  return test_wrapper
diff --git a/xbob/io/utils.py b/xbob/io/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..a60aec7b9e513be8553ca027a2fa842dc0332cdf
--- /dev/null
+++ b/xbob/io/utils.py
@@ -0,0 +1,198 @@
+#!/usr/bin/env python
+# vim: set fileencoding=utf-8 :
+# Andre Anjos <andre.dos.anjos@gmail.com>
+# Thu 14 Mar 17:00:58 2013
+
+"""Some utilities to generate fake patterns
+"""
+
+import numpy
+
+DEFAULT_FONT = __import__('pkg_resources').resource_filename(__name__,
+    __import__('os').path.join("fonts", "regular.ttf"))
+
+def estimate_fontsize(height, width, format):
+  """Estimates the best fontsize to fit into a image that is (height, width)"""
+
+  try:
+    # if PIL is installed this works:
+    import Image, ImageFont, ImageDraw
+  except ImportError:
+    # if Pillow is installed, this works better:
+    from PIL import Image, ImageFont, ImageDraw
+
+  best_size = min(height, width)
+  fit = False
+  while best_size > 0:
+    font = ImageFont.truetype(DEFAULT_FONT, best_size)
+    (text_width, text_height) = font.getsize(format % 0)
+    if text_width < width and text_height < height: break
+    best_size -= 1
+
+  if best_size <= 0:
+    raise RuntimeError("Cannot find best size for font")
+
+  return best_size
+
+def print_numbers(frame, counter, format, fontsize):
+  """Generates an image that serves as a test pattern for encoding/decoding and
+  accuracy tests."""
+
+  try:
+    # if PIL is installed this works:
+    import Image, ImageFont, ImageDraw
+  except ImportError:
+    # if Pillow is installed, this works better:
+    from PIL import Image, ImageFont, ImageDraw
+
+  _, height, width = frame.shape
+
+  # text at the center, indicating the frame number
+  text = format % counter
+  dim = min(width, height)
+  font = ImageFont.truetype(DEFAULT_FONT, fontsize)
+  (text_width, text_height) = font.getsize(text)
+  x_pos = int((width - text_width) / 2)
+  y_pos = int((height - text_height) / 2)
+  # this is buggy in Pillow-2.0.0, so we do it manually
+  #img = Image.fromarray(frame.transpose(1,2,0))
+  img = Image.fromstring('RGB', (frame.shape[1], frame.shape[2]), frame.transpose(1,2,0).tostring())
+  draw = ImageDraw.Draw(img)
+  draw.text((x_pos, y_pos), text, font=font, fill=(255,255,255))
+  return numpy.asarray(img).transpose(2,0,1)
+
+def generate_colors(height, width, shift):
+  """Generates an image that serves as a test pattern for encoding/decoding and
+  accuracy tests."""
+
+  retval = numpy.ndarray((3, height, width), dtype='uint8')
+
+  # standard color test pattern
+  w = width / 7; w2 = 2*w; w3 = 3*w; w4 = 4*w; w5 = 5*w; w6 = 6*w
+  retval[0,:,0:w]   = 255; retval[1,:,0:w]   = 255; retval[2,:,0:w]   = 255;
+  retval[0,:,w:w2]  = 255; retval[1,:,w:w2]  = 255; retval[2,:,w:w2]  = 0;
+  retval[0,:,w2:w3] = 0;   retval[1,:,w2:w3] = 255; retval[2,:,w2:w3] = 255;
+  retval[0,:,w3:w4] = 0;   retval[1,:,w3:w4] = 255; retval[2,:,w3:w4] = 0;
+  retval[0,:,w4:w5] = 255; retval[1,:,w4:w5] = 0;   retval[2,:,w4:w5] = 255;
+  retval[0,:,w5:w6] = 255; retval[1,:,w5:w6] = 0;   retval[2,:,w5:w6] = 0;
+  retval[0,:,w6:]   = 0;   retval[1,:,w6:]  = 0;   retval[2,:,w6:]   = 255;
+
+  # rotate by 'shift'
+  retval = numpy.roll(retval, shift, axis=2)
+  return retval
+
+def color_distortion(shape, framerate, format, codec, filename):
+  """Returns distortion patterns for a set of frames with moving colors.
+
+  Keyword parameters:
+
+  shape (int, int, int)
+    The length (number of frames), height and width for the generated sequence
+
+  format
+    The string that identifies the format to be used for the output file
+
+  codec
+    The codec to be used for the output file
+
+  filename
+    The name of the file to use for encoding the test
+  """
+
+  length, height, width = shape
+  from . import VideoReader, VideoWriter
+  outv = VideoWriter(filename, height, width, framerate, codec=codec,
+      format=format, check=False)
+  orig = []
+  text_format = "%%0%dd" % len(str(length-1))
+  fontsize = estimate_fontsize(height, width, text_format)
+  fontsize = int(fontsize/4)
+  for i in range(0, length):
+    newframe = generate_colors(height, width, i%width)
+    newframe = print_numbers(newframe, i, text_format, fontsize)
+    outv.append(newframe)
+    orig.append(newframe)
+  outv.close()
+  orig = numpy.array(orig, dtype='uint8')
+  return orig, framerate, VideoReader(filename, check=False)
+
+def frameskip_detection(shape, framerate, format, codec, filename):
+  """Returns distortion patterns for a set of frames with big numbers.
+
+  Keyword parameters:
+
+  shape (int, int, int)
+    The length (number of frames), height and width for the generated sequence
+
+  format
+    The string that identifies the format to be used for the output file
+
+  codec
+    The codec to be used for the output file
+
+  filename
+    The name of the file to use for encoding the test
+  """
+
+  length, height, width = shape
+  from . import VideoReader, VideoWriter
+  text_format = "%%0%dd" % len(str(length-1))
+  fontsize = estimate_fontsize(height, width, text_format)
+  outv = VideoWriter(filename, height, width, framerate, codec=codec,
+      format=format, check=False)
+  orig = []
+  for i in range(0, length):
+    newframe = numpy.zeros((3, height, width), dtype='uint8')
+    newframe = print_numbers(newframe, i, text_format, fontsize)
+    outv.append(newframe)
+    orig.append(newframe)
+  outv.close()
+  orig = numpy.array(orig, dtype='uint8')
+  return orig, framerate, VideoReader(filename, check=False)
+
+def quality_degradation(shape, framerate, format, codec, filename):
+  """Returns noise patterns for a set of frames.
+
+  Keyword parameters:
+
+  shape (int, int, int)
+    The length (number of frames), height and width for the generated sequence
+
+  format
+    The string that identifies the format to be used for the output file
+
+  codec
+    The codec to be used for the output file
+
+  filename
+    The name of the file to use for encoding the test
+  """
+
+  length, height, width = shape
+  from . import VideoReader, VideoWriter
+  text_format = "%%0%dd" % len(str(length-1))
+  fontsize = estimate_fontsize(height, width, text_format)
+  fontsize = int(fontsize/4)
+  outv = VideoWriter(filename, height, width, framerate, codec=codec,
+      format=format, check=False)
+  orig = []
+  for i in range(0, length):
+    newframe = numpy.random.randint(0, 256, (3, height, width)).astype('uint8')
+    newframe = print_numbers(newframe, i, text_format, fontsize)
+    outv.append(newframe)
+    orig.append(newframe)
+  outv.close()
+  orig = numpy.array(orig, dtype='uint8')
+  return orig, framerate, VideoReader(filename, check=False)
+
+def is_string(s):
+  """Returns ``True`` if the given object is a string
+
+  This method can be used with Python-2.x or 3.x and returns a string
+  respecting each environment's constraints.
+  """
+
+  from sys import version_info
+
+  return (version_info[0] < 3 and isinstance(s, (str, unicode))) or \
+    isinstance(s, (bytes, str))