pinvokecairoparallel.foreachlibrsvggdkpixbuf

Why is the deprecated "rsvg_pixbuf_from_file_at_size" faster/more efficient that the non-deprecated method (Cairo)?


I'm using C# and P/Invoke to access the GDK libraries. My goal is to convert an set of SVG files into raster images (specifically, png), and using the GDK libraries seems to be the most reliable/accurate.

After doing some reading of the Gnome/Cairo documentation, I have found two basic approaches to accomplish this. One of these approaches uses deprecated functions, and the other does not.

The first approach, which is deprecated, but arguably more simple/straightforward, looks basically like this:

bool result = false;
ptrPixbuf = rsvg_pixbuf_from_file_at_size(svgFilePath, width, height, out ptrError);
if (ptrError == UIntPtr.Zero && ptrPixbuf != UIntPtr.Zero)
{
    bool isSaved = gdk_pixbuf_save(ptrPixbuf, outputFilePath, out ptrError, UIntPtr.Zero);
    if (ptrError == UIntPtr.Zero && isSaved == true)
    {
        result = true;
    }
}
return result;

The second approach, which is not deprecated, involves setting up a Cairo Surface, rendering it, and then saving it to a file. It looks basically like this:

bool result = false;
ptrRsvgHandle = rsvg_handle_new_from_file(svgFilePath, out ptrError);
if (ptrError == UIntPtr.Zero)
{
    ptrCairoSurface = cairo_image_surface_create(CairoFormat.CAIRO_FORMAT_ARGB32, width, height);
    if ((cairo_surface_status(ptrCairoSurface) == CairoStatus.CAIRO_STATUS_SUCCESS)
    {
        ptrcairoContext = cairo_create(ptrCairoSurface);
        if ((cairo_status(ptrCairoContext) == CairoStatus.CAIRO_STATUS_SUCCESS))
        {
            bool isRendered = rsvg_handle_render_cairo(ptrRsvgHandle, ptrCairoContext);
            if (isRendered)
            {
                ptrPixbuf = rsvg_handle_get_pixbuf(ptrRsvgHandle);
                if (ptrPixbuf != UIntPtr.Zero)
                {
                    bool isSaved = gdk_pixbuf_save(ptrPixbuf, outputFilePath, out ptrError, UIntPtr.Zero);
                    if (ptrError == UIntPtr.Zero && isSaved == true)
                    {
                        result = true;
                    }
                }
            }
        }
    }
}
return result;

Both of these approaches seem to work - they generate correct raster image output (although, the "Cairo way" has some bugs when I try to do a bunch of operations in parallel - I end up running out of memory).

My question is this: why is the old/deprecated way (rsvg_pixbuf_from_file_at_size) noticeably faster than the new/Cairo way? My testing shows the first approach being faster across the board (one file/multiple files, standard C# ForEach/Parallel.ForEach).

For example, with 16 input files (output dimensions of 6000x4200) and no parallel processing, the first approach takes ~2:15.89 seconds. The second approach takes around ~2:37.95. With parallel processing (Parallel.Foreach calls my P/Invoke code), the results are similar -- with MaxDegreesOfParallelism set to default, it takes 30.7 seconds with the deprecated approach, and 36.95 with the Cairo approach.

Cairo seems to use substantially more memory too. Furthermore, in addition to simply using more resources for each conversion, Cairo also seems to not know how to avoid using all of my RAM. For example, if I up the number of input files to 720 (from 16), and use the Parallel.ForEach loop, I end up with 0 MB of free RAM, and the system grinds to a halt (eventually, the debug process exits, and my system comes back...but it locks up for a minute or so).

The easy answer to my question is to just use the deprecated approach, but why is it deprecated? Is the Cairo approach better in any way at all?

If anyone wants to see more of my code, let me know, and I will add it. I tried to trim down the code I posted to just the relevant bits (the actual P/Invoke code, and not the C# code calling it).


Solution

  • First, your out of memory error likely occurs because your code has memory leaks. Your example doesn't call any cleanup functions (like cairo_destroy(), cairo_surface_destroy(), gdk_pixbuf_unref(), g_object_unref(), possibly something for any GError that happens).

    Secondly, what does your second code actually draw to? You create a cairo surface and context, have rsvg draw to it, but then call rsvg_handle_get_pixbuf() which internally creates a new cairo surface and draws everything again. In other words, shouldn't it be enough for you to call rsvg_handle_new_from_file(), rsvg_handle_get_pixbuf() and gdk_pixbuf_save()?