Writing a File Format Addin - PintaProject/Pinta GitHub Wiki
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.
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 thefilename
parameter, the currentparent
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 aGdk.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 theImport
method when implementing this method. ThemaxWidth
andmaxHeight
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 aPinta.Core.Document
to the specified filename. As with theIImageImporter
methods, the currentparent
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 ();
}
}
}
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 theIImageImporter
for the format, ornull
if importing is not supported. -
exporter
is theIImageExporter
for the format, ornull
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:
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 ();
}
}
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);
}
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