/* vi:set et ai sw=2 sts=2 ts=2: */
/*-
 * Copyright (c) 2005-2007 Benedikt Meurer <benny@xfce.org>
 * Copyright (c) 2009 Jannis Pohlmann <jannis@xfce.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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 Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General 
 * Public License along with this library; if not, write to the 
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 * Based on code written by Alexander Larsson <alexl@redhat.com>
 * for libgnomeui.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#ifdef HAVE_SYS_MMAN_H
#include <sys/mman.h>
#endif
#ifdef HAVE_SYS_STAT_H
#include <sys/stat.h>
#endif
#ifdef HAVE_SYS_TYPES_H
#include <sys/types.h>
#endif

#ifdef HAVE_FCNTL_H
#include <fcntl.h>
#endif
#ifdef HAVE_MEMORY_H
#include <memory.h>
#endif
#ifdef HAVE_SETJMP_H
#include <setjmp.h>
#endif
#ifdef HAVE_STDIO_H
#include <stdio.h>
#endif
#ifdef HAVE_STDLIB_H
#include <stdlib.h>
#endif
#ifdef HAVE_STRING_H
#include <string.h>
#endif
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#include <math.h>

#include <jpeglib.h>

#include <glib.h>
#include <glib/gi18n.h>
#include <glib-object.h>

#include <gdk-pixbuf/gdk-pixbuf.h>

#include <tumbler/tumbler.h>

#include <jpeg-thumbnailer/jpeg-thumbnailer.h>



static void fatal_error_handler     (j_common_ptr                cinfo) G_GNUC_NORETURN;
static void jpeg_thumbnailer_create (TumblerAbstractThumbnailer *thumbnailer,
                                     GCancellable               *cancellable,
                                     TumblerFileInfo            *info);



struct _JPEGThumbnailerClass
{
  TumblerAbstractThumbnailerClass __parent__;
};

struct _JPEGThumbnailer
{
  TumblerAbstractThumbnailer __parent__;
};



G_DEFINE_DYNAMIC_TYPE (JPEGThumbnailer, 
                       jpeg_thumbnailer,
                       TUMBLER_TYPE_ABSTRACT_THUMBNAILER);



void
jpeg_thumbnailer_register (TumblerProviderPlugin *plugin)
{
  jpeg_thumbnailer_register_type (G_TYPE_MODULE (plugin));
}



static void
jpeg_thumbnailer_class_init (JPEGThumbnailerClass *klass)
{
  TumblerAbstractThumbnailerClass *abstractthumbnailer_class;

  abstractthumbnailer_class = TUMBLER_ABSTRACT_THUMBNAILER_CLASS (klass);
  abstractthumbnailer_class->create = jpeg_thumbnailer_create;
}



static void
jpeg_thumbnailer_class_finalize (JPEGThumbnailerClass *klass)
{
}



static void
jpeg_thumbnailer_init (JPEGThumbnailer *thumbnailer)
{
}



static void
tvtj_noop (void)
{
}



typedef struct
{
  struct jpeg_error_mgr mgr;
  jmp_buf               setjmp_buffer;
} TvtjErrorHandler;



static void
fatal_error_handler (j_common_ptr cinfo)
{
  TvtjErrorHandler *handler = (TvtjErrorHandler *) cinfo->err;
  longjmp (handler->setjmp_buffer, 1);
}



static gboolean
tvtj_fill_input_buffer (j_decompress_ptr cinfo)
{
  struct jpeg_source_mgr *source = cinfo->src;

  /* return a fake EOI marker so we will eventually terminate */
  if (G_LIKELY (source->bytes_in_buffer == 0))
    {
      static const JOCTET FAKE_EOI[2] =
      {
        (JOCTET) 0xff,
        (JOCTET) JPEG_EOI,
      };

      source->next_input_byte = FAKE_EOI;
      source->bytes_in_buffer = G_N_ELEMENTS (FAKE_EOI);
    }

  return TRUE;
}



static void
tvtj_skip_input_data (j_decompress_ptr cinfo,
                      glong            num_bytes)
{
  struct jpeg_source_mgr *source = cinfo->src;

  if (G_LIKELY (num_bytes > 0))
    {
      num_bytes = MIN (num_bytes, (glong) source->bytes_in_buffer);

      source->next_input_byte += num_bytes;
      source->bytes_in_buffer -= num_bytes;
    }
}



static inline gint
tvtj_denom (gint width,
            gint height,
            gint size)
{
  if (width > size * 8 && height > size * 8)
    return 8;
  else if (width > size * 4 && height > size * 4)
    return 4;
  else if (width > size * 2 && height > size * 2)
    return 2;
  else
    return 1;
}



static inline void
tvtj_convert_cmyk_to_rgb (j_decompress_ptr cinfo,
                          guchar          *line)
{
  guchar *p;
  gint    c, k, m, n, y;

  g_return_if_fail (cinfo != NULL);
  g_return_if_fail (cinfo->output_components == 4);
  g_return_if_fail (cinfo->out_color_space == JCS_CMYK);

  for (n = cinfo->output_width, p = line; n > 0; --n, p += 4)
    {
      c = p[0];
      m = p[1];
      y = p[2];
      k = p[3];

      if (cinfo->saw_Adobe_marker)
        {
          p[0] = k * c / 255;
          p[1] = k * m / 255;
          p[2] = k * y / 255;
        }
      else
        {
          p[0] = (255 - k) * (255 - c) / 255;
          p[1] = (255 - k) * (255 - m) / 255;
          p[2] = (255 - k) * (255 - y) / 255;
        }

      p[3] = 255;
    }
}



static GdkPixbuf*
tvtj_jpeg_load (const JOCTET *content,
                gsize         length,
                gint          size)
{
  struct jpeg_decompress_struct cinfo;
  struct jpeg_source_mgr        source;
  TvtjErrorHandler              handler;
  guchar                       *lines[1];
  guchar                       *buffer = NULL;
  guchar                       *pixels = NULL;
  guchar                       *p;
  gint                          out_num_components;
  guint                         n;

  /* setup JPEG error handling */
  cinfo.err = jpeg_std_error (&handler.mgr);
  handler.mgr.error_exit = fatal_error_handler;
  handler.mgr.output_message = (gpointer) tvtj_noop;
  if (setjmp (handler.setjmp_buffer))
    goto error;

  /* setup the source */
  source.bytes_in_buffer = length;
  source.next_input_byte = content;
  source.init_source = (gpointer) tvtj_noop;
  source.fill_input_buffer = tvtj_fill_input_buffer;
  source.skip_input_data = tvtj_skip_input_data;
  source.resync_to_restart = jpeg_resync_to_restart;
  source.term_source = (gpointer) tvtj_noop;

  /* setup the JPEG decompress struct */
  jpeg_create_decompress (&cinfo);
  cinfo.src = &source;

  /* read the JPEG header from the file */
  jpeg_read_header (&cinfo, TRUE);

  /* configure the JPEG decompress struct */
  cinfo.scale_num = 1;
  cinfo.scale_denom = tvtj_denom (cinfo.image_width, cinfo.image_height, size);
  cinfo.dct_method = JDCT_FASTEST;
  cinfo.do_fancy_upsampling = FALSE;

  /* calculate the output dimensions */
  jpeg_calc_output_dimensions (&cinfo);

  /* verify the JPEG color space */
  if (cinfo.out_color_space != JCS_GRAYSCALE
      && cinfo.out_color_space != JCS_CMYK
      && cinfo.out_color_space != JCS_RGB)
    {
      /* we don't support this color space */
      goto error;
    }

  /* start the decompression */
  jpeg_start_decompress (&cinfo);

  /* allocate the pixel buffer and extra space for grayscale data */
  if (G_LIKELY (cinfo.num_components != 1))
    {
      pixels = g_malloc (cinfo.output_width * cinfo.output_height * cinfo.num_components);
      out_num_components = cinfo.num_components;
      lines[0] = pixels;
    }
  else
    {
      pixels = g_malloc (cinfo.output_width * cinfo.output_height * 3);
      buffer = g_malloc (cinfo.output_width);
      out_num_components = 3;
      lines[0] = buffer;
    }

  /* process the JPEG data */
  for (p = pixels; cinfo.output_scanline < cinfo.output_height; )
    {
      jpeg_read_scanlines (&cinfo, lines, 1);

      /* convert the data to RGB */
      if (cinfo.num_components == 1)
        {
          for (n = 0; n < cinfo.output_width; ++n)
            {
              p[n * 3 + 0] = buffer[n];
              p[n * 3 + 1] = buffer[n];
              p[n * 3 + 2] = buffer[n];
            }
          p += cinfo.output_width * 3;
        }
      else
        {
          if (cinfo.out_color_space == JCS_CMYK)
            tvtj_convert_cmyk_to_rgb (&cinfo, lines[0]);
          lines[0] += cinfo.output_width * cinfo.num_components;
        }
    }

  /* release the grayscale buffer */
  g_free (buffer);
  buffer = NULL;

  /* finish the JPEG decompression */
  jpeg_finish_decompress (&cinfo);
  jpeg_destroy_decompress (&cinfo);

  /* generate a pixbuf for the pixel data */
  return gdk_pixbuf_new_from_data (pixels, GDK_COLORSPACE_RGB,
                                   (cinfo.out_color_components == 4), 8,
                                   cinfo.output_width, cinfo.output_height,
                                   cinfo.output_width * out_num_components,
                                   (GdkPixbufDestroyNotify) g_free, NULL);

error:
  jpeg_destroy_decompress (&cinfo);
  g_free (buffer);
  g_free (pixels);
  return NULL;
}



typedef struct
{
  const guchar *data_ptr;
  guint         data_len;

  guint         thumb_compression;
  union
  {
    struct /* thumbnail JPEG */
    {
      guint     length;
      guint     offset;
      guint     orientation;
    } thumb_jpeg;
    struct /* thumbnail TIFF */
    {
      guint     length;
      guint     offset;
      guint     interp;
      guint     height;
      guint     width;
    } thumb_tiff;
  } thumb;

  gboolean      big_endian;
} TvtjExif;



static guint
tvtj_exif_get_ushort (const TvtjExif *exif,
                      const guchar   *data)
{
  if (G_UNLIKELY (exif->big_endian))
    return ((data[0] << 8) | data[1]);
  else
    return ((data[1] << 8) | data[0]);
}



static guint
tvtj_exif_get_ulong (const TvtjExif *exif,
                     const guchar   *data)
{
  if (G_UNLIKELY (exif->big_endian))
    return ((data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3]);
  else
    return ((data[3] << 24) | (data[2] << 16) | (data[1] << 8) | data[0]);
}



static void
tvtj_exif_parse_ifd (TvtjExif     *exif,
                     const guchar *ifd_ptr,
                     guint         ifd_len,
                     GSList       *ifd_previous_list)
{
  const guchar *subifd_ptr;
  GSList        ifd_list;
  guint         subifd_off;
  guint         value;
  guint         tag;
  guint         n;

  /* make sure we have a valid IFD here */
  if (G_UNLIKELY (ifd_len < 2))
    return;

  /* make sure we don't recurse into IFDs that are already being processed */
  if (g_slist_find (ifd_previous_list, ifd_ptr) != NULL)
    return;
  ifd_list.next = ifd_previous_list;
  ifd_list.data = (gpointer) ifd_ptr;

  /* determine the number of entries */
  n = tvtj_exif_get_ushort (exif, ifd_ptr);

  /* advance to the IFD content */
  ifd_ptr += 2;
  ifd_len -= 2;

  /* validate the number of entries */
  if (G_UNLIKELY (n * 12 > ifd_len))
    n = ifd_len / 12;

  /* process all IFD entries */
  for (; n > 0; ifd_ptr += 12, --n)
    {
      /* determine the tag of this entry */
      tag = tvtj_exif_get_ushort (exif, ifd_ptr);
      if (tag == 0x8769 || tag == 0xa005)
        {
          /* check if we have a valid sub IFD offset here */
          subifd_off = tvtj_exif_get_ulong (exif, ifd_ptr + 8);
          subifd_ptr = exif->data_ptr + subifd_off;
          if (G_LIKELY (subifd_off < exif->data_len))
            {
              /* process the sub IFD recursively */
              tvtj_exif_parse_ifd (exif, subifd_ptr, exif->data_len - subifd_off, &ifd_list);
            }
        }
      else if (tag == 0x0103)
        {
          /* verify that we have an ushort here (format 3) */
          if (tvtj_exif_get_ushort (exif, ifd_ptr + 2) == 3)
            {
              /* determine the thumbnail compression */
              exif->thumb_compression = tvtj_exif_get_ushort (exif, ifd_ptr + 8);
            }
        }
      else if (tag == 0x0100 || tag == 0x0101 || tag == 0x0106 || tag == 0x0111 || tag == 0x0117)
        {
          /* this can be either ushort or ulong */
          if (tvtj_exif_get_ushort (exif, ifd_ptr + 2) == 3)
            value = tvtj_exif_get_ushort (exif, ifd_ptr + 8);
          else if (tvtj_exif_get_ushort (exif, ifd_ptr + 2) == 4)
            value = tvtj_exif_get_ulong (exif, ifd_ptr + 8);
          else
            value = 0;

          /* and remember it appropriately */
          if (tag == 0x0100)
            exif->thumb.thumb_tiff.width = value;
          else if (tag == 0x0100)
            exif->thumb.thumb_tiff.height = value;
          else if (tag == 0x0106)
            exif->thumb.thumb_tiff.interp = value;
          else if (tag == 0x0111)
            exif->thumb.thumb_tiff.offset = value;
          else
            exif->thumb.thumb_tiff.length = value;
        }
      else if (tag == 0x0201 || tag == 0x0202)
        {
          /* verify that we have an ulong here (format 4) */
          if (tvtj_exif_get_ushort (exif, ifd_ptr + 2) == 4)
            {
              /* determine the value (thumbnail JPEG offset or length) */
              value = tvtj_exif_get_ulong (exif, ifd_ptr + 8);

              /* and remember it appropriately */
              if (G_LIKELY (tag == 0x201))
                exif->thumb.thumb_jpeg.offset = value;
              else
                exif->thumb.thumb_jpeg.length = value;
            }
        }
      else if (tag == 0x112)
        {
          /* verify that we have a 2-byte integer (format 3) */
          if (tvtj_exif_get_ushort (exif, ifd_ptr + 2) == 3
              && tvtj_exif_get_ulong (exif, ifd_ptr + 4) == 1)
            {
              /* determine the orientation value */
              value = tvtj_exif_get_ushort (exif, ifd_ptr + 8);
              exif->thumb.thumb_jpeg.orientation = MIN (value, 8);
            }
        }
    }

  /* check for link to next IFD */
  subifd_off = tvtj_exif_get_ulong (exif, ifd_ptr);
  if (subifd_off != 0 && subifd_off < exif->data_len)
    {
      /* parse next IFD recursively as well */
      tvtj_exif_parse_ifd (exif, exif->data_ptr + subifd_off, exif->data_len - subifd_off, &ifd_list);
    }
}



static GdkPixbuf *
tvtj_rotate_pixbuf (GdkPixbuf *src,
                    guint      orientation)
{
  GdkPixbuf *dest = NULL;
  GdkPixbuf *temp;

  g_return_val_if_fail (GDK_IS_PIXBUF (src), NULL);

  switch (orientation)
    {
    case 1:
      dest = g_object_ref (src);
      break;

    case 2:
      dest = gdk_pixbuf_flip (src, TRUE);
      break;

    case 3:
      dest = gdk_pixbuf_rotate_simple (src, GDK_PIXBUF_ROTATE_UPSIDEDOWN);
      break;

    case 4:
      dest = gdk_pixbuf_flip (src, FALSE);
      break;

    case 5:
      temp = gdk_pixbuf_rotate_simple (src, GDK_PIXBUF_ROTATE_CLOCKWISE);
      dest = gdk_pixbuf_flip (temp, TRUE);
      g_object_unref (temp);
      break;

    case 6:
      dest = gdk_pixbuf_rotate_simple (src, GDK_PIXBUF_ROTATE_CLOCKWISE);
      break;

    case 7:
      temp = gdk_pixbuf_rotate_simple (src, GDK_PIXBUF_ROTATE_CLOCKWISE);
      dest = gdk_pixbuf_flip (temp, FALSE);
      g_object_unref (temp);
      break;

    case 8:
      dest = gdk_pixbuf_rotate_simple (src, GDK_PIXBUF_ROTATE_COUNTERCLOCKWISE);
      break;

    default:
      /* if no orientation tag was present */
      dest = g_object_ref (src);
      break;
    }

  return dest;
}



static GdkPixbuf*
tvtj_exif_extract_thumbnail (const guchar *data,
                             guint         length,
                             gint          size)
{
  TvtjExif   exif;
  guint      offset;
  GdkPixbuf *thumb = NULL;
  GdkPixbuf *rotated;

  /* make sure we have enough data */
  if (G_UNLIKELY (length < 6 + 8))
    return NULL;

  /* validate Exif header */
  if (memcmp (data, "Exif\0\0", 6) != 0)
    return NULL;

  /* advance to TIFF header */
  data += 6;
  length -= 6;

  /* setup Exif data struct */
  memset (&exif, 0, sizeof (exif));
  exif.data_ptr = data;
  exif.data_len = length;

  /* determine byte order */
  if (memcmp (data, "II", 2) == 0)
    exif.big_endian = FALSE;
  else if (memcmp (data, "MM", 2) == 0)
    exif.big_endian = TRUE;
  else
    return NULL;

  /* validate the TIFF header */
  if (tvtj_exif_get_ushort (&exif, data + 2) != 0x2a)
    return NULL;

  /* determine the first IFD offset */
  offset = tvtj_exif_get_ulong (&exif, data + 4);

  /* validate the offset */
  if (G_LIKELY (offset < length))
    {
      /* parse the first IFD (recursively parses the remaining...) */
      tvtj_exif_parse_ifd (&exif, data + offset, length - offset, NULL);

      /* check thumbnail compression type */
      if (G_LIKELY (exif.thumb_compression == 6)) /* JPEG */
        {
          /* check if we have a valid thumbnail JPEG */
          if (exif.thumb.thumb_jpeg.offset > 0 && exif.thumb.thumb_jpeg.length > 0
              && exif.thumb.thumb_jpeg.offset + exif.thumb.thumb_jpeg.length <= length)
            {
              /* try to load the embedded thumbnail JPEG */
              thumb = tvtj_jpeg_load (data + exif.thumb.thumb_jpeg.offset, exif.thumb.thumb_jpeg.length, size);
            }
        }
      else if (exif.thumb_compression == 1) /* Uncompressed */
        {
          /* check if we have a valid thumbnail (current only RGB interpretations) */
          if (G_LIKELY (exif.thumb.thumb_tiff.interp == 2)
              && exif.thumb.thumb_tiff.offset > 0 && exif.thumb.thumb_tiff.length > 0
              && exif.thumb.thumb_tiff.offset + exif.thumb.thumb_tiff.length <= length
              && exif.thumb.thumb_tiff.height * exif.thumb.thumb_tiff.width == exif.thumb.thumb_tiff.length)
            {
              /* plain RGB data, just what we need for a GdkPixbuf */
              thumb = gdk_pixbuf_new_from_data (g_memdup (data + exif.thumb.thumb_tiff.offset, exif.thumb.thumb_tiff.length),
                                                GDK_COLORSPACE_RGB, FALSE, 8, exif.thumb.thumb_tiff.width,
                                                exif.thumb.thumb_tiff.height, exif.thumb.thumb_tiff.width,
                                                (GdkPixbufDestroyNotify) g_free, NULL);
            }
        }

     if (thumb != NULL
         && exif.thumb.thumb_jpeg.orientation > 1)
       {
         /* rotate thumbnail */
         rotated = tvtj_rotate_pixbuf (thumb, exif.thumb.thumb_jpeg.orientation);
         g_object_unref (thumb);
         thumb = rotated;
       }
    }

  return thumb;
}



static GdkPixbuf*
tvtj_jpeg_load_thumbnail (const JOCTET *content,
                          gsize         length,
                          gint          size)
{
  guint marker_len;
  guint marker;
  gsize n;

  /* valid JPEG headers begin with SOI (Start Of Image) */
  if (G_LIKELY (length >= 2 && content[0] == 0xff && content[1] == 0xd8))
    {
      /* search for an EXIF marker */
      for (length -= 2, n = 2; n < length; )
        {
          /* check for valid marker start */
          if (G_UNLIKELY (content[n++] != 0xff))
            break;

          /* determine the next marker */
          marker = content[n];

          /* skip additional padding */
          if (G_UNLIKELY (marker == 0xff))
            continue;

          /* stop at SOS marker */
          if (marker == 0xda)
            break;

          /* advance */
          ++n;

          /* check if valid */
          if (G_UNLIKELY (n + 2 >= length))
            break;

          /* determine the marker length */
          marker_len = (content[n] << 8) | content[n + 1];

          /* check if we have an exif marker here */
          if (marker == 0xe1 && n + marker_len <= length)
            {
              /* try to extract the Exif thumbnail */
              return tvtj_exif_extract_thumbnail (content + n + 2, marker_len - 2, size);
            }

          /* try next one then */
          n += marker_len;
        }
    }

  return NULL;
}



static GdkPixbuf*
scale_pixbuf (GdkPixbuf *source,
              gint       dest_width,
              gint       dest_height)
{
  gdouble wratio;
  gdouble hratio;
  gint    source_width;
  gint    source_height;

  /* determine source pixbuf dimensions */
  source_width  = gdk_pixbuf_get_width  (source);
  source_height = gdk_pixbuf_get_height (source);

  /* don't do anything if there is no need to resize */
  if (source_width <= dest_width && source_height <= dest_height)
    return g_object_ref (source);

  /* determine which axis needs to be scaled down more */
  wratio = (gdouble) source_width  / (gdouble) dest_width;
  hratio = (gdouble) source_height / (gdouble) dest_height;

  /* adjust the other axis */
  if (hratio > wratio)
    dest_width = rint (source_width / hratio);
  else
    dest_height = rint (source_height / wratio);

  /* scale the pixbuf down to the desired size */
  return gdk_pixbuf_scale_simple (source, MAX (dest_width, 1),
                                  MAX (dest_height, 1),
                                  GDK_INTERP_BILINEAR);
}



static void
jpeg_thumbnailer_create (TumblerAbstractThumbnailer *thumbnailer,
                         GCancellable               *cancellable,
                         TumblerFileInfo            *info)
{
  TumblerThumbnailFlavor *flavor;
  TumblerImageData        data;
  TumblerThumbnail       *thumbnail;
  struct stat             statb;
  const gchar            *uri;
  GdkPixbuf              *pixbuf = NULL;
  GdkPixbuf              *scaled;
  gboolean                streaming_needed = TRUE;
  JOCTET                 *content;
  GError                 *error = NULL;
  GFile                  *file;
  gchar                  *path;
  gsize                   length;
  gint                    fd;
  gint                    height;
  gint                    width;
  gint                    size;

  g_return_if_fail (IS_JPEG_THUMBNAILER (thumbnailer));
  g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
  g_return_if_fail (TUMBLER_IS_FILE_INFO (info));

  /* do nothing if cancelled */
  if (g_cancellable_is_cancelled (cancellable)) 
    return;

  uri = tumbler_file_info_get_uri (info);

  /* try to open the source file for reading */
  file = g_file_new_for_uri (uri);

  thumbnail = tumbler_file_info_get_thumbnail (info);
  g_assert (thumbnail != NULL);

  flavor = tumbler_thumbnail_get_flavor (thumbnail);
  g_assert (flavor != NULL);

  tumbler_thumbnail_flavor_get_size (flavor, &width, &height);
  size = MIN (width, height);

#ifdef HAVE_MMAP
  if (g_file_is_native (file))
    {
      path = g_file_get_path (file);

      /* try to open the file at the given path */
      fd = open (path, O_RDONLY);
      if (G_LIKELY (fd >= 0))
        {
          /* determine the status of the file */
          if (G_LIKELY (fstat (fd, &statb) == 0 && statb.st_size > 0))
            {
              /* try to mmap the file */
              content = (JOCTET *) mmap (NULL, statb.st_size, PROT_READ, 
                                         MAP_SHARED, fd, 0);

              /* verify whether the mmap was successful */
              if (G_LIKELY (content != (JOCTET *) MAP_FAILED))
                {
                  /* try to load the embedded thumbnail first */
                  pixbuf = tvtj_jpeg_load_thumbnail (content, statb.st_size, size);
                  if (pixbuf == NULL)
                    {
                      /* fall back to loading and scaling the image itself */
                      pixbuf = tvtj_jpeg_load (content, statb.st_size, size);

                      if (pixbuf == NULL)
                        {
                          g_set_error (&error, TUMBLER_ERROR, 
                                       TUMBLER_ERROR_INVALID_FORMAT,
                                       _("Thumbnail could not be inferred from file contents"));
                        }
                    }

                  /* we have successfully mmapped the file. we may not have
                   * a thumbnail but trying to read the image from a stream
                   * won't help us here, so we don't need to attempt streaming
                   * as a fallback */
                  streaming_needed = FALSE;
                }

              /* unmap the file content */
              munmap ((void *) content, statb.st_size);
            }

          /* close the file */
          close (fd);
        }

      g_free (path);
    }
#endif

  if (streaming_needed)
    {
      g_file_load_contents (file, cancellable, (gchar **)&content, &length, 
                            NULL, &error);

      if (error == NULL)
        {
          pixbuf = tvtj_jpeg_load_thumbnail (content, length, size);

          if (pixbuf == NULL)
            {
              pixbuf = tvtj_jpeg_load (content, length, size);
              if (pixbuf == NULL)
                {
                  g_set_error (&error, TUMBLER_ERROR, TUMBLER_ERROR_INVALID_FORMAT,
                               _("Thumbnail could not be inferred from file contents"));
                }
            }
        }
    }

  /* either we have an error now or we have a valid thumbnail pixbuf */
  g_assert (error != NULL || pixbuf != NULL);

  if (pixbuf != NULL)
    {
      scaled = scale_pixbuf (pixbuf, width, height);
      g_object_unref (pixbuf);
      pixbuf = scaled;

      data.data = gdk_pixbuf_get_pixels (pixbuf);
      data.has_alpha = gdk_pixbuf_get_has_alpha (pixbuf);
      data.bits_per_sample = gdk_pixbuf_get_bits_per_sample (pixbuf);
      data.width = gdk_pixbuf_get_width (pixbuf);
      data.height = gdk_pixbuf_get_height (pixbuf);
      data.rowstride = gdk_pixbuf_get_rowstride (pixbuf);
      data.colorspace = (TumblerColorspace) gdk_pixbuf_get_colorspace (pixbuf);

      tumbler_thumbnail_save_image_data (thumbnail, &data, 
                                         tumbler_file_info_get_mtime (info), 
                                         NULL, &error);

      g_object_unref (pixbuf);
    }

  if (error != NULL)
    {
      g_signal_emit_by_name (thumbnailer, "error", uri, error->code, error->message);
      g_error_free (error);
    }
  else
    {
      g_signal_emit_by_name (thumbnailer, "ready", uri);
    }

  g_object_unref (flavor);
  g_object_unref (thumbnail);
  g_object_unref (file);
}
