diff options
Diffstat (limited to 'doc/chalk/developers-plugins.docbook')
-rw-r--r-- | doc/chalk/developers-plugins.docbook | 1553 |
1 files changed, 1553 insertions, 0 deletions
diff --git a/doc/chalk/developers-plugins.docbook b/doc/chalk/developers-plugins.docbook new file mode 100644 index 00000000..81a4ef49 --- /dev/null +++ b/doc/chalk/developers-plugins.docbook @@ -0,0 +1,1553 @@ +<sect1 id="developers-plugins"> +<title>Developing &chalk; Plugins</title> + +<sect2 id="developers-plugins-introduction"> +<title>Introduction</title> + +<para> +&chalk; is infinitely extensible with plugins. Tools, filters, large +chunks of the user interface and even colorspaces are plugins. In fact, +&chalk; recognizes these six types of plugins: +</para> + +<itemizedlist> +<listitem><para>colorspaces — these define the channels that constitute +a single pixel</para></listitem> +<listitem><para>tools — anything that is done with a mouse or tablet +input device</para></listitem> +<listitem><para>paint operations — pluggable painting effects for +tools</para></listitem> +<listitem><para>image filters — change all pixels, or just the selected +pixels in a layer</para></listitem> +<listitem><para>viewplugins — extend Chalk’s user interface with new +dialog boxes, palettes and operations</para></listitem> +<listitem><para>import/export filters — read and write all kinds of +image formats</para></listitem> +</itemizedlist> + +<para> +&chalk; itself consists of three layered libraries and a directory with some +common support classes: chalkcolor, chalkimage and chalkui. Within +&chalk;, objects can by identified by a <classname>KisID</classname>, that is +the combination of a unique untranslated string (used when saving, for +instance) and a translated string for GUI purposes. +</para><para> +A word on compatibility: &chalk; is still in development. From &chalk; 1.5 to +1.6 not many API changes are expected, but there may be some. From &chalk; 1.6 +to 2.0 we will move from &Qt;3 to &Qt;4, from &kde;3 to &kde;4, from +<command>automake</command> to <command>cmake</command>: many changes are to +be expected. If you develop a plugin for &chalk; and choose to do so in +&chalk;’s subversion repository, chances are excellent that we’ll help you +porting. These changes may also render parts of this document out of date. +Always check with the latest API documentation or the header files installed +on your system. +</para> + +<sect3 id="developers-plugins-introduction-chalkcolor"> +<title>ChalkColor</title> + +<para> +The first library is chalkcolor. This library loads the colorspace plugins. +</para><para> +A colorspace plugin should implement the <classname>KisColorSpace</classname> +abstract class or, if the basic capabilities of the new colorspace will be +implemented by <command>lcms</command> (<ulink url="http://www.littlecms.com/" +/>), extend <classname>KisAbstractColorSpace</classname>. The chalkcolor +library could be used from other applications and does not depend on +&koffice;. +</para> +</sect3> + +<sect3 id="developers-plugins-introduction-chalkimage"> +<title>ChalkImage</title> + +<para> +The libchalkimage library loads the filter and paintop plugins and is +responsible for working with image data: changing pixels, compositing and +painting. Brushes, palettes, gradients and patterns are also loaded by +libchalkimage. It is our stated goal to make libchalkimage independent of +&koffice;, but we currently share the gradient loading code with &koffice;. +</para><para> +It is not easy at the moment to add new types of resources such as brushes, +palettes, gradients or patterns to &chalk;. (Adding new brushes, palettes, +gradients and patterns is easy, of course.) &chalk; follows the guidelines of +the Create project (<ulink url="http://create.freedesktop.org/" />) for these. +Adding support for Photoshop's brush file format needs libchalkimage hacking; +adding more gimp brush data files not. +</para><para> +<classname>ChalkImage</classname> loads the following types of plugins: +</para> + +<itemizedlist> +<listitem><para>&chalk; filters must extend and implement the abstract class +<classname>KisFilter</classname>, +<classname>KisFilterConfiguration</classname> and possibly +<classname>KisFilterConfigurationWidget</classname>. +An example of a filter is Unsharp Mask.</para></listitem> +<listitem><para>Paint operations or paintops are the set of operations +painting tools suchs as freehand or circle have access to. Examples of +paintops are pen, airbrush or eraser. Paintops should extend the +<classname>KisPaintop</classname> base class. Examples of new paintops could +be a chalk brush, an oilpaint brush or a complex programmable +brush.</para></listitem> +</itemizedlist> + +</sect3> + +<sect3 id="developers-plugins-introduction-chalkui"> +<title>ChalkUI</title> + +<para> +The libchalkui library loads the tool and viewplugins. This library is a +&koffice; Part, but also contains a number of widgets that are useful for +graphics applications. Maybe we will have to split this library in chalkpart +and chalkui in the 2.0 release. For now, script writers are not given access +to this library and plugin writers are only allowed to use this library when +writing tools or viewplugins. <classname>ChalkUI</classname> loads the +following types of plugins: +</para> + +<itemizedlist> +<listitem><para>Tools are derived from <classname>KisTool</classname> or one +of the specialized tool base classes such as +<classname>KisToolPaint</classname>, <classname>KisToolNonPaint</classname> or +<classname>KisToolFreehand</classname>. A new tool could be a foreground +object selection tool. Painting tools (and that includes tools that paint on +the selection) can use any paintop to determine the way pixels are +changed.</para></listitem> +<listitem><para>Viewplugins are ordinary KParts that use +<command>kxmlgui</command> to insinuate themselves into &chalk;'s user +interface. Menu options, dialogs, toolbars — any kind of user interface +extension can be a viewplugin. In fact, important functionality like &chalk;'s +scripting support is written as a viewplugin.</para></listitem> +</itemizedlist> + +</sect3> + +<sect3 id="developers-plugins-introduction-importexport"> +<title>Import/Export filters</title> + +<para> +Import/Export filters are &koffice; filters, subclasses of +<classname>KoFilter</classname>. Filters read and write image data in any of +the myriad image formats in existence. And example of a new &chalk; +import/export filter could be a PDF filter. Filters are loaded by the +&koffice; libraries. +</para> + +</sect3> + +</sect2> + +<sect2 id="developers-plugins-creating"> +<title>Creating plugins</title> + +<para> +Plugins are written in C++ and can use all of &kde; and &Qt; and the &chalk; +developer API. Only viewplugins should use the &koffice; API. Don’t worry: +&chalk;’s API’s are quite clear and rather extensively documented (for free +software) and coding your first filter is really easy. +</para><para> +If you do not want to use C++, you can write scripts in Python or Ruby; that +is a different thing altogether, though, and you cannot currently write tools, +colorspaces, paintops or import/export filters as scripts. +</para><para> +&chalk; plugins use &kde;'s parts mechanism for loading, so the parts +documentation at <ulink url="http://developer.kde.org" /> is relevant here, too. +</para><para> +Your distribution should have either installed the relevant header files with +&chalk; itself, or might have split the header files into either a &koffice; +dev or a &chalk; dev package. You can find the API documentation for &chalk;'s +public API at <ulink url="http://koffice.org/developer/apidocs/chalk/html/" />. +</para> + +<sect3 id="developers-plugins-creating-automake"> +<title>Automake (and CMake)</title> + +<para> +&kde; 3.x and thus &koffice; 1.5 and 1.6 use <command>automake</command>; +&kde; 4.0 and &koffice; 2.0 use <command>cmake</command>. This tutorial +describes the <command>automake</command> way of creating plugins. +<!-- If I have not updated this manual when we release KOffice 2.0, please +remind me to do so. --> +</para><para> +Plugins are &kde; modules and should be tagged as such in their +<filename>Makefile.am</filename>. Filters, tools, paintops, colorspaces and +import/export filters need <literal role="extension">.desktop</literal> files; +viewplugins need a <application>KXMLGui</application> +<filename>pluginname.rc</filename> file in addition. The easiest way to get +started is to checkout the chalk-plugins project from the &koffice; Subversion +repository and use it as the basis for your own project. We intend to prepare +a skeleton &chalk; plugin pack for KDevelop, but haven’t had the time to do +so yet. +</para> + +<sect4 id="d-p-c-a-makefile"> +<title><filename>Makefile.am</filename></title> + +<para> +Let's look at the skeleton for a plugin module. First, the +<filename>Makefile.am</filename>. This is what &kde; uses to generate the +makefile that builds your plugin: + +<programlisting> +kde_services_DATA = chalkLIBRARYNAME.desktop + +INCLUDES = $(all_includes) + +chalkLIBRARYNAME_la_SOURCES = sourcefile1.cc sourcefile2.cc + +kde_module_LTLIBRARIES = chalkLIBRARYNAME.la +noinst_HEADERS = header1.h header2.h + +chalkLIBRARYNAME_la_LDFLAGS = $(all_libraries) -module $(KDE_PLUGIN) +chalkLIBRARY_la_LIBADD = -lchalkcommon + +chalkextensioncolorsfilters_la_METASOURCES = AUTO +</programlisting> + +This is the makefile for a filter plugin. Replace +<replaceable>LIBRARYNAME</replaceable> with the name of your work, and you are +set. +</para><para> +If your plugin is a viewplugin, you will likely also install a <literal +role="extension">.rc</literal> file with entries for menubars and toolbars. +Likewise, you may need to install cursors and icons. That is all done through +the ordinary &kde; <filename>Makefile.am</filename> magic incantantions: + +<programlisting> +chalkrcdir = $(kde_datadir)/chalk/chalkplugins +chalkrc_DATA = LIBRARYNAME.rc +EXTRA_DIST = $(chalkrc_DATA) + +chalkpics_DATA = \ + bla.png \ + bla_cursor.png +chalkpicsdir = $(kde_datadir)/chalk/pics +</programlisting> + +</para> +</sect4> + +<sect4 id="d-p-c-a-desktop"> +<title>Desktop files</title> + +<para> +The <literal role="extension">.desktop</literal> file announces the type of plugin: + +<programlisting> +[Desktop Entry] +Encoding=UTF-8 +Icon= +Name=User-visible Name +ServiceTypes=Chalk/Filter +Type=Service +X-KDE-Library=chalkLIBRARYNAME +X-KDE-Version=2 +</programlisting> +</para><para> +Possible ServiceTypes are: +</para> + +<itemizedlist> +<listitem><para>Chalk/Filter</para></listitem> +<listitem><para>Chalk/Paintop</para></listitem> +<listitem><para>Chalk/ViewPlugin</para></listitem> +<listitem><para>Chalk/Tool</para></listitem> +<listitem><para>Chalk/ColorSpace</para></listitem> +</itemizedlist> + +<para> +File import and export filters use the generic &koffice; filter framework and +need to be discussed separately. +</para> +</sect4> + +<sect4 id="d-p-c-a-boilerplate"> +<title>Boilerplate</title> + +<para> +You also need a bit of boilerplate code that is called by the &kde; part +framework to instantiate the plugin — a header file and an implementation file. +</para><para> +A header file: +<programlisting> +#ifndef TOOL_STAR_H_ +#define TOOL_STAR_H_ + +#include <kparts/plugin.h> + +/** +* A module that provides a star tool. +*/ +class ToolStar : public KParts::Plugin +{ + Q_OBJECT +public: + ToolStar(QObject *parent, const char *name, const QStringList &); + virtual ~ToolStar(); + +}; + +#endif // TOOL_STAR_H_ +</programlisting> +</para> + +<para> +And an implementation file: +<programlisting> +#include <kinstance.h> +#include <kgenericfactory.h> + +#include <kis_tool_registry.h> + +#include "tool_star.h" +#include "kis_tool_star.h" + + +typedef KGenericFactory<ToolStar> ToolStarFactory; +K_EXPORT_COMPONENT_FACTORY( chalktoolstar, ToolStarFactory( "chalk" ) ) + + +ToolStar::ToolStar(QObject *parent, const char *name, const QStringList &) + : KParts::Plugin(parent, name) +{ + setInstance(ToolStarFactory::instance()); + if ( parent->inherits("KisToolRegistry") ) + { + KisToolRegistry * r = dynamic_cast<KisToolRegistry*>( parent ); + r -> add(new KisToolStarFactory()); + } + +} + +ToolStar::~ToolStar() +{ +} + +#include "tool_star.moc" +</programlisting> +</para> +</sect4> + +<sect4 id="d-p-c-a-registries"> +<title>Registries</title> + +<para> +Tools are loaded by the tool registry and register themselves with the tool +registry. Plugins like tools, filters and paintops are loaded only once: view +plugins are loaded for every view that is created. Note that we register +factories, generally speaking. For instance, with tools a new instance of a +tool is created for every pointer (mouse, stylus, eraser) for every few. And a +new paintop is created whenever a tool gets a mouse-down event. +</para> + +<para> +Filters call the filter registry: +<programlisting> + if (parent->inherits("KisFilterRegistry")) { + KisFilterRegistry * manager = dynamic_cast<KisFilterRegistry *>(parent); + manager->add(new KisFilterInvert()); + } +</programlisting> +</para><para> +Paintops the paintop registry: +<programlisting> + if ( parent->inherits("KisPaintOpRegistry") ) { + KisPaintOpRegistry * r = dynamic_cast<KisPaintOpRegistry*>(parent); + r -> add ( new KisSmearyOpFactory ); + } +</programlisting> +</para><para> +Colorspaces the colorspace registry (with some complications): +<programlisting> + if ( parent->inherits("KisColorSpaceFactoryRegistry") ) { + KisColorSpaceFactoryRegistry * f = dynamic_cast<isColorSpaceFactoryRegistry*>(parent); + + KisProfile *defProfile = new KisProfile(cmsCreate_sRGBProfile()); + f->addProfile(defProfile); + + KisColorSpaceFactory * csFactory = new KisRgbColorSpaceFactory(); + f->add(csFactory); + + KisColorSpace * colorSpaceRGBA = new KisRgbColorSpace(f, 0); + KisHistogramProducerFactoryRegistry::instance() -> add( + new KisBasicHistogramProducerFactory<KisBasicU8HistogramProducer> + (KisID("RGB8HISTO", i18n("RGB8 Histogram")), colorSpaceRGBA) ); + } +</programlisting> +</para><para> +View plugins do not have to register themselves, and they get access to a +<classname>KisView</classname> object: +<programlisting> + if ( parent->inherits("KisView") ) + { + setInstance(ShearImageFactory::instance()); + setXMLFile(locate("data","chalkplugins/shearimage.rc"), true); + + (void) new KAction(i18n("&Shear Image..."), 0, 0, this, SLOT(slotShearImage()), actionCollection(), "shearimage"); + (void) new KAction(i18n("&Shear Layer..."), 0, 0, this, SLOT(slotShearLayer()), actionCollection(), "shearlayer"); + + m_view = (KisView*) parent; + } +</programlisting> +</para><para> +Remember that this means that a view plugin will be created for every view the +user creates: splitting a view means loading all view plugins again. +</para> +</sect4> + +<sect4 id="d-p-c-a-versioning"> +<title>Plugin versioning</title> + +<para> +&chalk; 1.5 loads plugins with <literal>X-KDE-Version=2</literal> set in the +<literal role="extension">.desktop</literal> file. &chalk; 1.6 plugins will +probably be binary incompatible with 1.5 plugins and will need the version +number 3. &chalk; 2.0 plugins will need the version number 3. Yes, this is not +entirely logical. +</para> + +</sect4> +</sect3> +</sect2> + +<sect2 id="developers-plugins-colorspaces"> +<title>Colorspaces</title> + +<para> +Colorspaces implement the <classname>KisColorSpace</classname> pure virtual +class. There are two types of colorspaces: those that can use +<command>lcms</command> for transformations between colorspaces, and those +that are too weird for <command>lcms</command> to handle. Examples of the +first are cmyk, rgb, yuv. An example of the latter is watercolor or wet & +sticky. Colorspaces that use <command>lcms</command> can be derived from +<classname>KisAbstractColorSpace</classname>, or of one of the base classes +that are specialized for a certain number of bits per channel. +</para><para> +Implementing a colorspace is pretty easy. The general principle is that +colorspaces work on a simple array of bytes. The interpretation of these bytes +is up to the colorspace. For instance, a pixel in 16-bit GrayA consists of +four bytes: two bytes for the gray value and two bytes for the alpha value. +You are free to use a struct to work with the memory layout of a pixel in your +colorspace implementation, but that representation is not exported. The only +way the rest of &chalk; can know what channels and types of channels your +colorspace pixels consist of is through the +<classname>KisChannelInfo</classname> class. +</para><para> +Filters and paintops make use of the rich set of methods offered by +<classname>KisColorSpace</classname> to do their work. In many cases, the +default implementation in <classname>KisAbstractColorSpace</classname> will +work, but more slowly than a custom implementation in your own colorspace +because <classname>KisAbstractColorSpace</classname> will convert all pixels +to 16-bit L*a*b and back. +</para> + +<sect3 id="developers-plugins-colorspaces-kischannelinfo"> +<title><classname>KisChannelInfo</classname></title> + +<programlisting> +(http://websvn.kde.org/trunk/koffice/chalk/chalkcolor/kis_channelinfo.h) +</programlisting> +<para> +This class defines the channels that make up a single pixel in a particular +colorspace. A channel has the following important characteristics: +</para> +<itemizedlist> +<listitem><para>a name for display in the user interface</para></listitem> +<listitem><para>a position: the byte where the bytes representing this channel +start in the pixel.</para></listitem> +<listitem><para>a type: color, alpha, substance or substrate. Color is plain +color, alpha is see-throughishness, substance is a representation of amount of +pigment or things like that, substrate is the representation of the canvas. +(Note that this may be refactored at the drop of a hat.)</para></listitem> +<listitem><para>a valuetype: byte, short, integer, float — or +other.</para></listitem> +<listitem><para>size: the number of bytes this channel takes</para></listitem> +<listitem><para>color: a <classname>QColor</classname> representation of this +channel for user interface visualization, for instance in +histograms.</para></listitem> +<listitem><para>an abbreviaton for use in the GUI when there’s not much +space</para></listitem> +</itemizedlist> +</sect3> + +<sect3 id="developers-plugins-colorspaces-kiscompositeop"> +<title><classname>KisCompositeOp</classname></title> + +<para> +As per original Porter-Duff, there are many ways of combining pixels to get a +new color. The <classname>KisCompositeOp</classname> class defines most of +them: this set is not easily extensible except by hacking the chalkcolor +library. +</para><para> +A colorspace plugin can support any subset of these possible composition +operations, but the set must always include "OVER" (same as "NORMAL") and +"COPY". The rest are more or less optional, although more is better, of +course. +</para> +</sect3> + +<sect3 id="developers-plugins-colorspaces-kiscolorspace"> +<title><classname>KisColorSpace</classname></title> + +<para> +The methods in the <classname>KisColorSpace</classname> pure virtual classs +can be divided into a number of groups: conversion, identification and +manipulation. +</para><para> +All classes must be able to convert a pixel from and to 8 bit RGB (i.e., a +<classname>QColor</classname>), and preferably also to and from 16 bit L*a*b. +Additionally, there is a method to convert to any colorspace from the current +colorspace. +</para><para> +Colorspaces are described by the <classname>KisChannelInfo</classname> vector, +number of channels, number of bytes in a single pixel, whether it supports +High Dynamic Range images and more. +</para><para> +Manipulation is for instance the combining of two pixels in a new +pixel: bitBlt, darkening or convolving of pixels. +</para><para> +Please consult the API documentation for a full description of all methods you +need to implement in a colorspace. +</para><para> +<classname>KisAbstractColorSpace</classname> implements many of the virtual +methods of <classname>KisColorSpace</classname> using functions from the +<command>lcms</command> library. On top of +<classname>KisAbstractColorSpace</classname> there are base colorspace classes +for 8 and 16 bit integer and 16 and 32 bit float colorspaces that define +common operations to move between bit depths. +</para> + +</sect3> +</sect2> + +<sect2 id="developers-plugins-filters"> +<title>Filters</title> + +<para> +Filters are plugins that examine the pixels in a layer and them make changes +to them. Although &chalk; uses an efficient tiled memory backend to store +pixels, filter writers do not have to bother with that. When writing a filter +plugin for the &Java; imaging API, Photoshop or The Gimp, you need to take care +of tile edges and <quote>cobble</quote> tiles together: &chalk; hides that +implementation detail. +</para> +<note><para>Note that it is theoretically easy to replace the current tile +image data storage backend with another backend, but that backens are not true +plugins at the moment, for performance reasons.</para></note> +<para> +&chalk; uses iterators to read and write pixel values. Alternatively, you can +read a block of pixels into a memory buffer, mess with it and then write it +back as a block. But that is not necessarily more efficient, it may even be +slower than using the iterators; it may just be more convenient. See the API +documentation. +</para><para> +&chalk; images are composed of layers, of which there are currently four +kinds: paint layers, group layers, adjustment layers (that contain a filter +that is applied dynamically to layers below the adjustment layer) and part +layers. Filters always operate on paint layers. Paint layers contain paint +devices, of the class <classname>KisPaintDevice</classname>. A paint device in +its turn gives access to the actual pixels. +</para><para> +<classname>PaintDevice</classname>s are generally passed around wrapped in +shared pointers. A shared pointer keeps track of in how many places the paint +device is currently used and deletes the paint device when it is no longer +used anywhere. You recognize the shared pointer version of a paint device +through its <literal>SP</literal> suffix. Just remember that you never have to +explicitly delete a <classname>KisPaintDeviceSP</classname>. +</para><para> +Let's examine a very simple filter, one that inverts every pixel. The code for +this filter is in the <filename +class="directory">koffice/chalk/plugins/filters/example</filename> directory. +The main method is +<programlisting> +KisFilterInvert::process(KisPaintDeviceSP src, KisPaintDeviceSP dst, + KisFilterConfiguration* /*config*/, const QRect& rect). +</programlisting> +The function gets passed two paint devices, a configuration object (which is +not used in this simple filter) and a <varname>rect</varname>. The +<varname>rect</varname> describes the area of the +paint device which the filter should act on. This area is described by +integers, which means no sub-pixel accuracy. +</para><para> +The <varname>src</varname> paint device is for reading from, the +<varname>dst</varname> paint device for writing to. These parameters may point +to the same actual paint device, or be two different paint devices. (Note: +this may change to only one paint device in the future.) +</para><para> +Now, let's look at the code line by line: +</para> +<programlisting> +void KisFilterInvert::process(KisPaintDeviceSP src, KisPaintDeviceSP dst, + KisFilterConfiguration* /*config*/, const QRect& rect) +{ + Q_ASSERT(src != 0); + Q_ASSERT(dst != 0); + + KisRectIteratorPixel srcIt = src->createRectIterator(rect.x(), rect.y(), rect.width(), rect.height(), false); <co id="invert1" /> + KisRectIteratorPixel dstIt = dst->createRectIterator(rect.x(), rect.y(), rect.width(), rect.height(), true ); <co id="invert2" /> + + int pixelsProcessed = 0; + setProgressTotalSteps(rect.width() * rect.height()); + + KisColorSpace * cs = src->colorSpace(); + Q_INT32 psize = cs->pixelSize(); + + while( ! srcIt.isDone() ) + { + if(srcIt.isSelected()) <co id="invert3" /> + { + memcpy(dstIt.rawData(), srcIt.oldRawData(), psize); <co id="invert4" /> + + cs->invertColor( dstIt.rawData(), 1); <co id="invert5" /> + } + setProgress(++pixelsProcessed); + ++srcIt; + ++dstIt; + } + setProgressDone(); // Must be called even if you don't really support progression +} +</programlisting> + +<calloutlist> +<callout arearefs="invert1"> +<para>This creates an iterator to read the existing pixels. Chalk has three +types of iterators: horizontal, vertical and rectangular. The rect iterator +takes the most efficient path through the image data, but does not guarantee +anything about the location of the next pixel it returns. That means that you +cannot be sure that the pixel you will retrieve next will be adjacent to the +pixel you just got. The horizontal and vertical line iterators do guarantee +the location of the pixels they return. +</para></callout> +<callout arearefs="invert2"><para> +(2) We create the destination iterator with the <literal>write</literal> +setting to <literal>true</literal>. This means that if the destination paint +device is smaller than the rect we write, it will automatically be enlarged to +fit every pixel we iterate over. Note that we have got a potential bug here: +if <varname>dst</varname> and <varname>src</varname> are not the same device, +then it is quite possible that the pixels returned by the iterators do not +correspond. For every position in the iterator, <varname>src</varname> may be, +for example, at 165,200, while <varname>dst</varname> could be at 20,8 — +and therefore the copy we perform below may distort the image... +</para></callout> +<callout arearefs="invert3"><para> +Want to know if a pixel is selected? That is easy — use the +<methodname>isSelected</methodname> method. But selectedness is not a binary +property of a pixel, a pixel can be half selected, barely selected or almost +completely selected. That value you can also got from the iterator. Selections +are actually a mask paint device with a range between 0 and 255, where 0 is +completely unselected and 255 completely selected. The iterator has two +methods: <methodname>isSelected()</methodname> and +<methodname>selectedNess()</methodname>. The first returns true if a pixel is +selected to any extent (i.e., the mask value is greater than 1), the other +returns the maskvalue. +</para></callout> +<callout arearefs="invert4"><para> +As noted above, this <literal>memcpy</literal> is a big bad bug... +<methodname>rawData()</methodname> returns the array of bytes which is the +current state of the pixel; <methodname>oldRawData()</methodname> returns the +array of bytes as it was before we created the iterator. However, we may be +copying the wrong pixel here. In actual practice, that will not happen too +often, unless <varname>dst</varname> already exists and is not aligned with +<varname>src</varname>. +</para></callout> +<callout arearefs="invert5"><para> +But this is correct: instead of figuring out which byte represents which +channel, we use a function supplied by all colorspaces to invert the current +pixel. The colorspaces have a lot of pixel operations you can make use of. +</para></callout> +</calloutlist> + +<para> +This is not all there is to creating a filter. Filters have two other +important components: a configuration object and a configuration widget. The +two interact closely. The configuration widget creates a configuration object, +but can also be filled from a pre-existing configuration object. Configuration +objects can represtent themselves as XML and can be created from XML. That is +what makes adjustment layers possible. +</para> + +<sect3 id="developers-plugins-filters-iterators"> +<title>Iterators</title> + +<para> +There are three types of iterators: +</para> + +<itemizedlist> +<listitem><para>Horizontal lines</para></listitem> +<listitem><para>Vertical lines</para></listitem> +<listitem><para>Rectangular iterors</para></listitem> +</itemizedlist> + +<para> +The horizontal and vertical line iterators have a method to move the iterator +to the next row or column: <methodname>nextRow()</methodname> and +<methodname>nextCol()</methodname>. Using these is much faster than creating a +new iterator for every line or column. +</para><para> +Iterators are thread-safe in &chalk;, so it is possible to divide the work +over multiple threads. However, future versions of &chalk; will use the +<methodname>supportsThreading()</methodname> method to determine whether your +filter can be applied to chunks of the image (&ie;, all pixels modified +independently, instead of changed by some value determined from an examination +of all pixels in the image) and automatically thread the execution your +filter. +</para> +</sect3> + +<sect3 id="developers-plugins-filters-kisfilterconfiguration"> +<title><classname>KisFilterConfiguration</classname></title> + +<para> +<classname>KisFilterConfiguration</classname> is a structure that is used to +save filter settings to disk, for instance for adjustment layers. The +scripting plugin uses the property map that’s at the back of +<classname>KisFilterConfigaration</classname> to make it possible to script +filters. Filters can provide a custom widget that &chalk; will show in the +filters gallery, the filter preview dialog or the tool option tab of the +paint-with-filters tool. +</para> +<para> +An example, taken from the oilpaint effect filter: +</para> +<programlisting> +class KisOilPaintFilterConfiguration : public KisFilterConfiguration +{ + +public: + + KisOilPaintFilterConfiguration(Q_UINT32 brushSize, Q_UINT32 smooth) + : KisFilterConfiguration( "oilpaint", 1 ) + { + setProperty("brushSize", brushSize); + setProperty("smooth", smooth); + }; +public: + + inline Q_UINT32 brushSize() { return getInt("brushSize"); }; + inline Q_UINT32 smooth() {return getInt("smooth"); }; + +}; +</programlisting> +</sect3> + +<sect3 id="developers-plugins-filters-kisfilterconfigurationwidget"> +<title><classname>KisFilterConfigurationWidget</classname></title> + +<para> +Most filters can be tweaked by the user. You can create a configuration widget +that Chalk will use where-ever your filter is used. An example: +</para> + +<para> +<screenshot> +<screeninfo>The <guilabel>Oilpaint</guilabel> dialog</screeninfo> +<mediaobject> +<imageobject> +<imagedata fileref="dialogs-oilpaint.png" format="PNG" /> +</imageobject> +<textobject> +<phrase>The <guilabel>Oilpaint</guilabel> dialog</phrase> +</textobject> +<caption><para>The <guilabel>Oilpaint</guilabel> dialog</para></caption> +</mediaobject> +</screenshot> +</para> + +<para> +Note that only the left-hand side of this dialog is your responsibility: +&chalk; takes care of the rest. There are three ways of going about creating +an option widget: +</para> + +<itemizedlist> +<listitem><para>Use &Qt; Designer to create a widget base, and subclass it for +your filter</para></listitem> +<listitem><para>Use one of the simple widgets that show a number of sliders +for lists of integers, doubles or bools. These are useful if, like the above +screenshot, your filter can be configured by a number of integers, doubles or +bools. See the API dox for <classname>KisMultiIntegerFilterWidget</classname>, +<classname>KisMultiDoubleFilterWidget</classname> and +<classname>KisMultiBoolFilterWidget</classname>.</para></listitem> +<listitem><para>Hand-code a widget. This is not recommended, and if you do so +and want your filter to become part of &chalk;’s official release, then I’ll ask +you to replate your hand-coded widget with a &Qt; Designer +widget.</para></listitem> +</itemizedlist> + +<para> +The oilpaint filter uses the multi integer widget: +</para> + +<programlisting> +KisFilterConfigWidget * KisOilPaintFilter::createConfigurationWidget(QWidget* parent, KisPaintDeviceSP /*dev*/) +{ + vKisIntegerWidgetParam param; + param.push_back( KisIntegerWidgetParam( 1, 5, 1, i18n("Brush size"), "brushSize" ) ); + param.push_back( KisIntegerWidgetParam( 10, 255, 30, i18n("Smooth"), "smooth" ) ); + return new KisMultiIntegerFilterWidget(parent, id().id().ascii(), id().id().ascii(), param ); +} + +KisFilterConfiguration* KisOilPaintFilter::configuration(QWidget* nwidget) +{ + KisMultiIntegerFilterWidget* widget = (KisMultiIntegerFilterWidget*) nwidget; + if( widget == 0 ) + { + return new KisOilPaintFilterConfiguration( 1, 30); + } else { + return new KisOilPaintFilterConfiguration( widget->valueAt( 0 ), widget->valueAt( 1 ) ); + } +} + +std::list<KisFilterConfiguration*> KisOilPaintFilter::listOfExamplesConfiguration(KisPaintDeviceSP ) +{ + std::list<KisFilterConfiguration*> list; + list.insert(list.begin(), new KisOilPaintFilterConfiguration( 1, 30)); + return list; +} +</programlisting> + +<para> +You can see how it works: fill a vector with your integer parameters and +create the widget. The <methodname>configuration()</methodname> method +inspects the widget and creates the right filter configuration object, in this +case, of course, <classname>KisOilPaintFilterConfiguration</classname>. The +<methodname>listOfExamplesConfiguration</methodname> method (which should be +renamed to correct English...) returns a list with example configuration +objects for the filters gallery dialog. +</para> +</sect3> + +<sect3 id="developers-plugins-filters-conclusion"> +<title>Filters conclusion</title> + +<para> +There’s more to coding interesting filters, of course, but with this +explanation, the API documentation and access to our source code, you should +be able to get started. Don’t hesitate to contact the &chalk; developers on +IRC or on the mailing list. +</para> +</sect3> + +</sect2> + +<sect2 id="developers-plugins-tools"> +<title>Tools</title> + +<para> +Tools appear in &chalk;’s toolbox. This means that there is limited space for +new tools — think carefully whether a paint operation isn’t enough for +your purposes. Tools can use the mouse/tablet and keyboard in complex ways, +which paint operations cannot. This is the reason that Duplicate is a tool, +but airbrush a paint operation. +</para><para> +Be careful with static data in your tool: a new instance of your tool is +created for every input device: mouse, stylus, eraser, airbrush — whatever. +Tools come divided into logical groups: +</para> +<itemizedlist> +<listitem><para>shape drawing tools (circle, rect)</para></listitem> +<listitem><para>freehand drawing tools (brush)</para></listitem> +<listitem><para>transform tools that mess up the geometry of a +layer</para></listitem> +<listitem><para>fill tools (like bucket fill or gradient)</para></listitem> +<listitem><para>view tools (that don’t change pixels, but alter the way you +view the canvas, such as zoom)</para></listitem> +<listitem><para>select tools (that change the selection +mask)</para></listitem> +</itemizedlist> + +<para> +The tool interface is described in the API documentation for +<classname>KisTool</classname>. There are three subclasses: +<classname>KisToolPaint</classname>, <classname>KisToolNonPaint</classname> +and <classname>KisToolShape</classname> (which is actually a subclass of +<classname>KisToolPaint</classname>) that specialize +<classname>KisTool</classname> for painting tasks (i.e., changing pixels) , +non-painting tasks and shape painting tasks. +</para><para> +A tool has an option widget, just like filters. Currently, the option widgets +are shown in a tab in a dock window. We may change that to a strip under the +main menu (which then replaces the toolbar) for &chalk; 2.0, but for now, +design your option widget to fit in a tab. As always, it’s best to use &Qt; +Designer for the design of the option widget. +</para><para> +A good example of a tool is the star tool: +</para> + +<screen> +kis_tool_star.cc Makefile.am tool_star_cursor.png wdg_tool_star.ui +kis_tool_star.h Makefile.in tool_star.h +chalktoolstar.desktop tool_star.cc tool_star.png +</screen> + +<para> +As you see, you need two images: one for the cursor and one for the toolbox. +<filename>tool_star.cc</filename> is just the plugin loader, similar to what +we have seen above. The real meat is in the implementation: +</para> + +<programlisting> +KisToolStar::KisToolStar() + : KisToolShape(i18n("Star")), + m_dragging (false), + m_currentImage (0) +{ + setName("tool_star"); + setCursor(KisCursor::load("tool_star_cursor.png", 6, 6)); + m_innerOuterRatio=40; + m_vertices=5; +} +</programlisting> + +<para> +The constructor sets the internal name — which is not translated +— and the call to the superclass sets the visible name. We also load the +cursor image and set a number of variables. +</para> + +<programlisting> +void KisToolStar::update (KisCanvasSubject *subject) +{ + KisToolShape::update (subject); + if (m_subject) + m_currentImage = m_subject->currentImg(); +} +</programlisting> + +<para> +The <methodname>update()</methodname> method is called when the tool is +selected. This is not a <classname>KisTool</classname> method, but a +<classname>KisCanvasObserver</classname> method. Canvas observers are notified +whenever something changes in the view, which can be useful for tools. +</para><para> +The following methods (<methodname>buttonPress</methodname>, +<methodname>move</methodname> and <methodname>buttonRelease</methodname>) are +called by &chalk; when the input device (mouse, stylus, eraser etc.) is +pressed down, moved or released. Note that you also get move events if the +mouse button isn’t pressed. The events are not the regular &Qt; events, but +synthetic &chalk; events because we make use of low-level trickery to get +enough events to draw a smooth line. By default, toolkits like &Qt; (and GTK) +drop events if they are too busy to handle them, and we want them all. +</para> + +<programlisting> +void KisToolStar::buttonPress(KisButtonPressEvent *event) +{ + if (m_currentImage && event->button() == LeftButton) { + m_dragging = true; + m_dragStart = event->pos(); + m_dragEnd = event->pos(); + m_vertices = m_optWidget->verticesSpinBox->value(); + m_innerOuterRatio = m_optWidget->ratioSpinBox->value(); + } +} + +void KisToolStar::move(KisMoveEvent *event) +{ + if (m_dragging) { + // erase old lines on canvas + draw(m_dragStart, m_dragEnd); + // move (alt) or resize star + if (event->state() & Qt::AltButton) { + KisPoint trans = event->pos() - m_dragEnd; + m_dragStart += trans; + m_dragEnd += trans; + } else { + m_dragEnd = event->pos(); + } + // draw new lines on canvas + draw(m_dragStart, m_dragEnd); + } +} + +void KisToolStar::buttonRelease(KisButtonReleaseEvent *event) +{ + if (!m_subject || !m_currentImage) + return; + + if (m_dragging && event->button() == LeftButton) { + // erase old lines on canvas + draw(m_dragStart, m_dragEnd); + m_dragging = false; + + if (m_dragStart == m_dragEnd) + return; + + if (!m_currentImage) + return; + + if (!m_currentImage->activeDevice()) + return; + + KisPaintDeviceSP device = m_currentImage->activeDevice ();; + KisPainter painter (device); + if (m_currentImage->undo()) painter.beginTransaction (i18n("Star")); + + painter.setPaintColor(m_subject->fgColor()); + painter.setBackgroundColor(m_subject->bgColor()); + painter.setFillStyle(fillStyle()); + painter.setBrush(m_subject->currentBrush()); + painter.setPattern(m_subject->currentPattern()); + painter.setOpacity(m_opacity); + painter.setCompositeOp(m_compositeOp); + KisPaintOp * op = + KisPaintOpRegistry::instance()->paintOp(m_subject->currentPaintop(), m_subject->currentPaintopSettings(), &painter); + painter.setPaintOp(op); // Painter takes ownership + + vKisPoint coord = starCoordinates(m_vertices, m_dragStart.x(), m_dragStart.y(), m_dragEnd.x(), m_dragEnd.y()); + + painter.paintPolygon(coord); + + device->setDirty( painter.dirtyRect() ); + notifyModified(); + + if (m_currentImage->undo()) { + m_currentImage->undoAdapter()->addCommand(painter.endTransaction()); + } + } +} +</programlisting> + +<para> +The <methodname>draw()</methodname> method is an internal method of +<classname>KisToolStar</classname> and draws the outline of the star. We call +this from the <methodname>move()</methodname> method to give the user feedback +of the size and shape of their star. Note that we use the +<varname>Qt::NotROP</varname> raster operation, which means that calling +<methodname>draw()</methodname> a second time with the same start and end +point the previously drawn star will be deleted. +</para> + +<programlisting> +void KisToolStar::draw(const KisPoint& start, const KisPoint& end ) +{ + if (!m_subject || !m_currentImage) + return; + + KisCanvasController *controller = m_subject->canvasController(); + KisCanvas *canvas = controller->kiscanvas(); + KisCanvasPainter p (canvas); + QPen pen(Qt::SolidLine); + + KisPoint startPos; + KisPoint endPos; + startPos = controller->windowToView(start); + endPos = controller->windowToView(end); + + p.setRasterOp(Qt::NotROP); + + vKisPoint points = starCoordinates(m_vertices, startPos.x(), startPos.y(), endPos.x(), endPos.y()); + + for (uint i = 0; i < points.count() - 1; i++) { + p.drawLine(points[i].floorQPoint(), points[i + 1].floorQPoint()); + } + p.drawLine(points[points.count() - 1].floorQPoint(), points[0].floorQPoint()); + + p.end (); +} +</programlisting> + +<para> +The <methodname>setup()</methodname> method is essential: here we create the +action that will be plugged into the toolbox so users can actually select the +tool. We also assign a shortcut key. Note that there’s some hackery going on: +remember that we create an instance of the tool for every input device. This +also means that we call <methodname>setup()</methodname> for every input +device and that means that an action with the same name is added several times +to the action collection. However, everything seems to work, so why worry? +</para> + +<programlisting> +void KisToolStar::setup(KActionCollection *collection) +{ + m_action = static_cast<KRadioAction *>(collection->action(name())); + + if (m_action == 0) { + KShortcut shortcut(Qt::Key_Plus); + shortcut.append(KShortcut(Qt::Key_F9)); + m_action = new KRadioAction(i18n("&Star"), + "tool_star", + shortcut, + this, + SLOT(activate()), + collection, + name()); + Q_CHECK_PTR(m_action); + + m_action->setToolTip(i18n("Draw a star")); + m_action->setExclusiveGroup("tools"); + m_ownAction = true; + } +} +</programlisting> + +<para> +The <methodname>starCoordinates()</methodname> method contains some funky math +— but is not too interesting for the discussion of how to create a tool +plugins. +</para> + +<programlisting> +KisPoint KisToolStar::starCoordinates(int N, double mx, double my, double x, double y) +{ + double R=0, r=0; + Q_INT32 n=0; + double angle; + + vKisPoint starCoordinatesArray(2*N); + + // the radius of the outer edges + R=sqrt((x-mx)*(x-mx)+(y-my)*(y-my)); + + // the radius of the inner edges + r=R*m_innerOuterRatio/100.0; + + // the angle + angle=-atan2((x-mx),(y-my)); + + //set outer edges + for(n=0;n<N;n++){ + starCoordinatesArray[2*n] = KisPoint(mx+R*cos(n * 2.0 * M_PI / N + angle),my+R*sin(n *2.0 * M_PI / N+angle)); + } + + //set inner edges + for(n=0;n<N;n++){ + starCoordinatesArray[2*n+1] = KisPoint(mx+r*cos((n + 0.5) * 2.0 * M_PI / N + angle),my+r*sin((n +0.5) * 2.0 * M_PI / N + angle)); + } + + return starCoordinatesArray; +} +</programlisting> + +<para> +The <methodname>createOptionWidget()</methodname> method is called to create +the option widget that &chalk; will show in the tab. Since there is a tool per +input device per view, the state of a tool can be kept in the tool. This +method is only called once: the option widget is stored and retrieved the next +time the tool is activated. +</para> + +<programlisting> +QWidget* KisToolStar::createOptionWidget(QWidget* parent) +{ + QWidget *widget = KisToolShape::createOptionWidget(parent); + + m_optWidget = new WdgToolStar(widget); + Q_CHECK_PTR(m_optWidget); + + m_optWidget->ratioSpinBox->setValue(m_innerOuterRatio); + + QGridLayout *optionLayout = new QGridLayout(widget, 1, 1); + super::addOptionWidgetLayout(optionLayout); + + optionLayout->addWidget(m_optWidget, 0, 0); + + return widget; +} +</programlisting> + +<sect3 id="developers-plugins-tools-conclusions"> +<title>Tool Conclusions</title> + +<para> +Tools are relatively simple plugins to create. You need to combine the +<classname>KisTool</classname> and <classname>KisCanvasObserver</classname> +interfaces in order to effectively create a tool. +</para> + +</sect3> + +</sect2> + +<sect2 id="developers-plugins-paintoperations"> +<title>Paint operations</title> + +<para> +PaintOps are one of the more innovative types of plugins in Chalk (together +with pluggable colorspaces). A paint operation defines how tools change the +pixels they touch. Airbrush, aliased pencil or antialiased pixel brush: these +are all paint operations. But you could — with a lot of work — +create a paintop that reads Corel Painter XML brush definitions and uses those +to determine how painting is done. +</para><para> +Paint operations are instantiated when a paint tool receives a +<literal>mouseDown</literal> event and are deleted when the mouseUp event is +received by a paint tool. In between, the paintop can keep track of previous +positions and other data, such as pressure levels if the user uses a tablet. +</para><para> +The basic operation of a paint operation is to change pixels at the cursor +position of a paint tool. That can be done only once, or the paint op can +demand to be run at regular intervals, using a timer. The first would be +useful for a pencil-type paint op, the second, of course, for an +airbrush-type paintop. +</para><para> +Paintops can have a small configuration widget which is placed in a toolbar. +Thus, paintop configuration widgets need to have a horizontal layout of +widgets that are not higher than a toolbar button. Otherwise, &chalk; will +look very funny. +</para><para> +Let’s look at a simple paintop plugin, one that shows a little bit of +programmatic intelligence. First, in the header file, there’s a factory +defined. This factory creates a paintop when the active tool needs one: +</para> + +<programlisting> +public: + KisSmearyOpFactory() {} + virtual ~KisSmearyOpFactory() {} + + virtual KisPaintOp * createOp(const KisPaintOpSettings *settings, KisPainter * painter); + virtual KisID id() { return KisID("paintSmeary", i18n("Smeary Brush")); } + virtual bool userVisible(KisColorSpace * ) { return false; } + virtual QString pixmap() { return ""; } + +}; +</programlisting> + +<para> +The factory also contains the <classname>KisID</classname> with the public and +private name for the paintop — make sure your paintop’s private name +does not clash with another paintop! — and may optionally return a +pixmap. &chalk; can then show the pixmap together with the name for visual +identifcation of your paintop. For instance, a painter’s knife paintop would +have the image of such an implement. +</para><para> +The implementation of a paintop is very straightforward: +</para> + +<programlisting> +KisSmearyOp::KisSmearyOp(KisPainter * painter) + : KisPaintOp(painter) +{ +} + +KisSmearyOp::~KisSmearyOp() +{ +} +void KisSmearyOp::paintAt(const KisPoint &pos, const KisPaintInformation& info) +{ +</programlisting> + +<para> +The <methodname>paintAt()</methodname> method really is where it’s at, with +paintops. This method receives two parameters: the current position (which is +in floats, not in whole pixels) and a +<classname>KisPaintInformation</classname> object. which contains the +pressure, x and y tilt, and movement vector, and may in the future be extended +with other information. +</para> + +<programlisting> + if (!m_painter->device()) return; + + KisBrush *brush = m_painter->brush(); +</programlisting> + +<para> +A <classname>KisBrush</classname> is the representation of a Gimp brush file: +that is a mask, either a single mask or a series of masks. Actually, we don’t +use the brush here, except to determine the <quote>hotspot</quote> under the +cursor. +</para> + +<programlisting> + Q_ASSERT(brush); + + if (!brush) return; + + if (! brush->canPaintFor(info) ) + return; + + KisPaintDeviceSP device = m_painter->device(); + KisColorSpace * colorSpace = device->colorSpace(); + KisColor kc = m_painter->paintColor(); + kc.convertTo(colorSpace); + + KisPoint hotSpot = brush->hotSpot(info); + KisPoint pt = pos - hotSpot; + + // Split the coordinates into integer plus fractional parts. The integer + // is where the dab will be positioned and the fractional part determines + // the sub-pixel positioning. + Q_INT32 x, y; + double xFraction, yFraction; + + splitCoordinate(pt.x(), &x, &xFraction); + splitCoordinate(pt.y(), &y, &yFraction); + + KisPaintDeviceSP dab = new KisPaintDevice(colorSpace, "smeary dab"); + Q_CHECK_PTR(dab); +</programlisting> + +<para> +We don’t change the pixels of a paint device directly: instead we create a +small paint device, a dab, and composite that onto the current paint device. +</para> + +<programlisting> + m_painter->setPressure(info.pressure); +</programlisting> + +<para> +As the comments say, the next bit code does some programmatic work to create +the actual dab. In this case, we draw a number of lines. When I am done with +this paintop, the length, position and thickness of the lines will be +dependent on pressure and paint load, and we’ll have create a stiff, smeary +oilpaint brush. But I haven’t had time to finish this yet. +</para> + +<programlisting> + // Compute the position of the tufts. The tufts are arranged in a line + // perpendicular to the motion of the brush, i.e, the straight line between + // the current position and the previous position. + // The tufts are spread out through the pressure + + KisPoint previousPoint = info.movement.toKisPoint(); + KisVector2D brushVector(-previousPoint.y(), previousPoint.x()); + KisVector2D currentPointVector = KisVector2D(pos); + brushVector.normalize(); + + KisVector2D vl, vr; + + for (int i = 0; i < (NUMBER_OF_TUFTS / 2); ++i) { + // Compute the positions on the new vector. + vl = currentPointVector + i * brushVector; + KisPoint pl = vl.toKisPoint(); + dab->setPixel(pl.roundX(), pl.roundY(), kc); + + vr = currentPointVector - i * brushVector; + KisPoint pr = vr.toKisPoint(); + dab->setPixel(pr.roundX(), pr.roundY(), kc); + } + + vr = vr - vl; + vr.normalize(); +</programlisting> + +<para> +Finally we blt the dab onto the original paint device and tell the painter +that we’ve dirtied a small rectangle of the paint device. +</para> + +<programlisting> + if (m_source->hasSelection()) { + m_painter->bltSelection(x - 32, y - 32, m_painter->compositeOp(), dab.data(), + m_source->selection(), m_painter->opacity(), x - 32, y -32, 64, 64); + } + else { + m_painter->bitBlt(x - 32, y - 32, m_painter->compositeOp(), dab.data(), m_painter->opacity(), x - 32, y -32, 64, 64); + } + + m_painter->addDirtyRect(QRect(x -32, y -32, 64, 64)); +} + + +KisPaintOp * KisSmearyOpFactory::createOp(const KisPaintOpSettings */*settings*/, KisPainter * painter) +{ + KisPaintOp * op = new KisSmearyOp(painter); + Q_CHECK_PTR(op); + return op; +} +</programlisting> + +<para> +That’s all: paintops are easy and fun! +</para> + +</sect2> + +<sect2 id="developers-plugins-viewplugins"> +<title>View plugins</title> + +<para> +View plugins are the weirdest of the bunch: a view plugin is an ordinary KPart +that can provide a bit of user interface and some functionality. For instance, +the histogram tab is a view plugin, as is the rotate dialog. +</para> + +</sect2> + +<sect2 id="developers-plugins-importexport"> +<title>Import/Export filters</title> + +<para> +&chalk; works with the ordinary &koffice; file filter architecture. There is a +tutorial, a bit old, but still useful, at: <ulink +url="http://koffice.org/developer/filters/oldfaq.php" />. It is probably best +to cooperate with the &chalk; team when developing file filters and do the +development in the &koffice; filter tree. Note that you can test your filters +without running &chalk; using the <command>koconverter</command> utility. +</para><para> +Filters have two sides: importing and exporting. These are usually two +different plugins that may share some code. +</para><para> +The important <filename>Makefile.am</filename> entries are: +</para> + +<programlisting> +service_DATA = chalk_XXX_import.desktop chalk_XXX_export.desktop +servicedir = $(kde_servicesdir) +kdelnk_DATA = chalk_XXX.desktop +kdelnkdir = $(kde_appsdir)/Office +libchalkXXXimport_la_SOURCES = XXXimport.cpp +libchalkXXXexport_la_SOURCES = XXXexport.cpp +METASOURCES = AUTO +</programlisting> + +<para> +Whether you are building an import filter or an export filter, your work always +boils down to implementing the following function: +</para> + +<programlisting> +virtual KoFilter::ConversionStatus convert(const QCString& from, const QCString& to); +</programlisting> + +<para> +It is the settings in the <literal role="extension">.desktop</literal> files +that determine which way a filter converts: +</para><para> +Import: +</para> + +<programlisting> +X-KDE-Export=application/x-chalk +X-KDE-Import=image/x-xcf-gimp +X-KDE-Weight=1 +X-KDE-Library=libchalkXXXimport +ServiceTypes=KOfficeFilter +</programlisting> + +<para> +Export: +</para> + +<programlisting> +X-KDE-Export=image/x-xcf-gimp +X-KDE-Import=application/x-chalk +ServiceTypes=KOfficeFilter +Type=Service +X-KDE-Weight=1 +X-KDE-Library=libchalkXXXexport +</programlisting> + +<para> +And yes, the mimetype chosen for the example is a hint. Please, pretty please, +implement an xcf filter? +</para> + +<sect3 id="plugins-developers-importexport-import"> +<title>Import</title> + +<para> +The big problem with import filters is of course your code to read the data on +disk. The boilerplate for calling that code is fairly simple: +</para> + +<note><para>Note: we really, really should find a way to enable &chalk; to keep +a file open and only read data on a as-needed basis, instead of copying the +entire contents to the internal paint device representation. But that would +mean datamanager backends that know about tiff files and so on, and is not +currently implemented. It would be ideal if some file filters could implement +a class provisionally named <classname>KisFileDataManager</classname>, create +an object of that instance with the current file and pass that to KisDoc. But +&chalk; handles storage per layer, not per document, so this would be a hard +refactor to do.</para></note> + +<programlisting> +KoFilter::ConversionStatus XXXImport::convert(const QCString&, const QCString& to) +{ + if (to != "application/x-chalk") <co id="import1" /> + return KoFilter::BadMimeType; + + KisDoc * doc = dynamic_cast<KisDoc*>(m_chain -> outputDocument()); <co id="import2" /> + KisView * view = static_cast<KisView*>(doc -> views().getFirst()); <co id="import3" /> + + QString filename = m_chain -> inputFile(); <co id="import4" /> + + if (!doc) + return KoFilter::CreationError; + + doc -> prepareForImport(); <co id="import5" /> + + if (!filename.isEmpty()) { + + KURL url(filename); + + if (url.isEmpty()) + return KoFilter::FileNotFound; + + KisImageXXXConverter ib(doc, doc -> undoAdapter()); <co id="import6" /> + + if (view != 0) + view -> canvasSubject() -> progressDisplay() -> setSubject(&ib, false, true); + + switch (ib.buildImage(url)) <co id="import7" /> { + case KisImageBuilder_RESULT_UNSUPPORTED: + return KoFilter::NotImplemented; + break; + case KisImageBuilder_RESULT_INVALID_ARG: + return KoFilter::BadMimeType; + break; + case KisImageBuilder_RESULT_NO_URI: + case KisImageBuilder_RESULT_NOT_LOCAL: + return KoFilter::FileNotFound; + break; + case KisImageBuilder_RESULT_BAD_FETCH: + case KisImageBuilder_RESULT_EMPTY: + return KoFilter::ParsingError; + break; + case KisImageBuilder_RESULT_FAILURE: + return KoFilter::InternalError; + break; + case KisImageBuilder_RESULT_OK: + doc -> setCurrentImage( ib.image()); <co id="import8" /> + return KoFilter::OK; + default: + break; + } + + } + return KoFilter::StorageCreationError; +} +</programlisting> + +<calloutlist> +<callout arearefs="import1"><para>This is supposed to be an importfilter, so +if it is not called to convert to a &chalk; image, then something is +wrong.</para></callout> +<callout arearefs="import2"><para>The filter chain already has created an +output document for us. We need to cast it to <classname>KisDocM</classname>, +because &chalk; documents need special treatment. It would not, actually, be +all that bad an idea to check whether the result of the cast is not 0, because +if it is, importing will fail.</para></callout> +<callout arearefs="import3"><para>If we call this filter from the GUI, we try +to get the view. If there is a view, the conversion code can try to update the +progressbar.</para></callout> +<callout arearefs="import4"><para>The filter has the filename for our input +file for us.</para></callout> +<callout arearefs="import5"><para><classname>KisDoc</classname> needs to be +prepared for import. Certain settings are initialized and undo is disabled. +Otherwise you could undo the adding of layers performed by the import filter +and that is weird behaviour.</para></callout> +<callout arearefs="import6"><para>I have chosed to implement the actual +importing code in a separate class that I instantiate here. You can also put +all your code right in this method, but that would be a bit +messy.</para></callout> +<callout arearefs="import7"><para>My importer returns a statuscode that I +can then use to set the status of the import filter. &koffice; takes care of +showing error messages.</para></callout> +<callout arearefs="import8"><para>If creating the +<classname>KisImage</classname> has succeeded we set the document's current +image to our newly created image. Then we are done: <literal>return +KoFilter::OK;</literal>.</para></callout> +</calloutlist> + +</sect3> + +</sect2> + +</sect1> |