author Andrew Sutherland Wed, 24 Mar 2010 00:32:40 -0700 changeset 39769 9f3391b5ca0ca5e00b39f440639dd7c9009e63cc parent 32382 9f811e15ca517c7805e27d5104f5f95b3d082e07 permissions -rw-r--r--
```
# HG changeset patch
# User Robert O'Callahan <robert@ocallahan.org>
# Date 1249558989 -43200
# Node ID 0bac4c903d2bb1d5c0d5426209001fc2a77cc105
Bug 508730. Don't repeat a Quartz gradient more times than necessary, to avoid Quartz quality problems when there are lots of repeated color stops. r=jmuizelaar

diff --git a/gfx/cairo/cairo/src/cairo-quartz-surface.c b/gfx/cairo/cairo/src/cairo-quartz-surface.c
--- a/gfx/cairo/cairo/src/cairo-quartz-surface.c
+++ b/gfx/cairo/cairo/src/cairo-quartz-surface.c
return CGFunctionCreate (pat,
1,
input_value_range,
4,
}

+static void
+UpdateLinearParametersToIncludePoint(double *min_t, double *max_t, CGPoint *start,
+                                     double dx, double dy,
+                                     double x, double y)
+{
+    /* 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).
+
+       Let px = x - start->x, py = y - start->y.
+       t is given by
+         (px - dx*t)*dx + (py - dy*t)*dy = 0
+
+       Solving for t we get
+         numerator = dx*px + dy*py
+         denominator = dx^2 + dy^2
+         t = numerator/denominator
+
+       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 (*min_t > t) {
+        *min_t = t;
+    }
+    if (*max_t < t) {
+        *max_t = t;
+    }
+}
+
static CGFunctionRef
CGPoint *start, CGPoint *end,
-				       CGAffineTransform matrix)
+				       cairo_rectangle_int_t *extents)
{
cairo_pattern_t *pat;
float input_value_range;
+    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;

-    CGPoint mstart, mend;
+    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);

-    double dx, dy;
-    int x_rep_start = 0, x_rep_end = 0;
-    int y_rep_start = 0, y_rep_end = 0;
+    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);

-    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);
-
-    if (dx > 1e-6) {
-	x_rep_start = (int) ceil(MIN(mstart.x, mend.x) / dx);
-	x_rep_end = (int) ceil((surface->extents.width - MAX(mstart.x, mend.x)) / dx);
-
-	if (mend.x < mstart.x) {
-	    int swap = x_rep_end;
-	    x_rep_end = x_rep_start;
-	    x_rep_start = swap;
-	}
-    }
-
-    if (dy > 1e-6) {
-	y_rep_start = (int) ceil(MIN(mstart.y, mend.y) / dy);
-	y_rep_end = (int) 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;
-	}
-    }
-
-    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;
+    /* 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 - 1.0 * rep_start;
-    input_value_range = 1.0 + 1.0 * rep_end;
+    input_value_range = t_min;
+    input_value_range = t_max;

if (_cairo_pattern_create_copy (&pat, &gpat->base))
/* quartz doesn't deal very well with malloc failing, so there's
* not much point in us trying either */
return NULL;

return CGFunctionCreate (pat,
1,
}
}

/* This must only be called when one of the circles properly contains the other */
static CGFunctionRef
+                                       cairo_rectangle_int_t *extents)
{
-    CGRect clip = CGContextGetClipBoundingBox (surface->cgContext);
-    CGAffineTransform transform;
cairo_pattern_t *pat;
float input_value_range;
CGPoint *inner;
CGPoint *outer;
/* minimum and maximum t-parameter values that will make our gradient
cover the clipBox */
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);

/* end circle contains start circle */
inner = start;
outer = end;
} else {
}

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. */
inner->x += t_min*dx;
inner->y += t_min*dy;

t_temp = 0.;
-                                        clip.origin.x, clip.origin.y);
+                                        bounds_x1, bounds_y1);
-                                        clip.origin.x + clip.size.width, clip.origin.y);
+                                        bounds_x2, bounds_y1);
-                                        clip.origin.x + clip.size.width, clip.origin.y + clip.size.height);
+                                        bounds_x2, bounds_y2);
-                                        clip.origin.x, clip.origin.y + clip.size.height);
+                                        bounds_x1, bounds_y2);
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;

/* 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. */
@@ -1218,33 +1245,57 @@ _cairo_quartz_setup_fallback_source (cai
surface->sourceImageRect = CGRectMake (0.0, 0.0, w, h);
surface->sourceImage = img;
surface->sourceImageSurface = fallback;
surface->sourceTransform = CGAffineTransformMakeTranslation (x0, y0);

return DO_IMAGE;
}

+/*
+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
+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;
CGPoint start, end;
CGColorSpaceRef rgb;
bool extend = abspat->extend == CAIRO_EXTEND_PAD;

if (lpat->base.n_stops == 0) {
CGContextSetRGBStrokeColor (surface->cgContext, 0., 0., 0., 0.);
CGContextSetRGBFillColor (surface->cgContext, 0., 0., 0., 0.);
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);

rgb = CGColorSpaceCreateDeviceRGB();

start = CGPointMake (_cairo_fixed_to_double (lpat->p1.x),
_cairo_fixed_to_double (lpat->p1.y));
@@ -1254,33 +1305,34 @@ _cairo_quartz_setup_linear_source (cairo
if (abspat->extend == CAIRO_EXTEND_NONE ||
{
} else {
&lpat->base,
&start, &end,
-						          surface->sourceTransform);
+						          extents);
}

start, end,
extend, extend);

CGColorSpaceRelease(rgb);

}

static cairo_quartz_action_t
+				   cairo_rectangle_int_t *extents)
{
const cairo_pattern_t *abspat = &rpat->base.base;
cairo_matrix_t mat;
CGPoint start, end;
CGColorSpaceRef rgb;
bool extend = abspat->extend == CAIRO_EXTEND_PAD;
double c1x = _cairo_fixed_to_double (rpat->c1.x);
@@ -1322,17 +1374,18 @@ _cairo_quartz_setup_radial_source (cairo
if (abspat->extend == CAIRO_EXTEND_NONE ||
{
} else {
&rpat->base,
&start, &r1,
-						          &end, &r2);
+						          &end, &r2,
+						          extents);
}

start,
r1,
end,
r2,
@@ -1341,17 +1394,18 @@ _cairo_quartz_setup_radial_source (cairo
CGColorSpaceRelease(rgb);

}

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));

surface->oldInterpolationQuality = CGContextGetInterpolationQuality (surface->cgContext);
CGContextSetInterpolationQuality (surface->cgContext, _cairo_quartz_filter_to_quartz (source->filter));

if (source->type == CAIRO_PATTERN_TYPE_SOLID) {
cairo_solid_pattern_t *solid = (cairo_solid_pattern_t *) source;
@@ -1367,24 +1421,22 @@ _cairo_quartz_setup_source (cairo_quartz
solid->color.blue,
solid->color.alpha);

return DO_SOLID;
}

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);
}

-
+	return _cairo_quartz_setup_radial_source (surface, rpat, extents);
}

if (source->type == CAIRO_PATTERN_TYPE_SURFACE &&
(source->extend == CAIRO_EXTEND_NONE || (CGContextDrawTiledImagePtr && source->extend == CAIRO_EXTEND_REPEAT)))
{
const cairo_surface_pattern_t *spat = (const cairo_surface_pattern_t *) source;
cairo_surface_t *pat_surf = spat->surface;
CGImageRef img;
@@ -1852,17 +1904,17 @@ _cairo_quartz_surface_paint (void *abstr
if (IS_EMPTY(surface))
return CAIRO_STATUS_SUCCESS;

if (op == CAIRO_OPERATOR_DEST)
return CAIRO_STATUS_SUCCESS;

CGContextSetCompositeOperation (surface->cgContext, _cairo_quartz_cairo_operator_to_quartz (op));

-    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,
surface->extents.y,
surface->extents.width,
surface->extents.height));
} else if (action == DO_SHADING) {
CGContextSaveGState (surface->cgContext);
@@ -1886,16 +1938,35 @@ _cairo_quartz_surface_paint (void *abstr
}

_cairo_quartz_teardown_source (surface, source);

ND((stderr, "-- paint\n"));
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->extend == CAIRO_EXTEND_REPEAT ||
+            source->extend == CAIRO_EXTEND_REFLECT);
+}
+
static cairo_int_status_t
_cairo_quartz_surface_fill (void *abstract_surface,
cairo_operator_t op,
const cairo_pattern_t *source,
cairo_path_fixed_t *path,
cairo_fill_rule_t fill_rule,
double tolerance,
cairo_antialias_t antialias,
@@ -1926,17 +1997,27 @@ _cairo_quartz_surface_fill (void *abstra
return CAIRO_STATUS_SUCCESS;
}

CGContextSaveGState (surface->cgContext);

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

-    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);
+    }

CGContextBeginPath (surface->cgContext);

stroke.cgContext = surface->cgContext;
stroke.ctm_inverse = NULL;
rv = _cairo_quartz_cairo_path_to_quartz_context (path, &stroke);
if (rv)
goto BAIL;
@@ -2059,17 +2140,24 @@ _cairo_quartz_surface_stroke (void *abst

CGContextSetLineDash (surface->cgContext, style->dash_offset, fdash, max_dashes);
if (fdash != sdash)
free (fdash);
}

CGContextSetCompositeOperation (surface->cgContext, _cairo_quartz_cairo_operator_to_quartz (op));

-    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);
+    }

CGContextBeginPath (surface->cgContext);

stroke.cgContext = surface->cgContext;
stroke.ctm_inverse = ctm_inverse;
rv = _cairo_quartz_cairo_path_to_quartz_context (path, &stroke);
if (rv)
goto BAIL;
@@ -2180,17 +2268,26 @@ _cairo_quartz_surface_show_glyphs (void
if (op == CAIRO_OPERATOR_DEST)
return CAIRO_STATUS_SUCCESS;

if (cairo_scaled_font_get_type (scaled_font) != CAIRO_FONT_TYPE_QUARTZ)
return CAIRO_INT_STATUS_UNSUPPORTED;

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);
+        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) {
CGContextSetTextDrawingMode (surface->cgContext, kCGTextClip);
isClipping = TRUE;
} else {
if (action != DO_NOTHING)
rv = CAIRO_INT_STATUS_UNSUPPORTED;
```