[cairo] Aligning graphics with the device pixel grid

Carl Worth cworth at cworth.org
Fri Oct 22 09:51:40 PDT 2004


As has been discussed much recently, it would be great if it were easier
to draw things that align well with the device-pixel grid. One of the
most common applications for this is the desire to stroke single-pixel
horizontal and vertical lines, (or fill sharp rectangles).

Under the default transformation, sharp filled rectangles are easy,
(just use integer coordinates), and as discussed in the FAQ,
single-pixel lines can be achieved by adjusting the path by 0.5 units.

But, of course, this is not satisfactory as a primary goal of cairo is
to provide scalable graphics. An approach that gets good results only
with the default transformation would make cairo much less useful.

A new, special stroke mode has also been proposed. This doesn't seem
ideal to me as snapping is also desirable when filling. I also don't
want any mode that takes its cue from a particular width as snapping is
desirable for various widths.

So, I've been playing with some code to do the snapping in more general
conditions. The basic idea is quite simple. We have user-space input
values which we want to adjust so that when they are transformed to
device-space they have a particular relationship to integers. So, the
approach is to transform each coordinate to device space, round to
integers or to some offset from integers, and then transform back.

The attached image demonstrates some results. It shows four groups of
nested boxes. The top two groups consists of 5 filled boxes, alternating
between black and white. The bottom two groups show 5 white stroked
boxes. Within each group, the path for each box is constructed the same
way, but with a different transform (scaling and translation). The two
groups on the left show unadjusted graphics, and the two groups on the
right show the results of snapping to the device-pixel grid.

The code for this image can be found in CVS under

	cairo-demo/png/snapping.png

The relevant portions of snapping.c are included below in three
functions:

snap_point_for_fill - Adjusts a coordinate so that it will transform to
	an integer in device space.

snap_point_for_stroke - Adjusts a coordinate so that it will transform
	to a point that is one-half the current line width from an
	integer in at least one direction, (both directions if the line
	width transforms to an integer).

snap_line_width - Adjusts the line width so that it transforms to an
	integer.

The question in my mind now is what to do with this code. Since there's
quite a bit more than an "adjust by 0.5" here, I think we should do more
than just place this code in the cairo documentation.

We can certainly provide these functions as convenience functions in
cairo. But, then stroking pixel-snapped paths would still require
calling one function per point in the path. I'd like to get this down to
one call per stroke, (or maybe even one call to set a mode in the
graphics state).

So, we could add the following functions:

	void cairo_snap_path_for_fill (cairo_t *cr);

	void cairo_snap_path_for_stroke (cairo_t *cr);

but there are still a few questions to answer first:

1) What points in the path should be snapped? We could specify this
   vaguely, allowing us to do something simple, (snap all points? snap
   points next to horizontal/vertical portions of the path?). Then, if
   we wanted to get fancier for extremal points of curves, etc. we could
   do that. What happens if we snap curve control points?

2) Do the snapping functions need to be modified to handle
   transformations that have more than scaling and translation? If so,
   do they become no-ops then or do we look for horizontal/vertical
   portions in device space?

3) The results of cairo_snap_path_for_stroke are dependent on the
   current line width. That width may change before the path is
   stroked. Do we just document this? It would be possible to avoid the
   problem by combining the snap and stroke into a new snapped_stroke
   function, but that would have the disadvantage of preventing the user
   from examining/modifying the snapped coordinate values.

4) Should cairo_snap_path_for_stroke also take care of adjusting the
   line width so that it transforms to an integer? I think the answer is
   yes, as this function is proposed as a means of getting easy access
   to snapping. Convenience functions should be as convenient as
   possible.

5) If we say yes to 4, then what do we do about line width in the face
   of non-uniform scaling in X and Y? Currently, cairo supports only a
   single line width parameter to specify a pen. Also, it only allows
   one line width throughout a stroke. But if there is non-uniform
   scaling in X and Y, then the line width needs to be adjusted by
   different amounts in X and Y (or at least by different amounts for
   horizontal and vertical portions of the path).

   Keith has been doing some work with arbitrary pens in twin, and I was
   already interested in playing with that. Maybe this gives us a good
   reason to look at that again.

6) Should we add a state bit in the graphics state so that all strokes
   and fills can be automatically snapped? This would again reduce the
   number of calls necessary. Any reason this would be a bad idea? Any
   suggestion for the name of the function call?

All feedback and any suggestions are most welcome,

-Carl

/* These snapping functions are designed to work properly with a
 * matrix that has only scale and translate components. I make no
 * guarantees about how they will behave under more interesting
 * transformations (such as rotation or shear). */

/* Snap the given coordinate so that it is on an integer coordinate of
 * the device pixel grid. This is the appropriate snapping to use for
 * horizontal/vertical portions of paths to be filled. */
static void
snap_point_for_fill (cairo_t *cr, double *x, double *y)
{
    /* Convert to device space, round, then convert back to user space. */
    cairo_transform_point (cr, x, y);
    *x = (int) (*x + 0.5);
    *y = (int) (*y + 0.5);
    cairo_inverse_transform_point (cr, x, y);
}

/* Snap the given path coordinate as appropriate for a path to be
 * stroked. This snapping is dependent on the current line width, so
 * it should be called when the line width is set to the value that
 * will be used for the stroke.
 *
 * The snapping is performed so that the stroke boundary of horizontal
 * and vertical portions will lie precisely between device pixels. If
 * the device-space line width is not an integer, then only one side
 * of the path will be properly aligned. The snap_line_width function
 * below can be used to constrain the line width to be an integer in
 * device space.
 */
static void
snap_point_for_stroke (cairo_t *cr, double *x, double *y)
{
    double x_width_dev_2, y_width_dev_2;
    double x_offset, y_offset;

    /*
     * Round in device space after adding the fractional portion of
     * one-half the (device space) line width.
     */
    x_width_dev_2 = y_width_dev_2 = cairo_current_line_width (cr);
    cairo_transform_distance (cr, &x_width_dev_2, &y_width_dev_2);
    x_width_dev_2 *= 0.5;
    y_width_dev_2 *= 0.5;

    x_offset = x_width_dev_2 - (int) (x_width_dev_2);
    y_offset = y_width_dev_2 - (int) (y_width_dev_2);

    cairo_transform_point (cr, x, y);
    *x = (int) (*x + 0.5 + x_offset);
    *y = (int) (*y + 0.5 + y_offset);
    *x -= x_offset;
    *y -= y_offset;
    cairo_inverse_transform_point (cr, x, y);
}

/* Snap the line width so that it is an integer number of device
 * pixels. Cairo currently only supports symmetrical pens, so if the
 * current transformation has non-uniform scaling in X and Y, we won't
 * be able to satisfy the constraint in both dimensions. So, this
 * function examines both directions and snaps to the dimension that
 * has the larger error. */
static void
snap_line_width (cairo_t *cr)
{
    double x_width, y_width;
    double x_width_snapped, y_width_snapped;
    double x_error, y_error;

    x_width = y_width = cairo_current_line_width (cr);

    cairo_transform_distance (cr, &x_width, &y_width);

    x_width_snapped = (int) (x_width + 0.5);
    if (x_width_snapped < 1.0)
	x_width_snapped = 1.0;

    y_width_snapped = (int) (y_width + 0.5);
    if (y_width_snapped < 1.0)
	y_width_snapped = 1.0;

    x_error = fabs (x_width - x_width_snapped);
    y_error = fabs (y_width - y_width_snapped);

    cairo_inverse_transform_distance (cr, &x_width_snapped, &y_width_snapped);

    if (x_error > y_error)
	cairo_set_line_width (cr, x_width_snapped);
    else
	cairo_set_line_width (cr, y_width_snapped);
}

-------------- next part --------------
A non-text attachment was scrubbed...
Name: snapping.png
Type: image/png
Size: 1729 bytes
Desc: not available
Url : http://lists.freedesktop.org/archives/cairo/attachments/20041022/863b71ac/snapping.png


More information about the cairo mailing list