App crashing: Dispatch queue: com.apple.root.user-interactive-qos / vBoxConvolve...
[WebKit-https.git] / Source / WebCore / platform / graphics / filters / FEGaussianBlur.cpp
1 /*
2  * Copyright (C) 2004, 2005, 2006, 2007 Nikolas Zimmermann <zimmermann@kde.org>
3  * Copyright (C) 2004, 2005 Rob Buis <buis@kde.org>
4  * Copyright (C) 2005 Eric Seidel <eric@webkit.org>
5  * Copyright (C) 2009 Dirk Schulze <krit@webkit.org>
6  * Copyright (C) 2010 Igalia, S.L.
7  * Copyright (C) Research In Motion Limited 2010. All rights reserved.
8  * Copyright (C) 2015-2016 Apple, Inc. All rights reserved.
9  *
10  * This library is free software; you can redistribute it and/or
11  * modify it under the terms of the GNU Library General Public
12  * License as published by the Free Software Foundation; either
13  * version 2 of the License, or (at your option) any later version.
14  *
15  * This library is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18  * Library General Public License for more details.
19  *
20  * You should have received a copy of the GNU Library General Public License
21  * along with this library; see the file COPYING.LIB.  If not, write to
22  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
23  * Boston, MA 02110-1301, USA.
24  */
25
26 #include "config.h"
27 #include "FEGaussianBlur.h"
28
29 #include "FEGaussianBlurNEON.h"
30 #include "Filter.h"
31 #include "GraphicsContext.h"
32 #include "TextStream.h"
33
34 #if USE(ACCELERATE)
35 #include <Accelerate/Accelerate.h>
36 #endif
37
38 #include <runtime/JSCInlines.h>
39 #include <runtime/TypedArrayInlines.h>
40 #include <runtime/Uint8ClampedArray.h>
41 #include <wtf/MathExtras.h>
42 #include <wtf/ParallelJobs.h>
43
44 static inline float gaussianKernelFactor()
45 {
46     return 3 / 4.f * sqrtf(2 * piFloat);
47 }
48
49 static const int gMaxKernelSize = 500;
50
51 namespace WebCore {
52
53 inline void kernelPosition(int blurIteration, unsigned& radius, int& deltaLeft, int& deltaRight)
54 {
55     // Check http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement for details.
56     switch (blurIteration) {
57     case 0:
58         if (!(radius % 2)) {
59             deltaLeft = radius / 2 - 1;
60             deltaRight = radius - deltaLeft;
61         } else {
62             deltaLeft = radius / 2;
63             deltaRight = radius - deltaLeft;
64         }
65         break;
66     case 1:
67         if (!(radius % 2)) {
68             deltaLeft++;
69             deltaRight--;
70         }
71         break;
72     case 2:
73         if (!(radius % 2)) {
74             deltaRight++;
75             radius++;
76         }
77         break;
78     }
79 }
80
81 FEGaussianBlur::FEGaussianBlur(Filter& filter, float x, float y, EdgeModeType edgeMode)
82     : FilterEffect(filter)
83     , m_stdX(x)
84     , m_stdY(y)
85     , m_edgeMode(edgeMode)
86 {
87 }
88
89 Ref<FEGaussianBlur> FEGaussianBlur::create(Filter& filter, float x, float y, EdgeModeType edgeMode)
90 {
91     return adoptRef(*new FEGaussianBlur(filter, x, y, edgeMode));
92 }
93
94 float FEGaussianBlur::stdDeviationX() const
95 {
96     return m_stdX;
97 }
98
99 void FEGaussianBlur::setStdDeviationX(float x)
100 {
101     m_stdX = x;
102 }
103
104 float FEGaussianBlur::stdDeviationY() const
105 {
106     return m_stdY;
107 }
108
109 void FEGaussianBlur::setStdDeviationY(float y)
110 {
111     m_stdY = y;
112 }
113
114 EdgeModeType FEGaussianBlur::edgeMode() const
115 {
116     return m_edgeMode;
117 }
118
119 void FEGaussianBlur::setEdgeMode(EdgeModeType edgeMode)
120 {
121     m_edgeMode = edgeMode;
122 }
123
124 // This function only operates on Alpha channel.
125 inline void boxBlurAlphaOnly(const Uint8ClampedArray* srcPixelArray, Uint8ClampedArray* dstPixelArray,
126     unsigned dx, int& dxLeft, int& dxRight, int& stride, int& strideLine, int& effectWidth, int& effectHeight, const int& maxKernelSize)
127 {
128     unsigned char* srcData = srcPixelArray->data();
129     unsigned char* dstData = dstPixelArray->data();
130     // Memory alignment is: RGBA, zero-index based.
131     const int channel = 3;
132
133     for (int y = 0; y < effectHeight; ++y) {
134         int line = y * strideLine;
135         int sum = 0;
136
137         // Fill the kernel.
138         for (int i = 0; i < maxKernelSize; ++i) {
139             unsigned offset = line + i * stride;
140             unsigned char* srcPtr = srcData + offset;
141             sum += srcPtr[channel];
142         }
143
144         // Blurring.
145         for (int x = 0; x < effectWidth; ++x) {
146             unsigned pixelByteOffset = line + x * stride + channel;
147             unsigned char* dstPtr = dstData + pixelByteOffset;
148             *dstPtr = static_cast<unsigned char>(sum / dx);
149
150             // Shift kernel.
151             if (x >= dxLeft) {
152                 unsigned leftOffset = pixelByteOffset - dxLeft * stride;
153                 unsigned char* srcPtr = srcData + leftOffset;
154                 sum -= *srcPtr;
155             }
156
157             if (x + dxRight < effectWidth) {
158                 unsigned rightOffset = pixelByteOffset + dxRight * stride;
159                 unsigned char* srcPtr = srcData + rightOffset;
160                 sum += *srcPtr;
161             }
162         }
163     }
164 }
165
166 inline void boxBlur(const Uint8ClampedArray* srcPixelArray, Uint8ClampedArray* dstPixelArray,
167     unsigned dx, int dxLeft, int dxRight, int stride, int strideLine, int effectWidth, int effectHeight, bool alphaImage, EdgeModeType edgeMode)
168 {
169     const int maxKernelSize = std::min(dxRight, effectWidth);
170     if (alphaImage) {
171         return boxBlurAlphaOnly(srcPixelArray, dstPixelArray, dx, dxLeft, dxRight, stride, strideLine,
172             effectWidth, effectHeight, maxKernelSize);
173     }
174
175     unsigned char* srcData = srcPixelArray->data();
176     unsigned char* dstData = dstPixelArray->data();
177
178     // Concerning the array width/length: it is Element size + Margin + Border. The number of pixels will be
179     // P = width * height * channels.
180     for (int y = 0; y < effectHeight; ++y) {
181         int line = y * strideLine;
182         int sumR = 0, sumG = 0, sumB = 0, sumA = 0;
183
184         if (edgeMode == EDGEMODE_NONE) {
185             // Fill the kernel.
186             for (int i = 0; i < maxKernelSize; ++i) {
187                 unsigned offset = line + i * stride;
188                 unsigned char* srcPtr = srcData + offset;
189                 sumR += *srcPtr++;
190                 sumG += *srcPtr++;
191                 sumB += *srcPtr++;
192                 sumA += *srcPtr;
193             }
194
195             // Blurring.
196             for (int x = 0; x < effectWidth; ++x) {
197                 unsigned pixelByteOffset = line + x * stride;
198                 unsigned char* dstPtr = dstData + pixelByteOffset;
199
200                 *dstPtr++ = static_cast<unsigned char>(sumR / dx);
201                 *dstPtr++ = static_cast<unsigned char>(sumG / dx);
202                 *dstPtr++ = static_cast<unsigned char>(sumB / dx);
203                 *dstPtr = static_cast<unsigned char>(sumA / dx);
204
205                 // Shift kernel.
206                 if (x >= dxLeft) {
207                     unsigned leftOffset = pixelByteOffset - dxLeft * stride;
208                     unsigned char* srcPtr = srcData + leftOffset;
209                     sumR -= srcPtr[0];
210                     sumG -= srcPtr[1];
211                     sumB -= srcPtr[2];
212                     sumA -= srcPtr[3];
213                 }
214
215                 if (x + dxRight < effectWidth) {
216                     unsigned rightOffset = pixelByteOffset + dxRight * stride;
217                     unsigned char* srcPtr = srcData + rightOffset;
218                     sumR += srcPtr[0];
219                     sumG += srcPtr[1];
220                     sumB += srcPtr[2];
221                     sumA += srcPtr[3];
222                 }
223             }
224
225         } else {
226             // FIXME: Add support for 'wrap' here.
227             // Get edge values for edgeMode 'duplicate'.
228             unsigned char* edgeValueLeft = srcData + line;
229             unsigned char* edgeValueRight  = srcData + (line + (effectWidth - 1) * stride);
230
231             // Fill the kernel.
232             for (int i = dxLeft * -1; i < dxRight; ++i) {
233                 // Is this right for negative values of 'i'?
234                 unsigned offset = line + i * stride;
235                 unsigned char* srcPtr = srcData + offset;
236
237                 if (i < 0) {
238                     sumR += edgeValueLeft[0];
239                     sumG += edgeValueLeft[1];
240                     sumB += edgeValueLeft[2];
241                     sumA += edgeValueLeft[3];
242                 } else if (i >= effectWidth) {
243                     sumR += edgeValueRight[0];
244                     sumG += edgeValueRight[1];
245                     sumB += edgeValueRight[2];
246                     sumA += edgeValueRight[3];
247                 } else {
248                     sumR += *srcPtr++;
249                     sumG += *srcPtr++;
250                     sumB += *srcPtr++;
251                     sumA += *srcPtr;
252                 }
253             }
254
255             // Blurring.
256             for (int x = 0; x < effectWidth; ++x) {
257                 unsigned pixelByteOffset = line + x * stride;
258                 unsigned char* dstPtr = dstData + pixelByteOffset;
259
260                 *dstPtr++ = static_cast<unsigned char>(sumR / dx);
261                 *dstPtr++ = static_cast<unsigned char>(sumG / dx);
262                 *dstPtr++ = static_cast<unsigned char>(sumB / dx);
263                 *dstPtr = static_cast<unsigned char>(sumA / dx);
264
265                 // Shift kernel.
266                 if (x < dxLeft) {
267                     sumR -= edgeValueLeft[0];
268                     sumG -= edgeValueLeft[1];
269                     sumB -= edgeValueLeft[2];
270                     sumA -= edgeValueLeft[3];
271                 } else {
272                     unsigned leftOffset = pixelByteOffset - dxLeft * stride;
273                     unsigned char* srcPtr = srcData + leftOffset;
274                     sumR -= srcPtr[0];
275                     sumG -= srcPtr[1];
276                     sumB -= srcPtr[2];
277                     sumA -= srcPtr[3];
278                 }
279
280                 if (x + dxRight >= effectWidth) {
281                     sumR += edgeValueRight[0];
282                     sumG += edgeValueRight[1];
283                     sumB += edgeValueRight[2];
284                     sumA += edgeValueRight[3];
285                 } else {
286                     unsigned rightOffset = pixelByteOffset + dxRight * stride;
287                     unsigned char* srcPtr = srcData + rightOffset;
288                     sumR += srcPtr[0];
289                     sumG += srcPtr[1];
290                     sumB += srcPtr[2];
291                     sumA += srcPtr[3];
292                 }
293             }
294         }
295     }
296 }
297
298 #if USE(ACCELERATE)
299 inline void accelerateBoxBlur(const Uint8ClampedArray* src, Uint8ClampedArray* dst, unsigned kernelSize, int stride, int effectWidth, int effectHeight)
300 {
301     if (!src || !src->data() || !dst || !dst->data()) {
302         ASSERT_NOT_REACHED();
303         return;
304     }
305
306     if (effectWidth <= 0 || effectHeight <= 0 || stride <= 0) {
307         ASSERT_NOT_REACHED();
308         return;
309     }
310
311     // We must always use an odd radius.
312     if (kernelSize % 2 != 1)
313         kernelSize += 1;
314
315     vImage_Buffer effectInBuffer;
316     effectInBuffer.data = src->data();
317     effectInBuffer.width = effectWidth;
318     effectInBuffer.height = effectHeight;
319     effectInBuffer.rowBytes = stride;
320
321     vImage_Buffer effectOutBuffer;
322     effectOutBuffer.data = dst->data();
323     effectOutBuffer.width = effectWidth;
324     effectOutBuffer.height = effectHeight;
325     effectOutBuffer.rowBytes = stride;
326
327     // Determine the size of a temporary buffer by calling the function first with a special flag. vImage will return
328     // the size needed, or an error (which are all negative).
329     size_t tmpBufferSize = vImageBoxConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, 0, 0, 0, kernelSize, kernelSize, 0, kvImageEdgeExtend | kvImageGetTempBufferSize);
330     if (tmpBufferSize <= 0)
331         return;
332
333     void* tmpBuffer = fastMalloc(tmpBufferSize);
334     vImageBoxConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, tmpBuffer, 0, 0, kernelSize, kernelSize, 0, kvImageEdgeExtend);
335     vImageBoxConvolve_ARGB8888(&effectOutBuffer, &effectInBuffer, tmpBuffer, 0, 0, kernelSize, kernelSize, 0, kvImageEdgeExtend);
336     vImageBoxConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, tmpBuffer, 0, 0, kernelSize, kernelSize, 0, kvImageEdgeExtend);
337     WTF::fastFree(tmpBuffer);
338
339     // The final result should be stored in src.
340     if (dst == src) {
341         ASSERT(src->length() == dst->length());
342         memcpy(dst->data(), src->data(), src->length());
343     }
344 }
345 #endif
346
347 inline void standardBoxBlur(Uint8ClampedArray* src, Uint8ClampedArray* dst, unsigned kernelSizeX, unsigned kernelSizeY, int stride, IntSize& paintSize, bool isAlphaImage, EdgeModeType edgeMode)
348 {
349     int dxLeft = 0;
350     int dxRight = 0;
351     int dyLeft = 0;
352     int dyRight = 0;
353
354     for (int i = 0; i < 3; ++i) {
355         if (kernelSizeX) {
356             kernelPosition(i, kernelSizeX, dxLeft, dxRight);
357 #if HAVE(ARM_NEON_INTRINSICS)
358             if (!isAlphaImage)
359                 boxBlurNEON(src, dst, kernelSizeX, dxLeft, dxRight, 4, stride, paintSize.width(), paintSize.height());
360             else
361                 boxBlur(src, dst, kernelSizeX, dxLeft, dxRight, 4, stride, paintSize.width(), paintSize.height(), true, edgeMode);
362 #else
363             boxBlur(src, dst, kernelSizeX, dxLeft, dxRight, 4, stride, paintSize.width(), paintSize.height(), isAlphaImage, edgeMode);
364 #endif
365             std::swap(src, dst);
366         }
367
368         if (kernelSizeY) {
369             kernelPosition(i, kernelSizeY, dyLeft, dyRight);
370 #if HAVE(ARM_NEON_INTRINSICS)
371             if (!isAlphaImage)
372                 boxBlurNEON(src, dst, kernelSizeY, dyLeft, dyRight, stride, 4, paintSize.height(), paintSize.width());
373             else
374                 boxBlur(src, dst, kernelSizeY, dyLeft, dyRight, stride, 4, paintSize.height(), paintSize.width(), true, edgeMode);
375 #else
376             boxBlur(src, dst, kernelSizeY, dyLeft, dyRight, stride, 4, paintSize.height(), paintSize.width(), isAlphaImage, edgeMode);
377 #endif
378             std::swap(src, dst);
379         }
380     }
381
382     // The final result should be stored in src.
383     if (dst == src) {
384         ASSERT(src->length() == dst->length());
385         memcpy(dst->data(), src->data(), src->length());
386     }
387 }
388
389 inline void FEGaussianBlur::platformApplyGeneric(Uint8ClampedArray* srcPixelArray, Uint8ClampedArray* tmpPixelArray, unsigned kernelSizeX, unsigned kernelSizeY, IntSize& paintSize)
390 {
391     int stride = 4 * paintSize.width();
392
393 #if USE(ACCELERATE)
394     if (kernelSizeX == kernelSizeY && (m_edgeMode == EDGEMODE_NONE || m_edgeMode == EDGEMODE_DUPLICATE)) {
395         accelerateBoxBlur(srcPixelArray, tmpPixelArray, kernelSizeX, stride, paintSize.width(), paintSize.height());
396         return;
397     }
398 #endif
399
400     standardBoxBlur(srcPixelArray, tmpPixelArray, kernelSizeX, kernelSizeY, stride, paintSize, isAlphaImage(), m_edgeMode);
401 }
402
403 void FEGaussianBlur::platformApplyWorker(PlatformApplyParameters* parameters)
404 {
405     IntSize paintSize(parameters->width, parameters->height);
406     parameters->filter->platformApplyGeneric(parameters->srcPixelArray.get(), parameters->dstPixelArray.get(),
407         parameters->kernelSizeX, parameters->kernelSizeY, paintSize);
408 }
409
410 inline void FEGaussianBlur::platformApply(Uint8ClampedArray* srcPixelArray, Uint8ClampedArray* tmpPixelArray, unsigned kernelSizeX, unsigned kernelSizeY, IntSize& paintSize)
411 {
412 #if !USE(ACCELERATE)
413     int scanline = 4 * paintSize.width();
414     int extraHeight = 3 * kernelSizeY * 0.5f;
415     int optimalThreadNumber = (paintSize.width() * paintSize.height()) / (s_minimalRectDimension + extraHeight * paintSize.width());
416
417     if (optimalThreadNumber > 1) {
418         WTF::ParallelJobs<PlatformApplyParameters> parallelJobs(&platformApplyWorker, optimalThreadNumber);
419
420         int jobs = parallelJobs.numberOfJobs();
421         if (jobs > 1) {
422             // Split the job into "blockHeight"-sized jobs but there a few jobs that need to be slightly larger since
423             // blockHeight * jobs < total size. These extras are handled by the remainder "jobsWithExtra".
424             const int blockHeight = paintSize.height() / jobs;
425             const int jobsWithExtra = paintSize.height() % jobs;
426
427             int currentY = 0;
428             for (int job = 0; job < jobs; job++) {
429                 PlatformApplyParameters& params = parallelJobs.parameter(job);
430                 params.filter = this;
431
432                 int startY = !job ? 0 : currentY - extraHeight;
433                 currentY += job < jobsWithExtra ? blockHeight + 1 : blockHeight;
434                 int endY = job == jobs - 1 ? currentY : currentY + extraHeight;
435
436                 int blockSize = (endY - startY) * scanline;
437                 if (!job) {
438                     params.srcPixelArray = srcPixelArray;
439                     params.dstPixelArray = tmpPixelArray;
440                 } else {
441                     params.srcPixelArray = Uint8ClampedArray::createUninitialized(blockSize);
442                     params.dstPixelArray = Uint8ClampedArray::createUninitialized(blockSize);
443                     memcpy(params.srcPixelArray->data(), srcPixelArray->data() + startY * scanline, blockSize);
444                 }
445
446                 params.width = paintSize.width();
447                 params.height = endY - startY;
448                 params.kernelSizeX = kernelSizeX;
449                 params.kernelSizeY = kernelSizeY;
450             }
451
452             parallelJobs.execute();
453
454             // Copy together the parts of the image.
455             currentY = 0;
456             for (int job = 1; job < jobs; job++) {
457                 PlatformApplyParameters& params = parallelJobs.parameter(job);
458                 int sourceOffset;
459                 int destinationOffset;
460                 int size;
461                 int adjustedBlockHeight = job < jobsWithExtra ? blockHeight + 1 : blockHeight;
462
463                 currentY += adjustedBlockHeight;
464                 sourceOffset = extraHeight * scanline;
465                 destinationOffset = currentY * scanline;
466                 size = adjustedBlockHeight * scanline;
467
468                 memcpy(srcPixelArray->data() + destinationOffset, params.srcPixelArray->data() + sourceOffset, size);
469             }
470             return;
471         }
472         // Fallback to single threaded mode.
473     }
474 #endif
475
476     // The selection here eventually should happen dynamically on some platforms.
477     platformApplyGeneric(srcPixelArray, tmpPixelArray, kernelSizeX, kernelSizeY, paintSize);
478 }
479
480 static int clampedToKernelSize(float value)
481 {
482     // Limit the kernel size to 500. A bigger radius won't make a big difference for the result image but
483     // inflates the absolute paint rect too much. This is compatible with Firefox' behavior.
484     unsigned size = std::max<unsigned>(2, static_cast<unsigned>(floorf(value * gaussianKernelFactor() + 0.5f)));
485     return clampTo<int>(std::min(size, static_cast<unsigned>(gMaxKernelSize)));
486 }
487     
488 IntSize FEGaussianBlur::calculateUnscaledKernelSize(const FloatPoint& stdDeviation)
489 {
490     ASSERT(stdDeviation.x() >= 0 && stdDeviation.y() >= 0);
491     IntSize kernelSize;
492
493     if (stdDeviation.x())
494         kernelSize.setWidth(clampedToKernelSize(stdDeviation.x()));
495
496     if (stdDeviation.y())
497         kernelSize.setHeight(clampedToKernelSize(stdDeviation.y()));
498
499     return kernelSize;
500 }
501
502 IntSize FEGaussianBlur::calculateKernelSize(const Filter& filter, const FloatPoint& stdDeviation)
503 {
504     FloatPoint stdFilterScaled(filter.applyHorizontalScale(stdDeviation.x()), filter.applyVerticalScale(stdDeviation.y()));
505     return calculateUnscaledKernelSize(stdFilterScaled);
506 }
507
508 void FEGaussianBlur::determineAbsolutePaintRect()
509 {
510     IntSize kernelSize = calculateKernelSize(filter(), FloatPoint(m_stdX, m_stdY));
511
512     FloatRect absolutePaintRect = inputEffect(0)->absolutePaintRect();
513     // Edge modes other than 'none' do not inflate the affected paint rect.
514     if (m_edgeMode != EDGEMODE_NONE) {
515         setAbsolutePaintRect(enclosingIntRect(absolutePaintRect));
516         return;
517     }
518
519     // We take the half kernel size and multiply it with three, because we run box blur three times.
520     absolutePaintRect.inflateX(3 * kernelSize.width() * 0.5f);
521     absolutePaintRect.inflateY(3 * kernelSize.height() * 0.5f);
522
523     if (clipsToBounds())
524         absolutePaintRect.intersect(maxEffectRect());
525     else
526         absolutePaintRect.unite(maxEffectRect());
527
528     setAbsolutePaintRect(enclosingIntRect(absolutePaintRect));
529 }
530
531 void FEGaussianBlur::platformApplySoftware()
532 {
533     FilterEffect* in = inputEffect(0);
534
535     Uint8ClampedArray* srcPixelArray = createPremultipliedImageResult();
536     if (!srcPixelArray)
537         return;
538
539     setIsAlphaImage(in->isAlphaImage());
540
541     IntRect effectDrawingRect = requestedRegionOfInputImageData(in->absolutePaintRect());
542     in->copyPremultipliedImage(srcPixelArray, effectDrawingRect);
543
544     if (!m_stdX && !m_stdY)
545         return;
546
547     IntSize kernelSize = calculateKernelSize(filter(), FloatPoint(m_stdX, m_stdY));
548     kernelSize.scale(filter().filterScale());
549
550     IntSize paintSize = absolutePaintRect().size();
551     paintSize.scale(filter().filterScale());
552     RefPtr<Uint8ClampedArray> tmpImageData = Uint8ClampedArray::createUninitialized((paintSize.area() * 4).unsafeGet());
553     if (!tmpImageData) {
554         WTFLogAlways("FEGaussianBlur::platformApplySoftware Unable to create buffer. Requested size was %d x %d\n", paintSize.width(), paintSize.height());
555         return;
556     }
557
558     platformApply(srcPixelArray, tmpImageData.get(), kernelSize.width(), kernelSize.height(), paintSize);
559 }
560
561 void FEGaussianBlur::dump()
562 {
563 }
564
565 TextStream& FEGaussianBlur::externalRepresentation(TextStream& ts, int indent) const
566 {
567     writeIndent(ts, indent);
568     ts << "[feGaussianBlur";
569     FilterEffect::externalRepresentation(ts);
570     ts << " stdDeviation=\"" << m_stdX << ", " << m_stdY << "\"]\n";
571     inputEffect(0)->externalRepresentation(ts, indent + 1);
572     return ts;
573 }
574
575 } // namespace WebCore