2006-02-23 01:01:29 +00:00
|
|
|
/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*-
|
2012-05-21 11:12:37 +00:00
|
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
2006-02-23 01:01:29 +00:00
|
|
|
|
2010-07-02 04:04:09 +00:00
|
|
|
#include "gfxXlibNativeRenderer.h"
|
2006-02-23 01:01:29 +00:00
|
|
|
|
2010-07-02 04:04:09 +00:00
|
|
|
#include "gfxXlibSurface.h"
|
2010-07-22 22:39:22 +00:00
|
|
|
#include "gfxImageSurface.h"
|
2010-07-02 04:04:09 +00:00
|
|
|
#include "gfxContext.h"
|
2013-09-20 02:00:35 +00:00
|
|
|
#include "gfxPlatform.h"
|
2010-08-09 02:19:17 +00:00
|
|
|
#include "gfxAlphaRecovery.h"
|
2006-02-23 01:01:29 +00:00
|
|
|
#include "cairo-xlib.h"
|
2010-07-02 04:04:09 +00:00
|
|
|
#include "cairo-xlib-xrender.h"
|
2013-09-23 03:28:16 +00:00
|
|
|
#include "mozilla/gfx/BorrowedContext.h"
|
2013-10-25 21:25:40 +00:00
|
|
|
#include "gfx2DGlue.h"
|
2006-02-23 01:01:29 +00:00
|
|
|
|
2013-09-20 02:00:35 +00:00
|
|
|
using namespace mozilla;
|
|
|
|
using namespace mozilla::gfx;
|
|
|
|
|
2006-03-17 22:24:30 +00:00
|
|
|
#if 0
|
|
|
|
#include <stdio.h>
|
2010-07-02 04:04:09 +00:00
|
|
|
#define NATIVE_DRAWING_NOTE(m) fprintf(stderr, m)
|
2006-03-17 22:24:30 +00:00
|
|
|
#else
|
2010-07-02 04:04:09 +00:00
|
|
|
#define NATIVE_DRAWING_NOTE(m) do {} while (0)
|
2006-03-17 22:24:30 +00:00
|
|
|
#endif
|
2006-02-23 01:01:29 +00:00
|
|
|
|
2010-07-22 22:39:22 +00:00
|
|
|
/* We have four basic strategies available:
|
|
|
|
|
|
|
|
1) 'direct': If the target is an xlib surface, and other conditions are met,
|
|
|
|
we can pass the underlying drawable directly to the callback.
|
|
|
|
|
|
|
|
2) 'simple': If the drawing is opaque, or we can draw to a surface with an
|
|
|
|
alpha channel, then we can create a temporary xlib surface, pass its
|
|
|
|
underlying drawable to the callback, and composite the result using
|
|
|
|
cairo.
|
|
|
|
|
|
|
|
3) 'copy-background': If the drawing is not opaque but the target is
|
|
|
|
opaque, and we can draw to a surface with format such that pixel
|
|
|
|
conversion to and from the target format is exact, we can create a
|
|
|
|
temporary xlib surface, copy the background from the target, pass the
|
|
|
|
underlying drawable to the callback, and copy back to the target.
|
|
|
|
|
|
|
|
This strategy is not used if the pixel format conversion is not exact,
|
|
|
|
because that would mean that drawing intended to be very transparent
|
|
|
|
messes with other content.
|
|
|
|
|
|
|
|
The strategy is prefered over simple for non-opaque drawing and opaque
|
|
|
|
targets on the same screen as compositing without alpha is a simpler
|
|
|
|
operation.
|
|
|
|
|
|
|
|
4) 'alpha-extraction': create a temporary xlib surface, fill with black,
|
|
|
|
pass its underlying drawable to the callback, copy the results to a
|
|
|
|
cairo image surface, repeat with a white background, update the on-black
|
2006-02-23 01:01:29 +00:00
|
|
|
image alpha values by comparing the two images, then paint the on-black
|
2010-07-22 22:39:22 +00:00
|
|
|
image using cairo.
|
|
|
|
|
|
|
|
Sure would be nice to have an X extension or GL to do this for us on the
|
|
|
|
server...
|
2006-02-23 01:01:29 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
static cairo_bool_t
|
2012-08-22 15:56:38 +00:00
|
|
|
_convert_coord_to_int (double coord, int32_t *v)
|
2006-02-23 01:01:29 +00:00
|
|
|
{
|
2012-08-22 15:56:38 +00:00
|
|
|
*v = (int32_t)coord;
|
2006-02-23 01:01:29 +00:00
|
|
|
/* XXX allow some tolerance here? */
|
|
|
|
return *v == coord;
|
|
|
|
}
|
|
|
|
|
2011-09-29 06:19:26 +00:00
|
|
|
static bool
|
2006-02-23 01:01:29 +00:00
|
|
|
_get_rectangular_clip (cairo_t *cr,
|
2010-07-02 04:04:09 +00:00
|
|
|
const nsIntRect& bounds,
|
2011-09-29 06:19:26 +00:00
|
|
|
bool *need_clip,
|
2010-07-02 04:04:09 +00:00
|
|
|
nsIntRect *rectangles, int max_rectangles,
|
2006-02-23 01:01:29 +00:00
|
|
|
int *num_rectangles)
|
|
|
|
{
|
2006-09-26 22:21:56 +00:00
|
|
|
cairo_rectangle_list_t *cliplist;
|
|
|
|
cairo_rectangle_t *clips;
|
2006-02-23 01:01:29 +00:00
|
|
|
int i;
|
2011-09-29 06:19:26 +00:00
|
|
|
bool retval = true;
|
2006-09-26 22:21:56 +00:00
|
|
|
|
2007-01-25 00:27:34 +00:00
|
|
|
cliplist = cairo_copy_clip_rectangle_list (cr);
|
2006-09-26 22:21:56 +00:00
|
|
|
if (cliplist->status != CAIRO_STATUS_SUCCESS) {
|
2011-10-17 14:59:28 +00:00
|
|
|
retval = false;
|
2010-07-22 22:39:22 +00:00
|
|
|
NATIVE_DRAWING_NOTE("FALLBACK: non-rectangular clip");
|
2006-09-26 22:21:56 +00:00
|
|
|
goto FINISH;
|
2006-02-23 01:01:29 +00:00
|
|
|
}
|
2006-03-17 22:24:30 +00:00
|
|
|
|
2010-07-22 22:25:19 +00:00
|
|
|
/* the clip is always in surface backend coordinates (i.e. native backend coords) */
|
2006-09-26 22:21:56 +00:00
|
|
|
clips = cliplist->rectangles;
|
2006-03-17 22:24:30 +00:00
|
|
|
|
2006-09-26 22:21:56 +00:00
|
|
|
for (i = 0; i < cliplist->num_rectangles; ++i) {
|
2006-02-23 01:01:29 +00:00
|
|
|
|
2010-07-22 22:25:19 +00:00
|
|
|
nsIntRect rect;
|
|
|
|
if (!_convert_coord_to_int (clips[i].x, &rect.x) ||
|
|
|
|
!_convert_coord_to_int (clips[i].y, &rect.y) ||
|
|
|
|
!_convert_coord_to_int (clips[i].width, &rect.width) ||
|
|
|
|
!_convert_coord_to_int (clips[i].height, &rect.height))
|
|
|
|
{
|
2011-10-17 14:59:28 +00:00
|
|
|
retval = false;
|
2010-07-22 22:39:22 +00:00
|
|
|
NATIVE_DRAWING_NOTE("FALLBACK: non-integer clip");
|
2006-09-26 22:21:56 +00:00
|
|
|
goto FINISH;
|
2006-02-23 01:01:29 +00:00
|
|
|
}
|
|
|
|
|
2011-04-19 03:07:23 +00:00
|
|
|
if (rect.IsEqualInterior(bounds)) {
|
2010-07-22 22:25:19 +00:00
|
|
|
/* the bounds are entirely inside the clip region so we don't need to clip. */
|
2011-10-17 14:59:28 +00:00
|
|
|
*need_clip = false;
|
2010-07-22 22:25:19 +00:00
|
|
|
goto FINISH;
|
|
|
|
}
|
|
|
|
|
|
|
|
NS_ASSERTION(bounds.Contains(rect),
|
|
|
|
"Was expecting to be clipped to bounds");
|
2006-02-23 01:01:29 +00:00
|
|
|
|
2010-07-22 22:25:19 +00:00
|
|
|
if (i >= max_rectangles) {
|
2011-10-17 14:59:28 +00:00
|
|
|
retval = false;
|
2010-07-22 22:39:22 +00:00
|
|
|
NATIVE_DRAWING_NOTE("FALLBACK: unsupported clip rectangle count");
|
2010-07-22 22:25:19 +00:00
|
|
|
goto FINISH;
|
2006-02-23 01:01:29 +00:00
|
|
|
}
|
2010-07-22 22:25:19 +00:00
|
|
|
|
|
|
|
rectangles[i] = rect;
|
2006-02-23 01:01:29 +00:00
|
|
|
}
|
|
|
|
|
2011-10-17 14:59:28 +00:00
|
|
|
*need_clip = true;
|
2010-07-22 22:25:19 +00:00
|
|
|
*num_rectangles = cliplist->num_rectangles;
|
2006-09-26 22:21:56 +00:00
|
|
|
|
|
|
|
FINISH:
|
|
|
|
cairo_rectangle_list_destroy (cliplist);
|
|
|
|
|
|
|
|
return retval;
|
2006-02-23 01:01:29 +00:00
|
|
|
}
|
|
|
|
|
2006-09-26 22:21:56 +00:00
|
|
|
#define MAX_STATIC_CLIP_RECTANGLES 50
|
|
|
|
|
2006-02-23 01:01:29 +00:00
|
|
|
/**
|
|
|
|
* Try the direct path.
|
|
|
|
* @return True if we took the direct path
|
|
|
|
*/
|
2011-09-29 06:19:26 +00:00
|
|
|
bool
|
2010-07-22 22:25:19 +00:00
|
|
|
gfxXlibNativeRenderer::DrawDirect(gfxContext *ctx, nsIntSize size,
|
2012-08-22 15:56:38 +00:00
|
|
|
uint32_t flags,
|
2010-07-02 04:04:09 +00:00
|
|
|
Screen *screen, Visual *visual)
|
2006-02-23 01:01:29 +00:00
|
|
|
{
|
2013-09-20 02:00:35 +00:00
|
|
|
if (ctx->IsCairo()) {
|
|
|
|
return DrawCairo(ctx->GetCairo(), size, flags, screen, visual);
|
|
|
|
}
|
|
|
|
|
|
|
|
// We need to actually borrow the context because we want to read out the
|
|
|
|
// clip rectangles.
|
|
|
|
BorrowedCairoContext borrowed(ctx->GetDrawTarget());
|
|
|
|
if (!borrowed.mCairo) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool direct = DrawCairo(borrowed.mCairo, size, flags, screen, visual);
|
|
|
|
borrowed.Finish();
|
2006-09-28 21:59:49 +00:00
|
|
|
|
2013-09-20 02:00:35 +00:00
|
|
|
return direct;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
gfxXlibNativeRenderer::DrawCairo(cairo_t* cr, nsIntSize size,
|
|
|
|
uint32_t flags,
|
|
|
|
Screen *screen, Visual *visual)
|
|
|
|
{
|
2010-07-22 22:25:19 +00:00
|
|
|
/* Check that the target surface is an xlib surface. */
|
|
|
|
cairo_surface_t *target = cairo_get_group_target (cr);
|
2010-07-02 04:04:09 +00:00
|
|
|
if (cairo_surface_get_type (target) != CAIRO_SURFACE_TYPE_XLIB) {
|
2010-07-22 22:39:22 +00:00
|
|
|
NATIVE_DRAWING_NOTE("FALLBACK: non-X surface");
|
2011-10-17 14:59:28 +00:00
|
|
|
return false;
|
2006-02-23 01:01:29 +00:00
|
|
|
}
|
2013-09-20 02:00:35 +00:00
|
|
|
|
2010-07-22 22:25:19 +00:00
|
|
|
cairo_matrix_t matrix;
|
|
|
|
cairo_get_matrix (cr, &matrix);
|
|
|
|
double device_offset_x, device_offset_y;
|
|
|
|
cairo_surface_get_device_offset (target, &device_offset_x, &device_offset_y);
|
|
|
|
|
|
|
|
/* Draw() checked that the matrix contained only a very-close-to-integer
|
|
|
|
translation. Here (and in several other places and thebes) device
|
|
|
|
offsets are assumed to be integer. */
|
2012-08-22 15:56:38 +00:00
|
|
|
NS_ASSERTION(int32_t(device_offset_x) == device_offset_x &&
|
|
|
|
int32_t(device_offset_y) == device_offset_y,
|
2010-07-22 22:25:19 +00:00
|
|
|
"Expected integer device offsets");
|
|
|
|
nsIntPoint offset(NS_lroundf(matrix.x0 + device_offset_x),
|
|
|
|
NS_lroundf(matrix.y0 + device_offset_y));
|
2013-09-20 02:00:35 +00:00
|
|
|
|
2010-07-22 22:25:19 +00:00
|
|
|
int max_rectangles = 0;
|
|
|
|
if (flags & DRAW_SUPPORTS_CLIP_RECT) {
|
|
|
|
max_rectangles = 1;
|
|
|
|
}
|
|
|
|
if (flags & DRAW_SUPPORTS_CLIP_LIST) {
|
|
|
|
max_rectangles = MAX_STATIC_CLIP_RECTANGLES;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* The client won't draw outside the surface so consider this when
|
|
|
|
analysing clip rectangles. */
|
|
|
|
nsIntRect bounds(offset, size);
|
|
|
|
bounds.IntersectRect(bounds,
|
|
|
|
nsIntRect(0, 0,
|
|
|
|
cairo_xlib_surface_get_width(target),
|
|
|
|
cairo_xlib_surface_get_height(target)));
|
|
|
|
|
2011-09-29 06:19:26 +00:00
|
|
|
bool needs_clip = true;
|
2010-07-22 22:25:19 +00:00
|
|
|
nsIntRect rectangles[MAX_STATIC_CLIP_RECTANGLES];
|
2010-12-05 18:38:12 +00:00
|
|
|
int rect_count = 0;
|
2010-07-22 22:25:19 +00:00
|
|
|
|
|
|
|
/* Check that the clip is rectangular and aligned on unit boundaries. */
|
|
|
|
/* Temporarily set the matrix for _get_rectangular_clip. It's basically
|
|
|
|
the identity matrix, but we must adjust for the fact that our
|
|
|
|
offset-rect is in device coordinates. */
|
|
|
|
cairo_identity_matrix (cr);
|
|
|
|
cairo_translate (cr, -device_offset_x, -device_offset_y);
|
2011-09-29 06:19:26 +00:00
|
|
|
bool have_rectangular_clip =
|
2010-07-22 22:25:19 +00:00
|
|
|
_get_rectangular_clip (cr, bounds, &needs_clip,
|
|
|
|
rectangles, max_rectangles, &rect_count);
|
|
|
|
cairo_set_matrix (cr, &matrix);
|
|
|
|
if (!have_rectangular_clip)
|
2011-10-17 14:59:28 +00:00
|
|
|
return false;
|
2010-07-22 22:25:19 +00:00
|
|
|
|
2010-11-07 20:21:04 +00:00
|
|
|
/* Stop now if everything is clipped out */
|
|
|
|
if (needs_clip && rect_count == 0)
|
2011-10-17 14:59:28 +00:00
|
|
|
return true;
|
2013-09-20 02:00:35 +00:00
|
|
|
|
2010-11-07 20:21:04 +00:00
|
|
|
/* Check that the screen is supported.
|
|
|
|
Visuals belong to screens, so, if alternate visuals are not supported,
|
2013-09-20 02:00:35 +00:00
|
|
|
then alternate screens cannot be supported. */
|
2011-09-29 06:19:26 +00:00
|
|
|
bool supports_alternate_visual =
|
2010-11-07 20:21:04 +00:00
|
|
|
(flags & DRAW_SUPPORTS_ALTERNATE_VISUAL) != 0;
|
2011-09-29 06:19:26 +00:00
|
|
|
bool supports_alternate_screen = supports_alternate_visual &&
|
2010-11-07 20:21:04 +00:00
|
|
|
(flags & DRAW_SUPPORTS_ALTERNATE_SCREEN);
|
|
|
|
if (!supports_alternate_screen &&
|
|
|
|
cairo_xlib_surface_get_screen (target) != screen) {
|
|
|
|
NATIVE_DRAWING_NOTE("FALLBACK: non-default screen");
|
2011-10-17 14:59:28 +00:00
|
|
|
return false;
|
2010-11-07 20:21:04 +00:00
|
|
|
}
|
2013-09-20 02:00:35 +00:00
|
|
|
|
2010-11-07 20:21:04 +00:00
|
|
|
/* Check that there is a visual */
|
|
|
|
Visual *target_visual = cairo_xlib_surface_get_visual (target);
|
|
|
|
if (!target_visual) {
|
|
|
|
NATIVE_DRAWING_NOTE("FALLBACK: no Visual for surface");
|
2011-10-17 14:59:28 +00:00
|
|
|
return false;
|
2013-09-20 02:00:35 +00:00
|
|
|
}
|
2010-11-07 20:21:04 +00:00
|
|
|
/* Check that the visual is supported */
|
|
|
|
if (!supports_alternate_visual && target_visual != visual) {
|
|
|
|
// Only the format of the visual is important (not the GLX properties)
|
|
|
|
// for Xlib or XRender drawing.
|
|
|
|
XRenderPictFormat *target_format =
|
|
|
|
cairo_xlib_surface_get_xrender_format (target);
|
|
|
|
if (!target_format ||
|
|
|
|
(target_format !=
|
|
|
|
XRenderFindVisualFormat (DisplayOfScreen(screen), visual))) {
|
|
|
|
NATIVE_DRAWING_NOTE("FALLBACK: unsupported Visual");
|
2011-10-17 14:59:28 +00:00
|
|
|
return false;
|
2010-11-07 20:21:04 +00:00
|
|
|
}
|
|
|
|
}
|
2013-09-20 02:00:35 +00:00
|
|
|
|
2006-02-23 01:01:29 +00:00
|
|
|
/* we're good to go! */
|
2010-07-02 04:04:09 +00:00
|
|
|
NATIVE_DRAWING_NOTE("TAKING FAST PATH\n");
|
2006-02-23 01:01:29 +00:00
|
|
|
cairo_surface_flush (target);
|
2013-10-25 21:25:40 +00:00
|
|
|
nsresult rv = DrawWithXlib(target,
|
2010-07-02 04:04:09 +00:00
|
|
|
offset, rectangles,
|
|
|
|
needs_clip ? rect_count : 0);
|
|
|
|
if (NS_SUCCEEDED(rv)) {
|
2008-08-19 03:22:42 +00:00
|
|
|
cairo_surface_mark_dirty (target);
|
2011-10-17 14:59:28 +00:00
|
|
|
return true;
|
2008-08-19 03:22:42 +00:00
|
|
|
}
|
2011-10-17 14:59:28 +00:00
|
|
|
return false;
|
2006-02-23 01:01:29 +00:00
|
|
|
}
|
|
|
|
|
2011-09-29 06:19:26 +00:00
|
|
|
static bool
|
2010-07-22 22:39:22 +00:00
|
|
|
VisualHasAlpha(Screen *screen, Visual *visual) {
|
|
|
|
// There may be some other visuals format with alpha but usually this is
|
|
|
|
// the only one we care about.
|
|
|
|
return visual->c_class == TrueColor &&
|
|
|
|
visual->bits_per_rgb == 8 &&
|
|
|
|
visual->red_mask == 0xff0000 &&
|
|
|
|
visual->green_mask == 0xff00 &&
|
|
|
|
visual->blue_mask == 0xff &&
|
|
|
|
gfxXlibSurface::DepthOfVisual(screen, visual) == 32;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns whether pixel conversion between visual and format is exact (in
|
|
|
|
// both directions).
|
2011-09-29 06:19:26 +00:00
|
|
|
static bool
|
2010-07-22 22:39:22 +00:00
|
|
|
FormatConversionIsExact(Screen *screen, Visual *visual, XRenderPictFormat *format) {
|
|
|
|
if (!format ||
|
|
|
|
visual->c_class != TrueColor ||
|
|
|
|
format->type != PictTypeDirect ||
|
|
|
|
gfxXlibSurface::DepthOfVisual(screen, visual) != format->depth)
|
2011-10-17 14:59:28 +00:00
|
|
|
return false;
|
2010-07-22 22:39:22 +00:00
|
|
|
|
|
|
|
XRenderPictFormat *visualFormat =
|
|
|
|
XRenderFindVisualFormat(DisplayOfScreen(screen), visual);
|
2008-08-06 20:48:55 +00:00
|
|
|
|
2010-07-22 22:39:22 +00:00
|
|
|
if (visualFormat->type != PictTypeDirect )
|
2011-10-17 14:59:28 +00:00
|
|
|
return false;
|
2008-11-09 23:39:41 +00:00
|
|
|
|
2010-07-22 22:39:22 +00:00
|
|
|
const XRenderDirectFormat& a = visualFormat->direct;
|
|
|
|
const XRenderDirectFormat& b = format->direct;
|
|
|
|
return a.redMask == b.redMask &&
|
|
|
|
a.greenMask == b.greenMask &&
|
|
|
|
a.blueMask == b.blueMask;
|
2010-07-02 04:04:09 +00:00
|
|
|
}
|
2008-08-06 23:24:13 +00:00
|
|
|
|
2010-07-22 22:39:22 +00:00
|
|
|
// The 3 non-direct strategies described above.
|
|
|
|
// The surface format and strategy are inter-dependent.
|
|
|
|
enum DrawingMethod {
|
|
|
|
eSimple,
|
|
|
|
eCopyBackground,
|
|
|
|
eAlphaExtraction
|
|
|
|
};
|
|
|
|
|
2013-10-25 21:25:40 +00:00
|
|
|
static cairo_surface_t*
|
|
|
|
CreateTempXlibSurface (cairo_surface_t* cairoTarget,
|
|
|
|
DrawTarget* drawTarget,
|
|
|
|
nsIntSize size,
|
2011-09-29 06:19:26 +00:00
|
|
|
bool canDrawOverBackground,
|
2012-08-22 15:56:38 +00:00
|
|
|
uint32_t flags, Screen *screen, Visual *visual,
|
2010-07-22 22:39:22 +00:00
|
|
|
DrawingMethod *method)
|
2010-07-02 04:04:09 +00:00
|
|
|
{
|
2013-10-25 21:25:40 +00:00
|
|
|
NS_ASSERTION(cairoTarget || drawTarget, "Must have some type");
|
|
|
|
|
2011-09-29 06:19:26 +00:00
|
|
|
bool drawIsOpaque = (flags & gfxXlibNativeRenderer::DRAW_IS_OPAQUE) != 0;
|
|
|
|
bool supportsAlternateVisual =
|
2010-07-22 22:39:22 +00:00
|
|
|
(flags & gfxXlibNativeRenderer::DRAW_SUPPORTS_ALTERNATE_VISUAL) != 0;
|
2011-09-29 06:19:26 +00:00
|
|
|
bool supportsAlternateScreen = supportsAlternateVisual &&
|
2010-07-22 22:39:22 +00:00
|
|
|
(flags & gfxXlibNativeRenderer::DRAW_SUPPORTS_ALTERNATE_SCREEN);
|
|
|
|
|
2013-10-25 21:25:40 +00:00
|
|
|
cairo_surface_type_t cairoTargetType =
|
|
|
|
cairoTarget ? cairo_surface_get_type (cairoTarget) : (cairo_surface_type_t)0xFF;
|
2010-07-22 22:39:22 +00:00
|
|
|
|
2013-10-25 21:25:40 +00:00
|
|
|
Screen *target_screen = cairoTargetType == CAIRO_SURFACE_TYPE_XLIB ?
|
|
|
|
cairo_xlib_surface_get_screen (cairoTarget) : screen;
|
2010-07-22 22:39:22 +00:00
|
|
|
|
|
|
|
// When the background has an alpha channel, we need to draw with an alpha
|
|
|
|
// channel anyway, so there is no need to copy the background. If
|
|
|
|
// doCopyBackground is set here, we'll also need to check below that the
|
|
|
|
// background can copied without any loss in format conversions.
|
2011-09-29 06:19:26 +00:00
|
|
|
bool doCopyBackground = !drawIsOpaque && canDrawOverBackground &&
|
2013-10-25 21:25:40 +00:00
|
|
|
cairoTarget && cairo_surface_get_content (cairoTarget) == CAIRO_CONTENT_COLOR;
|
2010-07-22 22:39:22 +00:00
|
|
|
|
|
|
|
if (supportsAlternateScreen && screen != target_screen && drawIsOpaque) {
|
|
|
|
// Prefer a visual on the target screen.
|
|
|
|
// (If !drawIsOpaque, we'll need doCopyBackground or an alpha channel.)
|
|
|
|
visual = DefaultVisualOfScreen(target_screen);
|
|
|
|
screen = target_screen;
|
|
|
|
|
|
|
|
} else if (doCopyBackground || (supportsAlternateVisual && drawIsOpaque)) {
|
|
|
|
// Analyse the pixel formats either to check whether we can
|
|
|
|
// doCopyBackground or to see if we can find a better visual for
|
|
|
|
// opaque drawing.
|
2013-07-31 15:44:31 +00:00
|
|
|
Visual *target_visual = nullptr;
|
|
|
|
XRenderPictFormat *target_format = nullptr;
|
2013-10-25 21:25:40 +00:00
|
|
|
if (cairoTargetType == CAIRO_SURFACE_TYPE_XLIB) {
|
|
|
|
target_visual = cairo_xlib_surface_get_visual (cairoTarget);
|
|
|
|
target_format = cairo_xlib_surface_get_xrender_format (cairoTarget);
|
|
|
|
} else if (cairoTargetType == CAIRO_SURFACE_TYPE_IMAGE || drawTarget) {
|
2013-09-24 20:45:13 +00:00
|
|
|
gfxImageFormat imageFormat =
|
2013-10-25 21:25:40 +00:00
|
|
|
drawTarget ? SurfaceFormatToImageFormat(drawTarget->GetFormat()) :
|
|
|
|
(gfxImageFormat)cairo_image_surface_get_format(cairoTarget);
|
2010-07-22 22:39:22 +00:00
|
|
|
target_visual = gfxXlibSurface::FindVisual(screen, imageFormat);
|
|
|
|
Display *dpy = DisplayOfScreen(screen);
|
|
|
|
if (target_visual) {
|
2011-03-25 03:38:59 +00:00
|
|
|
target_format = XRenderFindVisualFormat(dpy, target_visual);
|
2010-07-22 22:39:22 +00:00
|
|
|
} else {
|
|
|
|
target_format =
|
|
|
|
gfxXlibSurface::FindRenderFormat(dpy, imageFormat);
|
|
|
|
}
|
|
|
|
}
|
2010-07-02 04:04:09 +00:00
|
|
|
|
2010-07-22 22:39:22 +00:00
|
|
|
if (supportsAlternateVisual &&
|
|
|
|
(supportsAlternateScreen || screen == target_screen)) {
|
|
|
|
if (target_visual) {
|
|
|
|
visual = target_visual;
|
|
|
|
screen = target_screen;
|
2010-07-02 04:04:09 +00:00
|
|
|
}
|
2010-07-22 22:39:22 +00:00
|
|
|
}
|
|
|
|
// Could try harder to match formats across screens for background
|
|
|
|
// copying when !supportsAlternateScreen, if we cared. Preferably
|
|
|
|
// we'll find a visual below with an alpha channel anyway; if so, the
|
|
|
|
// background won't need to be copied.
|
|
|
|
|
|
|
|
if (doCopyBackground && visual != target_visual &&
|
|
|
|
!FormatConversionIsExact(screen, visual, target_format)) {
|
2011-10-17 14:59:28 +00:00
|
|
|
doCopyBackground = false;
|
2010-07-22 22:39:22 +00:00
|
|
|
}
|
|
|
|
}
|
2008-08-06 20:48:55 +00:00
|
|
|
|
2010-07-22 22:39:22 +00:00
|
|
|
if (supportsAlternateVisual && !drawIsOpaque &&
|
|
|
|
(screen != target_screen ||
|
|
|
|
!(doCopyBackground || VisualHasAlpha(screen, visual)))) {
|
|
|
|
// Try to find a visual with an alpha channel.
|
|
|
|
Screen *visualScreen =
|
|
|
|
supportsAlternateScreen ? target_screen : screen;
|
|
|
|
Visual *argbVisual =
|
|
|
|
gfxXlibSurface::FindVisual(visualScreen,
|
2014-01-23 18:26:40 +00:00
|
|
|
gfxImageFormat::ARGB32);
|
2010-07-22 22:39:22 +00:00
|
|
|
if (argbVisual) {
|
|
|
|
visual = argbVisual;
|
|
|
|
screen = visualScreen;
|
|
|
|
} else if (!doCopyBackground &&
|
|
|
|
gfxXlibSurface::DepthOfVisual(screen, visual) != 24) {
|
|
|
|
// Will need to do alpha extraction; prefer a 24-bit visual.
|
|
|
|
// No advantage in using the target screen.
|
|
|
|
Visual *rgb24Visual =
|
|
|
|
gfxXlibSurface::FindVisual(screen,
|
2014-01-23 18:26:40 +00:00
|
|
|
gfxImageFormat::RGB24);
|
2010-07-22 22:39:22 +00:00
|
|
|
if (rgb24Visual) {
|
|
|
|
visual = rgb24Visual;
|
|
|
|
}
|
2010-07-02 04:04:09 +00:00
|
|
|
}
|
2008-08-06 23:24:13 +00:00
|
|
|
}
|
2008-08-06 20:48:55 +00:00
|
|
|
|
2010-07-22 22:39:22 +00:00
|
|
|
Drawable drawable =
|
2013-10-25 21:25:40 +00:00
|
|
|
(screen == target_screen && cairoTargetType == CAIRO_SURFACE_TYPE_XLIB) ?
|
|
|
|
cairo_xlib_surface_get_drawable (cairoTarget) : RootWindowOfScreen(screen);
|
|
|
|
|
|
|
|
cairo_surface_t *surface =
|
|
|
|
gfxXlibSurface::CreateCairoSurface(screen, visual,
|
|
|
|
gfxIntSize(size.width, size.height),
|
|
|
|
drawable);
|
|
|
|
if (!surface) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
2010-07-22 22:39:22 +00:00
|
|
|
|
|
|
|
if (drawIsOpaque ||
|
2013-10-25 21:25:40 +00:00
|
|
|
cairo_surface_get_content(surface) == CAIRO_CONTENT_COLOR_ALPHA) {
|
2010-07-22 22:39:22 +00:00
|
|
|
NATIVE_DRAWING_NOTE(drawIsOpaque ?
|
|
|
|
", SIMPLE OPAQUE\n" : ", SIMPLE WITH ALPHA");
|
|
|
|
*method = eSimple;
|
|
|
|
} else if (doCopyBackground) {
|
|
|
|
NATIVE_DRAWING_NOTE(", COPY BACKGROUND\n");
|
|
|
|
*method = eCopyBackground;
|
|
|
|
} else {
|
|
|
|
NATIVE_DRAWING_NOTE(", SLOW ALPHA EXTRACTION\n");
|
|
|
|
*method = eAlphaExtraction;
|
2006-02-23 01:01:29 +00:00
|
|
|
}
|
2010-07-22 22:39:22 +00:00
|
|
|
|
2013-10-25 21:25:40 +00:00
|
|
|
return surface;
|
2006-02-23 01:01:29 +00:00
|
|
|
}
|
|
|
|
|
2011-09-29 06:19:26 +00:00
|
|
|
bool
|
2013-10-25 21:25:40 +00:00
|
|
|
gfxXlibNativeRenderer::DrawOntoTempSurface(cairo_surface_t *tempXlibSurface,
|
2010-07-22 22:25:19 +00:00
|
|
|
nsIntPoint offset)
|
2006-02-23 01:01:29 +00:00
|
|
|
{
|
2013-10-25 21:25:40 +00:00
|
|
|
cairo_surface_flush(tempXlibSurface);
|
2006-02-23 01:01:29 +00:00
|
|
|
/* no clipping is needed because the callback can't draw outside the native
|
|
|
|
surface anyway */
|
2013-10-25 21:25:40 +00:00
|
|
|
nsresult rv = DrawWithXlib(tempXlibSurface, offset, nullptr, 0);
|
|
|
|
cairo_surface_mark_dirty(tempXlibSurface);
|
2010-07-02 04:04:09 +00:00
|
|
|
return NS_SUCCEEDED(rv);
|
2006-02-23 01:01:29 +00:00
|
|
|
}
|
|
|
|
|
2010-08-09 02:19:17 +00:00
|
|
|
static already_AddRefed<gfxImageSurface>
|
2013-10-25 21:25:40 +00:00
|
|
|
CopyXlibSurfaceToImage(cairo_surface_t *tempXlibSurface,
|
|
|
|
gfxIntSize size,
|
2013-09-24 20:45:13 +00:00
|
|
|
gfxImageFormat format)
|
2006-02-23 01:01:29 +00:00
|
|
|
{
|
2013-10-25 21:25:40 +00:00
|
|
|
nsRefPtr<gfxImageSurface> result = new gfxImageSurface(size, format);
|
2006-02-23 01:01:29 +00:00
|
|
|
|
2013-10-25 21:25:40 +00:00
|
|
|
cairo_t* copyCtx = cairo_create(result->CairoSurface());
|
|
|
|
cairo_set_source_surface(copyCtx, tempXlibSurface, 0, 0);
|
|
|
|
cairo_set_operator(copyCtx, CAIRO_OPERATOR_SOURCE);
|
|
|
|
cairo_paint(copyCtx);
|
|
|
|
cairo_destroy(copyCtx);
|
2006-02-23 01:01:29 +00:00
|
|
|
|
2010-08-09 02:19:17 +00:00
|
|
|
return result.forget();
|
2006-02-23 01:01:29 +00:00
|
|
|
}
|
|
|
|
|
2010-07-02 04:04:09 +00:00
|
|
|
void
|
|
|
|
gfxXlibNativeRenderer::Draw(gfxContext* ctx, nsIntSize size,
|
2013-10-25 21:25:40 +00:00
|
|
|
uint32_t flags, Screen *screen, Visual *visual)
|
2006-02-23 01:01:29 +00:00
|
|
|
{
|
2010-07-22 22:39:22 +00:00
|
|
|
gfxMatrix matrix = ctx->CurrentMatrix();
|
|
|
|
|
|
|
|
// We can only draw direct or onto a copied background if pixels align and
|
|
|
|
// native drawing is compatible with the current operator. (The matrix is
|
|
|
|
// actually also pixel-exact for flips and right-angle rotations, which
|
|
|
|
// would permit copying the background but not drawing direct.)
|
2011-09-29 06:19:26 +00:00
|
|
|
bool matrixIsIntegerTranslation = !matrix.HasNonIntegerTranslation();
|
|
|
|
bool canDrawOverBackground = matrixIsIntegerTranslation &&
|
2010-07-22 22:39:22 +00:00
|
|
|
ctx->CurrentOperator() == gfxContext::OPERATOR_OVER;
|
2010-07-22 22:25:19 +00:00
|
|
|
|
|
|
|
// The padding of 0.5 for non-pixel-exact transformations used here is
|
|
|
|
// the same as what _cairo_pattern_analyze_filter uses.
|
|
|
|
const gfxFloat filterRadius = 0.5;
|
|
|
|
gfxRect affectedRect(0.0, 0.0, size.width, size.height);
|
|
|
|
if (!matrixIsIntegerTranslation) {
|
|
|
|
// The filter footprint means that the affected rectangle is a
|
|
|
|
// little larger than the drawingRect;
|
2011-04-19 03:07:51 +00:00
|
|
|
affectedRect.Inflate(filterRadius);
|
2010-07-22 22:25:19 +00:00
|
|
|
|
2010-07-22 22:39:22 +00:00
|
|
|
NATIVE_DRAWING_NOTE("FALLBACK: matrix not integer translation");
|
|
|
|
} else if (!canDrawOverBackground) {
|
|
|
|
NATIVE_DRAWING_NOTE("FALLBACK: unsupported operator");
|
2010-07-22 22:25:19 +00:00
|
|
|
}
|
2010-07-22 22:39:22 +00:00
|
|
|
|
2010-07-22 22:25:19 +00:00
|
|
|
// Clipping to the region affected by drawing allows us to consider only
|
|
|
|
// the portions of the clip region that will be affected by drawing.
|
|
|
|
gfxRect clipExtents;
|
|
|
|
{
|
|
|
|
gfxContextAutoSaveRestore autoSR(ctx);
|
|
|
|
ctx->Clip(affectedRect);
|
|
|
|
|
|
|
|
clipExtents = ctx->GetClipExtents();
|
|
|
|
if (clipExtents.IsEmpty())
|
|
|
|
return; // nothing to do
|
|
|
|
|
2010-07-22 22:39:22 +00:00
|
|
|
if (canDrawOverBackground &&
|
2010-07-22 22:25:19 +00:00
|
|
|
DrawDirect(ctx, size, flags, screen, visual))
|
2013-09-20 02:00:35 +00:00
|
|
|
return;
|
2010-07-22 22:25:19 +00:00
|
|
|
}
|
2006-02-23 01:01:29 +00:00
|
|
|
|
2010-07-22 22:25:19 +00:00
|
|
|
nsIntRect drawingRect(nsIntPoint(0, 0), size);
|
2010-07-22 22:39:22 +00:00
|
|
|
// Drawing need only be performed within the clip extents
|
|
|
|
// (and padding for the filter).
|
|
|
|
if (!matrixIsIntegerTranslation) {
|
|
|
|
// The source surface may need to be a little larger than the clip
|
|
|
|
// extents due to the filter footprint.
|
2011-04-19 03:07:51 +00:00
|
|
|
clipExtents.Inflate(filterRadius);
|
2010-07-22 22:25:19 +00:00
|
|
|
}
|
2010-07-22 22:39:22 +00:00
|
|
|
clipExtents.RoundOut();
|
|
|
|
|
2012-08-22 15:56:38 +00:00
|
|
|
nsIntRect intExtents(int32_t(clipExtents.X()),
|
|
|
|
int32_t(clipExtents.Y()),
|
|
|
|
int32_t(clipExtents.Width()),
|
|
|
|
int32_t(clipExtents.Height()));
|
2010-07-22 22:39:22 +00:00
|
|
|
drawingRect.IntersectRect(drawingRect, intExtents);
|
2013-09-20 02:00:35 +00:00
|
|
|
|
2013-10-25 21:25:40 +00:00
|
|
|
gfxPoint offset(drawingRect.x, drawingRect.y);
|
|
|
|
|
|
|
|
DrawingMethod method;
|
|
|
|
cairo_surface_t* cairoTarget = nullptr;
|
|
|
|
DrawTarget* drawTarget = nullptr;
|
2013-12-01 21:10:50 +00:00
|
|
|
gfxPoint deviceTranslation;
|
2013-09-20 02:00:35 +00:00
|
|
|
if (ctx->IsCairo()) {
|
2013-10-25 21:25:40 +00:00
|
|
|
cairoTarget = cairo_get_group_target(ctx->GetCairo());
|
2013-12-01 21:10:50 +00:00
|
|
|
deviceTranslation = ctx->CurrentMatrix().GetTranslation();
|
2013-09-20 02:00:35 +00:00
|
|
|
} else {
|
2013-10-25 21:25:40 +00:00
|
|
|
drawTarget = ctx->GetDrawTarget();
|
2013-12-01 21:10:50 +00:00
|
|
|
Matrix dtTransform = drawTarget->GetTransform();
|
|
|
|
deviceTranslation = gfxPoint(dtTransform._31, dtTransform._32);
|
2013-10-25 21:25:40 +00:00
|
|
|
cairoTarget = static_cast<cairo_surface_t*>
|
2014-01-10 19:06:16 +00:00
|
|
|
(drawTarget->GetNativeSurface(NativeSurfaceType::CAIRO_SURFACE));
|
2013-09-20 02:00:35 +00:00
|
|
|
}
|
2008-08-19 03:22:42 +00:00
|
|
|
|
2013-10-25 21:25:40 +00:00
|
|
|
cairo_surface_t* tempXlibSurface =
|
|
|
|
CreateTempXlibSurface(cairoTarget, drawTarget, size,
|
2010-07-22 22:39:22 +00:00
|
|
|
canDrawOverBackground, flags, screen, visual,
|
|
|
|
&method);
|
|
|
|
if (!tempXlibSurface)
|
2006-02-23 01:01:29 +00:00
|
|
|
return;
|
2013-09-20 02:00:35 +00:00
|
|
|
|
|
|
|
bool drawIsOpaque = (flags & DRAW_IS_OPAQUE) != 0;
|
2010-07-22 22:25:19 +00:00
|
|
|
if (!drawIsOpaque) {
|
2013-10-25 21:25:40 +00:00
|
|
|
cairo_t* tmpCtx = cairo_create(tempXlibSurface);
|
2010-07-22 22:39:22 +00:00
|
|
|
if (method == eCopyBackground) {
|
2013-10-25 21:25:40 +00:00
|
|
|
NS_ASSERTION(cairoTarget, "eCopyBackground only used when there's a cairoTarget");
|
|
|
|
cairo_set_operator(tmpCtx, CAIRO_OPERATOR_SOURCE);
|
2013-12-01 21:10:50 +00:00
|
|
|
gfxPoint pt = -(offset + deviceTranslation);
|
2013-10-25 21:25:40 +00:00
|
|
|
cairo_set_source_surface(tmpCtx, cairoTarget, pt.x, pt.y);
|
2010-07-22 22:39:22 +00:00
|
|
|
// The copy from the tempXlibSurface to the target context should
|
|
|
|
// use operator SOURCE, but that would need a mask to bound the
|
|
|
|
// operation. Here we only copy opaque backgrounds so operator
|
|
|
|
// OVER will behave like SOURCE masked by the surface.
|
2013-10-25 21:25:40 +00:00
|
|
|
NS_ASSERTION(cairo_surface_get_content(tempXlibSurface) == CAIRO_CONTENT_COLOR,
|
2010-07-22 22:39:22 +00:00
|
|
|
"Don't copy background with a transparent surface");
|
|
|
|
} else {
|
2013-10-25 21:25:40 +00:00
|
|
|
cairo_set_operator(tmpCtx, CAIRO_OPERATOR_CLEAR);
|
2010-07-22 22:39:22 +00:00
|
|
|
}
|
2013-10-25 21:25:40 +00:00
|
|
|
cairo_paint(tmpCtx);
|
|
|
|
cairo_destroy(tmpCtx);
|
2010-07-22 22:25:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!DrawOntoTempSurface(tempXlibSurface, -drawingRect.TopLeft())) {
|
2013-10-25 21:25:40 +00:00
|
|
|
cairo_surface_destroy(tempXlibSurface);
|
2006-02-23 01:01:29 +00:00
|
|
|
return;
|
|
|
|
}
|
2014-05-07 23:23:44 +00:00
|
|
|
|
2013-10-25 21:25:40 +00:00
|
|
|
SurfaceFormat moz2DFormat =
|
|
|
|
cairo_surface_get_content(tempXlibSurface) == CAIRO_CONTENT_COLOR ?
|
2014-01-10 19:06:16 +00:00
|
|
|
SurfaceFormat::B8G8R8A8 : SurfaceFormat::B8G8R8X8;
|
2010-07-22 22:39:22 +00:00
|
|
|
if (method != eAlphaExtraction) {
|
2013-09-20 02:00:35 +00:00
|
|
|
if (drawTarget) {
|
2014-05-07 23:23:44 +00:00
|
|
|
NativeSurface native;
|
|
|
|
native.mFormat = moz2DFormat;
|
|
|
|
native.mType = NativeSurfaceType::CAIRO_SURFACE;
|
|
|
|
native.mSurface = tempXlibSurface;
|
|
|
|
native.mSize = ToIntSize(size);
|
2013-10-25 21:25:40 +00:00
|
|
|
RefPtr<SourceSurface> sourceSurface =
|
2014-05-07 23:23:44 +00:00
|
|
|
drawTarget->CreateSourceSurfaceFromNativeSurface(native);
|
2013-10-25 21:25:40 +00:00
|
|
|
if (sourceSurface) {
|
|
|
|
drawTarget->DrawSurface(sourceSurface,
|
|
|
|
Rect(offset.x, offset.y, size.width, size.height),
|
|
|
|
Rect(0, 0, size.width, size.height));
|
|
|
|
}
|
2013-09-20 02:00:35 +00:00
|
|
|
} else {
|
2013-10-25 21:25:40 +00:00
|
|
|
nsRefPtr<gfxASurface> tmpSurf = gfxASurface::Wrap(tempXlibSurface);
|
|
|
|
ctx->SetSource(tmpSurf, offset);
|
2013-09-20 02:00:35 +00:00
|
|
|
ctx->Paint();
|
|
|
|
}
|
2013-10-25 21:25:40 +00:00
|
|
|
cairo_surface_destroy(tempXlibSurface);
|
2006-02-23 01:01:29 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2010-08-09 02:19:17 +00:00
|
|
|
nsRefPtr<gfxImageSurface> blackImage =
|
2014-01-23 18:26:40 +00:00
|
|
|
CopyXlibSurfaceToImage(tempXlibSurface, size, gfxImageFormat::ARGB32);
|
2006-02-23 01:01:29 +00:00
|
|
|
|
2013-10-25 21:25:40 +00:00
|
|
|
cairo_t* tmpCtx = cairo_create(tempXlibSurface);
|
|
|
|
cairo_set_source_rgba(tmpCtx, 1.0, 1.0, 1.0, 1.0);
|
|
|
|
cairo_set_operator(tmpCtx, CAIRO_OPERATOR_SOURCE);
|
|
|
|
cairo_paint(tmpCtx);
|
|
|
|
cairo_destroy(tmpCtx);
|
2010-07-22 22:25:19 +00:00
|
|
|
DrawOntoTempSurface(tempXlibSurface, -drawingRect.TopLeft());
|
2010-08-09 02:19:17 +00:00
|
|
|
nsRefPtr<gfxImageSurface> whiteImage =
|
2014-01-23 18:26:40 +00:00
|
|
|
CopyXlibSurfaceToImage(tempXlibSurface, size, gfxImageFormat::RGB24);
|
2006-02-23 01:01:29 +00:00
|
|
|
|
2010-08-09 02:19:17 +00:00
|
|
|
if (blackImage->CairoStatus() == CAIRO_STATUS_SUCCESS &&
|
2011-09-20 16:20:51 +00:00
|
|
|
whiteImage->CairoStatus() == CAIRO_STATUS_SUCCESS) {
|
2013-10-25 21:25:40 +00:00
|
|
|
if (!gfxAlphaRecovery::RecoverAlpha(blackImage, whiteImage)) {
|
2013-10-25 21:25:40 +00:00
|
|
|
cairo_surface_destroy(tempXlibSurface);
|
2010-08-09 02:19:17 +00:00
|
|
|
return;
|
2013-10-25 21:25:40 +00:00
|
|
|
}
|
2010-08-09 02:19:17 +00:00
|
|
|
|
2013-09-20 02:00:35 +00:00
|
|
|
gfxASurface* paintSurface = blackImage;
|
|
|
|
if (drawTarget) {
|
2014-05-07 23:23:44 +00:00
|
|
|
NativeSurface native;
|
|
|
|
native.mFormat = moz2DFormat;
|
|
|
|
native.mType = NativeSurfaceType::CAIRO_SURFACE;
|
|
|
|
native.mSurface = tempXlibSurface;
|
|
|
|
native.mSize = ToIntSize(size);
|
2013-10-25 21:25:40 +00:00
|
|
|
RefPtr<SourceSurface> sourceSurface =
|
2014-05-07 23:23:44 +00:00
|
|
|
drawTarget->CreateSourceSurfaceFromNativeSurface(native);
|
2013-10-25 21:25:40 +00:00
|
|
|
if (sourceSurface) {
|
|
|
|
drawTarget->DrawSurface(sourceSurface,
|
|
|
|
Rect(offset.x, offset.y, size.width, size.height),
|
|
|
|
Rect(0, 0, size.width, size.height));
|
|
|
|
}
|
2013-09-20 02:00:35 +00:00
|
|
|
} else {
|
|
|
|
ctx->SetSource(paintSurface, offset);
|
|
|
|
ctx->Paint();
|
|
|
|
}
|
2006-02-23 01:01:29 +00:00
|
|
|
}
|
2013-10-25 21:25:40 +00:00
|
|
|
cairo_surface_destroy(tempXlibSurface);
|
2006-02-23 01:01:29 +00:00
|
|
|
}
|