Home | History | Annotate | Download | only in gm
      1 /*
      2  * Copyright 2014 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 
      8 #include "gm.h"
      9 #include "sk_tool_utils.h"
     10 
     11 #include "SkColorFilter.h"
     12 #include "SkMultiPictureDraw.h"
     13 #include "SkPictureRecorder.h"
     14 #include "SkSurface.h"
     15 
     16 constexpr SkScalar kRoot3Over2 = 0.86602545f;  // sin(60)
     17 constexpr SkScalar kRoot3      = 1.73205081f;
     18 
     19 constexpr int kHexSide = 30;
     20 constexpr int kNumHexX = 6;
     21 constexpr int kNumHexY = 6;
     22 constexpr int kPicWidth = kNumHexX * kHexSide;
     23 constexpr int kPicHeight = (int)((kNumHexY - 0.5f) * 2 * kHexSide * kRoot3Over2 + 0.5f);
     24 constexpr SkScalar kInset = 20.0f;
     25 constexpr int kNumPictures = 4;
     26 
     27 constexpr int kTriSide = 40;
     28 
     29 // Create a hexagon centered at (originX, originY)
     30 static SkPath make_hex_path(SkScalar originX, SkScalar originY) {
     31     SkPath hex;
     32     hex.moveTo(originX-kHexSide, originY);
     33     hex.rLineTo(SkScalarHalf(kHexSide), kRoot3Over2 * kHexSide);
     34     hex.rLineTo(SkIntToScalar(kHexSide), 0);
     35     hex.rLineTo(SkScalarHalf(kHexSide), -kHexSide * kRoot3Over2);
     36     hex.rLineTo(-SkScalarHalf(kHexSide), -kHexSide * kRoot3Over2);
     37     hex.rLineTo(-SkIntToScalar(kHexSide), 0);
     38     hex.close();
     39     return hex;
     40 }
     41 
     42 // Make a picture that is a tiling of the plane with stroked hexagons where
     43 // each hexagon is in its own layer. The layers are to exercise Ganesh's
     44 // layer hoisting.
     45 static sk_sp<SkPicture> make_hex_plane_picture(SkColor fillColor) {
     46 
     47     // Create a hexagon with its center at the origin
     48     SkPath hex = make_hex_path(0, 0);
     49 
     50     SkPaint fill;
     51     fill.setStyle(SkPaint::kFill_Style);
     52     fill.setColor(fillColor);
     53 
     54     SkPaint stroke;
     55     stroke.setStyle(SkPaint::kStroke_Style);
     56     stroke.setStrokeWidth(3);
     57 
     58     SkPictureRecorder recorder;
     59     SkRTreeFactory bbhFactory;
     60 
     61     SkCanvas* canvas = recorder.beginRecording(SkIntToScalar(kPicWidth),
     62                                                SkIntToScalar(kPicHeight),
     63                                                &bbhFactory);
     64 
     65     SkScalar xPos, yPos = 0;
     66 
     67     for (int y = 0; y < kNumHexY; ++y) {
     68         xPos = 0;
     69 
     70         for (int x = 0; x < kNumHexX; ++x) {
     71             canvas->saveLayer(nullptr, nullptr);
     72             canvas->translate(xPos, yPos + ((x % 2) ? kRoot3Over2 * kHexSide : 0));
     73             canvas->drawPath(hex, fill);
     74             canvas->drawPath(hex, stroke);
     75             canvas->restore();
     76 
     77             xPos += 1.5f * kHexSide;
     78         }
     79 
     80         yPos += 2 * kHexSide * kRoot3Over2;
     81     }
     82 
     83     return recorder.finishRecordingAsPicture();
     84 }
     85 
     86 // Create a picture that consists of a single large layer that is tiled
     87 // with hexagons.
     88 // This is intended to exercise the layer hoisting code's clip handling (in
     89 // tile mode).
     90 static sk_sp<SkPicture> make_single_layer_hex_plane_picture() {
     91 
     92     // Create a hexagon with its center at the origin
     93     SkPath hex = make_hex_path(0, 0);
     94 
     95     SkPaint whiteFill;
     96     whiteFill.setStyle(SkPaint::kFill_Style);
     97     whiteFill.setColor(SK_ColorWHITE);
     98 
     99     SkPaint greyFill;
    100     greyFill.setStyle(SkPaint::kFill_Style);
    101     greyFill.setColor(sk_tool_utils::color_to_565(SK_ColorLTGRAY));
    102 
    103     SkPaint stroke;
    104     stroke.setStyle(SkPaint::kStroke_Style);
    105     stroke.setStrokeWidth(3);
    106 
    107     SkPictureRecorder recorder;
    108     SkRTreeFactory bbhFactory;
    109 
    110     constexpr SkScalar kBig = 10000.0f;
    111     SkCanvas* canvas = recorder.beginRecording(kBig, kBig, &bbhFactory);
    112 
    113     canvas->saveLayer(nullptr, nullptr);
    114 
    115     SkScalar xPos = 0.0f, yPos = 0.0f;
    116 
    117     for (int y = 0; yPos < kBig; ++y) {
    118         xPos = 0;
    119 
    120         for (int x = 0; xPos < kBig; ++x) {
    121             canvas->save();
    122             canvas->translate(xPos, yPos + ((x % 2) ? kRoot3Over2 * kHexSide : 0));
    123             // The color of the filled hex is swapped to yield a different
    124             // pattern in each tile. This allows an error in layer hoisting (e.g.,
    125             // the clip isn't blocking cache reuse) to cause a visual discrepancy.
    126             canvas->drawPath(hex, ((x+y) % 3) ? whiteFill : greyFill);
    127             canvas->drawPath(hex, stroke);
    128             canvas->restore();
    129 
    130             xPos += 1.5f * kHexSide;
    131         }
    132 
    133         yPos += 2 * kHexSide * kRoot3Over2;
    134     }
    135 
    136     canvas->restore();
    137 
    138     return recorder.finishRecordingAsPicture();
    139 }
    140 
    141 // Make an equilateral triangle path with its top corner at (originX, originY)
    142 static SkPath make_tri_path(SkScalar originX, SkScalar originY) {
    143     SkPath tri;
    144     tri.moveTo(originX, originY);
    145     tri.rLineTo(SkScalarHalf(kTriSide), 1.5f * kTriSide / kRoot3);
    146     tri.rLineTo(-kTriSide, 0);
    147     tri.close();
    148     return tri;
    149 }
    150 
    151 static sk_sp<SkPicture> make_tri_picture() {
    152     SkPath tri = make_tri_path(SkScalarHalf(kTriSide), 0);
    153 
    154     SkPaint fill;
    155     fill.setStyle(SkPaint::kFill_Style);
    156     fill.setColor(sk_tool_utils::color_to_565(SK_ColorLTGRAY));
    157 
    158     SkPaint stroke;
    159     stroke.setStyle(SkPaint::kStroke_Style);
    160     stroke.setStrokeWidth(3);
    161 
    162     SkPictureRecorder recorder;
    163     SkRTreeFactory bbhFactory;
    164 
    165     SkCanvas* canvas = recorder.beginRecording(SkIntToScalar(kPicWidth),
    166                                                SkIntToScalar(kPicHeight),
    167                                                &bbhFactory);
    168     SkRect r = tri.getBounds();
    169     r.outset(2.0f, 2.0f);       // outset for stroke
    170     canvas->clipRect(r);
    171     // The saveLayer/restore block is to exercise layer hoisting
    172     canvas->saveLayer(nullptr, nullptr);
    173         canvas->drawPath(tri, fill);
    174         canvas->drawPath(tri, stroke);
    175     canvas->restore();
    176 
    177     return recorder.finishRecordingAsPicture();
    178 }
    179 
    180 static sk_sp<SkPicture> make_sub_picture(const SkPicture* tri) {
    181     SkPictureRecorder recorder;
    182     SkRTreeFactory bbhFactory;
    183 
    184     SkCanvas* canvas = recorder.beginRecording(SkIntToScalar(kPicWidth),
    185                                                SkIntToScalar(kPicHeight),
    186                                                &bbhFactory);
    187 
    188     canvas->scale(1.0f/2.0f, 1.0f/2.0f);
    189 
    190     canvas->save();
    191     canvas->translate(SkScalarHalf(kTriSide), 0);
    192     canvas->drawPicture(tri);
    193     canvas->restore();
    194 
    195     canvas->save();
    196     canvas->translate(SkIntToScalar(kTriSide), 1.5f * kTriSide / kRoot3);
    197     canvas->drawPicture(tri);
    198     canvas->restore();
    199 
    200     canvas->save();
    201     canvas->translate(0, 1.5f * kTriSide / kRoot3);
    202     canvas->drawPicture(tri);
    203     canvas->restore();
    204 
    205     return recorder.finishRecordingAsPicture();
    206 }
    207 
    208 // Create a Sierpinkski-like picture that starts with a top row with a picture
    209 // that just contains a triangle. Subsequent rows take the prior row's picture,
    210 // shrinks it and replicates it 3 times then draws and appropriate number of
    211 // copies of it.
    212 static sk_sp<SkPicture> make_sierpinski_picture() {
    213     sk_sp<SkPicture> pic(make_tri_picture());
    214 
    215     SkPictureRecorder recorder;
    216     SkRTreeFactory bbhFactory;
    217 
    218     SkCanvas* canvas = recorder.beginRecording(SkIntToScalar(kPicWidth),
    219                                                SkIntToScalar(kPicHeight),
    220                                                &bbhFactory);
    221 
    222     constexpr int kNumLevels = 4;
    223     for (int i = 0; i < kNumLevels; ++i) {
    224         canvas->save();
    225             canvas->translate(kPicWidth/2 - (i+1) * (kTriSide/2.0f), 0.0f);
    226             for (int j = 0; j < i+1; ++j) {
    227                 canvas->drawPicture(pic);
    228                 canvas->translate(SkIntToScalar(kTriSide), 0);
    229             }
    230         canvas->restore();
    231 
    232         pic = make_sub_picture(pic.get());
    233 
    234         canvas->translate(0, 1.5f * kTriSide / kRoot3);
    235     }
    236 
    237     return recorder.finishRecordingAsPicture();
    238 }
    239 
    240 static sk_sp<SkSurface> create_compat_surface(SkCanvas* canvas, int width, int height) {
    241     SkImageInfo info = SkImageInfo::MakeN32Premul(width, height);
    242 
    243     auto surface = canvas->makeSurface(info);
    244     if (nullptr == surface) {
    245         // picture canvas returns nullptr so fall back to raster
    246         surface = SkSurface::MakeRaster(info);
    247     }
    248     return surface;
    249 }
    250 
    251 // This class stores the information required to compose all the result
    252 // fragments potentially generated by the MultiPictureDraw object
    253 class ComposeStep {
    254 public:
    255     ComposeStep() : fX(0.0f), fY(0.0f), fPaint(nullptr) { }
    256     ~ComposeStep() {
    257         delete fPaint;
    258     }
    259 
    260     sk_sp<SkSurface> fSurf;
    261     SkScalar   fX;
    262     SkScalar   fY;
    263     SkPaint*   fPaint;
    264 };
    265 
    266 typedef void (*PFContentMtd)(SkCanvas* canvas, const SkPicture* pictures[kNumPictures]);
    267 
    268 // Just a single picture with no clip
    269 static void no_clip(SkCanvas* canvas, const SkPicture* pictures[kNumPictures]) {
    270     canvas->drawPicture(pictures[0]);
    271 }
    272 
    273 // Two pictures with a rect clip on the second one
    274 static void rect_clip(SkCanvas* canvas, const SkPicture* pictures[kNumPictures]) {
    275     canvas->drawPicture(pictures[0]);
    276 
    277     SkRect rect = pictures[0]->cullRect();
    278     rect.inset(kInset, kInset);
    279 
    280     canvas->clipRect(rect);
    281 
    282     canvas->drawPicture(pictures[1]);
    283 }
    284 
    285 // Two pictures with a round rect clip on the second one
    286 static void rrect_clip(SkCanvas* canvas, const SkPicture* pictures[kNumPictures]) {
    287     canvas->drawPicture(pictures[0]);
    288 
    289     SkRect rect = pictures[0]->cullRect();
    290     rect.inset(kInset, kInset);
    291 
    292     SkRRect rrect;
    293     rrect.setRectXY(rect, kInset, kInset);
    294 
    295     canvas->clipRRect(rrect);
    296 
    297     canvas->drawPicture(pictures[1]);
    298 }
    299 
    300 // Two pictures with a clip path on the second one
    301 static void path_clip(SkCanvas* canvas, const SkPicture* pictures[kNumPictures]) {
    302     canvas->drawPicture(pictures[0]);
    303 
    304     // Create a hexagon centered on the middle of the hex grid
    305     SkPath hex = make_hex_path((kNumHexX / 2.0f) * kHexSide, kNumHexY * kHexSide * kRoot3Over2);
    306 
    307     canvas->clipPath(hex);
    308 
    309     canvas->drawPicture(pictures[1]);
    310 }
    311 
    312 // Two pictures with an inverse clip path on the second one
    313 static void invpath_clip(SkCanvas* canvas, const SkPicture* pictures[kNumPictures]) {
    314     canvas->drawPicture(pictures[0]);
    315 
    316     // Create a hexagon centered on the middle of the hex grid
    317     SkPath hex = make_hex_path((kNumHexX / 2.0f) * kHexSide, kNumHexY * kHexSide * kRoot3Over2);
    318     hex.setFillType(SkPath::kInverseEvenOdd_FillType);
    319 
    320     canvas->clipPath(hex);
    321 
    322     canvas->drawPicture(pictures[1]);
    323 }
    324 
    325 // Reuse a single base (triangular) picture a _lot_ (rotated, scaled and translated).
    326 static void sierpinski(SkCanvas* canvas, const SkPicture* pictures[kNumPictures]) {
    327     canvas->save();
    328         canvas->drawPicture(pictures[2]);
    329 
    330         canvas->rotate(180.0f);
    331         canvas->translate(-SkIntToScalar(kPicWidth), -SkIntToScalar(kPicHeight));
    332         canvas->drawPicture(pictures[2]);
    333     canvas->restore();
    334 }
    335 
    336 static void big_layer(SkCanvas* canvas, const SkPicture* pictures[kNumPictures]) {
    337     canvas->drawPicture(pictures[3]);
    338 }
    339 
    340 constexpr PFContentMtd gContentMthds[] = {
    341     no_clip,
    342     rect_clip,
    343     rrect_clip,
    344     path_clip,
    345     invpath_clip,
    346     sierpinski,
    347     big_layer,
    348 };
    349 
    350 static void create_content(SkMultiPictureDraw* mpd, PFContentMtd pfGen,
    351                            const SkPicture* pictures[kNumPictures],
    352                            SkCanvas* dest, const SkMatrix& xform) {
    353     sk_sp<SkPicture> composite;
    354 
    355     {
    356         SkPictureRecorder recorder;
    357         SkRTreeFactory bbhFactory;
    358 
    359         SkCanvas* pictureCanvas = recorder.beginRecording(SkIntToScalar(kPicWidth),
    360                                                           SkIntToScalar(kPicHeight),
    361                                                           &bbhFactory);
    362 
    363         (*pfGen)(pictureCanvas, pictures);
    364 
    365         composite = recorder.finishRecordingAsPicture();
    366     }
    367 
    368     mpd->add(dest, composite.get(), &xform);
    369 }
    370 
    371 typedef void(*PFLayoutMtd)(SkCanvas* finalCanvas, SkMultiPictureDraw* mpd,
    372                            PFContentMtd pfGen, const SkPicture* pictures[kNumPictures],
    373                            SkTArray<ComposeStep>* composeSteps);
    374 
    375 // Draw the content into a single canvas
    376 static void simple(SkCanvas* finalCanvas, SkMultiPictureDraw* mpd,
    377                    PFContentMtd pfGen,
    378                    const SkPicture* pictures[kNumPictures],
    379                    SkTArray<ComposeStep> *composeSteps) {
    380 
    381     ComposeStep& step = composeSteps->push_back();
    382 
    383     step.fSurf = create_compat_surface(finalCanvas, kPicWidth, kPicHeight);
    384 
    385     SkCanvas* subCanvas = step.fSurf->getCanvas();
    386 
    387     create_content(mpd, pfGen, pictures, subCanvas, SkMatrix::I());
    388 }
    389 
    390 // Draw the content into multiple canvases/tiles
    391 static void tiled(SkCanvas* finalCanvas, SkMultiPictureDraw* mpd,
    392                   PFContentMtd pfGen,
    393                   const SkPicture* pictures[kNumPictures],
    394                   SkTArray<ComposeStep> *composeSteps) {
    395     const int kNumTilesX = 2;
    396     const int kNumTilesY = 2;
    397     const int kTileWidth = kPicWidth / kNumTilesX;
    398     const int kTileHeight = kPicHeight / kNumTilesY;
    399 
    400     SkASSERT(kPicWidth == kNumTilesX * kTileWidth);
    401     SkASSERT(kPicHeight == kNumTilesY * kTileHeight);
    402 
    403     const SkColor colors[kNumTilesX][kNumTilesY] = {
    404         { SK_ColorCYAN,   SK_ColorMAGENTA },
    405         { SK_ColorYELLOW, SK_ColorGREEN   }
    406     };
    407 
    408     for (int y = 0; y < kNumTilesY; ++y) {
    409         for (int x = 0; x < kNumTilesX; ++x) {
    410             ComposeStep& step = composeSteps->push_back();
    411 
    412             step.fX = SkIntToScalar(x*kTileWidth);
    413             step.fY = SkIntToScalar(y*kTileHeight);
    414             step.fPaint = new SkPaint;
    415             step.fPaint->setColorFilter(
    416                 SkColorFilter::MakeModeFilter(colors[x][y], SkBlendMode::kModulate));
    417 
    418             step.fSurf = create_compat_surface(finalCanvas, kTileWidth, kTileHeight);
    419 
    420             SkCanvas* subCanvas = step.fSurf->getCanvas();
    421 
    422             const SkMatrix trans = SkMatrix::MakeTrans(-SkIntToScalar(x*kTileWidth),
    423                                                        -SkIntToScalar(y*kTileHeight));
    424 
    425             create_content(mpd, pfGen, pictures, subCanvas, trans);
    426         }
    427     }
    428 }
    429 
    430 constexpr PFLayoutMtd gLayoutMthds[] = { simple, tiled };
    431 
    432 namespace skiagm {
    433     /**
    434      * This GM exercises the SkMultiPictureDraw object. It tests the
    435      * cross product of:
    436      *      tiled vs. all-at-once rendering (e.g., into many or just 1 canvas)
    437      *      different clips (e.g., none, rect, rrect)
    438      *      single vs. multiple pictures (e.g., normal vs. picture-pile-style content)
    439      */
    440     class MultiPictureDraw : public GM {
    441     public:
    442         enum Content {
    443             kNoClipSingle_Content,
    444             kRectClipMulti_Content,
    445             kRRectClipMulti_Content,
    446             kPathClipMulti_Content,
    447             kInvPathClipMulti_Content,
    448             kSierpinski_Content,
    449             kBigLayer_Content,
    450 
    451             kLast_Content = kBigLayer_Content
    452         };
    453 
    454         const int kContentCnt = kLast_Content + 1;
    455 
    456         enum Layout {
    457             kSimple_Layout,
    458             kTiled_Layout,
    459 
    460             kLast_Layout = kTiled_Layout
    461         };
    462 
    463         const int kLayoutCnt = kLast_Layout + 1;
    464 
    465         MultiPictureDraw(Content content, Layout layout) : fContent(content), fLayout(layout) {
    466             SkASSERT(SK_ARRAY_COUNT(gLayoutMthds) == kLayoutCnt);
    467             SkASSERT(SK_ARRAY_COUNT(gContentMthds) == kContentCnt);
    468 
    469             for (int i = 0; i < kNumPictures; ++i) {
    470                 fPictures[i] = nullptr;
    471             }
    472         }
    473 
    474         ~MultiPictureDraw() override {
    475             for (int i = 0; i < kNumPictures; ++i) {
    476                 SkSafeUnref(fPictures[i]);
    477             }
    478         }
    479 
    480     protected:
    481         Content          fContent;
    482         Layout           fLayout;
    483         const SkPicture* fPictures[kNumPictures];
    484 
    485         void onOnceBeforeDraw() override {
    486             fPictures[0] = make_hex_plane_picture(SK_ColorWHITE).release();
    487             fPictures[1] = make_hex_plane_picture(sk_tool_utils::color_to_565(SK_ColorGRAY)).release();
    488             fPictures[2] = make_sierpinski_picture().release();
    489             fPictures[3] = make_single_layer_hex_plane_picture().release();
    490         }
    491 
    492         void onDraw(SkCanvas* canvas) override {
    493             SkMultiPictureDraw mpd;
    494             SkTArray<ComposeStep> composeSteps;
    495 
    496             // Fill up the MultiPictureDraw
    497             (*gLayoutMthds[fLayout])(canvas, &mpd,
    498                                      gContentMthds[fContent],
    499                                      fPictures, &composeSteps);
    500 
    501             mpd.draw();
    502 
    503             // Compose all the drawn canvases into the final canvas
    504             for (int i = 0; i < composeSteps.count(); ++i) {
    505                 const ComposeStep& step = composeSteps[i];
    506 
    507                 canvas->drawImage(step.fSurf->makeImageSnapshot().get(),
    508                                   step.fX, step.fY, step.fPaint);
    509             }
    510         }
    511 
    512         SkISize onISize() override { return SkISize::Make(kPicWidth, kPicHeight); }
    513 
    514         SkString onShortName() override {
    515             const char* gContentNames[] = {
    516                 "noclip", "rectclip", "rrectclip", "pathclip",
    517                 "invpathclip", "sierpinski", "biglayer"
    518             };
    519             const char* gLayoutNames[] = { "simple", "tiled" };
    520 
    521             SkASSERT(SK_ARRAY_COUNT(gLayoutNames) == kLayoutCnt);
    522             SkASSERT(SK_ARRAY_COUNT(gContentNames) == kContentCnt);
    523 
    524             SkString name("multipicturedraw_");
    525 
    526             name.append(gContentNames[fContent]);
    527             name.append("_");
    528             name.append(gLayoutNames[fLayout]);
    529             return name;
    530         }
    531 
    532         bool runAsBench() const override { return true; }
    533 
    534     private:
    535         typedef GM INHERITED;
    536     };
    537 
    538     DEF_GM(return new MultiPictureDraw(MultiPictureDraw::kNoClipSingle_Content,
    539                                        MultiPictureDraw::kSimple_Layout);)
    540     DEF_GM(return new MultiPictureDraw(MultiPictureDraw::kRectClipMulti_Content,
    541                                        MultiPictureDraw::kSimple_Layout);)
    542     DEF_GM(return new MultiPictureDraw(MultiPictureDraw::kRRectClipMulti_Content,
    543                                        MultiPictureDraw::kSimple_Layout);)
    544     DEF_GM(return new MultiPictureDraw(MultiPictureDraw::kPathClipMulti_Content,
    545                                        MultiPictureDraw::kSimple_Layout);)
    546     DEF_GM(return new MultiPictureDraw(MultiPictureDraw::kInvPathClipMulti_Content,
    547                                        MultiPictureDraw::kSimple_Layout);)
    548     DEF_GM(return new MultiPictureDraw(MultiPictureDraw::kSierpinski_Content,
    549                                        MultiPictureDraw::kSimple_Layout);)
    550     DEF_GM(return new MultiPictureDraw(MultiPictureDraw::kBigLayer_Content,
    551                                        MultiPictureDraw::kSimple_Layout);)
    552 
    553     DEF_GM(return new MultiPictureDraw(MultiPictureDraw::kNoClipSingle_Content,
    554                                        MultiPictureDraw::kTiled_Layout);)
    555     DEF_GM(return new MultiPictureDraw(MultiPictureDraw::kRectClipMulti_Content,
    556                                        MultiPictureDraw::kTiled_Layout);)
    557     DEF_GM(return new MultiPictureDraw(MultiPictureDraw::kRRectClipMulti_Content,
    558                                        MultiPictureDraw::kTiled_Layout);)
    559     DEF_GM(return new MultiPictureDraw(MultiPictureDraw::kPathClipMulti_Content,
    560                                        MultiPictureDraw::kTiled_Layout);)
    561     DEF_GM(return new MultiPictureDraw(MultiPictureDraw::kInvPathClipMulti_Content,
    562                                        MultiPictureDraw::kTiled_Layout);)
    563     DEF_GM(return new MultiPictureDraw(MultiPictureDraw::kSierpinski_Content,
    564                                        MultiPictureDraw::kTiled_Layout);)
    565     DEF_GM(return new MultiPictureDraw(MultiPictureDraw::kBigLayer_Content,
    566                                        MultiPictureDraw::kTiled_Layout);)
    567 }
    568