Writing a File Format Addin

Cameron White edited this page Jun 20, 2013 · 7 revisions

This guide assumes that you have set up a basic add-in as described in [Writing an Add-in] (https://github.com/PintaProject/Pinta/wiki/Getting-Started:-Writing-an-Addin).

This guide will demonstrate how to write a custom file format add-in for Pinta. We'll be writing an add-in to support importing and exporting the [WebP image format] 1. The full source code for this example is available at https://github.com/PintaProject/WebPAddin, and the add-in can be installed from Pinta's add-in repository.

The Basics

Ensure that the add-in description file (.addin.xml) has the category set to File Formats. This will make it easier for users to find your brush in the Add-in Gallery. The add-in description file should now look like:

<?xml version="1.0" encoding="UTF-8" ?>
<Addin id="WebP" version="0.1" category="File Formats">    
    <Header>
        <Name>WebP</Name>
        <Description>Provides support for the WebP image format.</Description>
        <Author>Pinta Project</Author>
        <Url>https://github.com/PintaProject/WebPAddin</Url>
    </Header>
    <Dependencies>
        <Addin id="Pinta" version="1.5" />
    </Dependencies>
</Addin>

All file format importers inherit from IImageImporter. There are two required methods that you must implement:

  • Import is responsible for loading an image and creating a new document with the contents of the image. In addition to the filename parameter, the current parent window is also given. This allows you to properly create a modal dialog if, for example, you need to display an error message.
  • LoadThumbnail is called by the Open Image dialog, and returns a Gdk.Pixbuf containing a thumbnail of the given image. For some file formats (such as the [OpenRaster] 2 format) it is possible to load a thumbnail more efficiently than when loading the full image. Otherwise, you can reuse most of your code from the Import method when implementing this method. The maxWidth and maxHeight parameters provide a suggested width and height to use when loading the thumbnail. If your image format does not allow you to take advantage of this information, you can return a full-size image and Pinta will resize it for you in order to fit in the Open Image dialog.

All file format exporters inherit from IImageExporter. There is only one required method that you must implement:

  • Export is responsible for saving a Pinta.Core.Document to the specified filename. As with the IImageImporter methods, the current parent window is passed as a parameter. This allows you to properly create a modal dialog if you need to display an error message or ask the user to choose settings (such as the image quality).

Your file format add-in does not need to provide both an importer and an exporter - for example, an ASCII art add-in would likely only want to provide an exporter.

Let's create a skeleton implementation of our importer and exporter:

using System;
using Gdk;
using Pinta.Core;

namespace WebPAddin
{
    public class WebPImporter : Pinta.Core.IImageImporter
    {
        public void Import (string filename, Gtk.Window parent)
        {
            throw new NotImplementedException ();
        }

        public Pixbuf LoadThumbnail (string filename, int maxWidth, int maxHeight, Gtk.Window parent)
        {
            throw new NotImplementedException ();
        }
    }

    public class WebPExporter : Pinta.Core.IImageExporter
    {
        public void Export (Document document, string fileName, Window parent)
        {
            throw new NotImplementedException ();
        }
    }
}

Registering the File Format

Before we go much further with implementing the add-in, we'll need to register the file format with Pinta so that we'll be able to test it out and debug it.

In the IExtension subclass, we need to call PintaCore.System.ImageFormats.RegisterFormat with a FormatDescriptor in order to register our file format, and call PintaCore.System.ImageFormats.UnregisterFormatByExtension with the file extension to unregister the file format.

The FormatDescriptor constructor takes four arguments:

  • displayPrefix is a descriptive name for the file format, and will be displayed in the Save dialog's file filter. Some example names are "WebP", "OpenRaster", and "JPEG".
  • extensions is a list of the supported file extensions (for example, {"jpeg", "JPEG", "jpg", "JPG"}).
  • importer is the IImageImporter for the format, or null if importing is not supported.
  • exporter is the IImageExporter for the format, or null if exporting is not supported.
using System;
using Pinta.Core;

namespace WebPAddin
{
    [Mono.Addins.Extension]
    public class WebPExtension : IExtension
    {
        public void Initialize ()
        {
            PintaCore.System.ImageFormats.RegisterFormat (
                    new FormatDescriptor("WebP", new string[] {"webp"},
                                         new WebPImporter (), new WebPExporter ()));
        }

        public void Uninitialize ()
        {
            PintaCore.System.ImageFormats.UnregisterFormatByExtension ("webp");
        }
    }
}

Now, we can load Pinta and see our file format in the file dialog:

File Dialog

Implementing the Importer

Our Import method looks like this:

public void Import (string filename, Gtk.Window parent)
{
    int width = -1, height = -1, stride = -1;
    byte[] image_data = null;

    if (!LoadImage (filename, ref image_data, ref width, ref height, ref stride, parent))
        return;

    // Create a new document and add an initial layer.
    Document doc = PintaCore.Workspace.CreateAndActivateDocument (filename, new Size (width, height));
    doc.HasFile = true;
    doc.Workspace.CanvasSize = doc.ImageSize;
    Layer layer = doc.AddNewLayer (Path.GetFileName (filename));

    // Copy over the image data to the layer's surface.
    CopyToSurface (image_data, layer.Surface);
}

LoadImage is a helper function that loads a WebP image into an array of bytes (in BGRA order) and returns the height and width of the image. You can view the implementation in the [WebPImporter source code] 3. We also pass the parent window to LoadImage, since that method will display a helpful error message to the user if the libwebp library could not be found on their system.

If we were able to load the image successfully, we now create a new document in Pinta with the same height and width as the image. The name of the document will be the same as the filename, and we set HasFile to true so that saving the document will not prompt the user for a new filename. Then, we add an initial layer, and copy over the data from our BGRA array to the layer's surface using the CopyToSurface helper method:

private static unsafe void CopyToSurface (byte[] image_data, Cairo.ImageSurface surf)
{
    if (image_data.Length != surf.Data.Length)
        throw new ArgumentException ("Mismatched image sizes");

    surf.Flush ();

    ColorBgra* dst = (ColorBgra *)surf.DataPtr;
    int len = image_data.Length / ColorBgra.SizeOf;

    fixed (byte *src_bytes = image_data) {
        ColorBgra *src = (ColorBgra *)src_bytes;

        for (int i = 0; i < len; ++i) {
            *dst++ = *src++;
        }
    }

    surf.MarkDirty ();
}

There are other ways to copy image data onto the layer's surface - see [Other Examples] (#other-examples) for more information.

Our implementation of LoadThumbnail is very similar. In the case of the WebP format, there isn't a more efficient way of loading a thumbnail, so we'll just reuse the same method from above. After loading the image data, we create a new Cairo.ImageSurface from the data and convert it to a Gdk.Pixbuf.

public Pixbuf LoadThumbnail (string filename, int maxWidth, int maxHeight, Gtk.Window parent)
{
    int width = -1, height = -1, stride = -1;
    byte[] image_data = null;

    if (!LoadImage (filename, ref image_data, ref width, ref height, ref stride, parent))
        return null;

    using (var surf = new Cairo.ImageSurface (image_data, Cairo.Format.ARGB32, width, height, stride)) {
        return surf.ToPixbuf ();
    }
}

Implementing the Exporter

When saving WebP files, we need to ask the user to choose a quality setting. In order to make the user experience better, we'll save their chosen quality setting to Pinta's settings file, and use it as the default when the dialog is next shown. For more information on Pinta's settings API, see [Saving and Loading Settings] 4. Additionally, we also check PintaCore.Workspace.ActiveDocument.HasBeenSavedInSession to avoid repeatedly prompting the user for the quality setting each time they save the document. Finally, we open up our Gtk.Dialog [subclass] 5 and get the quality setting from the user.

Now that we're ready to save the file, we call document.GetFlattenedImage() to get a Cairo.ImageSurface with all of the layers merged together. We then call a method in the libwebp library to encode the image data into the WebP format, and save it to the specified filename. Finally, we save the user's chosen quality setting, and we're done!

public void Export (Document document, string fileName, Window parent)
{
    // Retrieve the last quality factor setting the user selected, or the default value.
    int quality_factor = PintaCore.Settings.GetSetting<int> (WebPQualityFactorSetting,
            DefaultQualityFactor);

    // Don't repeatedly prompt the user after they've already selected a
    // quality setting for this document.
    if (!PintaCore.Workspace.ActiveDocument.HasBeenSavedInSession) {
        var dialog = new WebPSettingsDialog (parent, quality_factor);
        try {
            if ((ResponseType)dialog.Run () == ResponseType.Ok) {
                quality_factor = dialog.QualityFactor;
            } else {
                return;
            }
        } finally {
            dialog.Destroy ();
        }
    }

    // Merge all of the layers and convert the image to WebP.
    using (var surface = document.GetFlattenedImage ()) {
        var output = IntPtr.Zero;

        try {
            uint length = NativeMethods.WebPEncodeBGRA (surface.Data, surface.Width, surface.Height,
                    surface.Stride, 80, ref output);
            byte[] data = new byte[length];
            Marshal.Copy (output, data, 0, (int)length);
            // The caller is responsible for calling free() with the array allocated by WebPEncodeBGRA.
            NativeMethods.Free (output);

            // Save the encoded data to the file.
            File.WriteAllBytes (fileName, data);

        } catch (DllNotFoundException) {
            NativeMethods.ShowErrorDialog (parent);
            return;
        }
    }

    // Save the chosen quality setting to Pinta's settings file so that it
    // can be the default the next time a file is saved.
    PintaCore.Settings.PutSetting (WebPQualityFactorSetting, quality_factor);
}

Other Examples

For other examples, you can take a look at the file format importers and exporters that are shipped with Pinta: https://github.com/PintaProject/Pinta/tree/master/Pinta.Core/ImageFormats