blob: f8d73c39161a5e735386285f1cc916e046b846fc [file] [log] [blame]
caryclark@google.com07393ca2013-04-08 11:47:37 +00001/*
2 * Copyright 2012 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 */
Mike Kleinc0bd9f92019-04-23 12:05:21 -05007#include "src/core/SkGeometry.h"
8#include "src/pathops/SkOpEdgeBuilder.h"
9#include "src/pathops/SkReduceOrder.h"
caryclark@google.com07393ca2013-04-08 11:47:37 +000010
11void SkOpEdgeBuilder::init() {
caryclark@google.com07393ca2013-04-08 11:47:37 +000012 fOperand = false;
Mike Reed7d34dc72019-11-26 12:17:17 -050013 fXorMask[0] = fXorMask[1] = ((int)fPath->getFillType() & 1) ? kEvenOdd_PathOpsMask
caryclark@google.com07393ca2013-04-08 11:47:37 +000014 : kWinding_PathOpsMask;
caryclark@google.com66560ca2013-04-26 19:51:16 +000015 fUnparseable = false;
caryclark@google.com07393ca2013-04-08 11:47:37 +000016 fSecondHalf = preFetch();
17}
18
caryclark27c015d2016-09-23 05:47:20 -070019// very tiny points cause numerical instability : don't allow them
Chris Dalton1fc1bd32020-05-06 11:44:51 -060020static SkPoint force_small_to_zero(const SkPoint& pt) {
21 SkPoint ret = pt;
22 if (SkScalarAbs(ret.fX) < FLT_EPSILON_ORDERABLE_ERR) {
23 ret.fX = 0;
caryclark27c015d2016-09-23 05:47:20 -070024 }
Chris Dalton1fc1bd32020-05-06 11:44:51 -060025 if (SkScalarAbs(ret.fY) < FLT_EPSILON_ORDERABLE_ERR) {
26 ret.fY = 0;
caryclark27c015d2016-09-23 05:47:20 -070027 }
Chris Dalton1fc1bd32020-05-06 11:44:51 -060028 return ret;
caryclark27c015d2016-09-23 05:47:20 -070029}
30
31static bool can_add_curve(SkPath::Verb verb, SkPoint* curve) {
32 if (SkPath::kMove_Verb == verb) {
33 return false;
34 }
caryclarkcc093722016-09-23 09:32:26 -070035 for (int index = 0; index <= SkPathOpsVerbToPoints(verb); ++index) {
Chris Dalton1fc1bd32020-05-06 11:44:51 -060036 curve[index] = force_small_to_zero(curve[index]);
caryclark27c015d2016-09-23 05:47:20 -070037 }
38 return SkPath::kLine_Verb != verb || !SkDPoint::ApproximatelyEqual(curve[0], curve[1]);
39}
40
caryclark@google.com07393ca2013-04-08 11:47:37 +000041void SkOpEdgeBuilder::addOperand(const SkPath& path) {
42 SkASSERT(fPathVerbs.count() > 0 && fPathVerbs.end()[-1] == SkPath::kDone_Verb);
caryclark54359292015-03-26 07:52:43 -070043 fPathVerbs.pop();
caryclark@google.com07393ca2013-04-08 11:47:37 +000044 fPath = &path;
Mike Reed7d34dc72019-11-26 12:17:17 -050045 fXorMask[1] = ((int)fPath->getFillType() & 1) ? kEvenOdd_PathOpsMask
caryclark@google.com07393ca2013-04-08 11:47:37 +000046 : kWinding_PathOpsMask;
47 preFetch();
48}
49
caryclark55888e42016-07-18 10:01:36 -070050bool SkOpEdgeBuilder::finish() {
caryclark54359292015-03-26 07:52:43 -070051 fOperand = false;
caryclark55888e42016-07-18 10:01:36 -070052 if (fUnparseable || !walk()) {
caryclark@google.com66560ca2013-04-26 19:51:16 +000053 return false;
54 }
caryclark@google.com07393ca2013-04-08 11:47:37 +000055 complete();
Cary Clarkff114282016-12-14 11:56:16 -050056 SkOpContour* contour = fContourBuilder.contour();
57 if (contour && !contour->count()) {
58 fContoursHead->remove(contour);
caryclark@google.com07393ca2013-04-08 11:47:37 +000059 }
caryclark@google.com66560ca2013-04-26 19:51:16 +000060 return true;
caryclark@google.com07393ca2013-04-08 11:47:37 +000061}
62
caryclark@google.com07e97fc2013-07-08 17:17:02 +000063void SkOpEdgeBuilder::closeContour(const SkPoint& curveEnd, const SkPoint& curveStart) {
caryclark@google.coma2bbc6e2013-11-01 17:36:03 +000064 if (!SkDPoint::ApproximatelyEqual(curveEnd, curveStart)) {
caryclark54359292015-03-26 07:52:43 -070065 *fPathVerbs.append() = SkPath::kLine_Verb;
66 *fPathPts.append() = curveStart;
caryclark@google.com07e97fc2013-07-08 17:17:02 +000067 } else {
Cary Clarkb9ae5372016-10-05 10:40:07 -040068 int verbCount = fPathVerbs.count();
69 int ptsCount = fPathPts.count();
70 if (SkPath::kLine_Verb == fPathVerbs[verbCount - 1]
71 && fPathPts[ptsCount - 2] == curveStart) {
72 fPathVerbs.pop();
73 fPathPts.pop();
74 } else {
75 fPathPts[ptsCount - 1] = curveStart;
76 }
caryclark@google.com07e97fc2013-07-08 17:17:02 +000077 }
caryclark54359292015-03-26 07:52:43 -070078 *fPathVerbs.append() = SkPath::kClose_Verb;
caryclark@google.com07e97fc2013-07-08 17:17:02 +000079}
80
caryclark@google.com07393ca2013-04-08 11:47:37 +000081int SkOpEdgeBuilder::preFetch() {
caryclark@google.com66560ca2013-04-26 19:51:16 +000082 if (!fPath->isFinite()) {
83 fUnparseable = true;
84 return 0;
85 }
caryclark@google.com6dc7df62013-04-25 11:51:54 +000086 SkPath::RawIter iter(*fPath);
caryclark@google.com07e97fc2013-07-08 17:17:02 +000087 SkPoint curveStart;
88 SkPoint curve[4];
caryclark@google.com07393ca2013-04-08 11:47:37 +000089 SkPoint pts[4];
90 SkPath::Verb verb;
caryclark@google.com07e97fc2013-07-08 17:17:02 +000091 bool lastCurve = false;
caryclark@google.com07393ca2013-04-08 11:47:37 +000092 do {
93 verb = iter.next(pts);
caryclark@google.com07e97fc2013-07-08 17:17:02 +000094 switch (verb) {
95 case SkPath::kMove_Verb:
96 if (!fAllowOpenContours && lastCurve) {
97 closeContour(curve[0], curveStart);
98 }
caryclark54359292015-03-26 07:52:43 -070099 *fPathVerbs.append() = verb;
Chris Dalton1fc1bd32020-05-06 11:44:51 -0600100 curve[0] = force_small_to_zero(pts[0]);
101 *fPathPts.append() = curve[0];
102 curveStart = curve[0];
caryclark@google.com07e97fc2013-07-08 17:17:02 +0000103 lastCurve = false;
104 continue;
105 case SkPath::kLine_Verb:
Chris Dalton1fc1bd32020-05-06 11:44:51 -0600106 curve[1] = force_small_to_zero(pts[1]);
107 if (SkDPoint::ApproximatelyEqual(curve[0], curve[1])) {
caryclark54359292015-03-26 07:52:43 -0700108 uint8_t lastVerb = fPathVerbs.top();
caryclark@google.coma2bbc6e2013-11-01 17:36:03 +0000109 if (lastVerb != SkPath::kLine_Verb && lastVerb != SkPath::kMove_Verb) {
Chris Dalton1fc1bd32020-05-06 11:44:51 -0600110 fPathPts.top() = curve[0] = curve[1];
caryclark@google.com570863f2013-09-16 15:55:01 +0000111 }
caryclark@google.com07e97fc2013-07-08 17:17:02 +0000112 continue; // skip degenerate points
113 }
114 break;
115 case SkPath::kQuad_Verb:
Chris Dalton1fc1bd32020-05-06 11:44:51 -0600116 curve[1] = force_small_to_zero(pts[1]);
117 curve[2] = force_small_to_zero(pts[2]);
118 verb = SkReduceOrder::Quad(curve, curve);
caryclark@google.com07e97fc2013-07-08 17:17:02 +0000119 if (verb == SkPath::kMove_Verb) {
120 continue; // skip degenerate points
121 }
122 break;
caryclark1049f122015-04-20 08:31:59 -0700123 case SkPath::kConic_Verb:
Chris Dalton1fc1bd32020-05-06 11:44:51 -0600124 curve[1] = force_small_to_zero(pts[1]);
125 curve[2] = force_small_to_zero(pts[2]);
126 verb = SkReduceOrder::Quad(curve, curve);
caryclark27c015d2016-09-23 05:47:20 -0700127 if (SkPath::kQuad_Verb == verb && 1 != iter.conicWeight()) {
128 verb = SkPath::kConic_Verb;
129 } else if (verb == SkPath::kMove_Verb) {
caryclark1049f122015-04-20 08:31:59 -0700130 continue; // skip degenerate points
caryclark@google.com07e97fc2013-07-08 17:17:02 +0000131 }
caryclark1049f122015-04-20 08:31:59 -0700132 break;
caryclark@google.com07e97fc2013-07-08 17:17:02 +0000133 case SkPath::kCubic_Verb:
Chris Dalton1fc1bd32020-05-06 11:44:51 -0600134 curve[1] = force_small_to_zero(pts[1]);
135 curve[2] = force_small_to_zero(pts[2]);
136 curve[3] = force_small_to_zero(pts[3]);
137 verb = SkReduceOrder::Cubic(curve, curve);
caryclark@google.com07e97fc2013-07-08 17:17:02 +0000138 if (verb == SkPath::kMove_Verb) {
139 continue; // skip degenerate points
140 }
141 break;
142 case SkPath::kClose_Verb:
143 closeContour(curve[0], curveStart);
144 lastCurve = false;
145 continue;
146 case SkPath::kDone_Verb:
147 continue;
caryclark@google.com07393ca2013-04-08 11:47:37 +0000148 }
caryclark54359292015-03-26 07:52:43 -0700149 *fPathVerbs.append() = verb;
caryclark@google.com07e97fc2013-07-08 17:17:02 +0000150 int ptCount = SkPathOpsVerbToPoints(verb);
Chris Dalton1fc1bd32020-05-06 11:44:51 -0600151 fPathPts.append(ptCount, &curve[1]);
caryclark1049f122015-04-20 08:31:59 -0700152 if (verb == SkPath::kConic_Verb) {
153 *fWeights.append() = iter.conicWeight();
154 }
Chris Dalton1fc1bd32020-05-06 11:44:51 -0600155 curve[0] = curve[ptCount];
caryclark@google.com07e97fc2013-07-08 17:17:02 +0000156 lastCurve = true;
caryclark@google.com07393ca2013-04-08 11:47:37 +0000157 } while (verb != SkPath::kDone_Verb);
caryclark@google.com07e97fc2013-07-08 17:17:02 +0000158 if (!fAllowOpenContours && lastCurve) {
159 closeContour(curve[0], curveStart);
160 }
caryclark54359292015-03-26 07:52:43 -0700161 *fPathVerbs.append() = SkPath::kDone_Verb;
caryclark@google.com07393ca2013-04-08 11:47:37 +0000162 return fPathVerbs.count() - 1;
163}
164
caryclark@google.com66560ca2013-04-26 19:51:16 +0000165bool SkOpEdgeBuilder::close() {
caryclark@google.com66560ca2013-04-26 19:51:16 +0000166 complete();
167 return true;
168}
169
caryclark55888e42016-07-18 10:01:36 -0700170bool SkOpEdgeBuilder::walk() {
caryclark@google.com07393ca2013-04-08 11:47:37 +0000171 uint8_t* verbPtr = fPathVerbs.begin();
172 uint8_t* endOfFirstHalf = &verbPtr[fSecondHalf];
Cary Clark79aa2f12018-05-31 16:22:02 -0400173 SkPoint* pointsPtr = fPathPts.begin();
caryclark1049f122015-04-20 08:31:59 -0700174 SkScalar* weightPtr = fWeights.begin();
caryclark@google.com07393ca2013-04-08 11:47:37 +0000175 SkPath::Verb verb;
Cary Clarkff114282016-12-14 11:56:16 -0500176 SkOpContour* contour = fContourBuilder.contour();
Cary Clark79aa2f12018-05-31 16:22:02 -0400177 int moveToPtrBump = 0;
caryclark@google.com6dc7df62013-04-25 11:51:54 +0000178 while ((verb = (SkPath::Verb) *verbPtr) != SkPath::kDone_Verb) {
179 if (verbPtr == endOfFirstHalf) {
180 fOperand = true;
181 }
182 verbPtr++;
caryclark@google.com07393ca2013-04-08 11:47:37 +0000183 switch (verb) {
184 case SkPath::kMove_Verb:
Cary Clarkff114282016-12-14 11:56:16 -0500185 if (contour && contour->count()) {
caryclark@google.com66560ca2013-04-26 19:51:16 +0000186 if (fAllowOpenContours) {
187 complete();
188 } else if (!close()) {
189 return false;
190 }
191 }
Cary Clarkff114282016-12-14 11:56:16 -0500192 if (!contour) {
193 fContourBuilder.setContour(contour = fContoursHead->appendContour());
caryclark@google.com07393ca2013-04-08 11:47:37 +0000194 }
Cary Clarkff114282016-12-14 11:56:16 -0500195 contour->init(fGlobalState, fOperand,
caryclark54359292015-03-26 07:52:43 -0700196 fXorMask[fOperand] == kEvenOdd_PathOpsMask);
Cary Clark79aa2f12018-05-31 16:22:02 -0400197 pointsPtr += moveToPtrBump;
198 moveToPtrBump = 1;
caryclark@google.com6dc7df62013-04-25 11:51:54 +0000199 continue;
caryclark@google.com07e97fc2013-07-08 17:17:02 +0000200 case SkPath::kLine_Verb:
Cary Clarkff114282016-12-14 11:56:16 -0500201 fContourBuilder.addLine(pointsPtr);
caryclark@google.com07e97fc2013-07-08 17:17:02 +0000202 break;
203 case SkPath::kQuad_Verb:
caryclark27c015d2016-09-23 05:47:20 -0700204 {
205 SkVector v1 = pointsPtr[1] - pointsPtr[0];
206 SkVector v2 = pointsPtr[2] - pointsPtr[1];
207 if (v1.dot(v2) < 0) {
208 SkPoint pair[5];
209 if (SkChopQuadAtMaxCurvature(pointsPtr, pair) == 1) {
210 goto addOneQuad;
211 }
212 if (!SkScalarsAreFinite(&pair[0].fX, SK_ARRAY_COUNT(pair) * 2)) {
213 return false;
214 }
Cary Clarkafca4d62017-12-01 15:23:00 -0500215 for (unsigned index = 0; index < SK_ARRAY_COUNT(pair); ++index) {
Chris Dalton1fc1bd32020-05-06 11:44:51 -0600216 pair[index] = force_small_to_zero(pair[index]);
Cary Clarkafca4d62017-12-01 15:23:00 -0500217 }
caryclark27c015d2016-09-23 05:47:20 -0700218 SkPoint cStorage[2][2];
219 SkPath::Verb v1 = SkReduceOrder::Quad(&pair[0], cStorage[0]);
220 SkPath::Verb v2 = SkReduceOrder::Quad(&pair[2], cStorage[1]);
caryclarkcc093722016-09-23 09:32:26 -0700221 SkPoint* curve1 = v1 != SkPath::kLine_Verb ? &pair[0] : cStorage[0];
222 SkPoint* curve2 = v2 != SkPath::kLine_Verb ? &pair[2] : cStorage[1];
caryclark27c015d2016-09-23 05:47:20 -0700223 if (can_add_curve(v1, curve1) && can_add_curve(v2, curve2)) {
Cary Clarkff114282016-12-14 11:56:16 -0500224 fContourBuilder.addCurve(v1, curve1);
225 fContourBuilder.addCurve(v2, curve2);
caryclark27c015d2016-09-23 05:47:20 -0700226 break;
227 }
228 }
229 }
230 addOneQuad:
Cary Clarkff114282016-12-14 11:56:16 -0500231 fContourBuilder.addQuad(pointsPtr);
caryclark@google.com07e97fc2013-07-08 17:17:02 +0000232 break;
caryclark27c015d2016-09-23 05:47:20 -0700233 case SkPath::kConic_Verb: {
234 SkVector v1 = pointsPtr[1] - pointsPtr[0];
235 SkVector v2 = pointsPtr[2] - pointsPtr[1];
236 SkScalar weight = *weightPtr++;
237 if (v1.dot(v2) < 0) {
238 // FIXME: max curvature for conics hasn't been implemented; use placeholder
239 SkScalar maxCurvature = SkFindQuadMaxCurvature(pointsPtr);
Chris Dalton1d474dd2018-07-24 01:08:31 -0600240 if (0 < maxCurvature && maxCurvature < 1) {
caryclark27c015d2016-09-23 05:47:20 -0700241 SkConic conic(pointsPtr, weight);
242 SkConic pair[2];
caryclark414c4292016-09-26 11:03:54 -0700243 if (!conic.chopAt(maxCurvature, pair)) {
244 // if result can't be computed, use original
Cary Clarkff114282016-12-14 11:56:16 -0500245 fContourBuilder.addConic(pointsPtr, weight);
caryclark414c4292016-09-26 11:03:54 -0700246 break;
247 }
caryclark27c015d2016-09-23 05:47:20 -0700248 SkPoint cStorage[2][3];
249 SkPath::Verb v1 = SkReduceOrder::Conic(pair[0], cStorage[0]);
250 SkPath::Verb v2 = SkReduceOrder::Conic(pair[1], cStorage[1]);
caryclarkcc093722016-09-23 09:32:26 -0700251 SkPoint* curve1 = v1 != SkPath::kLine_Verb ? pair[0].fPts : cStorage[0];
252 SkPoint* curve2 = v2 != SkPath::kLine_Verb ? pair[1].fPts : cStorage[1];
caryclark27c015d2016-09-23 05:47:20 -0700253 if (can_add_curve(v1, curve1) && can_add_curve(v2, curve2)) {
Cary Clarkff114282016-12-14 11:56:16 -0500254 fContourBuilder.addCurve(v1, curve1, pair[0].fW);
255 fContourBuilder.addCurve(v2, curve2, pair[1].fW);
caryclark27c015d2016-09-23 05:47:20 -0700256 break;
257 }
caryclark1049f122015-04-20 08:31:59 -0700258 }
caryclark54359292015-03-26 07:52:43 -0700259 }
Cary Clarkff114282016-12-14 11:56:16 -0500260 fContourBuilder.addConic(pointsPtr, weight);
caryclark54359292015-03-26 07:52:43 -0700261 } break;
caryclark27c015d2016-09-23 05:47:20 -0700262 case SkPath::kCubic_Verb:
263 {
264 // Split complex cubics (such as self-intersecting curves or
265 // ones with difficult curvature) in two before proceeding.
266 // This can be required for intersection to succeed.
Cary Clark7eb01e02016-12-08 14:36:32 -0500267 SkScalar splitT[3];
268 int breaks = SkDCubic::ComplexBreak(pointsPtr, splitT);
269 if (!breaks) {
Cary Clarkff114282016-12-14 11:56:16 -0500270 fContourBuilder.addCubic(pointsPtr);
Cary Clark7eb01e02016-12-08 14:36:32 -0500271 break;
272 }
Cary Clarkff114282016-12-14 11:56:16 -0500273 SkASSERT(breaks <= (int) SK_ARRAY_COUNT(splitT));
274 struct Splitsville {
275 double fT[2];
276 SkPoint fPts[4];
277 SkPoint fReduced[4];
278 SkPath::Verb fVerb;
279 bool fCanAdd;
Cary Clark0eb6ed42016-12-16 16:31:11 -0500280 } splits[4];
281 SkASSERT(SK_ARRAY_COUNT(splits) == SK_ARRAY_COUNT(splitT) + 1);
282 SkTQSort(splitT, &splitT[breaks - 1]);
Cary Clark7eb01e02016-12-08 14:36:32 -0500283 for (int index = 0; index <= breaks; ++index) {
Cary Clarkff114282016-12-14 11:56:16 -0500284 Splitsville* split = &splits[index];
285 split->fT[0] = index ? splitT[index - 1] : 0;
286 split->fT[1] = index < breaks ? splitT[index] : 1;
287 SkDCubic part = SkDCubic::SubDivide(pointsPtr, split->fT[0], split->fT[1]);
288 if (!part.toFloatPoints(split->fPts)) {
caryclark27c015d2016-09-23 05:47:20 -0700289 return false;
290 }
Cary Clarkff114282016-12-14 11:56:16 -0500291 split->fVerb = SkReduceOrder::Cubic(split->fPts, split->fReduced);
Kevin Lubick67c905c2020-04-21 12:20:38 -0400292 SkPoint* curve = SkPath::kCubic_Verb == split->fVerb
Cary Clarkff114282016-12-14 11:56:16 -0500293 ? split->fPts : split->fReduced;
294 split->fCanAdd = can_add_curve(split->fVerb, curve);
295 }
296 for (int index = 0; index <= breaks; ++index) {
297 Splitsville* split = &splits[index];
298 if (!split->fCanAdd) {
299 continue;
300 }
301 int prior = index;
302 while (prior > 0 && !splits[prior - 1].fCanAdd) {
303 --prior;
304 }
305 if (prior < index) {
306 split->fT[0] = splits[prior].fT[0];
Cary Clark4efcb7d2018-02-02 15:09:49 -0500307 split->fPts[0] = splits[prior].fPts[0];
Cary Clarkff114282016-12-14 11:56:16 -0500308 }
309 int next = index;
Brian Osman788b9162020-02-07 10:36:46 -0500310 int breakLimit = std::min(breaks, (int) SK_ARRAY_COUNT(splits) - 1);
Eric Boren746e2632017-06-21 13:39:32 -0400311 while (next < breakLimit && !splits[next + 1].fCanAdd) {
Cary Clarkff114282016-12-14 11:56:16 -0500312 ++next;
313 }
314 if (next > index) {
315 split->fT[1] = splits[next].fT[1];
Cary Clark4efcb7d2018-02-02 15:09:49 -0500316 split->fPts[3] = splits[next].fPts[3];
Cary Clarkff114282016-12-14 11:56:16 -0500317 }
318 if (prior < index || next > index) {
Cary Clarkff114282016-12-14 11:56:16 -0500319 split->fVerb = SkReduceOrder::Cubic(split->fPts, split->fReduced);
320 }
321 SkPoint* curve = SkPath::kCubic_Verb == split->fVerb
322 ? split->fPts : split->fReduced;
Cary Clark74b42902018-03-09 07:38:47 -0500323 if (!can_add_curve(split->fVerb, curve)) {
324 return false;
325 }
Cary Clarkff114282016-12-14 11:56:16 -0500326 fContourBuilder.addCurve(split->fVerb, curve);
caryclark27c015d2016-09-23 05:47:20 -0700327 }
328 }
caryclark27c015d2016-09-23 05:47:20 -0700329 break;
caryclark@google.com07393ca2013-04-08 11:47:37 +0000330 case SkPath::kClose_Verb:
Cary Clarkff114282016-12-14 11:56:16 -0500331 SkASSERT(contour);
caryclark@google.com66560ca2013-04-26 19:51:16 +0000332 if (!close()) {
333 return false;
caryclark@google.com07393ca2013-04-08 11:47:37 +0000334 }
Cary Clarkff114282016-12-14 11:56:16 -0500335 contour = nullptr;
caryclark@google.com6dc7df62013-04-25 11:51:54 +0000336 continue;
caryclark@google.com07393ca2013-04-08 11:47:37 +0000337 default:
338 SkDEBUGFAIL("bad verb");
caryclark@google.com66560ca2013-04-26 19:51:16 +0000339 return false;
caryclark@google.com07393ca2013-04-08 11:47:37 +0000340 }
Cary Clarkff114282016-12-14 11:56:16 -0500341 SkASSERT(contour);
342 if (contour->count()) {
343 contour->debugValidate();
344 }
caryclark54359292015-03-26 07:52:43 -0700345 pointsPtr += SkPathOpsVerbToPoints(verb);
caryclark@google.com07393ca2013-04-08 11:47:37 +0000346 }
Cary Clarkff114282016-12-14 11:56:16 -0500347 fContourBuilder.flush();
348 if (contour && contour->count() &&!fAllowOpenContours && !close()) {
349 return false;
350 }
351 return true;
caryclark@google.com07393ca2013-04-08 11:47:37 +0000352}