1 /*
2 * Copyright 2013 The Android Open Source Project
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
8 #include "include/effects/SkImageFilters.h"
9
10 #include "include/core/SkBlendMode.h"
11 #include "include/core/SkBlender.h"
12 #include "include/core/SkColor.h"
13 #include "include/core/SkFlattenable.h"
14 #include "include/core/SkImageFilter.h"
15 #include "include/core/SkM44.h"
16 #include "include/core/SkRect.h"
17 #include "include/core/SkRefCnt.h"
18 #include "include/core/SkScalar.h"
19 #include "include/core/SkShader.h"
20 #include "include/core/SkTypes.h"
21 #include "include/effects/SkBlenders.h"
22 #include "include/private/base/SkSpan_impl.h"
23 #include "include/private/base/SkTo.h"
24 #include "src/core/SkBlendModePriv.h"
25 #include "src/core/SkBlenderBase.h"
26 #include "src/core/SkImageFilterTypes.h"
27 #include "src/core/SkImageFilter_Base.h"
28 #include "src/core/SkPicturePriv.h"
29 #include "src/core/SkReadBuffer.h"
30 #include "src/core/SkRectPriv.h"
31 #include "src/core/SkWriteBuffer.h"
32
33 #include <cstdint>
34 #include <optional>
35 #include <utility>
36
37 namespace {
38
39 class SkBlendImageFilter : public SkImageFilter_Base {
40 // Input image filter indices
41 static constexpr int kBackground = 0;
42 static constexpr int kForeground = 1;
43
44 public:
SkBlendImageFilter(sk_sp<SkBlender> blender,const std::optional<SkV4> & coefficients,bool enforcePremul,sk_sp<SkImageFilter> inputs[2])45 SkBlendImageFilter(sk_sp<SkBlender> blender,
46 const std::optional<SkV4>& coefficients,
47 bool enforcePremul,
48 sk_sp<SkImageFilter> inputs[2])
49 : SkImageFilter_Base(inputs, 2)
50 , fBlender(std::move(blender))
51 , fArithmeticCoefficients(coefficients)
52 , fEnforcePremul(enforcePremul) {
53 // A null blender represents src-over, which should have been filled in by the factory
54 SkASSERT(fBlender);
55 }
56
57 SkRect computeFastBounds(const SkRect& bounds) const override;
58
59 protected:
60 void flatten(SkWriteBuffer&) const override;
61
62 private:
63 static constexpr uint32_t kArithmetic_SkBlendMode = kCustom_SkBlendMode + 1;
64
65 friend void ::SkRegisterBlendImageFilterFlattenable();
66 SK_FLATTENABLE_HOOKS(SkBlendImageFilter)
67 static sk_sp<SkFlattenable> LegacyArithmeticCreateProc(SkReadBuffer& buffer);
68
onGetCTMCapability() const69 MatrixCapability onGetCTMCapability() const override { return MatrixCapability::kComplex; }
70
onAffectsTransparentBlack() const71 bool onAffectsTransparentBlack() const override {
72 // An arbitrary runtime blender or an arithmetic runtime blender with k3 != 0 affects
73 // transparent black.
74 return !as_BB(fBlender)->asBlendMode().has_value() &&
75 (!fArithmeticCoefficients.has_value() || (*fArithmeticCoefficients)[3] != 0.f);
76 }
77
78 skif::FilterResult onFilterImage(const skif::Context&) const override;
79
80 skif::LayerSpace<SkIRect> onGetInputLayerBounds(
81 const skif::Mapping& mapping,
82 const skif::LayerSpace<SkIRect>& desiredOutput,
83 std::optional<skif::LayerSpace<SkIRect>> contentBounds) const override;
84
85 std::optional<skif::LayerSpace<SkIRect>> onGetOutputLayerBounds(
86 const skif::Mapping& mapping,
87 std::optional<skif::LayerSpace<SkIRect>> contentBounds) const override;
88
89 sk_sp<SkShader> makeBlendShader(sk_sp<SkShader> bg, sk_sp<SkShader> fg) const;
90
91 sk_sp<SkBlender> fBlender;
92
93 // Normally runtime SkBlenders are pessimistic about the bounds they affect. For Arithmetic,
94 // we remember the coefficients so that bounds can be reasoned about.
95 std::optional<SkV4> fArithmeticCoefficients;
96 bool fEnforcePremul; // Remembered to serialize the Arithmetic variant correctly
97 };
98
make_blend(sk_sp<SkBlender> blender,sk_sp<SkImageFilter> background,sk_sp<SkImageFilter> foreground,const SkImageFilters::CropRect & cropRect,std::optional<SkV4> coefficients={},bool enforcePremul=false)99 sk_sp<SkImageFilter> make_blend(sk_sp<SkBlender> blender,
100 sk_sp<SkImageFilter> background,
101 sk_sp<SkImageFilter> foreground,
102 const SkImageFilters::CropRect& cropRect,
103 std::optional<SkV4> coefficients = {},
104 bool enforcePremul = false) {
105 if (!blender) {
106 blender = SkBlender::Mode(SkBlendMode::kSrcOver);
107 }
108
__anon60fa5f0c0202(sk_sp<SkImageFilter> filter) 109 auto cropped = [cropRect](sk_sp<SkImageFilter> filter) {
110 if (cropRect) {
111 filter = SkImageFilters::Crop(*cropRect, std::move(filter));
112 }
113 return filter;
114 };
115
116 if (auto bm = as_BB(blender)->asBlendMode()) {
117 if (bm == SkBlendMode::kSrc) {
118 return cropped(std::move(foreground));
119 } else if (bm == SkBlendMode::kDst) {
120 return cropped(std::move(background));
121 } else if (bm == SkBlendMode::kClear) {
122 return SkImageFilters::Empty();
123 }
124 }
125
126 sk_sp<SkImageFilter> inputs[2] = { std::move(background), std::move(foreground) };
127 sk_sp<SkImageFilter> filter{new SkBlendImageFilter(blender, coefficients,
128 enforcePremul, inputs)};
129 return cropped(std::move(filter));
130 }
131
132 } // anonymous namespace
133
Blend(SkBlendMode mode,sk_sp<SkImageFilter> background,sk_sp<SkImageFilter> foreground,const CropRect & cropRect)134 sk_sp<SkImageFilter> SkImageFilters::Blend(SkBlendMode mode,
135 sk_sp<SkImageFilter> background,
136 sk_sp<SkImageFilter> foreground,
137 const CropRect& cropRect) {
138 return make_blend(SkBlender::Mode(mode),
139 std::move(background),
140 std::move(foreground),
141 cropRect);
142 }
143
Blend(sk_sp<SkBlender> blender,sk_sp<SkImageFilter> background,sk_sp<SkImageFilter> foreground,const CropRect & cropRect)144 sk_sp<SkImageFilter> SkImageFilters::Blend(sk_sp<SkBlender> blender,
145 sk_sp<SkImageFilter> background,
146 sk_sp<SkImageFilter> foreground,
147 const CropRect& cropRect) {
148 return make_blend(std::move(blender), std::move(background), std::move(foreground), cropRect);
149 }
150
Arithmetic(SkScalar k1,SkScalar k2,SkScalar k3,SkScalar k4,bool enforcePMColor,sk_sp<SkImageFilter> background,sk_sp<SkImageFilter> foreground,const CropRect & cropRect)151 sk_sp<SkImageFilter> SkImageFilters::Arithmetic(SkScalar k1,
152 SkScalar k2,
153 SkScalar k3,
154 SkScalar k4,
155 bool enforcePMColor,
156 sk_sp<SkImageFilter> background,
157 sk_sp<SkImageFilter> foreground,
158 const CropRect& cropRect) {
159 auto blender = SkBlenders::Arithmetic(k1, k2, k3, k4, enforcePMColor);
160 if (!blender) {
161 // Arithmetic() returns null on an error, not to optimize src-over
162 return nullptr;
163 }
164 return make_blend(std::move(blender),
165 std::move(background),
166 std::move(foreground),
167 cropRect,
168 // Carry arithmetic coefficients and premul behavior into image filter for
169 // serialization and bounds analysis
170 SkV4{k1, k2, k3, k4},
171 enforcePMColor);
172 }
173
SkRegisterBlendImageFilterFlattenable()174 void SkRegisterBlendImageFilterFlattenable() {
175 SK_REGISTER_FLATTENABLE(SkBlendImageFilter);
176 // TODO (michaelludwig) - Remove after grace period for SKPs to stop using old name
177 SkFlattenable::Register("SkXfermodeImageFilter_Base", SkBlendImageFilter::CreateProc);
178 SkFlattenable::Register("SkXfermodeImageFilterImpl", SkBlendImageFilter::CreateProc);
179 SkFlattenable::Register("ArithmeticImageFilterImpl",
180 SkBlendImageFilter::LegacyArithmeticCreateProc);
181 SkFlattenable::Register("SkArithmeticImageFilter",
182 SkBlendImageFilter::LegacyArithmeticCreateProc);
183 }
184
LegacyArithmeticCreateProc(SkReadBuffer & buffer)185 sk_sp<SkFlattenable> SkBlendImageFilter::LegacyArithmeticCreateProc(SkReadBuffer& buffer) {
186 // Newer SKPs should be using the updated Blend CreateProc.
187 if (!buffer.validate(buffer.isVersionLT(SkPicturePriv::kCombineBlendArithmeticFilters))) {
188 SkASSERT(false); // debug-only, so release will just see a failed deserialization
189 return nullptr;
190 }
191
192 SK_IMAGEFILTER_UNFLATTEN_COMMON(common, 2);
193 float k[4];
194 for (int i = 0; i < 4; ++i) {
195 k[i] = buffer.readScalar();
196 }
197 const bool enforcePremul = buffer.readBool();
198 return SkImageFilters::Arithmetic(k[0], k[1], k[2], k[3], enforcePremul,
199 common.getInput(0), common.getInput(1), common.cropRect());
200 }
201
CreateProc(SkReadBuffer & buffer)202 sk_sp<SkFlattenable> SkBlendImageFilter::CreateProc(SkReadBuffer& buffer) {
203 SK_IMAGEFILTER_UNFLATTEN_COMMON(common, 2);
204
205 sk_sp<SkBlender> blender;
206 std::optional<SkV4> coefficients;
207 bool enforcePremul = false;
208
209 const uint32_t mode = buffer.read32();
210 if (mode == kArithmetic_SkBlendMode) {
211 // Should only see this sentinel value in newer SKPs
212 if (buffer.validate(!buffer.isVersionLT(SkPicturePriv::kCombineBlendArithmeticFilters))) {
213 SkV4 k;
214 for (int i = 0; i < 4; ++i) {
215 k[i] = buffer.readScalar();
216 }
217 coefficients = k;
218 enforcePremul = buffer.readBool();
219 blender = SkBlenders::Arithmetic(k.x, k.y, k.z, k.w, enforcePremul);
220 if (!buffer.validate(SkToBool(blender))) {
221 return nullptr; // A null arithmetic blender is an error condition
222 }
223 }
224 } else if (mode == kCustom_SkBlendMode) {
225 blender = buffer.readBlender();
226 } else {
227 if (!buffer.validate(mode <= (unsigned) SkBlendMode::kLastMode)) {
228 return nullptr;
229 }
230 blender = SkBlender::Mode((SkBlendMode)mode);
231 }
232
233 return make_blend(std::move(blender),
234 common.getInput(kBackground),
235 common.getInput(kForeground),
236 common.cropRect(),
237 coefficients,
238 enforcePremul);
239 }
240
flatten(SkWriteBuffer & buffer) const241 void SkBlendImageFilter::flatten(SkWriteBuffer& buffer) const {
242 this->SkImageFilter_Base::flatten(buffer);
243 if (fArithmeticCoefficients.has_value()) {
244 buffer.write32(kArithmetic_SkBlendMode);
245
246 const SkV4& k = *fArithmeticCoefficients;
247 buffer.writeScalar(k[0]);
248 buffer.writeScalar(k[1]);
249 buffer.writeScalar(k[2]);
250 buffer.writeScalar(k[3]);
251 buffer.writeBool(fEnforcePremul);
252 } else if (auto bm = as_BB(fBlender)->asBlendMode()) {
253 buffer.write32((unsigned)bm.value());
254 } else {
255 buffer.write32(kCustom_SkBlendMode);
256 buffer.writeFlattenable(fBlender.get());
257 }
258 }
259
260 ///////////////////////////////////////////////////////////////////////////////////////////////////
261
makeBlendShader(sk_sp<SkShader> bg,sk_sp<SkShader> fg) const262 sk_sp<SkShader> SkBlendImageFilter::makeBlendShader(sk_sp<SkShader> bg, sk_sp<SkShader> fg) const {
263 // A null input shader signifies transparent black when image filtering, but SkShaders::Blend
264 // expects non-null shaders. So we have to do some clean up.
265 if (!bg || !fg) {
266 // If we don't affect transparent black and both inputs are null, then return a null
267 // shader to skip any evaluation.
268 if (!this->onAffectsTransparentBlack() && !bg && !fg) {
269 return nullptr;
270 }
271 // Otherwise if only one input is null, we might be able to just return that one.
272 if (auto bm = as_BB(fBlender)->asBlendMode()) {
273 SkBlendModeCoeff src, dst;
274 if (SkBlendMode_AsCoeff(*bm, &src, &dst)) {
275 if (bg && (dst == SkBlendModeCoeff::kOne ||
276 dst == SkBlendModeCoeff::kISA ||
277 dst == SkBlendModeCoeff::kISC)) {
278 return bg;
279 }
280 if (fg && (src == SkBlendModeCoeff::kOne ||
281 src == SkBlendModeCoeff::kIDA)) {
282 return fg;
283 }
284 }
285 }
286 // If we made it this far, the blend has non-trivial behavior even when one of the
287 // inputs is transparent black, so replace the null shaders with that color.
288 if (!bg) { bg = SkShaders::Color(SK_ColorTRANSPARENT); }
289 if (!fg) { fg = SkShaders::Color(SK_ColorTRANSPARENT); }
290 }
291
292 return SkShaders::Blend(fBlender, std::move(bg), std::move(fg));
293 }
294
onFilterImage(const skif::Context & ctx) const295 skif::FilterResult SkBlendImageFilter::onFilterImage(const skif::Context& ctx) const {
296 // We could just request 'desiredOutput' for the blend's required input size, since that's what
297 // it is expected to fill. However, some blend modes restrict the output to something other
298 // than the union of the foreground and background. To make this restriction available to both
299 // children before evaluating them, we determine the maximum possible output the blend can
300 // produce from the contentBounds and require that for both children to produce.
301 auto requiredInput = this->onGetOutputLayerBounds(ctx.mapping(), ctx.source().layerBounds());
302 if (requiredInput) {
303 if (!requiredInput->intersect(ctx.desiredOutput())) {
304 return {};
305 }
306 } else {
307 requiredInput = ctx.desiredOutput();
308 }
309
310 skif::Context inputCtx = ctx.withNewDesiredOutput(*requiredInput);
311 skif::FilterResult::Builder builder{ctx};
312 builder.add(this->getChildOutput(kBackground, inputCtx));
313 builder.add(this->getChildOutput(kForeground, inputCtx));
314 return builder.eval(
315 [&](SkSpan<sk_sp<SkShader>> inputs) -> sk_sp<SkShader> {
316 return this->makeBlendShader(inputs[kBackground], inputs[kForeground]);
317 }, requiredInput);
318 }
319
onGetInputLayerBounds(const skif::Mapping & mapping,const skif::LayerSpace<SkIRect> & desiredOutput,std::optional<skif::LayerSpace<SkIRect>> contentBounds) const320 skif::LayerSpace<SkIRect> SkBlendImageFilter::onGetInputLayerBounds(
321 const skif::Mapping& mapping,
322 const skif::LayerSpace<SkIRect>& desiredOutput,
323 std::optional<skif::LayerSpace<SkIRect>> contentBounds) const {
324
325 skif::LayerSpace<SkIRect> requiredInput;
326 std::optional<skif::LayerSpace<SkIRect>> maxOutput;
327 if (contentBounds && (maxOutput = this->onGetOutputLayerBounds(mapping, *contentBounds))) {
328 // See comment in onFilterImage().
329 requiredInput = *maxOutput;
330 if (!requiredInput.intersect(desiredOutput)) {
331 // Don't bother recursing if we know the blend will discard everything
332 return skif::LayerSpace<SkIRect>::Empty();
333 }
334 } else {
335 // The content and/or the output of the child are unbounded so the intersection with the
336 // desired output is simply the desired output.
337 requiredInput = desiredOutput;
338 }
339
340 // Return the union of both FG and BG required inputs to ensure both have all necessary pixels
341 skif::LayerSpace<SkIRect> bgInput =
342 this->getChildInputLayerBounds(kBackground, mapping, requiredInput, contentBounds);
343 skif::LayerSpace<SkIRect> fgInput =
344 this->getChildInputLayerBounds(kForeground, mapping, requiredInput, contentBounds);
345
346 bgInput.join(fgInput);
347 return bgInput;
348 }
349
onGetOutputLayerBounds(const skif::Mapping & mapping,std::optional<skif::LayerSpace<SkIRect>> contentBounds) const350 std::optional<skif::LayerSpace<SkIRect>> SkBlendImageFilter::onGetOutputLayerBounds(
351 const skif::Mapping& mapping,
352 std::optional<skif::LayerSpace<SkIRect>> contentBounds) const {
353 // Blending is (k0*FG*BG + k1*FG + k2*BG + k3) for arithmetic blenders OR
354 // ( 0*FG*BG + srcCoeff*FG + dstCoeff*BG + 0 ) for Porter-Duff blend modes OR
355 // un-inspectable(FG, BG) for advanced blend modes and other runtime blenders.
356 //
357 // There are six possible output bounds that can be produced:
358 // 1. No output: K = (0,0,0,0) or (srcCoeff,dstCoeff) = (kZero,kZero)
359 // 2. intersect(FG,BG): K = (non-zero, 0,0,0) or (srcCoeff,dstCoeff) = (kZero|kDA, kZero|kSA)
360 // 3. FG-only: K = (0, non-zero, 0,0) or (srcCoeff,dstCoeff) = (!kZero&!kDA, kZero|kSA)
361 // 4. BG-only: K = (0,0, non-zero, 0) or (srcCoeff,dstCoeff) = (kZero|kDA, !kZero&!kSA)
362 // 5. union(FG,BG): K = (*,*,*,0) or (srcCoeff,dstCoeff) = (!kZero&!kDA, !kZero&!kSA)
363 // or an advanced blend mode.
364 // 6. infinite: K = (*,*,*, non-zero) or a runtime blender other than SkBlenders::Arithmetic.
365 bool transparentOutsideFG = false;
366 bool transparentOutsideBG = false;
367 if (auto bm = as_BB(fBlender)->asBlendMode()) {
368 SkASSERT(*bm != SkBlendMode::kClear); // Should have been caught at creation time
369 SkBlendModeCoeff src, dst;
370 if (SkBlendMode_AsCoeff(*bm, &src, &dst)) {
371 // If dst's coefficient is 0 then nothing can produce non-transparent content outside
372 // of the foreground. When dst coefficient is SA, it will always be 0 outside the FG.
373 // For purposes of transparency analysis, SC == SA.
374 transparentOutsideFG = dst == SkBlendModeCoeff::kZero || dst == SkBlendModeCoeff::kSA
375 || dst == SkBlendModeCoeff::kSC;
376 // And the reverse is true for src and the background content.
377 transparentOutsideBG = src == SkBlendModeCoeff::kZero || src == SkBlendModeCoeff::kDA;
378 }
379 // NOTE: advanced blends use src-over for their alpha channel, which should produce the
380 // union of FG and BG. That is the outcome if we leave transparentOutsideFG/BG false.
381 } else if (fArithmeticCoefficients.has_value()) {
382 [[maybe_unused]] static constexpr SkV4 kClearCoeff = {0.f, 0.f, 0.f, 0.f};
383 const SkV4& k = *fArithmeticCoefficients;
384 SkASSERT(k != kClearCoeff); // Should have been converted to an empty filter
385
386 if (k[3] != 0.f) {
387 // The arithmetic equation produces non-transparent black everywhere
388 return skif::LayerSpace<SkIRect>::Unbounded();
389 } else {
390 // Given the earlier assert and if, then (k[1] == k[2] == 0) implies k[0] != 0. If only
391 // one of k[1] or k[2] are non-zero then, regardless of k[0], then only that bounds
392 // has non-transparent content.
393 transparentOutsideFG = k[2] == 0.f;
394 transparentOutsideBG = k[1] == 0.f;
395 }
396 } else {
397 // A non-arithmetic runtime blender, so pessimistically assume it can return non-transparent
398 // black anywhere.
399 return skif::LayerSpace<SkIRect>::Unbounded();
400 }
401
402 auto foregroundBounds = this->getChildOutputLayerBounds(kForeground, mapping, contentBounds);
403 auto backgroundBounds = this->getChildOutputLayerBounds(kBackground, mapping, contentBounds);
404 if (transparentOutsideFG) {
405 if (transparentOutsideBG) {
406 // Output is the intersection of both
407 if (!foregroundBounds && backgroundBounds) {
408 foregroundBounds = *backgroundBounds;
409 } else if (backgroundBounds && !foregroundBounds->intersect(*backgroundBounds)) {
410 return skif::LayerSpace<SkIRect>::Empty();
411 }
412 // When both fore and background are infinite, foregroundBounds remains uninstantiated.
413 // When only foreground is provided, it's left unmodified, which is the correct result.
414 }
415 return foregroundBounds;
416 } else {
417 if (!transparentOutsideBG) {
418 // Output is the union of both (infinite blend-induced bounds were detected earlier).
419 if (foregroundBounds && backgroundBounds) {
420 backgroundBounds->join(*foregroundBounds);
421 } else {
422 // At least one of the union arguments is unbounded, so the union is infinite
423 backgroundBounds.reset();
424 }
425 }
426 return backgroundBounds;
427 }
428 }
429
computeFastBounds(const SkRect & bounds) const430 SkRect SkBlendImageFilter::computeFastBounds(const SkRect& bounds) const {
431 // TODO: This is a prime example of why computeFastBounds() and onGetOutputLayerBounds() should
432 // be combined into the same function.
433 bool transparentOutsideFG = false;
434 bool transparentOutsideBG = false;
435 if (auto bm = as_BB(fBlender)->asBlendMode()) {
436 SkASSERT(*bm != SkBlendMode::kClear); // Should have been caught at creation time
437 SkBlendModeCoeff src, dst;
438 if (SkBlendMode_AsCoeff(*bm, &src, &dst)) {
439 // If dst's coefficient is 0 then nothing can produce non-transparent content outside
440 // of the foreground. When dst coefficient is SA, it will always be 0 outside the FG.
441 transparentOutsideFG = dst == SkBlendModeCoeff::kZero || dst == SkBlendModeCoeff::kSA;
442 // And the reverse is true for src and the background content.
443 transparentOutsideBG = src == SkBlendModeCoeff::kZero || src == SkBlendModeCoeff::kDA;
444 }
445 } else if (fArithmeticCoefficients.has_value()) {
446 [[maybe_unused]] static constexpr SkV4 kClearCoeff = {0.f, 0.f, 0.f, 0.f};
447 const SkV4& k = *fArithmeticCoefficients;
448 SkASSERT(k != kClearCoeff); // Should have been converted to an empty image filter
449
450 if (k[3] != 0.f) {
451 // The arithmetic equation produces non-transparent black everywhere
452 return SkRectPriv::MakeLargeS32();
453 } else {
454 // Given the earlier assert and if, then (k[1] == k[2] == 0) implies k[0] != 0. If only
455 // one of k[1] or k[2] are non-zero then, regardless of k[0], then only that bounds
456 // has non-transparent content.
457 transparentOutsideFG = k[2] == 0.f;
458 transparentOutsideBG = k[1] == 0.f;
459 }
460 } else {
461 // A non-arithmetic runtime blender, so pessimistically assume it can return non-transparent
462 // black anywhere.
463 return SkRectPriv::MakeLargeS32();
464 }
465
466 SkRect foregroundBounds = this->getInput(kForeground) ?
467 this->getInput(kForeground)->computeFastBounds(bounds) : bounds;
468 SkRect backgroundBounds = this->getInput(kBackground) ?
469 this->getInput(kBackground)->computeFastBounds(bounds) : bounds;
470 if (transparentOutsideFG) {
471 if (transparentOutsideBG) {
472 // Output is the intersection of both
473 if (!foregroundBounds.intersect(backgroundBounds)) {
474 return SkRect::MakeEmpty();
475 }
476 }
477 return foregroundBounds;
478 } else {
479 if (!transparentOutsideBG) {
480 // Output is the union of both (infinite bounds were detected earlier).
481 backgroundBounds.join(foregroundBounds);
482 }
483 return backgroundBounds;
484 }
485 }
486