[cairo] Aligning graphics with the device pixel grid
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
The relevant portions of snapping.c are included below in three
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
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
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
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,
/* 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. */
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.
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. */
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);
cairo_set_line_width (cr, y_width_snapped);
-------------- next part --------------
A non-text attachment was scrubbed...
Size: 1729 bytes
Desc: not available
Url : http://lists.freedesktop.org/archives/cairo/attachments/20041022/863b71ac/snapping.png
More information about the cairo