Filter effects internals
This page describes the internal workings of Inkscape Filter effects subsystem.
- 1 Overview
- 2 Coordinate systems
- 3 Filters in document layer
- 4 Filter effects renderer
- 5 NR::Filter initialization
- 6 Modification signals for filters
From Filter effects viewpoint, Inkscape is essentially three-tiered system. The tiers are XML representation, SVG document and SVG renderer.
XML representation level is merely an representation of XML tree of the document. It contains XML nodes, which have attributes, content, children etc. At this level these nodes and whatever content they have, don't have any special meaning. Inkscape XML representation layer could be used for any type of XML document, not just SVG.
SVG document layer builds upon the XML layer. This layer gives meaning to XML node names, node attributes, node content etc. Mostly, nodes are handled by different classes depending on the name of the node. For example, nodes named feOffset are handled by SPFeOffset class, which resides in src/sp-feoffset.*
Main tasks for SVG document layer are reading essential parameters from XML tree, parsing parameter values read from XML tree, writing out internal state to XML node and initializing SVG renderer classes.
SVG renderer produces bitmap images from the SVG graphics. Again, there are own classes for most different node names. For example, feOffset node is handled by NR::FilterOffset class, which resides in src/display/nr-filter-offset.*
Note that Inkscape uses two types of classes here: SVG document layer uses GTK style classes, which don't make use of C++ object oriented programming functionality, but are usable in plain C. SVG renderer in turn uses C++ classes.
This page talks a lot about coordinate systems and it some parts might be hard to follow, if you're not somewhat familiar with coordinate system transforms.
In SVG transformations like moving, scaling, rotating and shearing objects are modelled as coordinate system transformations. Each object is defined in its own coordinate system, which is combination of its own transformation and all its ancestors transformations. This allows for relatively simple cumulative transforms, like when you rotate a group, it rotates as a whole.
Let's try this out in Inkscape. Draw a rectangle, visibly wider than it's high. Rotate it 90 degrees anti-clockwise. Now, using the numerical inputs of the rectangle tool, change the width of the rectangle. You can notice, you're actually modifying the height of the rectangle. Why is this? Well, let's suppose that originally rectangle's width increased to right and height upwards. Now that you've rotated the rectangle by 90 degrees, you've actually rotated these base directions - or should we say vectors - by 90 degrees. For this rectangle, width now increases upwards and height to left.
For a more through and detailed description, see Coordinate Systems, Transformations and Units chapter in SVG specifications
Filters in document layer
In document layer, the classes responsible for filter effects are SPFilter, SPFilterPrimitive and it's subclasses. Each of SPFilterPrimitive's subclasses handles one type of filter primitive. There is also related code in SPItem, SPStyle, NRArenaShape, NRArenaGroup and NRArenaImage.
Reading parameters from XML tree
The build method of a filter primitive class should read all possible parameters, that are special for that filter primitive. For example, sp_feOffset_build calls sp_object_read_attr for parameters dx and dy. Any parameters that are common for all filter primitives, should be read in build-method for SPFilterPrimitive, which is superclass for all filter primitive classes.
Note that the build method doesn't need to make any attempt to handle any contents, the read attributes may have. It is handled by the set-method, which is described in next paragraph.
Parsing parameters read from XML tree
When the parameters in XML tree are modified or read by build method, the set method for corresponding document layer object gets called. This method receives the name and content of the parameter and should set object's internal state according to these values.
Writing out XML nodes
The write function in filter primitive class should write the relevant parts of object's internal state to given node. Note that many filter primitives don't do this, but instead just duplicate the contents of their XML node. For some examples how to do this right, see sp-shape.cpp or sp-filter.cpp.
Initializing SVG renderer classes
Each filter primitive class should have build_renderer method, which will add correct type of filter primitive to the filter renderer object and give correct settings for created filter primitive.
Filter renderer uses item bounding box for its calculations, this information is set in sp_item_update in sp-item.cpp.
Also in this file contains sp_item_write_transform, which handles transforming items. Some transformations, like scaling a rectangle, can be done by embedding transform into object's properteries (i.e. changing rectangle width and height) instead of writing out normal transformation. If an item has a filter set, this might give odd behaviour, so this method checks for set filters before embedding transforms.
Filter effect applied to an object can be considered as a part of object style and in Inkscape codebase it is a natural place to handle such object property.
SPStyle can locate the filter referenced by an SVG item, and provides a pointer to corresponding SPFilter object.
NRArenaShape, -Group and -Image
While these are renderer level classes, they contain filtering code, that interfaces document level and renderer level.
Here, in set_style method for each class, if the SVG item references some filter, the corresponding SPFilter is fetched from SPStyle and a NR::Filter object is constructed from it.
Specifically, the set_style method calls sp_filter_build_renderer, which will initialize the filter renderer (NR::Filter object).
Filter effects renderer
The filter effects renderer is somewhat more complicated than the document level. The main components are NR::Filter, NR::FilterSlot, NR::FilterUnits and filter primitive renderers, which are NR::FilterPrimitive subclasses.
NR::Filter is the main interface to filter effects renderer internals for rest of Inkscape renderer and document level filter effect classes.
Temporary images used in rendering the filter effect are stored in 'image slots'. Slots are referenced to by integers. Positive numbers are free to be used for user-defined temporary images. Negative numbers in turn are reserved for special images like SourceGraphic. Each slot may contain one bitmap image.
Slot numbers are created in document level, according to temporary image names given in SVG file. Each user-defined name receives own integer, unique inside that filter, and each SVG defined name is mapped to corresponding pre-defined negative number. See sp_filter_*_image_name in src/sp-filter.cpp and sp_filter_primitive_read in src/display/sp-filter-primitive.cpp for further info.
Each input image name defined in SVG also has a symbolical name like NR::NR_FILTER_SOURCEGRAPHIC, so that developer doesn't need to remember, which negative integer corresponds to which input image. There's also a special value NR::NR_FILTER_SLOT_NOT_SET, which means that input image to be used is not defined in SVG file. If this is the case, the output from previous filter or SourceGraphic is used, per SVG specifications. See src/display/nr-filter-types.h for these names.
All images stored in one FilterSlot object are in same coordinate system. This coordinate system might be different from the coordinate system of NR::Filter::render input image. If this is the case, FilterSlot::set transforms these input images to the internal coordinate system and FilterSlot::get_final transforms the output image from the internal coordinate system back to the coordinate system, the original input was in.
FilterUnits is a helper class to simplify using different coordinate systems and units in filter effect rendering. Essentially NR::Filter::render initializes a FilterUnits object with information about object's SVG bounding box, current object space to screen coordinates transformation matrix, desired filtering resolution and such. After this, different parts of filtering code like FilterSlot::set and filter primitive renderers can use information derived from information stored in FilterUnits.
This is essential part in making rotating and shearing filtered SVG items work. Many of the filter primitives use lengths, which are defined along object's x- and y-axis. Take for example gaussian blur, for which one can define different blur radius along x- and y-axis. Now, when these items are rotated and/or sheared, object's axis may not point any more to same direction as the axis of the bitmap image being rendered. It wouldn't be feasible to implement the required functionality in all filter primitives, this applies to, so the input image is first transformed to such an coordinate system, where the object's axis are to same direction as image axis.
FilterUnits has functions that provide FilterSlot with necessary transformation matrices to transform input image to a coordinate system, that all filters primitives know how to use and back to original coordinate system.
A filter primitive can indicate, it needs input where the image axis point to same direction as object axis, by returning TRAIT_PARALLER from get_input_traits.
Filter primitive renderers
Filter primitive renderers are the workhorses of filter effects rendering. Each renderer can render one of the filter primitives specified in SVG.
The basic workflow for rendering a filter primitive is
- Fetch input images from FilterSlot received as parameter
- Read pixel data from these images and write filter result to a new image
- Save the result image to FilterSlot
The filter primitive renderer can use FilterUnits object it has received as a parameter for various tasks related to lengths and coordinate systems. A couple examples:
- FilterOffset uses primitive units to input image coordinates transformation matrix to find out, by how many pixels it must transform the image, when it knows the length in object's coordinate system.
- FilterTurbulence uses the filter effects area transformed to input image coordinates to find out, to how big area it should render to.
This is a brief explanation on how NR::Filter (filter effects renderer) objects are constructed.
SPFilter has a single method sp_filter_build_renderer, which will initialize given renderer object (NR::Filter) to a correct state. Calling this method is all that needs to be done in those three nr-arena-* classes to set the correct filter renderer state. This method takes in the NR::Filter object instead of returning one, because this way that object can be reserved and freed on the same level in code. Also, this makes it easier to re-use the object instead of allocating new objects.
The inside workings of sp_filter_build_renderer are as follows: each filter primitive (SPFilterPrimitive subclasses) has a build_renderer virtual function that will add the correct NR::FilterPrimitive object in the filter renderer. Before doing any filter specific initialization, this function should call sp_filter_primitive_renderer_common, which will do the part of initialization, which is common for all filter primitives.
Modification signals for filters
This is explanation on how different parts of document tree are notified of changes to filter primitives. This is done so that the display can be updated as the filters are modified.
As the underlying XML representation of the drawing is modified, the corresponding document level objects are notified of the change. Let's suppose, that the changed value was stdDeviation in feGaussianBlur. For the SPGaussianBlur object, this will show as call to sp_gaussianBlur_set method, with key=SP_ATTR_STDDEVIATION and 'value' containing the new value.
After modifying its internal state according to new values, the _set method should pass the update notification onwards - this will allow objects using this filter to update their own state. As for now, this happens by calling ::requestModified(SP_OBJECT_MODIFIED_FLAG) on the filter primitive's parent (which should be SPFilter). This may not be the best way to do this, though.
Filters are referenced from object style. When SPStyle object is built, it subscribes for update notifications from SPFilter it references (if any). Now when ::requestModified is called on SPFilter, these update notifications are also called (eventually, as requestModified only schedules modification event, instead of executing the event immediately).
The SPStyle object in turn knows, which object it's part of - these objects are the actual drawable objects, to which the filters are applied to. SPStyle propagates the modification event to that object, which in turn applies the modifications to its internal state and schedules redraw for itself.