Saturday, 6 February 2016

Custom gdk_pixbuf_loader to handle embedded RAW preview images

The Gtk/Gnome subsystem provides the basis for many of the popular (along with KDE) window managers on Linux. One of the areas of the current gtk subsystem that handles image formats can be extended if needed via gdk_pixbuf_loaders. By implementing and installing a newer loader for your image format, the current image viewers such as eog (Eye of Gnome) will seamlessly display these images.

The standard installation of loaders on Fedora 23 does not provide a suitable image loader for RAW files...

Well, that's not strictly true. Nikon NEFs are loosely based on the TIFF format, so when an image viewer wants to display a NEF, the gdk pixbuf subsystem searches for loaders that can handle the file signature (which matches a TIFF). For a straight out of camera NEF, the gdk subsystem will identify the file as a TIFF and use the TIFF loader to handle it - however, this results in the TIFF loader using the smallest preview image (also in TIFF format) embedded in the NEF (smallest of 3 - the largest preview image being the full resolution jpeg that the camera uses to display).

When doing quick reviews on a set of NEFs out of camera, seeing a 320x212 image is ok but not great, particularly if you have a full resolution image available. Of course, you can use other tools to extract the preview image and then load the image viewers but this is painful and requires manual cleanup. There exists libopenraw that provides a gdk pixbuf loader to decode RAW files. This however is overkill for my specific purpose - quick review of images/check focus whilst on the move for quick selects - never mind slow given that the target machine will be a netbook.

What is better (and not currently implemented by the stock gdk loaders) is for a loader to recognise RAW files and then to extract and use the largest preview available when presenting that back to the image viewer.

The following code is a simple loader that uses Exiv2 to do exactly that and works with Nikon NEFs as well Canon CR2, although the underlying support is handled directly by the capabilities of the installed Exiv2 dependency.

Github repo source file

/* exiv2_pixbuf_loader.cc
 *
 * a dirty hack gdk_pixbuf_loader that uses exiv2 to extract raw file embedded
 * preview images and present them to the gdk pixbuf system via OTHER loaders
 * such as jpg.  For some reason it doesnt handle tifs pulled from the raw file
 *
 # compile and link Makefile targets

   exiv2_pixbuf_loader.o:  exiv2_pixbuf_loader.cc
           g++ -DNDEBUG -c -fPIC -g -Wwrite-strings $(shell pkg-config gdk-pixbuf-2.0 --cflags) $(shell pkg-config exiv2 --cflags) $^

   libexiv2_pixbuf_loader.so:      exiv2_pixbuf_loader.o
           g++ -shared -g $(shell pkg-config gdk-pixbuf-2.0 --libs) $(shell pkg-config exiv2 --libs) $^ -o $@

 # install to system area and regenerating loader cache
 # following this, the loader will be available to items usch as eog
 # ... libopenraw also provides a gdk pixbuf loader but it actually decodes
 #     the raw file - for eog/image viewers i'm not so interested

   cp libexiv2_pixbuf_loader.so /usr/lib64/gdk-pixbuf-2.0/2.10.0/loaders
   gdk-pixbuf-query-loaders-64 > /usr/lib64/gdk-pixbuf-2.0/2.10.0/loaders.cache

 *
 */

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>


#define GDK_PIXBUF_ENABLE_BACKEND
#include <gdk-pixbuf/gdk-pixbuf-io.h>
#include <gdk-pixbuf/gdk-pixbuf.h>

#include <exiv2/exiv2.hpp>

extern "C" {
G_MODULE_EXPORT void fill_vtable (GdkPixbufModule *module);
G_MODULE_EXPORT void fill_info (GdkPixbufFormat *info);
}


struct Exiv2PxbufCtx
{
    GdkPixbufModuleSizeFunc     size_func;
    GdkPixbufModulePreparedFunc prepared_func;
    GdkPixbufModuleUpdatedFunc  updated_func;
    gpointer                    user_data;

#ifndef NDEBUG
    int          fd;
#endif
    GByteArray*  data;
};






extern "C" {

// never called even if we assign in vfunc_fill()
static
GdkPixbuf*  _gpxbf_load(FILE* f_, GError** err_)
{
    printf("%s, line %ld - starting load\n", __FILE__, __LINE__);
    return NULL;
}
static
GdkPixbuf* _gpxbf_load_xpm_data(const char **data)
{
    printf("%s, line %ld - load xpm data\n", __FILE__, __LINE__);
    return NULL;
}

static 
gpointer _gpxbuf_bload(GdkPixbufModuleSizeFunc     szfunc_,
                       GdkPixbufModulePreparedFunc prepfunc_,
                       GdkPixbufModuleUpdatedFunc  updfunc_,
                       gpointer udata_,
                       GError **error_)
{
    struct Exiv2PxbufCtx*  ctx = (Exiv2PxbufCtx*)malloc(sizeof(struct Exiv2PxbufCtx));
    memset(ctx, 0, sizeof(struct Exiv2PxbufCtx));

    ctx->size_func     = szfunc_;
    ctx->prepared_func = prepfunc_;
    ctx->updated_func  = updfunc_;
    ctx->user_data     = udata_;
    ctx->data = g_byte_array_new();

#ifndef NDEBUG
    printf("%s, line %ld - begin load\n", __FILE__, __LINE__);
    const mode_t  umsk = umask(0);
    umask(umsk);
    if ( (ctx->fd = open("exiv2_pixbuf_incrload.dat", O_CREAT | O_TRUNC | O_WRONLY, umsk | 0666)) < 0) {
        printf("failed to create dump file - %s\n", strerror(errno));
        if (*error_ != NULL) {
            g_set_error_literal(error_, 0, errno, "failed to create dump file");
        }
        return NULL;
    }
#endif

    return (gpointer)ctx;
}


static 
gboolean _gpxbuf_lincr(gpointer ctx_,
                       const guchar* buf_, guint size_,
                       GError **error_)
{
#ifndef NDEBUG
    printf("%s, line %ld - incr load, %ld bytes\n", __FILE__, __LINE__, size_);
#endif
    Exiv2PxbufCtx*  ctx = (Exiv2PxbufCtx*)ctx_;
    g_byte_array_append(ctx->data, buf_, size_);
    return TRUE;
}

static
gboolean _gpxbuf_sload(gpointer ctx_, GError **error_)
{
    Exiv2PxbufCtx*  ctx = (Exiv2PxbufCtx*)ctx_;
    gboolean result = FALSE;

#ifndef NDEBUG
    printf("%s, line %ld - stop load, data len=%ld\n", __FILE__, __LINE__, ctx->data->len);
    write(ctx->fd, ctx->data->data, ctx->data->len);
    close(ctx->fd);
#endif

    try
    {
        Exiv2::Image::AutoPtr  orig = Exiv2::ImageFactory::open(ctx->data->data, ctx->data->len);
        orig->readMetadata();

        Exiv2::PreviewManager  exvprldr(*orig);
        Exiv2::PreviewPropertiesList  list =  exvprldr.getPreviewProperties();

        /* exiv2 provides images sorted from small->large -  grabbing the
         * largest preview
         */
        Exiv2::PreviewPropertiesList::iterator prevp = list.begin();
        if (prevp != list.end()) {
            advance(prevp, list.size()-1);
        }
        Exiv2::PreviewImage  preview =  exvprldr.getPreviewImage(*prevp);
        Exiv2::Image::AutoPtr  upd = Exiv2::ImageFactory::open( preview.pData(), preview.size() );

#if 0
        /* at this point EOG has already tried to read the exif from the data buf
         * and of course at that point, we havent updated the preview image with the
         * exif from the original RAW file yet
         */
        upd->setByteOrder(orig->byteOrder());
        upd->setExifData(orig->exifData());
        upd->setIptcData(orig->iptcData());
        upd->setXmpData(orig->xmpData());
        upd->writeMetadata();
#endif

        Exiv2::BasicIo&  rawio = upd->io();
        rawio.seek(0, Exiv2::BasicIo::beg);

        /* let the other better placed loaders deal with the RAW img
         * preview (tif/jpeg)
         */
#ifndef NDEBUG
        printf("%s, line %ld - sload, internal loader with buf=%ld\n", __FILE__, __LINE__, rawio.size());
        const mode_t  umsk = umask(0);
        umask(umsk);
        int  fd;
        if ( (fd = open("exiv2_pixbuf_incrload.preview.dat", O_CREAT | O_TRUNC | O_WRONLY, umsk | 0666)) < 0) {
            printf("failed to create dump file - %s\n", strerror(errno));
        }
        else
        {
            write(fd, rawio.mmap(), rawio.size());
            close(fd);
        }
#endif
        GdkPixbufLoader*  gpxbldr = gdk_pixbuf_loader_new();
        if (gdk_pixbuf_loader_write(gpxbldr, rawio.mmap(), rawio.size(), error_) == TRUE)
        {
            GdkPixbuf*  pixbuf = gdk_pixbuf_loader_get_pixbuf(gpxbldr);

#ifndef NDEBUG
            printf("%s, line %ld - sload, internal loader done, pixbuf=%ld\n", __FILE__, __LINE__, pixbuf);
#endif

            if (ctx->prepared_func != NULL) {
                (*ctx->prepared_func)(pixbuf, NULL, ctx->user_data);
            }
            if (ctx->updated_func != NULL) {
                (*ctx->updated_func)(pixbuf, 0, 0,
                                     gdk_pixbuf_get_width(pixbuf),
                                     gdk_pixbuf_get_height(pixbuf),
                                     ctx->user_data);
            }
            result = TRUE;
        }
        gdk_pixbuf_loader_close(gpxbldr, NULL);
    }
    catch (const std::exception& ex)
    {
        printf("%s, line %ld - %s\n", __FILE__, __LINE__, ex.what());
    }
    catch (...)
    {
        printf("%s, line %ld - unknown exception\n", __FILE__, __LINE__);
    }
    g_byte_array_free(ctx->data, TRUE);
    free(ctx);
    return result;
}


void
fill_vtable (GdkPixbufModule *module)
{
    module->begin_load     = _gpxbuf_bload;
    module->stop_load      = _gpxbuf_sload;
    module->load_increment = _gpxbuf_lincr;
}


void
fill_info (GdkPixbufFormat *info)
{
    static GdkPixbufModulePattern signature[] = {
        { "MM \x2a", "  z ", 80 }, /* TIFF */
        { "II\x2a \x10   CR\x02 ", "   z zzz   z", 100 }, /* CR2 */
        { "II\x2a ", "   z", 80 }, /* TIFF */
        { "II\x1a   HEAPCCDR", "   zzz        ", 100 }, /* CRW */
        { NULL, NULL, 0 }
    };

    static gchar *mime_types[] = {
        "image/x-canon-cr2",
        "image/x-canon-crw",
        "image/x-nikon-nef",
        NULL
    };

    static gchar *extensions[] = {
        "cr2",
        "crw",
        "nef",
        NULL
    };

    info->name        = (char*)"exiv2 RAW preview loader";
    info->signature   = signature;
    info->description = (char*)"RAW preview image loader (via exiv2)";
    info->mime_types  = mime_types;
    info->extensions  = extensions;
    info->flags       = 0;
    info->license     = (char*)"LGPL";
}
}

No comments:

Post a Comment