blob: e492ca94a0ae2726d13b7fceffaf39ac0e8645a9 [file] [log] [blame]
reed@google.com209c4152011-10-26 15:03:48 +00001/*
2 * Copyright 2011 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
Mike Kleinc0bd9f92019-04-23 12:05:21 -05008#include "include/core/SkBitmap.h"
9#include "include/core/SkCanvas.h"
10#include "include/core/SkColor.h"
11#include "include/core/SkImageInfo.h"
12#include "include/core/SkMatrix.h"
13#include "include/core/SkPath.h"
14#include "include/core/SkRRect.h"
15#include "include/core/SkRect.h"
16#include "include/core/SkRegion.h"
17#include "include/core/SkScalar.h"
18#include "include/core/SkTypes.h"
19#include "include/private/SkMalloc.h"
20#include "include/utils/SkRandom.h"
21#include "src/core/SkAAClip.h"
22#include "src/core/SkMask.h"
23#include "src/core/SkRasterClip.h"
24#include "tests/Test.h"
reed@google.comd8676d22011-10-26 18:01:25 +000025
Ben Wagner1ebeefe2018-03-02 16:59:53 -050026#include <string.h>
27
mike@reedtribe.org95b85bd2011-11-23 03:12:58 +000028static bool operator==(const SkMask& a, const SkMask& b) {
29 if (a.fFormat != b.fFormat || a.fBounds != b.fBounds) {
30 return false;
31 }
32 if (!a.fImage && !b.fImage) {
33 return true;
34 }
35 if (!a.fImage || !b.fImage) {
36 return false;
37 }
38
39 size_t wbytes = a.fBounds.width();
40 switch (a.fFormat) {
41 case SkMask::kBW_Format:
42 wbytes = (wbytes + 7) >> 3;
43 break;
44 case SkMask::kA8_Format:
45 case SkMask::k3D_Format:
46 break;
47 case SkMask::kLCD16_Format:
48 wbytes <<= 1;
49 break;
mike@reedtribe.org95b85bd2011-11-23 03:12:58 +000050 case SkMask::kARGB32_Format:
51 wbytes <<= 2;
52 break;
53 default:
mtklein@google.com330313a2013-08-22 15:37:26 +000054 SkDEBUGFAIL("unknown mask format");
mike@reedtribe.org95b85bd2011-11-23 03:12:58 +000055 return false;
56 }
57
58 const int h = a.fBounds.height();
59 const char* aptr = (const char*)a.fImage;
60 const char* bptr = (const char*)b.fImage;
61 for (int y = 0; y < h; ++y) {
62 if (memcmp(aptr, bptr, wbytes)) {
63 return false;
64 }
65 aptr += wbytes;
66 bptr += wbytes;
67 }
68 return true;
69}
70
71static void copyToMask(const SkRegion& rgn, SkMask* mask) {
reed@google.coma052aca2011-11-23 19:43:46 +000072 mask->fFormat = SkMask::kA8_Format;
73
mike@reedtribe.org95b85bd2011-11-23 03:12:58 +000074 if (rgn.isEmpty()) {
mike@reedtribe.org95b85bd2011-11-23 03:12:58 +000075 mask->fBounds.setEmpty();
76 mask->fRowBytes = 0;
halcanary96fcdcc2015-08-27 07:41:13 -070077 mask->fImage = nullptr;
mike@reedtribe.org95b85bd2011-11-23 03:12:58 +000078 return;
79 }
80
81 mask->fBounds = rgn.getBounds();
82 mask->fRowBytes = mask->fBounds.width();
mike@reedtribe.org95b85bd2011-11-23 03:12:58 +000083 mask->fImage = SkMask::AllocImage(mask->computeImageSize());
84 sk_bzero(mask->fImage, mask->computeImageSize());
85
commit-bot@chromium.orgfa9e5fa2014-02-13 22:00:04 +000086 SkImageInfo info = SkImageInfo::Make(mask->fBounds.width(),
87 mask->fBounds.height(),
88 kAlpha_8_SkColorType,
89 kPremul_SkAlphaType);
mike@reedtribe.org95b85bd2011-11-23 03:12:58 +000090 SkBitmap bitmap;
commit-bot@chromium.org00f8d6c2014-05-29 15:57:20 +000091 bitmap.installPixels(info, mask->fImage, mask->fRowBytes);
mike@reedtribe.org95b85bd2011-11-23 03:12:58 +000092
93 // canvas expects its coordinate system to always be 0,0 in the top/left
94 // so we translate the rgn to match that before drawing into the mask.
95 //
96 SkRegion tmpRgn(rgn);
97 tmpRgn.translate(-rgn.getBounds().fLeft, -rgn.getBounds().fTop);
98
99 SkCanvas canvas(bitmap);
100 canvas.clipRegion(tmpRgn);
101 canvas.drawColor(SK_ColorBLACK);
102}
103
commit-bot@chromium.orge0e7cfe2013-09-09 20:09:12 +0000104static SkIRect rand_rect(SkRandom& rand, int n) {
mike@reedtribe.org95b85bd2011-11-23 03:12:58 +0000105 int x = rand.nextS() % n;
106 int y = rand.nextS() % n;
107 int w = rand.nextU() % n;
108 int h = rand.nextU() % n;
109 return SkIRect::MakeXYWH(x, y, w, h);
110}
111
commit-bot@chromium.orge0e7cfe2013-09-09 20:09:12 +0000112static void make_rand_rgn(SkRegion* rgn, SkRandom& rand) {
mike@reedtribe.org95b85bd2011-11-23 03:12:58 +0000113 int count = rand.nextU() % 20;
114 for (int i = 0; i < count; ++i) {
115 rgn->op(rand_rect(rand, 100), SkRegion::kXOR_Op);
116 }
117}
118
reed@google.coma052aca2011-11-23 19:43:46 +0000119static bool operator==(const SkRegion& rgn, const SkAAClip& aaclip) {
120 SkMask mask0, mask1;
121
122 copyToMask(rgn, &mask0);
123 aaclip.copyToMask(&mask1);
124 bool eq = (mask0 == mask1);
125
126 SkMask::FreeImage(mask0.fImage);
127 SkMask::FreeImage(mask1.fImage);
128 return eq;
129}
130
131static bool equalsAAClip(const SkRegion& rgn) {
132 SkAAClip aaclip;
133 aaclip.setRegion(rgn);
134 return rgn == aaclip;
135}
136
137static void setRgnToPath(SkRegion* rgn, const SkPath& path) {
138 SkIRect ir;
139 path.getBounds().round(&ir);
140 rgn->setPath(path, SkRegion(ir));
141}
142
mike@reedtribe.org95b85bd2011-11-23 03:12:58 +0000143// aaclip.setRegion should create idential masks to the region
144static void test_rgn(skiatest::Reporter* reporter) {
commit-bot@chromium.orge0e7cfe2013-09-09 20:09:12 +0000145 SkRandom rand;
mike@reedtribe.org95b85bd2011-11-23 03:12:58 +0000146 for (int i = 0; i < 1000; i++) {
147 SkRegion rgn;
148 make_rand_rgn(&rgn, rand);
reed@google.coma052aca2011-11-23 19:43:46 +0000149 REPORTER_ASSERT(reporter, equalsAAClip(rgn));
150 }
151
152 {
153 SkRegion rgn;
154 SkPath path;
155 path.addCircle(0, 0, SkIntToScalar(30));
156 setRgnToPath(&rgn, path);
157 REPORTER_ASSERT(reporter, equalsAAClip(rgn));
158
159 path.reset();
160 path.moveTo(0, 0);
161 path.lineTo(SkIntToScalar(100), 0);
162 path.lineTo(SkIntToScalar(100 - 20), SkIntToScalar(20));
163 path.lineTo(SkIntToScalar(20), SkIntToScalar(20));
164 setRgnToPath(&rgn, path);
165 REPORTER_ASSERT(reporter, equalsAAClip(rgn));
mike@reedtribe.org95b85bd2011-11-23 03:12:58 +0000166 }
167}
168
reed@google.comd8676d22011-10-26 18:01:25 +0000169static const SkRegion::Op gRgnOps[] = {
reed@google.comc9041912011-10-27 16:58:46 +0000170 SkRegion::kDifference_Op,
reed@google.comd8676d22011-10-26 18:01:25 +0000171 SkRegion::kIntersect_Op,
172 SkRegion::kUnion_Op,
173 SkRegion::kXOR_Op,
reed@google.comc9041912011-10-27 16:58:46 +0000174 SkRegion::kReverseDifference_Op,
reed@google.comd8676d22011-10-26 18:01:25 +0000175 SkRegion::kReplace_Op
176};
177
178static const char* gRgnOpNames[] = {
reed@google.comc9041912011-10-27 16:58:46 +0000179 "DIFF", "INTERSECT", "UNION", "XOR", "REVERSE_DIFF", "REPLACE"
reed@google.comd8676d22011-10-26 18:01:25 +0000180};
reed@google.com209c4152011-10-26 15:03:48 +0000181
reed@google.com12e15252011-10-26 15:19:36 +0000182static void imoveTo(SkPath& path, int x, int y) {
183 path.moveTo(SkIntToScalar(x), SkIntToScalar(y));
184}
185
186static void icubicTo(SkPath& path, int x0, int y0, int x1, int y1, int x2, int y2) {
187 path.cubicTo(SkIntToScalar(x0), SkIntToScalar(y0),
188 SkIntToScalar(x1), SkIntToScalar(y1),
189 SkIntToScalar(x2), SkIntToScalar(y2));
190}
191
reed@google.comd8676d22011-10-26 18:01:25 +0000192static void test_path_bounds(skiatest::Reporter* reporter) {
reed@google.com209c4152011-10-26 15:03:48 +0000193 SkPath path;
194 SkAAClip clip;
195 const int height = 40;
196 const SkScalar sheight = SkIntToScalar(height);
197
198 path.addOval(SkRect::MakeWH(sheight, sheight));
199 REPORTER_ASSERT(reporter, sheight == path.getBounds().height());
halcanary96fcdcc2015-08-27 07:41:13 -0700200 clip.setPath(path, nullptr, true);
reed@google.com209c4152011-10-26 15:03:48 +0000201 REPORTER_ASSERT(reporter, height == clip.getBounds().height());
202
203 // this is the trimmed height of this cubic (with aa). The critical thing
204 // for this test is that it is less than height, which represents just
205 // the bounds of the path's control-points.
206 //
207 // This used to fail until we tracked the MinY in the BuilderBlitter.
208 //
209 const int teardrop_height = 12;
210 path.reset();
reed@google.com12e15252011-10-26 15:19:36 +0000211 imoveTo(path, 0, 20);
212 icubicTo(path, 40, 40, 40, 0, 0, 20);
reed@google.com209c4152011-10-26 15:03:48 +0000213 REPORTER_ASSERT(reporter, sheight == path.getBounds().height());
halcanary96fcdcc2015-08-27 07:41:13 -0700214 clip.setPath(path, nullptr, true);
reed@google.com209c4152011-10-26 15:03:48 +0000215 REPORTER_ASSERT(reporter, teardrop_height == clip.getBounds().height());
216}
217
reed@google.comd8676d22011-10-26 18:01:25 +0000218static void test_empty(skiatest::Reporter* reporter) {
219 SkAAClip clip0, clip1;
220
221 REPORTER_ASSERT(reporter, clip0.isEmpty());
222 REPORTER_ASSERT(reporter, clip0.getBounds().isEmpty());
223 REPORTER_ASSERT(reporter, clip1 == clip0);
224
225 clip0.translate(10, 10); // should have no effect on empty
226 REPORTER_ASSERT(reporter, clip0.isEmpty());
227 REPORTER_ASSERT(reporter, clip0.getBounds().isEmpty());
228 REPORTER_ASSERT(reporter, clip1 == clip0);
229
230 SkIRect r = { 10, 10, 40, 50 };
231 clip0.setRect(r);
232 REPORTER_ASSERT(reporter, !clip0.isEmpty());
233 REPORTER_ASSERT(reporter, !clip0.getBounds().isEmpty());
234 REPORTER_ASSERT(reporter, clip0 != clip1);
235 REPORTER_ASSERT(reporter, clip0.getBounds() == r);
236
237 clip0.setEmpty();
238 REPORTER_ASSERT(reporter, clip0.isEmpty());
239 REPORTER_ASSERT(reporter, clip0.getBounds().isEmpty());
240 REPORTER_ASSERT(reporter, clip1 == clip0);
241
242 SkMask mask;
reed@google.comd8676d22011-10-26 18:01:25 +0000243 clip0.copyToMask(&mask);
halcanary96fcdcc2015-08-27 07:41:13 -0700244 REPORTER_ASSERT(reporter, nullptr == mask.fImage);
reed@google.comd8676d22011-10-26 18:01:25 +0000245 REPORTER_ASSERT(reporter, mask.fBounds.isEmpty());
246}
247
commit-bot@chromium.orge0e7cfe2013-09-09 20:09:12 +0000248static void rand_irect(SkIRect* r, int N, SkRandom& rand) {
reed@google.comd8676d22011-10-26 18:01:25 +0000249 r->setXYWH(0, 0, rand.nextU() % N, rand.nextU() % N);
250 int dx = rand.nextU() % (2*N);
251 int dy = rand.nextU() % (2*N);
252 // use int dx,dy to make the subtract be signed
253 r->offset(N - dx, N - dy);
254}
255
256static void test_irect(skiatest::Reporter* reporter) {
commit-bot@chromium.orge0e7cfe2013-09-09 20:09:12 +0000257 SkRandom rand;
reed@google.comd8676d22011-10-26 18:01:25 +0000258
reed@google.comc9041912011-10-27 16:58:46 +0000259 for (int i = 0; i < 10000; i++) {
reed@google.comd8676d22011-10-26 18:01:25 +0000260 SkAAClip clip0, clip1;
261 SkRegion rgn0, rgn1;
262 SkIRect r0, r1;
263
264 rand_irect(&r0, 10, rand);
265 rand_irect(&r1, 10, rand);
266 clip0.setRect(r0);
267 clip1.setRect(r1);
268 rgn0.setRect(r0);
269 rgn1.setRect(r1);
270 for (size_t j = 0; j < SK_ARRAY_COUNT(gRgnOps); ++j) {
271 SkRegion::Op op = gRgnOps[j];
272 SkAAClip clip2;
273 SkRegion rgn2;
274 bool nonEmptyAA = clip2.op(clip0, clip1, op);
275 bool nonEmptyBW = rgn2.op(rgn0, rgn1, op);
276 if (nonEmptyAA != nonEmptyBW || clip2.getBounds() != rgn2.getBounds()) {
halcanary7d571242016-02-24 17:59:16 -0800277 ERRORF(reporter, "%s %s "
278 "[%d %d %d %d] %s [%d %d %d %d] = BW:[%d %d %d %d] AA:[%d %d %d %d]\n",
279 nonEmptyAA == nonEmptyBW ? "true" : "false",
280 clip2.getBounds() == rgn2.getBounds() ? "true" : "false",
281 r0.fLeft, r0.fTop, r0.right(), r0.bottom(),
282 gRgnOpNames[j],
283 r1.fLeft, r1.fTop, r1.right(), r1.bottom(),
284 rgn2.getBounds().fLeft, rgn2.getBounds().fTop,
285 rgn2.getBounds().right(), rgn2.getBounds().bottom(),
286 clip2.getBounds().fLeft, clip2.getBounds().fTop,
287 clip2.getBounds().right(), clip2.getBounds().bottom());
reed@google.comd8676d22011-10-26 18:01:25 +0000288 }
rmistry@google.comd6176b02012-08-23 18:14:13 +0000289
reed@google.com80cdb9a2012-02-16 18:56:17 +0000290 SkMask maskBW, maskAA;
291 copyToMask(rgn2, &maskBW);
292 clip2.copyToMask(&maskAA);
tomhudson@google.com0435f162012-03-15 18:16:39 +0000293 SkAutoMaskFreeImage freeBW(maskBW.fImage);
294 SkAutoMaskFreeImage freeAA(maskAA.fImage);
reed@google.com80cdb9a2012-02-16 18:56:17 +0000295 REPORTER_ASSERT(reporter, maskBW == maskAA);
reed@google.comd8676d22011-10-26 18:01:25 +0000296 }
297 }
298}
299
reed@google.com80cdb9a2012-02-16 18:56:17 +0000300static void test_path_with_hole(skiatest::Reporter* reporter) {
301 static const uint8_t gExpectedImage[] = {
302 0xFF, 0xFF, 0xFF, 0xFF,
303 0xFF, 0xFF, 0xFF, 0xFF,
304 0x00, 0x00, 0x00, 0x00,
305 0x00, 0x00, 0x00, 0x00,
306 0xFF, 0xFF, 0xFF, 0xFF,
307 0xFF, 0xFF, 0xFF, 0xFF,
308 };
309 SkMask expected;
Mike Reed92b33352019-08-24 19:39:13 -0400310 expected.fBounds.setWH(4, 6);
reed@google.com80cdb9a2012-02-16 18:56:17 +0000311 expected.fRowBytes = 4;
312 expected.fFormat = SkMask::kA8_Format;
313 expected.fImage = (uint8_t*)gExpectedImage;
314
315 SkPath path;
316 path.addRect(SkRect::MakeXYWH(0, 0,
317 SkIntToScalar(4), SkIntToScalar(2)));
318 path.addRect(SkRect::MakeXYWH(0, SkIntToScalar(4),
319 SkIntToScalar(4), SkIntToScalar(2)));
320
321 for (int i = 0; i < 2; ++i) {
322 SkAAClip clip;
halcanary96fcdcc2015-08-27 07:41:13 -0700323 clip.setPath(path, nullptr, 1 == i);
rmistry@google.comd6176b02012-08-23 18:14:13 +0000324
reed@google.com80cdb9a2012-02-16 18:56:17 +0000325 SkMask mask;
326 clip.copyToMask(&mask);
tomhudson@google.com0435f162012-03-15 18:16:39 +0000327 SkAutoMaskFreeImage freeM(mask.fImage);
328
reed@google.com80cdb9a2012-02-16 18:56:17 +0000329 REPORTER_ASSERT(reporter, expected == mask);
330 }
331}
332
reed202ab2a2014-08-07 11:48:10 -0700333static void test_really_a_rect(skiatest::Reporter* reporter) {
334 SkRRect rrect;
335 rrect.setRectXY(SkRect::MakeWH(100, 100), 5, 5);
336
337 SkPath path;
338 path.addRRect(rrect);
339
340 SkAAClip clip;
341 clip.setPath(path);
342
343 REPORTER_ASSERT(reporter, clip.getBounds() == SkIRect::MakeWH(100, 100));
344 REPORTER_ASSERT(reporter, !clip.isRect());
345
346 // This rect should intersect the clip, but slice-out all of the "soft" parts,
347 // leaving just a rect.
348 const SkIRect ir = SkIRect::MakeLTRB(10, -10, 50, 90);
halcanary9d524f22016-03-29 09:03:52 -0700349
reed202ab2a2014-08-07 11:48:10 -0700350 clip.op(ir, SkRegion::kIntersect_Op);
351
352 REPORTER_ASSERT(reporter, clip.getBounds() == SkIRect::MakeLTRB(10, 0, 50, 90));
353 // the clip recognized that that it is just a rect!
354 REPORTER_ASSERT(reporter, clip.isRect());
355}
356
reed@google.com18c464b2012-05-11 20:57:25 +0000357static void did_dx_affect(skiatest::Reporter* reporter, const SkScalar dx[],
358 size_t count, bool changed) {
senorblancoafc7cce2016-02-02 18:44:15 -0800359 const SkIRect baseBounds = SkIRect::MakeXYWH(0, 0, 10, 10);
reed@google.com18c464b2012-05-11 20:57:25 +0000360 SkIRect ir = { 0, 0, 10, 10 };
361
362 for (size_t i = 0; i < count; ++i) {
363 SkRect r;
364 r.set(ir);
rmistry@google.comd6176b02012-08-23 18:14:13 +0000365
reed@google.com18c464b2012-05-11 20:57:25 +0000366 SkRasterClip rc0(ir);
367 SkRasterClip rc1(ir);
368 SkRasterClip rc2(ir);
369
Brian Salomona3b45d42016-10-03 11:36:16 -0400370 rc0.op(r, SkMatrix::I(), baseBounds, SkRegion::kIntersect_Op, false);
reed@google.com18c464b2012-05-11 20:57:25 +0000371 r.offset(dx[i], 0);
Brian Salomona3b45d42016-10-03 11:36:16 -0400372 rc1.op(r, SkMatrix::I(), baseBounds, SkRegion::kIntersect_Op, true);
reed@google.com18c464b2012-05-11 20:57:25 +0000373 r.offset(-2*dx[i], 0);
Brian Salomona3b45d42016-10-03 11:36:16 -0400374 rc2.op(r, SkMatrix::I(), baseBounds, SkRegion::kIntersect_Op, true);
rmistry@google.comd6176b02012-08-23 18:14:13 +0000375
reed@google.com18c464b2012-05-11 20:57:25 +0000376 REPORTER_ASSERT(reporter, changed != (rc0 == rc1));
377 REPORTER_ASSERT(reporter, changed != (rc0 == rc2));
378 }
379}
380
381static void test_nearly_integral(skiatest::Reporter* reporter) {
382 // All of these should generate equivalent rasterclips
rmistry@google.comd6176b02012-08-23 18:14:13 +0000383
reed@google.com18c464b2012-05-11 20:57:25 +0000384 static const SkScalar gSafeX[] = {
385 0, SK_Scalar1/1000, SK_Scalar1/100, SK_Scalar1/10,
386 };
387 did_dx_affect(reporter, gSafeX, SK_ARRAY_COUNT(gSafeX), false);
388
389 static const SkScalar gUnsafeX[] = {
390 SK_Scalar1/4, SK_Scalar1/3,
391 };
392 did_dx_affect(reporter, gUnsafeX, SK_ARRAY_COUNT(gUnsafeX), true);
393}
394
sugoi@google.com54f0d1b2013-02-27 19:17:41 +0000395static void test_regressions() {
reed@google.com9b0da232012-02-29 13:59:15 +0000396 // these should not assert in the debug build
397 // bug was introduced in rev. 3209
398 {
399 SkAAClip clip;
400 SkRect r;
commit-bot@chromium.org4b413c82013-11-25 19:44:07 +0000401 r.fLeft = 129.892181f;
402 r.fTop = 10.3999996f;
403 r.fRight = 130.892181f;
404 r.fBottom = 20.3999996f;
reed@google.com9b0da232012-02-29 13:59:15 +0000405 clip.setRect(r, true);
406 }
407}
408
reed157f36d2014-10-15 07:05:09 -0700409// Building aaclip meant aa-scan-convert a path into a huge clip.
410// the old algorithm sized the supersampler to the size of the clip, which overflowed
411// its internal 16bit coordinates. The fix was to intersect the clip+path_bounds before
412// sizing the supersampler.
413//
414// Before the fix, the following code would assert in debug builds.
415//
416static void test_crbug_422693(skiatest::Reporter* reporter) {
reed157f36d2014-10-15 07:05:09 -0700417 SkRasterClip rc(SkIRect::MakeLTRB(-25000, -25000, 25000, 25000));
418 SkPath path;
419 path.addCircle(50, 50, 50);
Brian Salomona3b45d42016-10-03 11:36:16 -0400420 rc.op(path, SkMatrix::I(), rc.getBounds(), SkRegion::kIntersect_Op, true);
reed157f36d2014-10-15 07:05:09 -0700421}
422
Mike Reed9fc53622018-01-03 15:35:33 -0500423static void test_huge(skiatest::Reporter* reporter) {
424 SkAAClip clip;
425 int big = 0x70000000;
426 SkIRect r = { -big, -big, big, big };
427 SkASSERT(r.width() < 0 && r.height() < 0);
428
429 clip.setRect(r);
430}
431
mtklein@google.com014f2c42013-09-19 20:56:46 +0000432DEF_TEST(AAClip, reporter) {
reed@google.comd8676d22011-10-26 18:01:25 +0000433 test_empty(reporter);
434 test_path_bounds(reporter);
435 test_irect(reporter);
mike@reedtribe.org95b85bd2011-11-23 03:12:58 +0000436 test_rgn(reporter);
reed@google.com80cdb9a2012-02-16 18:56:17 +0000437 test_path_with_hole(reporter);
sugoi@google.com54f0d1b2013-02-27 19:17:41 +0000438 test_regressions();
reed@google.com18c464b2012-05-11 20:57:25 +0000439 test_nearly_integral(reporter);
reed202ab2a2014-08-07 11:48:10 -0700440 test_really_a_rect(reporter);
reed157f36d2014-10-15 07:05:09 -0700441 test_crbug_422693(reporter);
Mike Reed9fc53622018-01-03 15:35:33 -0500442 test_huge(reporter);
reed@google.com209c4152011-10-26 15:03:48 +0000443}