Intention / Introduction to Operations

Operations are objects abstracting the rendering backend from the client. They provide an API that let the client build macros that are performing a chain of rendering operations. Operations are introduced to provide an easy way to apply all kinds of transformations and filters to one object. For example, you may want to apply a Rotate operation, a Blur operation and a ChangeContrast operation over one rectangle. To let clients do this easily, these operations can be chained into Macros.

The client is provided with an API resembling the unterlying Cairo calls, similar to this:

// Set a custom item up that draws a rectangle.
Canvas::Operation::Macro         macro;   ///< A Macro is an operation chain.
Canvas::Operation::VectorMoveTo  moveto(x1, y1);
Canvas::Operation::VectorLineTo  lineto(x2, y2);
Canvas::Operation::VectorStroke  stroke;
Canvas::Operation::Blur          blur(20);
Canvas::Item                     item;    ///< An item in the canvas.

// Put all operations into one chain. The resulting macro will
// - Create a (vectorial representation of a) line.
macro.append(moveto);
macro.append(lineto);
// - Draw that line to the RGBA buffer.
macro.append(stroke);
// - Apply a blur with intesity 20 to the buffer.
macro.append(blur);
// Now, tell the canvas item to use that macro.
item.set_macro(macro);

// Now, it is possible to alter the line shape...
lineto.set(10, 20);

// ...or even to alter the operation chain completely.
Canvas::Operation::VectorElipse elipse(x1, y1, x2, y2);
Canvas::Operation::Rotate       rotate(180);
macro.remove(moveto);
macro.remove(lineto);
macro.insert_before(stroke, elipse);
macro.insert_before(blur,   rotate);

Considerations

  • To ensure that a client can not connect nosensical operations, each operation needs to enforce compatibility at connect time.
  • Whenever a Macro (= a chain of Operations) is executed, the first Operation in a chain gets a !Plane property object passed that owns all data regarding a specific image.

Implementation

The Operation implementation consists of the following classes:

The Operation Class

/// The component in the composite pattern.
/**
 * Defines a common interface for all Operations. Operations can be connected
 * to each other.
 */
class Operation {
public:
  virtual ~Operation() { };
  
  /// Notifies the Operation that it has received an inbound connection.
  /**
   * Each Operation can be connected to other Operations, with one inbound
   * connection and one outbound connection.
   * When another Operation has connected itself, it should call this method
   * to ensure that no more than one inbound connection can be created.
   * Since not all Operations are compatible with each other, when another 
   * Operation wants to connect to this Operation, it must also make sure that
   * it has sufficient capabilities to do that. This is done through the
   * _capabilities argument.
   * \param A bitmask listing the capabilities of the Operation that wants to
   *        connect.
   * \return TRUE if the Operations are compatible, were not yet connected, and
   *         the connection was successfully made, FALSE otherwise.
   */
  virtual const bool notify_connect(int _capabilities);
  
  /// Notifies the Operation that the inbound connection has been removed
  /**
   * Each Operation can be connected to two other Operations, one inbound
   * connection and one outbound connection.
   * When another Operation disconnected itself, it should call this method.
   */
  virtual void notify_disconnect(void);
  
  /// Create an outbound connection to another Operation.
  /**
   * Each Operation can be connected to two other Operations, one inbound
   * connection and one outbound connection. This method connects the outbound
   * direction of this plug to the inbound connection of another. It also notifies
   * the remote Operation using notifiy_connect().
   * \param The operation that we want to connect.
   * \return TRUE if the connection was successfully made, FALSE otherwise.
   */
  virtual const bool connect_to(Operation* _operation);
  
  /// Disconnects the outbound connection.
  /**
   * Each Operation can be connected to two other Operations, one inbound
   * connection and one outbound connection. This method disconnects the
   * outbound direction of this plug from another. It also notifies
   * the remote Operation using notifiy_disconnect().
   */
  virtual void disconnect(void);
  
  /// Returns whether this Operation is executable.
  /**
   * Only the first operation in a Macro can be executed, since all other
   * Operations are triggered automatically through the chain.
   * \return TRUE if the Operation is executable, FALSE otherwise.
   */
  virtual const bool can_execute(void);
  
  /// Executes the operation.
  /**
   * Only the first operation in a Macro can be executed, since all other
   * operations are triggered automatically through the chain.
   * During the operation, the proceed_fragment() method of the connected 
   * Operation may be called to perform a part of a task.
   * When the operation is finished, the proceed() method of the connected 
   * Operation is called.
   * \param _plane A reference to the Plane to draw to.
   * \return TRUE if the Operation was successfully finished, FALSE otherwise.
   */
  virtual const bool execute(Plane* _plane);
  
  /// Inserts the given Operation into the Macro before an existing Operation.
  /**
   * This method is only implemented by Macros, Operations will always
   * return NULL.

   * Adds the given Operation into the Macro, inserting it before the
   * Operation given in _existing. Returns a pointer to the inserted Operation,
   * or NULL on failure. If _manage is TRUE, the Macro will automatically
   * delete the Operation (and free the resources) on removal of the Component
   * or on destroyal of the parent Macro.
   * \param _existing A pointer to an Operation that is already in the Macro.
   * \param _operation A pointer to the Operation that is to be inserted.
   * \param _manage When TRUE, the Macro takes care of freeing the _operation
   *                memory.
   * \return A pointer to the inserted Operation.
   */
  virtual Operation* insert_before(Operation* _existing,
                                   Operation* _operation,
                                   bool       _manage = FALSE);
  
  /// Appends the given Operation to the end of the chain in the Macro.
  /**
   * This method is only implemented by Macros, Operations will always
   * return NULL.
   * Adds the given operation into the Macro, appending it to the Operation
   * chain. Returns a pointer to the Operation, or NULL on failure. If _manage
   * is TRUE, the Macro will automatically delete the Operation (and free the
   * resources) on removal of the Component or on destroyal of the Macro.
   * \param _operation A pointer to the Operation that is to be appended.
   * \param _manage When TRUE, the Macro takes care of freeing the _operation
   *                memory.
   * \return A pointer to the inserted Operation.
   */
  virtual Operation* append(Operation* _operation, bool _manage = FALSE);
  
  /// Removes an Operation from a Macro.
  /**
   * This method is only implemented by Macros, do not use it on Operations.
   * Removes the given Operation from the Macro. If _manage was set TRUE
   * on insertion, the object is also being destroyed.
   * \param _operation A pointer to the Operation that is to be removed.
   */
  virtual void remove(Operation* _operation);
  
  /// Whether the Operation acts as a cache.
  /**
   * \return TRUE if this Operation acts as a cache, FALSE otherwise.
   */
  virtual bool caches(void) { return FALSE; }
  
protected:
  /// Proceeds with the Operation.
  /**
   * This method does the same as execute(), except that it can also be called
   * on Operations that are not the first in the chain.
   * \param _plane A reference to the Plane to draw to.
   * \return TRUE if the Operation was successfully finished, FALSE otherwise.
   */
  virtual const bool proceed(Plane* _plane);
  
  /// Proceeds with a fragment of the operation.
  /**
   * This method does the same as proceed(), except that it performs only a
   * part of the task. This is useful in slow Operations to give a more 
   * responsive impression to the user.
   * \param _plane A reference to the Plane to draw to.
   * \return TRUE if the fragment was successfully processed, FALSE otherwise.
   */
  virtual const bool proceed_fragment(Plane* _plane);
  
  bool       has_inbound_connection;
  Operation* outbound;
};

The Plane Class

class Plane {
public:
  /// Instantiate a new Plane.
  /**
   * \param _x The initial width in pixels.
   * \param _y The initial height in pixels.
   */
  Plane(double _x, double _y);
  ~Plane();
  
  /// Resizes the plane.
  /**
   * By default, when the buffer is resized its content is destroyed.
   * By passing the _copy parameter, copies the old content into the
   * new buffer, with an optional offset.
   * \param _w    The new width of the buffer in pixels.
   * \param _h    The new height of the buffer in pixels.
   * \param _copy If TRUE, the buffer contents are not destroyed.
   * \param _xoff The x-offset at which the old buffer is copied into the new buffer.
   * \param _xoff The y-offset at which the old buffer is copied into the new buffer.
   * \return TRUE on success, FALSE = out of memory.
   */
  bool resize(double _w,
              double _h,
              bool   _copy = FALSE,
              double _xoff = 0,
              double _yoff = 0);
  
  /// Renders using the attached Macro.
  /**
   * \param _hard If TRUE, all caches are ignored (hard refresh).
   */
  void render(bool _hard = FALSE);
  
  /// Attaches an Operation.
  /**
   * \param _operation A pointer to the Operation responsible for drawing.
   */
  void set_operation(Operation* _operation);
  
protected:
  /// Callback, invoked whenever the Operation was changed.
  /**
   * FIXME
   */
  void on_operation_changed(void);
  
  cairo_t*       cr;
  unsigned char* buffer;      ///< The RGBA buffer.
  gulong         alloc;       ///< The buffer size in bytes.
  double         w, h;        ///< The size of the boundary box in pixels.
  double         stride;
  Operation*     operation;   ///< A pointer to the responsible Operation.
  Operation*     cache;       ///< A pointer to the last valid cache in the Operation.
};

Use Case

A client creates the following simple Operation chain:

VectorElipse -> VectorStroke -> WritePNG

and executes the resulting Macro.

Outline:

  • The client instantiates a Plane. On construction, that object allocates a cairo surface with a size of X x Y pixels.
  • The client instantiates a VectorElipse operation, passing the elipse geometry as an argument.
  • The client instantiates a VectorStroke operation. This object does not do anything on construction.
  • The client instantiates a Macro.
  • The client calls Macro->append(VectorElipse). The Macro registers the Operation in a list.
  • The client calls Macro->append(VectorStroke). The Macro registers the Operation in a list and connects VectorElipse with VectorStroke by using VectorElipse->connect_to(VectorStroke).
  • VectorElipse->connect_to() makes sure that VectorStroke is compatible and establishes the connection using VectorStroke?->notify_connect().
  • The client calls Macro->append(WritePNG). The Macro registers the Operation in a list and connects VectorStroke with WritePNG by using VectorStroke->connect_to(WritePNG).
  • VectorStroke->connect_to() makes sure that WritePNG is compatible and establishes the connection using WritePNG->notify_connect().
  • The client executes the Macro using the execute(Plane*) method, passing the Plane as an argument.
  • The Macro's execute() method calls the execute() method of the first operation in the list, VectorElipse.
  • VectorElipse calls cairo_arc(Plane->cr, ...).
  • VectorElipse calls the execute method of the next Operation, VectorStroke.
  • VectorStroke checks the extents of the path and resizes the Plane accordingly. It calls cairo_stroke(Plane->cr).
  • VectorStroke calls the execute method of the next Operation, WritePNG.
  • WritePNG saves the Plane->buffer as a PNG file.
  • WritePNG has no successor, thus just returns.
  • Control returns to the client, who can savely destroy the Plane and whatever else.