# HG changeset patch # User Robert O'Callahan # Date 1249558156 -43200 # Node ID e564f3ab4ea6e3b5dd9c4e9e6042d3a84c229dde # Parent 6ef9993a30bf2f983c9d64d7441d2e3b6b935de1 Bug 508227. Don't fallback to Quartz for repeating radial gradients. 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 @@ -708,20 +708,20 @@ CreateGradientFunction (const cairo_grad 1, input_value_range, 4, output_value_ranges, &callbacks); } static CGFunctionRef -CreateRepeatingGradientFunction (cairo_quartz_surface_t *surface, - const cairo_gradient_pattern_t *gpat, - CGPoint *start, CGPoint *end, - CGAffineTransform matrix) +CreateRepeatingLinearGradientFunction (cairo_quartz_surface_t *surface, + const cairo_gradient_pattern_t *gpat, + CGPoint *start, CGPoint *end, + CGAffineTransform matrix) { cairo_pattern_t *pat; float input_value_range[2]; float output_value_ranges[8] = { 0.f, 1.f, 0.f, 1.f, 0.f, 1.f, 0.f, 1.f }; CGFunctionCallbacks callbacks = { 0, ComputeGradientValue, (CGFunctionReleaseInfoCallback) cairo_pattern_destroy }; @@ -791,16 +791,156 @@ CreateRepeatingGradientFunction (cairo_q return CGFunctionCreate (pat, 1, input_value_range, 4, output_value_ranges, &callbacks); } +static void +UpdateRadialParameterToIncludePoint(double *max_t, CGPoint *center, + double dr, double dx, double dy, + double x, double y) +{ + /* Compute a parameter t such that a circle centered at + (center->x + dx*t, center->y + dy*t) with radius dr*t contains the + point (x,y). + + Let px = x - center->x, py = y - center->y. + Parameter values for which t is on the circle are given by + (px - dx*t)^2 + (py - dy*t)^2 = (t*dr)^2 + + Solving for t using the quadratic formula, and simplifying, we get + numerator = dx*px + dy*py +- + sqrt( dr^2*(px^2 + py^2) - (dx*py - dy*px)^2 ) + denominator = dx^2 + dy^2 - dr^2 + t = numerator/denominator + + In CreateRepeatingRadialGradientFunction we know the outer circle + contains the inner circle. Therefore the distance between the circle + centers plus the radius of the inner circle is less than the radius of + the outer circle. (This is checked in _cairo_quartz_setup_radial_source.) + Therefore + dx^2 + dy^2 < dr^2 + So the denominator is negative and the larger solution for t is given by + numerator = dx*px + dy*py - + sqrt( dr^2*(px^2 + py^2) - (dx*py - dy*px)^2 ) + denominator = dx^2 + dy^2 - dr^2 + t = numerator/denominator + dx^2 + dy^2 < dr^2 also ensures that the operand of sqrt is positive. + */ + double px = x - center->x; + double py = y - center->y; + double dx_py_minus_dy_px = dx*py - dy*px; + double numerator = dx*px + dy*py - + sqrt (dr*dr*(px*px + py*py) - dx_py_minus_dy_px*dx_py_minus_dy_px); + double denominator = dx*dx + dy*dy - dr*dr; + double t = numerator/denominator; + + if (*max_t < t) { + *max_t = t; + } +} + +/* This must only be called when one of the circles properly contains the other */ +static CGFunctionRef +CreateRepeatingRadialGradientFunction (cairo_quartz_surface_t *surface, + const cairo_gradient_pattern_t *gpat, + CGPoint *start, double *start_radius, + CGPoint *end, double *end_radius) +{ + CGRect clip = CGContextGetClipBoundingBox (surface->cgContext); + CGAffineTransform transform; + cairo_pattern_t *pat; + float input_value_range[2]; + float output_value_ranges[8] = { 0.f, 1.f, 0.f, 1.f, 0.f, 1.f, 0.f, 1.f }; + CGFunctionCallbacks callbacks = { + 0, ComputeGradientValue, (CGFunctionReleaseInfoCallback) cairo_pattern_destroy + }; + CGPoint *inner; + double *inner_radius; + CGPoint *outer; + double *outer_radius; + /* 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; + + _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 (*start_radius < *end_radius) { + /* end circle contains start circle */ + inner = start; + outer = end; + inner_radius = start_radius; + outer_radius = end_radius; + } else { + /* start circle contains end circle */ + inner = end; + outer = start; + inner_radius = end_radius; + outer_radius = start_radius; + } + + dr = *outer_radius - *inner_radius; + dx = outer->x - inner->x; + dy = outer->y - inner->y; + + t_min = -(*inner_radius/dr); + inner->x += t_min*dx; + inner->y += t_min*dy; + *inner_radius = 0.; + + t_temp = 0.; + UpdateRadialParameterToIncludePoint(&t_temp, inner, dr, dx, dy, + clip.origin.x, clip.origin.y); + UpdateRadialParameterToIncludePoint(&t_temp, inner, dr, dx, dy, + clip.origin.x + clip.size.width, clip.origin.y); + UpdateRadialParameterToIncludePoint(&t_temp, inner, dr, dx, dy, + clip.origin.x + clip.size.width, clip.origin.y + clip.size.height); + UpdateRadialParameterToIncludePoint(&t_temp, inner, dr, dx, dy, + clip.origin.x, clip.origin.y + clip.size.height); + /* 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; + t_max = t_min + t_temp; + outer->x = inner->x + t_temp*dx; + outer->y = inner->y + t_temp*dy; + *outer_radius = t_temp*dr; + + /* 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. */ + if (*start_radius < *end_radius) { + input_value_range[0] = t_min; + input_value_range[1] = t_max; + } else { + input_value_range[0] = -t_max; + input_value_range[1] = -t_min; + } + + 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, + input_value_range, + 4, + output_value_ranges, + &callbacks); +} + /* Obtain a CGImageRef from a #cairo_surface_t * */ static void DataProviderReleaseCallback (void *info, const void *data, size_t size) { cairo_surface_t *surface = (cairo_surface_t *) info; cairo_surface_destroy (surface); } @@ -1112,23 +1252,24 @@ _cairo_quartz_setup_linear_source (cairo rgb = CGColorSpaceCreateDeviceRGB(); start = CGPointMake (_cairo_fixed_to_double (lpat->p1.x), _cairo_fixed_to_double (lpat->p1.y)); end = CGPointMake (_cairo_fixed_to_double (lpat->p2.x), _cairo_fixed_to_double (lpat->p2.y)); if (abspat->extend == CAIRO_EXTEND_NONE || - abspat->extend == CAIRO_EXTEND_PAD) + abspat->extend == CAIRO_EXTEND_PAD) { gradFunc = CreateGradientFunction (&lpat->base); } else { - gradFunc = CreateRepeatingGradientFunction (surface, - &lpat->base, - &start, &end, surface->sourceTransform); + gradFunc = CreateRepeatingLinearGradientFunction (surface, + &lpat->base, + &start, &end, + surface->sourceTransform); } surface->sourceShading = CGShadingCreateAxial (rgb, start, end, gradFunc, extend, extend); CGColorSpaceRelease(rgb); @@ -1142,52 +1283,68 @@ _cairo_quartz_setup_radial_source (cairo const cairo_radial_pattern_t *rpat) { const cairo_pattern_t *abspat = &rpat->base.base; cairo_matrix_t mat; CGPoint start, end; CGFunctionRef gradFunc; CGColorSpaceRef rgb; bool extend = abspat->extend == CAIRO_EXTEND_PAD; + double c1x = _cairo_fixed_to_double (rpat->c1.x); + double c1y = _cairo_fixed_to_double (rpat->c1.y); + double c2x = _cairo_fixed_to_double (rpat->c2.x); + double c2y = _cairo_fixed_to_double (rpat->c2.y); + double r1 = _cairo_fixed_to_double (rpat->r1); + double r2 = _cairo_fixed_to_double (rpat->r2); + double dx = c1x - c2x; + double dy = c1y - c2y; + double centerDistance = sqrt (dx*dx + dy*dy); if (rpat->base.n_stops == 0) { CGContextSetRGBStrokeColor (surface->cgContext, 0., 0., 0., 0.); CGContextSetRGBFillColor (surface->cgContext, 0., 0., 0., 0.); return DO_SOLID; } - if (abspat->extend == CAIRO_EXTEND_REPEAT || - abspat->extend == CAIRO_EXTEND_REFLECT) - { - /* I started trying to map these to Quartz, but it's much harder - * then the linear case (I think it would involve doing multiple - * Radial shadings). So, instead, let's just render an image - * for pixman to draw the shading into, and use that. + if (r2 <= centerDistance + r1 + 1e-6 && /* circle 2 doesn't contain circle 1 */ + r1 <= centerDistance + r2 + 1e-6) { /* circle 1 doesn't contain circle 2 */ + /* Quartz handles cases where neither circle contains the other very + * differently from pixman. + * Whatever the correct behaviour is, let's at least have only pixman's + * implementation to worry about. + * Note that this also catches the cases where r1 == r2. */ - return _cairo_quartz_setup_fallback_source (surface, &rpat->base.base); + 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 (rpat->c1.x), - _cairo_fixed_to_double (rpat->c1.y)); - end = CGPointMake (_cairo_fixed_to_double (rpat->c2.x), - _cairo_fixed_to_double (rpat->c2.y)); + start = CGPointMake (c1x, c1y); + end = CGPointMake (c2x, c2y); - gradFunc = CreateGradientFunction (&rpat->base); + if (abspat->extend == CAIRO_EXTEND_NONE || + abspat->extend == CAIRO_EXTEND_PAD) + { + gradFunc = CreateGradientFunction (&rpat->base); + } else { + gradFunc = CreateRepeatingRadialGradientFunction (surface, + &rpat->base, + &start, &r1, + &end, &r2); + } surface->sourceShading = CGShadingCreateRadial (rgb, start, - _cairo_fixed_to_double (rpat->r1), + r1, end, - _cairo_fixed_to_double (rpat->r2), + r2, gradFunc, extend, extend); CGColorSpaceRelease(rgb); CGFunctionRelease(gradFunc); return DO_SHADING; }