# HG changeset patch # User Robert O'Callahan <robert@ocallahan.org> # Date 1249558989 -43200 # Node ID 0bac4c903d2bb1d5c0d5426209001fc2a77cc105 # Parent 963b9451ad305924738d05d997a640698cd3af91 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 @@ -710,82 +710,100 @@ CreateGradientFunction (const cairo_grad return CGFunctionCreate (pat, 1, input_value_range, 4, gradient_output_value_ranges, &gradient_callbacks); } +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 CreateRepeatingLinearGradientFunction (cairo_quartz_surface_t *surface, const cairo_gradient_pattern_t *gpat, CGPoint *start, CGPoint *end, - CGAffineTransform matrix) + cairo_rectangle_int_t *extents) { cairo_pattern_t *pat; float 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; - 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.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 * not much point in us trying either */ return NULL; return CGFunctionCreate (pat, 1, @@ -840,35 +858,43 @@ UpdateRadialParameterToIncludePoint(doub } } /* 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) + 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; 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; + 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 */ inner = start; outer = end; inner_radius = start_radius; outer_radius = end_radius; } else { @@ -878,36 +904,37 @@ CreateRepeatingRadialGradientFunction (c inner_radius = end_radius; outer_radius = start_radius; } dr = *outer_radius - *inner_radius; 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; *inner_radius = 0.; 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; *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) { @@ -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; } +/* +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; CGPoint start, end; CGFunctionRef gradFunc; 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 || abspat->extend == CAIRO_EXTEND_PAD) { gradFunc = CreateGradientFunction (&lpat->base); } else { gradFunc = CreateRepeatingLinearGradientFunction (surface, &lpat->base, &start, &end, - surface->sourceTransform); + extents); } surface->sourceShading = CGShadingCreateAxial (rgb, start, end, gradFunc, extend, extend); CGColorSpaceRelease(rgb); CGFunctionRelease(gradFunc); return DO_SHADING; } 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; CGPoint start, end; CGFunctionRef gradFunc; 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 || abspat->extend == CAIRO_EXTEND_PAD) { gradFunc = CreateGradientFunction (&rpat->base); } else { gradFunc = CreateRepeatingRadialGradientFunction (surface, &rpat->base, &start, &r1, - &end, &r2); + &end, &r2, + extents); } surface->sourceShading = CGShadingCreateRadial (rgb, start, r1, end, r2, gradFunc, @@ -1341,17 +1394,18 @@ _cairo_quartz_setup_radial_source (cairo CGColorSpaceRelease(rgb); CGFunctionRelease(gradFunc); return DO_SHADING; } 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); } 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 && (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->type == CAIRO_PATTERN_TYPE_RADIAL) && + (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;