blob: f975a1887ead036816bd2f190fd36cb8f76a6c5e [file] [log] [blame]
rileya@google.com589708b2012-07-26 20:04:23 +00001
2/*
3 * Copyright 2012 Google Inc.
4 *
5 * Use of this source code is governed by a BSD-style license that can be
6 * found in the LICENSE file.
7 */
8
9#include "SkSweepGradient.h"
10
reed@google.com437d6eb2013-05-23 19:03:05 +000011SkSweepGradient::SkSweepGradient(SkScalar cx, SkScalar cy,
12 const Descriptor& desc)
reed@google.com3d3a8602013-05-24 14:58:44 +000013 : SkGradientShaderBase(desc)
14 , fCenter(SkPoint::Make(cx, cy))
rileya@google.com589708b2012-07-26 20:04:23 +000015{
16 fPtsToUnit.setTranslate(-cx, -cy);
reed@google.com437d6eb2013-05-23 19:03:05 +000017
18 // overwrite the tilemode to a canonical value (since sweep ignores it)
19 fTileMode = SkShader::kClamp_TileMode;
rileya@google.com589708b2012-07-26 20:04:23 +000020}
21
22SkShader::BitmapType SkSweepGradient::asABitmap(SkBitmap* bitmap,
23 SkMatrix* matrix, SkShader::TileMode* xy) const {
24 if (bitmap) {
rileya@google.com1c6d64b2012-07-27 15:49:05 +000025 this->getGradientTableBitmap(bitmap);
rileya@google.com589708b2012-07-26 20:04:23 +000026 }
27 if (matrix) {
28 *matrix = fPtsToUnit;
29 }
30 if (xy) {
31 xy[0] = fTileMode;
32 xy[1] = kClamp_TileMode;
33 }
34 return kSweep_BitmapType;
35}
36
37SkShader::GradientType SkSweepGradient::asAGradient(GradientInfo* info) const {
38 if (info) {
39 commonAsAGradient(info);
40 info->fPoint[0] = fCenter;
41 }
42 return kSweep_GradientType;
43}
44
rileya@google.com589708b2012-07-26 20:04:23 +000045SkSweepGradient::SkSweepGradient(SkFlattenableReadBuffer& buffer)
46 : INHERITED(buffer),
47 fCenter(buffer.readPoint()) {
48}
49
50void SkSweepGradient::flatten(SkFlattenableWriteBuffer& buffer) const {
51 this->INHERITED::flatten(buffer);
52 buffer.writePoint(fCenter);
53}
54
55#ifndef SK_SCALAR_IS_FLOAT
56#ifdef COMPUTE_SWEEP_TABLE
57#define PI 3.14159265
58static bool gSweepTableReady;
59static uint8_t gSweepTable[65];
60
61/* Our table stores precomputed values for atan: [0...1] -> [0..PI/4]
62 We scale the results to [0..32]
63*/
64static const uint8_t* build_sweep_table() {
65 if (!gSweepTableReady) {
66 const int N = 65;
67 const double DENOM = N - 1;
68
69 for (int i = 0; i < N; i++)
70 {
71 double arg = i / DENOM;
72 double v = atan(arg);
73 int iv = (int)round(v * DENOM * 2 / PI);
74// printf("[%d] atan(%g) = %g %d\n", i, arg, v, iv);
75 printf("%d, ", iv);
76 gSweepTable[i] = iv;
77 }
78 gSweepTableReady = true;
79 }
80 return gSweepTable;
81}
82#else
83static const uint8_t gSweepTable[] = {
84 0, 1, 1, 2, 3, 3, 4, 4, 5, 6, 6, 7, 8, 8, 9, 9,
85 10, 11, 11, 12, 12, 13, 13, 14, 15, 15, 16, 16, 17, 17, 18, 18,
86 19, 19, 20, 20, 21, 21, 22, 22, 23, 23, 24, 24, 25, 25, 25, 26,
87 26, 27, 27, 27, 28, 28, 29, 29, 29, 30, 30, 30, 31, 31, 31, 32,
88 32
89};
90static const uint8_t* build_sweep_table() { return gSweepTable; }
91#endif
92#endif
93
94// divide numer/denom, with a bias of 6bits. Assumes numer <= denom
95// and denom != 0. Since our table is 6bits big (+1), this is a nice fit.
96// Same as (but faster than) SkFixedDiv(numer, denom) >> 10
97
98//unsigned div_64(int numer, int denom);
99#ifndef SK_SCALAR_IS_FLOAT
100static unsigned div_64(int numer, int denom) {
101 SkASSERT(numer <= denom);
102 SkASSERT(numer > 0);
103 SkASSERT(denom > 0);
104
105 int nbits = SkCLZ(numer);
106 int dbits = SkCLZ(denom);
107 int bits = 6 - nbits + dbits;
108 SkASSERT(bits <= 6);
109
110 if (bits < 0) { // detect underflow
111 return 0;
112 }
113
114 denom <<= dbits - 1;
115 numer <<= nbits - 1;
116
117 unsigned result = 0;
118
119 // do the first one
120 if ((numer -= denom) >= 0) {
121 result = 1;
122 } else {
123 numer += denom;
124 }
125
126 // Now fall into our switch statement if there are more bits to compute
127 if (bits > 0) {
128 // make room for the rest of the answer bits
129 result <<= bits;
130 switch (bits) {
131 case 6:
132 if ((numer = (numer << 1) - denom) >= 0)
133 result |= 32;
134 else
135 numer += denom;
136 case 5:
137 if ((numer = (numer << 1) - denom) >= 0)
138 result |= 16;
139 else
140 numer += denom;
141 case 4:
142 if ((numer = (numer << 1) - denom) >= 0)
143 result |= 8;
144 else
145 numer += denom;
146 case 3:
147 if ((numer = (numer << 1) - denom) >= 0)
148 result |= 4;
149 else
150 numer += denom;
151 case 2:
152 if ((numer = (numer << 1) - denom) >= 0)
153 result |= 2;
154 else
155 numer += denom;
156 case 1:
157 default: // not strictly need, but makes GCC make better ARM code
158 if ((numer = (numer << 1) - denom) >= 0)
159 result |= 1;
160 else
161 numer += denom;
162 }
163 }
164 return result;
165}
166#endif
167
168// Given x,y in the first quadrant, return 0..63 for the angle [0..90]
169#ifndef SK_SCALAR_IS_FLOAT
170static unsigned atan_0_90(SkFixed y, SkFixed x) {
171#ifdef SK_DEBUG
172 {
173 static bool gOnce;
174 if (!gOnce) {
175 gOnce = true;
176 SkASSERT(div_64(55, 55) == 64);
177 SkASSERT(div_64(128, 256) == 32);
178 SkASSERT(div_64(2326528, 4685824) == 31);
179 SkASSERT(div_64(753664, 5210112) == 9);
180 SkASSERT(div_64(229376, 4882432) == 3);
181 SkASSERT(div_64(2, 64) == 2);
182 SkASSERT(div_64(1, 64) == 1);
183 // test that we handle underflow correctly
184 SkASSERT(div_64(12345, 0x54321234) == 0);
185 }
186 }
187#endif
188
189 SkASSERT(y > 0 && x > 0);
190 const uint8_t* table = build_sweep_table();
191
192 unsigned result;
193 bool swap = (x < y);
194 if (swap) {
195 // first part of the atan(v) = PI/2 - atan(1/v) identity
196 // since our div_64 and table want v <= 1, where v = y/x
197 SkTSwap<SkFixed>(x, y);
198 }
199
200 result = div_64(y, x);
201
202#ifdef SK_DEBUG
203 {
204 unsigned result2 = SkDivBits(y, x, 6);
205 SkASSERT(result2 == result ||
206 (result == 1 && result2 == 0));
207 }
208#endif
209
210 SkASSERT(result < SK_ARRAY_COUNT(gSweepTable));
211 result = table[result];
212
213 if (swap) {
214 // complete the atan(v) = PI/2 - atan(1/v) identity
215 result = 64 - result;
216 // pin to 63
217 result -= result >> 6;
218 }
219
220 SkASSERT(result <= 63);
221 return result;
222}
223#endif
224
225// returns angle in a circle [0..2PI) -> [0..255]
226#ifdef SK_SCALAR_IS_FLOAT
227static unsigned SkATan2_255(float y, float x) {
228 // static const float g255Over2PI = 255 / (2 * SK_ScalarPI);
229 static const float g255Over2PI = 40.584510488433314f;
rmistry@google.comfbfcd562012-08-23 18:09:54 +0000230
rileya@google.com589708b2012-07-26 20:04:23 +0000231 float result = sk_float_atan2(y, x);
232 if (result < 0) {
233 result += 2 * SK_ScalarPI;
234 }
235 SkASSERT(result >= 0);
236 // since our value is always >= 0, we can cast to int, which is faster than
237 // calling floorf()
238 int ir = (int)(result * g255Over2PI);
239 SkASSERT(ir >= 0 && ir <= 255);
240 return ir;
241}
242#else
243static unsigned SkATan2_255(SkFixed y, SkFixed x) {
244 if (x == 0) {
245 if (y == 0) {
246 return 0;
247 }
248 return y < 0 ? 192 : 64;
249 }
250 if (y == 0) {
251 return x < 0 ? 128 : 0;
252 }
253
254 /* Find the right quadrant for x,y
255 Since atan_0_90 only handles the first quadrant, we rotate x,y
256 appropriately before calling it, and then add the right amount
257 to account for the real quadrant.
258 quadrant 0 : add 0 | x > 0 && y > 0
259 quadrant 1 : add 64 (90 degrees) | x < 0 && y > 0
260 quadrant 2 : add 128 (180 degrees) | x < 0 && y < 0
261 quadrant 3 : add 192 (270 degrees) | x > 0 && y < 0
262
263 map x<0 to (1 << 6)
264 map y<0 to (3 << 6)
265 add = map_x ^ map_y
266 */
267 int xsign = x >> 31;
268 int ysign = y >> 31;
269 int add = ((-xsign) ^ (ysign & 3)) << 6;
270
271#ifdef SK_DEBUG
272 if (0 == add)
273 SkASSERT(x > 0 && y > 0);
274 else if (64 == add)
275 SkASSERT(x < 0 && y > 0);
276 else if (128 == add)
277 SkASSERT(x < 0 && y < 0);
278 else if (192 == add)
279 SkASSERT(x > 0 && y < 0);
280 else
281 SkDEBUGFAIL("bad value for add");
282#endif
283
284 /* This ^ trick makes x, y positive, and the swap<> handles quadrants
285 where we need to rotate x,y by 90 or -90
286 */
287 x = (x ^ xsign) - xsign;
288 y = (y ^ ysign) - ysign;
289 if (add & 64) { // quads 1 or 3 need to swap x,y
290 SkTSwap<SkFixed>(x, y);
291 }
292
293 unsigned result = add + atan_0_90(y, x);
294 SkASSERT(result < 256);
295 return result;
296}
297#endif
298
299void SkSweepGradient::shadeSpan(int x, int y, SkPMColor* SK_RESTRICT dstC,
300 int count) {
301 SkMatrix::MapXYProc proc = fDstToIndexProc;
302 const SkMatrix& matrix = fDstToIndex;
303 const SkPMColor* SK_RESTRICT cache = this->getCache32();
reed@google.com60040292013-02-04 18:21:23 +0000304 int toggle = init_dither_toggle(x, y);
rileya@google.com589708b2012-07-26 20:04:23 +0000305 SkPoint srcPt;
306
307 if (fDstToIndexClass != kPerspective_MatrixClass) {
308 proc(matrix, SkIntToScalar(x) + SK_ScalarHalf,
309 SkIntToScalar(y) + SK_ScalarHalf, &srcPt);
310 SkScalar dx, fx = srcPt.fX;
311 SkScalar dy, fy = srcPt.fY;
312
313 if (fDstToIndexClass == kFixedStepInX_MatrixClass) {
314 SkFixed storage[2];
315 (void)matrix.fixedStepInX(SkIntToScalar(y) + SK_ScalarHalf,
316 &storage[0], &storage[1]);
317 dx = SkFixedToScalar(storage[0]);
318 dy = SkFixedToScalar(storage[1]);
319 } else {
320 SkASSERT(fDstToIndexClass == kLinear_MatrixClass);
321 dx = matrix.getScaleX();
322 dy = matrix.getSkewY();
323 }
324
325 for (; count > 0; --count) {
reed@google.com60040292013-02-04 18:21:23 +0000326 *dstC++ = cache[toggle + SkATan2_255(fy, fx)];
rileya@google.com589708b2012-07-26 20:04:23 +0000327 fx += dx;
328 fy += dy;
reed@google.com60040292013-02-04 18:21:23 +0000329 toggle = next_dither_toggle(toggle);
rileya@google.com589708b2012-07-26 20:04:23 +0000330 }
331 } else { // perspective case
332 for (int stop = x + count; x < stop; x++) {
333 proc(matrix, SkIntToScalar(x) + SK_ScalarHalf,
334 SkIntToScalar(y) + SK_ScalarHalf, &srcPt);
reed@google.com60040292013-02-04 18:21:23 +0000335 *dstC++ = cache[toggle + SkATan2_255(srcPt.fY, srcPt.fX)];
reed@google.com60040292013-02-04 18:21:23 +0000336 toggle = next_dither_toggle(toggle);
rileya@google.com589708b2012-07-26 20:04:23 +0000337 }
338 }
339}
340
341void SkSweepGradient::shadeSpan16(int x, int y, uint16_t* SK_RESTRICT dstC,
342 int count) {
343 SkMatrix::MapXYProc proc = fDstToIndexProc;
344 const SkMatrix& matrix = fDstToIndex;
345 const uint16_t* SK_RESTRICT cache = this->getCache16();
reed@google.com55853db2013-02-01 19:34:59 +0000346 int toggle = init_dither_toggle16(x, y);
rileya@google.com589708b2012-07-26 20:04:23 +0000347 SkPoint srcPt;
348
349 if (fDstToIndexClass != kPerspective_MatrixClass) {
350 proc(matrix, SkIntToScalar(x) + SK_ScalarHalf,
351 SkIntToScalar(y) + SK_ScalarHalf, &srcPt);
352 SkScalar dx, fx = srcPt.fX;
353 SkScalar dy, fy = srcPt.fY;
354
355 if (fDstToIndexClass == kFixedStepInX_MatrixClass) {
356 SkFixed storage[2];
357 (void)matrix.fixedStepInX(SkIntToScalar(y) + SK_ScalarHalf,
358 &storage[0], &storage[1]);
359 dx = SkFixedToScalar(storage[0]);
360 dy = SkFixedToScalar(storage[1]);
361 } else {
362 SkASSERT(fDstToIndexClass == kLinear_MatrixClass);
363 dx = matrix.getScaleX();
364 dy = matrix.getSkewY();
365 }
366
367 for (; count > 0; --count) {
368 int index = SkATan2_255(fy, fx) >> (8 - kCache16Bits);
369 *dstC++ = cache[toggle + index];
reed@google.com55853db2013-02-01 19:34:59 +0000370 toggle = next_dither_toggle16(toggle);
rileya@google.com589708b2012-07-26 20:04:23 +0000371 fx += dx;
372 fy += dy;
373 }
374 } else { // perspective case
375 for (int stop = x + count; x < stop; x++) {
376 proc(matrix, SkIntToScalar(x) + SK_ScalarHalf,
377 SkIntToScalar(y) + SK_ScalarHalf, &srcPt);
378
379 int index = SkATan2_255(srcPt.fY, srcPt.fX);
380 index >>= (8 - kCache16Bits);
381 *dstC++ = cache[toggle + index];
reed@google.com55853db2013-02-01 19:34:59 +0000382 toggle = next_dither_toggle16(toggle);
rileya@google.com589708b2012-07-26 20:04:23 +0000383 }
384 }
385}
386
rileya@google.comd7cc6512012-07-27 14:00:39 +0000387/////////////////////////////////////////////////////////////////////
388
bsalomon@google.comcf8fb1f2012-08-02 14:03:32 +0000389#if SK_SUPPORT_GPU
390
bsalomon@google.com2eaaefd2012-10-29 19:51:22 +0000391#include "GrTBackendEffectFactory.h"
392
bsalomon@google.com0707c292012-10-25 21:45:42 +0000393class GrGLSweepGradient : public GrGLGradientEffect {
rileya@google.comd7cc6512012-07-27 14:00:39 +0000394public:
395
bsalomon@google.com396e61f2012-10-25 19:00:29 +0000396 GrGLSweepGradient(const GrBackendEffectFactory& factory,
bsalomon@google.comc7818882013-03-20 19:19:53 +0000397 const GrDrawEffect&) : INHERITED (factory) { }
rileya@google.comd7cc6512012-07-27 14:00:39 +0000398 virtual ~GrGLSweepGradient() { }
399
bsalomon@google.comf78df332012-10-29 12:43:38 +0000400 virtual void emitCode(GrGLShaderBuilder*,
bsalomon@google.comc7818882013-03-20 19:19:53 +0000401 const GrDrawEffect&,
bsalomon@google.comf78df332012-10-29 12:43:38 +0000402 EffectKey,
bsalomon@google.comf78df332012-10-29 12:43:38 +0000403 const char* outputColor,
404 const char* inputColor,
405 const TextureSamplerArray&) SK_OVERRIDE;
rileya@google.comd7cc6512012-07-27 14:00:39 +0000406
bsalomon@google.comc7818882013-03-20 19:19:53 +0000407 static EffectKey GenKey(const GrDrawEffect& drawEffect, const GrGLCaps&) {
408 return GenMatrixKey(drawEffect);
bsalomon@google.comd8b5fac2012-11-01 17:02:46 +0000409 }
rileya@google.comd7cc6512012-07-27 14:00:39 +0000410
411private:
412
bsalomon@google.com0707c292012-10-25 21:45:42 +0000413 typedef GrGLGradientEffect INHERITED;
rileya@google.comd7cc6512012-07-27 14:00:39 +0000414
415};
416
rileya@google.com98e8b6d2012-07-31 20:38:06 +0000417/////////////////////////////////////////////////////////////////////
418
419class GrSweepGradient : public GrGradientEffect {
420public:
bsalomon@google.com0ac6af42013-01-16 15:16:18 +0000421 static GrEffectRef* Create(GrContext* ctx,
422 const SkSweepGradient& shader,
423 const SkMatrix& matrix) {
bsalomon@google.com6340a412013-01-22 19:55:59 +0000424 AutoEffectUnref effect(SkNEW_ARGS(GrSweepGradient, (ctx, shader, matrix)));
bsalomon@google.coma1ebbe42013-01-16 15:51:47 +0000425 return CreateEffectRef(effect);
bsalomon@google.com0ac6af42013-01-16 15:16:18 +0000426 }
rileya@google.com98e8b6d2012-07-31 20:38:06 +0000427 virtual ~GrSweepGradient() { }
428
429 static const char* Name() { return "Sweep Gradient"; }
bsalomon@google.com396e61f2012-10-25 19:00:29 +0000430 virtual const GrBackendEffectFactory& getFactory() const SK_OVERRIDE {
431 return GrTBackendEffectFactory<GrSweepGradient>::getInstance();
rileya@google.com98e8b6d2012-07-31 20:38:06 +0000432 }
433
bsalomon@google.com422e81a2012-10-25 14:11:03 +0000434 typedef GrGLSweepGradient GLEffect;
rileya@google.com98e8b6d2012-07-31 20:38:06 +0000435
bsalomon@google.comd4726202012-08-03 14:34:46 +0000436private:
bsalomon@google.com0ac6af42013-01-16 15:16:18 +0000437 GrSweepGradient(GrContext* ctx,
438 const SkSweepGradient& shader,
439 const SkMatrix& matrix)
440 : INHERITED(ctx, shader, matrix, SkShader::kClamp_TileMode) { }
bsalomon@google.comf271cc72012-10-24 19:35:13 +0000441 GR_DECLARE_EFFECT_TEST;
rileya@google.com98e8b6d2012-07-31 20:38:06 +0000442
443 typedef GrGradientEffect INHERITED;
444};
445
446/////////////////////////////////////////////////////////////////////
447
bsalomon@google.comf271cc72012-10-24 19:35:13 +0000448GR_DEFINE_EFFECT_TEST(GrSweepGradient);
bsalomon@google.comd4726202012-08-03 14:34:46 +0000449
bsalomon@google.com73a96942013-02-13 16:31:19 +0000450GrEffectRef* GrSweepGradient::TestCreate(SkMWCRandom* random,
bsalomon@google.com0ac6af42013-01-16 15:16:18 +0000451 GrContext* context,
bsalomon@google.comc26d94f2013-03-25 18:19:00 +0000452 const GrDrawTargetCaps&,
bsalomon@google.com0ac6af42013-01-16 15:16:18 +0000453 GrTexture**) {
bsalomon@google.comd4726202012-08-03 14:34:46 +0000454 SkPoint center = {random->nextUScalar1(), random->nextUScalar1()};
455
456 SkColor colors[kMaxRandomGradientColors];
457 SkScalar stopsArray[kMaxRandomGradientColors];
458 SkScalar* stops = stopsArray;
459 SkShader::TileMode tmIgnored;
460 int colorCount = RandomGradientParams(random, colors, &stops, &tmIgnored);
461 SkAutoTUnref<SkShader> shader(SkGradientShader::CreateSweep(center.fX, center.fY,
462 colors, stops, colorCount));
bsalomon@google.come197cbf2013-01-14 16:46:26 +0000463 SkPaint paint;
464 return shader->asNewEffect(context, paint);
bsalomon@google.comd4726202012-08-03 14:34:46 +0000465}
466
467/////////////////////////////////////////////////////////////////////
468
bsalomon@google.comf78df332012-10-29 12:43:38 +0000469void GrGLSweepGradient::emitCode(GrGLShaderBuilder* builder,
bsalomon@google.comc7818882013-03-20 19:19:53 +0000470 const GrDrawEffect&,
bsalomon@google.comd8b5fac2012-11-01 17:02:46 +0000471 EffectKey key,
bsalomon@google.comf78df332012-10-29 12:43:38 +0000472 const char* outputColor,
473 const char* inputColor,
474 const TextureSamplerArray& samplers) {
475 this->emitYCoordUniform(builder);
bsalomon@google.comd8b5fac2012-11-01 17:02:46 +0000476 const char* coords;
bsalomon@google.comc7818882013-03-20 19:19:53 +0000477 this->setupMatrix(builder, key, &coords);
rileya@google.comd7cc6512012-07-27 14:00:39 +0000478 SkString t;
bsalomon@google.comd8b5fac2012-11-01 17:02:46 +0000479 t.printf("atan(- %s.y, - %s.x) * 0.1591549430918 + 0.5", coords, coords);
bsalomon@google.comf06df1b2012-09-06 20:22:31 +0000480 this->emitColorLookup(builder, t.c_str(), outputColor, inputColor, samplers[0]);
rileya@google.comd7cc6512012-07-27 14:00:39 +0000481}
482
483/////////////////////////////////////////////////////////////////////
484
bsalomon@google.com0ac6af42013-01-16 15:16:18 +0000485GrEffectRef* SkSweepGradient::asNewEffect(GrContext* context, const SkPaint&) const {
bsalomon@google.comdfdb7e52012-10-16 15:19:45 +0000486 SkMatrix matrix;
bsalomon@google.comf94b3a42012-10-31 18:09:01 +0000487 if (!this->getLocalMatrix().invert(&matrix)) {
humper@google.com84831ac2013-01-14 22:09:54 +0000488 return NULL;
bsalomon@google.comdfdb7e52012-10-16 15:19:45 +0000489 }
bsalomon@google.comf94b3a42012-10-31 18:09:01 +0000490 matrix.postConcat(fPtsToUnit);
bsalomon@google.com0ac6af42013-01-16 15:16:18 +0000491 return GrSweepGradient::Create(context, *this, matrix);
rileya@google.comd7cc6512012-07-27 14:00:39 +0000492}
493
bsalomon@google.comcf8fb1f2012-08-02 14:03:32 +0000494#else
495
bsalomon@google.com5d2cd202013-01-16 15:31:06 +0000496GrEffectRef* SkSweepGradient::asNewEffect(GrContext*, const SkPaint&) const {
bsalomon@google.comcf8fb1f2012-08-02 14:03:32 +0000497 SkDEBUGFAIL("Should not call in GPU-less build");
bsalomon@google.come197cbf2013-01-14 16:46:26 +0000498 return NULL;
bsalomon@google.comcf8fb1f2012-08-02 14:03:32 +0000499}
500
501#endif
robertphillips@google.com76f9e932013-01-15 20:17:47 +0000502
503#ifdef SK_DEVELOPER
504void SkSweepGradient::toString(SkString* str) const {
505 str->append("SkSweepGradient: (");
506
507 str->append("center: (");
508 str->appendScalar(fCenter.fX);
509 str->append(", ");
510 str->appendScalar(fCenter.fY);
511 str->append(") ");
512
513 this->INHERITED::toString(str);
514
515 str->append(")");
516}
517#endif