[cairo] [PATCH 2/2] Don't repeat a Quartz gradient more times than necessary, to avoid Quartz quality problems when there are lots of repeated color stops.

Jeff Muizelaar jeff at infidigm.net
Thu May 6 19:23:48 PDT 2010


commit 7b21e111b22561f77a89c9cdb41077805512aa19
Author: Robert O'Callahan <robert at ocallahan.org>
Date:   Tue Sep 8 13:22:40 2009 -0400

     Don't repeat a Quartz gradient more times than necessary, to avoid 
Quartz quality problems when there are lots of repeated color stops.

     -- Add a parameter to _cairo_quartz_setup_source so we can pass 
down the
     extents of the object we're drawing
     -- Compute fill/stroke/glyph extents and pass them down in the 
cases we need to
     (repeating/reflecting gradients)
     -- Pass those extents on down to where we set up the gradients
     -- Make _cairo_quartz_setup_linear_source fall back to pixman for the
     degenerate case where the linear gradient vector has no length
     -- In CreateRepeatingRadialGradientFunction and
     CreateRepeatingLinearGradientFunction, use the object extents (or 
surface
     extents, for the paint() case) instead of the clip box to calculate the
     parameters for the gradient
     -- I've changed the way CreateRepeatingLinearGradientFunction 
calculates the
     repetition count. The new approach gives much more precise bounds 
on the number
     of repetitions needed (and is very similar to what we do for radial 
gradients).
     This is important because if we specify a much larger input range 
than we
     really need for our gradient color function, Quartz samples it too 
coarsely
     over the range we actually care about, and the gradients look bad.

     For example, suppose start = (5,0), end = (6,10), the CTM is 
identity and the
     bounds we want to cover is (0,0)-(10,10). I think the current 
approach sets up
     the gradient to be repeated 10 times. In fact only 3 repetitions 
are needed.

     Also, using 'width' here didn't look right:
     -       y_rep_end = (int) ceil((surface->extents.width - 
MAX(mstart.y, mend.y))
     / dy

     From https://bugzilla.mozilla.org/show_bug.cgi?id=508730

diff --git a/src/cairo-quartz-surface.c b/src/cairo-quartz-surface.c
index bba466f..463fc6e 100644
--- a/src/cairo-quartz-surface.c
+++ b/src/cairo-quartz-surface.c
@@ -820,72 +820,90 @@ CreateGradientFunction (const 
cairo_gradient_pattern_t *gpat)
&gradient_callbacks);
  }

-static CGFunctionRef
-CreateRepeatingLinearGradientFunction (cairo_quartz_surface_t *surface,
-                       const cairo_gradient_pattern_t *gpat,
-                       CGPoint *start, CGPoint *end,
-                       CGAffineTransform matrix)
+static void
+UpdateLinearParametersToIncludePoint(double *min_t, double *max_t, 
CGPoint *start,
+                                     double dx, double dy,
+                                     double x, double y)
  {
-    cairo_pattern_t *pat;
-    cairo_quartz_float_t input_value_range[2];
-
-    CGPoint mstart, mend;
+    /* Compute a parameter t such that a line perpendicular to the (dx,dy)
+       vector, passing through (start->x + dx*t, start->y + dy*t), also
+       passes through (x,y).

-    double dx, dy;
-    int x_rep_start = 0, x_rep_end = 0;
-    int y_rep_start = 0, y_rep_end = 0;
+       Let px = x - start->x, py = y - start->y.
+       t is given by
+         (px - dx*t)*dx + (py - dy*t)*dy = 0

-    int rep_start, rep_end;
-
-    // figure out how many times we'd need to repeat the gradient pattern
-    // to cover the whole (transformed) surface area
-    mstart = CGPointApplyAffineTransform (*start, matrix);
-    mend = CGPointApplyAffineTransform (*end, matrix);
-
-    dx = fabs (mend.x - mstart.x);
-    dy = fabs (mend.y - mstart.y);
+       Solving for t we get
+         numerator = dx*px + dy*py
+         denominator = dx^2 + dy^2
+         t = numerator/denominator

-    if (dx > 1e-6) {
-    x_rep_start = ceil(MIN(mstart.x, mend.x) / dx);
-    x_rep_end = ceil((surface->extents.width - MAX(mstart.x, mend.x)) / 
dx);
+       In CreateRepeatingLinearGradientFunction we know the length of 
(dx,dy)
+       is not zero. (This is checked in _cairo_quartz_setup_linear_source.)
+    */
+    double px = x - start->x;
+    double py = y - start->y;
+    double numerator = dx*px + dy*py;
+    double denominator = dx*dx + dy*dy;
+    double t = numerator/denominator;

-    if (mend.x < mstart.x) {
-        int swap = x_rep_end;
-        x_rep_end = x_rep_start;
-        x_rep_start = swap;
-    }
+    if (*min_t > t) {
+        *min_t = t;
      }
-
-    if (dy > 1e-6) {
-    y_rep_start = ceil(MIN(mstart.y, mend.y) / dy);
-    y_rep_end = ceil((surface->extents.width - MAX(mstart.y, mend.y)) / 
dy);
-
-    if (mend.y < mstart.y) {
-        int swap = y_rep_end;
-        y_rep_end = y_rep_start;
-        y_rep_start = swap;
-    }
+    if (*max_t < t) {
+        *max_t = t;
      }
+}

-    rep_start = MAX(x_rep_start, y_rep_start);
-    rep_end = MAX(x_rep_end, y_rep_end);
-
-    // extend the line between start and end by rep_start times from 
the start
-    // and rep_end times from the end
-
-    dx = end->x - start->x;
-    dy = end->y - start->y;
-
-    start->x = start->x - dx * rep_start;
-    start->y = start->y - dy * rep_start;
-
-    end->x = end->x + dx * rep_end;
-    end->y = end->y + dy * rep_end;
+static CGFunctionRef
+CreateRepeatingLinearGradientFunction (cairo_quartz_surface_t *surface,
+                       const cairo_gradient_pattern_t *gpat,
+                       CGPoint *start, CGPoint *end,
+                       cairo_rectangle_int_t *extents)
+{
+    cairo_pattern_t *pat;
+    cairo_quartz_float_t input_value_range[2];
+    double t_min = 0.;
+    double t_max = 0.;
+    double dx = end->x - start->x;
+    double dy = end->y - start->y;
+    double bounds_x1, bounds_x2, bounds_y1, bounds_y2;
+
+    if (!extents) {
+        extents = &surface->extents;
+    }
+    bounds_x1 = extents->x;
+    bounds_y1 = extents->y;
+    bounds_x2 = extents->x + extents->width;
+    bounds_y2 = extents->y + extents->height;
+    _cairo_matrix_transform_bounding_box (&gpat->base.matrix,
+ &bounds_x1, &bounds_y1,
+ &bounds_x2, &bounds_y2,
+                                          NULL);
+
+    UpdateLinearParametersToIncludePoint(&t_min, &t_max, start, dx, dy,
+                                         bounds_x1, bounds_y1);
+    UpdateLinearParametersToIncludePoint(&t_min, &t_max, start, dx, dy,
+                                         bounds_x2, bounds_y1);
+    UpdateLinearParametersToIncludePoint(&t_min, &t_max, start, dx, dy,
+                                         bounds_x2, bounds_y2);
+    UpdateLinearParametersToIncludePoint(&t_min, &t_max, start, dx, dy,
+                                         bounds_x1, bounds_y2);
+
+    /* Move t_min and t_max to the nearest usable integer to try to avoid
+       subtle variations due to numerical instability, especially 
accidentally
+       cutting off a pixel. Extending the gradient repetitions is 
always safe. */
+    t_min = floor (t_min);
+    t_max = ceil (t_max);
+    end->x = start->x + dx*t_max;
+    end->y = start->y + dy*t_max;
+    start->x = start->x + dx*t_min;
+    start->y = start->y + dy*t_min;

      // set the input range for the function -- the function knows how to
      // map values outside of 0.0 .. 1.0 to that range for REPEAT/REFLECT.
-    input_value_range[0] = 0.0 - 1.0 * rep_start;
-    input_value_range[1] = 1.0 + 1.0 * rep_end;
+    input_value_range[0] = t_min;
+    input_value_range[1] = t_max;

      if (_cairo_pattern_create_copy (&pat, &gpat->base))
      /* quartz doesn't deal very well with malloc failing, so there's
@@ -950,10 +968,9 @@ static CGFunctionRef
  CreateRepeatingRadialGradientFunction (cairo_quartz_surface_t *surface,
                                         const cairo_gradient_pattern_t 
*gpat,
                                         CGPoint *start, double 
*start_radius,
-                                       CGPoint *end, double *end_radius)
+                                       CGPoint *end, double *end_radius,
+                                       cairo_rectangle_int_t *extents)
  {
-    CGRect clip = CGContextGetClipBoundingBox (surface->cgContext);
-    CGAffineTransform transform;
      cairo_pattern_t *pat;
      float input_value_range[2];
      CGPoint *inner;
@@ -965,10 +982,19 @@ CreateRepeatingRadialGradientFunction 
(cairo_quartz_surface_t *surface,
      double t_min, t_max, t_temp;
      /* outer minus inner */
      double dr, dx, dy;
+    double bounds_x1, bounds_x2, bounds_y1, bounds_y2;

-    _cairo_quartz_cairo_matrix_to_quartz (&gpat->base.matrix, &transform);
-    /* clip is in cairo device coordinates; get it into cairo user space */
-    clip = CGRectApplyAffineTransform (clip, transform);
+    if (!extents) {
+        extents = &surface->extents;
+    }
+    bounds_x1 = extents->x;
+    bounds_y1 = extents->y;
+    bounds_x2 = extents->x + extents->width;
+    bounds_y2 = extents->y + extents->height;
+    _cairo_matrix_transform_bounding_box (&gpat->base.matrix,
+ &bounds_x1, &bounds_y1,
+ &bounds_x2, &bounds_y2,
+                                          NULL);

      if (*start_radius < *end_radius) {
          /* end circle contains start circle */
@@ -988,6 +1014,7 @@ CreateRepeatingRadialGradientFunction 
(cairo_quartz_surface_t *surface,
      dx = outer->x - inner->x;
      dy = outer->y - inner->y;

+    /* We can't round or fudge t_min here, it has to be as accurate as 
possible. */
      t_min = -(*inner_radius/dr);
      inner->x += t_min*dx;
      inner->y += t_min*dy;
@@ -995,19 +1022,19 @@ CreateRepeatingRadialGradientFunction 
(cairo_quartz_surface_t *surface,

      t_temp = 0.;
      UpdateRadialParameterToIncludePoint(&t_temp, inner, dr, dx, dy,
-                                        clip.origin.x, clip.origin.y);
+                                        bounds_x1, bounds_y1);
      UpdateRadialParameterToIncludePoint(&t_temp, inner, dr, dx, dy,
-                                        clip.origin.x + 
clip.size.width, clip.origin.y);
+                                        bounds_x2, bounds_y1);
      UpdateRadialParameterToIncludePoint(&t_temp, inner, dr, dx, dy,
-                                        clip.origin.x + 
clip.size.width, clip.origin.y + clip.size.height);
+                                        bounds_x2, bounds_y2);
      UpdateRadialParameterToIncludePoint(&t_temp, inner, dr, dx, dy,
-                                        clip.origin.x, clip.origin.y + 
clip.size.height);
+                                        bounds_x1, bounds_y2);
      /* UpdateRadialParameterToIncludePoint assumes t=0 means radius 0.
         But for the parameter values we use with Quartz, t_min means 
radius 0.
-       Also, add a small fudge factor to avoid rounding issues. Since the
-       circles are alway expanding and containing the earlier circles, 
this is
-       OK. */
-    t_temp += 1e-6;
+       Since the circles are alway expanding and contain the earlier 
circles,
+       it's safe to extend t_max/t_temp as much as we want, so round 
t_temp up
+       to the nearest integer. This may help us give stable results. */
+    t_temp = ceil (t_temp);
      t_max = t_min + t_temp;
      outer->x = inner->x + t_temp*dx;
      outer->y = inner->y + t_temp*dy;
@@ -1346,9 +1373,23 @@ _cairo_quartz_setup_fallback_source 
(cairo_quartz_surface_t *surface,
      return DO_IMAGE;
  }

+/*
+Quartz does not support repeating radients. We handle repeating gradients
+by manually extending the gradient and repeating color stops. We need to
+minimize the number of repetitions since Quartz seems to sample our color
+function across the entire range, even if part of that range is not needed
+for the visible area of the gradient, and it samples with some fixed 
resolution,
+so if the gradient range is too large it samples with very low 
resolution and
+the gradient is very coarse. CreateRepeatingLinearGradientFunction and
+CreateRepeatingRadialGradientFunction compute the number of repetitions 
needed
+based on the extents of the object (the clip region cannot be used here 
since
+we don't want the rasterization of the entire gradient to depend on the
+clip region).
+*/
  static cairo_quartz_action_t
  _cairo_quartz_setup_linear_source (cairo_quartz_surface_t *surface,
-                   const cairo_linear_pattern_t *lpat)
+                   const cairo_linear_pattern_t *lpat,
+                   cairo_rectangle_int_t *extents)
  {
      const cairo_pattern_t *abspat = &lpat->base.base;
      cairo_matrix_t mat;
@@ -1363,6 +1404,16 @@ _cairo_quartz_setup_linear_source 
(cairo_quartz_surface_t *surface,
      return DO_SOLID;
      }

+    if (lpat->p1.x == lpat->p2.x &&
+        lpat->p1.y == lpat->p2.y) {
+    /* Quartz handles cases where the vector has no length very
+     * differently from pixman.
+     * Whatever the correct behaviour is, let's at least have only pixman's
+     * implementation to worry about.
+     */
+    return _cairo_quartz_setup_fallback_source (surface, abspat);
+    }
+
      mat = abspat->matrix;
      cairo_matrix_invert (&mat);
      _cairo_quartz_cairo_matrix_to_quartz (&mat, 
&surface->sourceTransform);
@@ -1382,7 +1433,7 @@ _cairo_quartz_setup_linear_source 
(cairo_quartz_surface_t *surface,
      gradFunc = CreateRepeatingLinearGradientFunction (surface,
&lpat->base,
&start, &end,
-                                  surface->sourceTransform);
+                                  extents);
      }

      surface->sourceShading = CGShadingCreateAxial (rgb,
@@ -1398,7 +1449,8 @@ _cairo_quartz_setup_linear_source 
(cairo_quartz_surface_t *surface,

  static cairo_quartz_action_t
  _cairo_quartz_setup_radial_source (cairo_quartz_surface_t *surface,
-                   const cairo_radial_pattern_t *rpat)
+                   const cairo_radial_pattern_t *rpat,
+                   cairo_rectangle_int_t *extents)
  {
      const cairo_pattern_t *abspat = &rpat->base.base;
      cairo_matrix_t mat;
@@ -1450,7 +1502,8 @@ _cairo_quartz_setup_radial_source 
(cairo_quartz_surface_t *surface,
      gradFunc = CreateRepeatingRadialGradientFunction (surface,
&rpat->base,
&start, &r1,
- &end, &r2);
+ &end, &r2,
+                                  extents);
      }

      surface->sourceShading = CGShadingCreateRadial (rgb,
@@ -1469,7 +1522,8 @@ _cairo_quartz_setup_radial_source 
(cairo_quartz_surface_t *surface,

  static cairo_quartz_action_t
  _cairo_quartz_setup_source (cairo_quartz_surface_t *surface,
-                const cairo_pattern_t *source)
+                const cairo_pattern_t *source,
+                cairo_rectangle_int_t *extents)
  {
      assert (!(surface->sourceImage || surface->sourceShading || 
surface->sourcePattern));

@@ -1495,14 +1549,12 @@ _cairo_quartz_setup_source 
(cairo_quartz_surface_t *surface,

      if (source->type == CAIRO_PATTERN_TYPE_LINEAR) {
      const cairo_linear_pattern_t *lpat = (const cairo_linear_pattern_t 
*)source;
-    return _cairo_quartz_setup_linear_source (surface, lpat);
-
+    return _cairo_quartz_setup_linear_source (surface, lpat, extents);
      }

      if (source->type == CAIRO_PATTERN_TYPE_RADIAL) {
      const cairo_radial_pattern_t *rpat = (const cairo_radial_pattern_t 
*)source;
-    return _cairo_quartz_setup_radial_source (surface, rpat);
-
+    return _cairo_quartz_setup_radial_source (surface, rpat, extents);
      }

      if (source->type == CAIRO_PATTERN_TYPE_SURFACE &&
@@ -2025,7 +2077,7 @@ _cairo_quartz_surface_paint_cg (void 
*abstract_surface,
      if (unlikely (rv))
      return rv == CAIRO_INT_STATUS_NOTHING_TO_DO ? CAIRO_STATUS_SUCCESS 
: rv;

-    action = _cairo_quartz_setup_source (surface, source);
+    action = _cairo_quartz_setup_source (surface, source, NULL);

      if (action == DO_SOLID || action == DO_PATTERN) {
      CGContextFillRect (surface->cgContext, CGRectMake(surface->extents.x,
@@ -2051,6 +2103,25 @@ _cairo_quartz_surface_paint_cg (void 
*abstract_surface,
      return rv;
  }

+static cairo_bool_t
+_cairo_quartz_source_needs_extents (const cairo_pattern_t *source)
+{
+    /* For repeating gradients we need to manually extend the gradient and
+       repeat stops, since Quartz doesn't support repeating gradients 
natively.
+       We need to minimze the number of repeated stops, and since 
rasterization
+       depends on the number of repetitions we use (even if some of the
+       repetitions go beyond the extents of the object or outside the clip
+       region), it's important to use the same number of repetitions when
+       rendering an object no matter what the clip region is. So the
+       computation of the repetition count cannot depended on the clip 
region,
+       and should only depend on the object extents, so we need to compute
+       the object extents for repeating gradients. */
+    return (source->type == CAIRO_PATTERN_TYPE_LINEAR ||
+            source->type == CAIRO_PATTERN_TYPE_RADIAL) &&
+           (source->extend == CAIRO_EXTEND_REPEAT ||
+            source->extend == CAIRO_EXTEND_REFLECT);
+}
+
  static cairo_int_status_t
  _cairo_quartz_surface_paint (void *abstract_surface,
                   cairo_operator_t op,
@@ -2110,7 +2181,17 @@ _cairo_quartz_surface_fill_cg (void 
*abstract_surface,

      CGContextSetShouldAntialias (surface->cgContext, (antialias != 
CAIRO_ANTIALIAS_NONE));

-    action = _cairo_quartz_setup_source (surface, source);
+    if (_cairo_quartz_source_needs_extents (source))
+    {
+        /* We don't need precise extents since these are only used to
+           compute the number of gradient reptitions needed to cover the
+           object. */
+        cairo_rectangle_int_t path_extents;
+        _cairo_path_fixed_approximate_fill_extents (path, &path_extents);
+        action = _cairo_quartz_setup_source (surface, source, 
&path_extents);
+    } else {
+        action = _cairo_quartz_setup_source (surface, source, NULL);
+    }

      _cairo_quartz_cairo_path_to_quartz_context (path, surface->cgContext);

@@ -2267,7 +2348,14 @@ _cairo_quartz_surface_stroke_cg (void 
*abstract_surface,
      CGContextSaveGState (surface->cgContext);


-    action = _cairo_quartz_setup_source (surface, source);
+    if (_cairo_quartz_source_needs_extents (source))
+    {
+        cairo_rectangle_int_t path_extents;
+        _cairo_path_fixed_approximate_stroke_extents (path, style, ctm, 
&path_extents);
+        action = _cairo_quartz_setup_source (surface, source, 
&path_extents);
+    } else {
+        action = _cairo_quartz_setup_source (surface, source, NULL);
+    }

      _cairo_quartz_cairo_path_to_quartz_context (path, surface->cgContext);

@@ -2406,7 +2494,16 @@ _cairo_quartz_surface_show_glyphs_cg (void 
*abstract_surface,

      CGContextSaveGState (surface->cgContext);

-    action = _cairo_quartz_setup_source (surface, source);
+    if (_cairo_quartz_source_needs_extents (source))
+    {
+        cairo_rectangle_int_t glyph_extents;
+        _cairo_scaled_font_glyph_device_extents (scaled_font, glyphs, 
num_glyphs,
+ &glyph_extents, NULL);
+        action = _cairo_quartz_setup_source (surface, source, 
&glyph_extents);
+    } else {
+        action = _cairo_quartz_setup_source (surface, source, NULL);
+    }
+
      if (action == DO_SOLID || action == DO_PATTERN) {
      CGContextSetTextDrawingMode (surface->cgContext, kCGTextFill);
      } else if (action == DO_IMAGE || action == DO_TILED_IMAGE || 
action == DO_SHADING) {



More information about the cairo mailing list