[cairo] drawing inconsistency

Bertram Felgenhauer bertram.felgenhauer at googlemail.com
Sun Jan 20 20:40:52 PST 2008


Adi Oanca wrote:
> Hi,
> 
>    I'm working with SDL and Cairo and with the following code:
> 
[snip]
> 	cairo_t *cr = cairo_create(cairoSurface);
> 
> 	cairo_set_line_width(cr, 1);
> 	cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE);

Oh, non-antialisad sampling again.

> 	cairo_set_source_rgb(cr, 1, 1, 1);
> 	cairo_rectangle(cr, 100, 100, 120, 40);
> 	cairo_stroke_preserve(cr);
> 	
> 	cairo_set_source_rgb(cr, 0.82, 0.16, 0.22);
> 	cairo_fill(cr);
[snip]
> 
>    ... is overwriting the top and the right side of the white
> rectangle previously stroked.
> 
>    Anyone has any idea what I am doing wrong?

Yes. cairo_stroke() draws a line that extends symmetrically to both
sides of the current path. At a line width of 1, the part that lies
outside the path will have width 1/2. Now cairo_fill() fills the
interior of the current path (not stroke!), so on a vector surface
you'd end up with a 120x40 rectangle with a 1/2 unit border around it.
Without antialiasing, cairo will miss a 1/2 unit wide horizontal or
vertical line 50% of the time.

A more detailed description follows. Use a fixed width font to
view the figures.

Consider  cairo_rectangle(cr, 1, 1, 3, 2). That call creates the
following path:

  .   .   .   .   .   .
  
  .   .___.___.___.   .
      |           |
  .   |   .   .   |   .
      |           |
  .   |___.___.___|   .
  
  .   .   .   .   .   .

In that picture, the '.' are the corners of the pixels of the image
surface. The path lies exactly on the pixel boundaries. Now with a
line width of 1 and miter bevels (the default), cairo_stroke(cr) will
result in filling this area:

  .   .   .   .   .   .
     _______________
  . | .   .   .   . | .
    |    _______    |
  . | . | .   . | . | .
    |   |_______|   |
  . | .   .   .   . | .
    |_______________|
  .   .   .   .   .   .

Now to actually draw this, cairo samples a reference point of each
pixel to see if it's inside the path [1]. (That point should be the
center of the pixel, but is currently in the center of the right edge
of the pixel. There's another thread going on about this.)

The result that cairo produces is this:

  .   .   .   .   .   .
                    
  .___.___.___.___.   .
  |               |  
  |   .___.___.   |   .
  |   |       |   |  
  |   |___.___|   |   .
  |               |  
  |___.___.___.___|   .

(What it should be drawing is the same thing, shifted one pixel to
the right.) And that's where your problem lies, because now when you
fill the path with another color, you end up with

  .   .   .   .   .   .
                    
  .___.___.___.___.   .
  |   |           |  
  |   |   .   .   |   .
  |   |           |  
  |   |___.___.___|   .
  |               |  
  |___.___.___.___|   .

which looks as if the top and right edges were missing.

There are a few ways to fix this:
- use a line width of 2 (which is, incidentally, cairo's default).
- accept the sampling artifacts, but fill before stroking.
- use a different path for the stroke:
  cairo_rectangle(cr, 99.5, 99.5, 121, 41);

>    I think I am to blame, or SDL, because if I create the surface this way:
> cairoSurface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 640, 480);
> ... and write that into a .png, the result is OK.

I can't reproduce that. Did you add the cairo_set_line_width(cr, 1)
line after testing that, perhaps?

HTH,

Bertram

[1]: While sampling like that has the disadvantage that thin lines may
  disappear completely in the output, it has advantages as well.
  For example, you can draw geometrically non-overlapping shapes in
  any order without changing the result.
  Also if two shapes "fit together" (say, they share an edge), then
  there will be no gap between them in the output produced by cairo.
  (Both these are not true for antialiased drawing btw.)



More information about the cairo mailing list