1 // Copyright 2017 PDFium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 // Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com 6 7 #include "xfa/fxfa/parser/cxfa_rectangle.h" 8 9 #include <utility> 10 11 #include "fxjs/xfa/cjx_rectangle.h" 12 #include "third_party/base/ptr_util.h" 13 #include "xfa/fxfa/parser/cxfa_corner.h" 14 #include "xfa/fxfa/parser/cxfa_stroke.h" 15 16 namespace { 17 18 const CXFA_Node::PropertyData kPropertyData[] = {{XFA_Element::Edge, 4, 0}, 19 {XFA_Element::Corner, 4, 0}, 20 {XFA_Element::Fill, 1, 0}, 21 {XFA_Element::Unknown, 0, 0}}; 22 const CXFA_Node::AttributeData kAttributeData[] = { 23 {XFA_Attribute::Id, XFA_AttributeType::CData, nullptr}, 24 {XFA_Attribute::Use, XFA_AttributeType::CData, nullptr}, 25 {XFA_Attribute::Usehref, XFA_AttributeType::CData, nullptr}, 26 {XFA_Attribute::Hand, XFA_AttributeType::Enum, 27 (void*)XFA_AttributeEnum::Even}, 28 {XFA_Attribute::Unknown, XFA_AttributeType::Integer, nullptr}}; 29 30 constexpr wchar_t kName[] = L"rectangle"; 31 32 } // namespace 33 34 CXFA_Rectangle::CXFA_Rectangle(CXFA_Document* doc, XFA_PacketType packet) 35 : CXFA_Box(doc, 36 packet, 37 (XFA_XDPPACKET_Template | XFA_XDPPACKET_Form), 38 XFA_ObjectType::Node, 39 XFA_Element::Rectangle, 40 kPropertyData, 41 kAttributeData, 42 kName, 43 pdfium::MakeUnique<CJX_Rectangle>(this)) {} 44 45 CXFA_Rectangle::CXFA_Rectangle(CXFA_Document* pDoc, 46 XFA_PacketType ePacket, 47 uint32_t validPackets, 48 XFA_ObjectType oType, 49 XFA_Element eType, 50 const PropertyData* properties, 51 const AttributeData* attributes, 52 const WideStringView& elementName, 53 std::unique_ptr<CJX_Object> js_node) 54 : CXFA_Box(pDoc, 55 ePacket, 56 validPackets, 57 oType, 58 eType, 59 properties, 60 attributes, 61 elementName, 62 std::move(js_node)) {} 63 64 CXFA_Rectangle::~CXFA_Rectangle() {} 65 66 void CXFA_Rectangle::GetFillPath(const std::vector<CXFA_Stroke*>& strokes, 67 const CFX_RectF& rtWidget, 68 CXFA_GEPath* fillPath) { 69 bool bSameStyles = true; 70 CXFA_Stroke* stroke1 = strokes[0]; 71 for (int32_t i = 1; i < 8; i++) { 72 CXFA_Stroke* stroke2 = strokes[i]; 73 if (!stroke1->SameStyles(stroke2, 0)) { 74 bSameStyles = false; 75 break; 76 } 77 stroke1 = stroke2; 78 } 79 80 if (bSameStyles) { 81 stroke1 = strokes[0]; 82 for (int32_t i = 2; i < 8; i += 2) { 83 CXFA_Stroke* stroke2 = strokes[i]; 84 if (!stroke1->SameStyles(stroke2, XFA_STROKE_SAMESTYLE_NoPresence | 85 XFA_STROKE_SAMESTYLE_Corner)) { 86 bSameStyles = false; 87 break; 88 } 89 stroke1 = stroke2; 90 } 91 if (bSameStyles) { 92 stroke1 = strokes[0]; 93 if (stroke1->IsInverted()) 94 bSameStyles = false; 95 if (stroke1->GetJoinType() != XFA_AttributeEnum::Square) 96 bSameStyles = false; 97 } 98 } 99 if (bSameStyles) { 100 fillPath->AddRectangle(rtWidget.left, rtWidget.top, rtWidget.width, 101 rtWidget.height); 102 return; 103 } 104 105 for (int32_t i = 0; i < 8; i += 2) { 106 float sx = 0.0f; 107 float sy = 0.0f; 108 float vx = 1.0f; 109 float vy = 1.0f; 110 float nx = 1.0f; 111 float ny = 1.0f; 112 CFX_PointF cp1, cp2; 113 auto* corner1 = static_cast<CXFA_Corner*>(strokes[i]); 114 auto* corner2 = static_cast<CXFA_Corner*>(strokes[(i + 2) % 8]); 115 float fRadius1 = corner1->GetRadius(); 116 float fRadius2 = corner2->GetRadius(); 117 bool bInverted = corner1->IsInverted(); 118 bool bRound = corner1->GetJoinType() == XFA_AttributeEnum::Round; 119 if (bRound) { 120 sy = FX_PI / 2; 121 } 122 switch (i) { 123 case 0: 124 cp1 = rtWidget.TopLeft(); 125 cp2 = rtWidget.TopRight(); 126 vx = 1, vy = 1; 127 nx = -1, ny = 0; 128 if (bRound) { 129 sx = bInverted ? FX_PI / 2 : FX_PI; 130 } else { 131 sx = 1, sy = 0; 132 } 133 break; 134 case 2: 135 cp1 = rtWidget.TopRight(); 136 cp2 = rtWidget.BottomRight(); 137 vx = -1, vy = 1; 138 nx = 0, ny = -1; 139 if (bRound) { 140 sx = bInverted ? FX_PI : FX_PI * 3 / 2; 141 } else { 142 sx = 0, sy = 1; 143 } 144 break; 145 case 4: 146 cp1 = rtWidget.BottomRight(); 147 cp2 = rtWidget.BottomLeft(); 148 vx = -1, vy = -1; 149 nx = 1, ny = 0; 150 if (bRound) { 151 sx = bInverted ? FX_PI * 3 / 2 : 0; 152 } else { 153 sx = -1, sy = 0; 154 } 155 break; 156 case 6: 157 cp1 = rtWidget.BottomLeft(); 158 cp2 = rtWidget.TopLeft(); 159 vx = 1, vy = -1; 160 nx = 0, ny = 1; 161 if (bRound) { 162 sx = bInverted ? 0 : FX_PI / 2; 163 } else { 164 sx = 0; 165 sy = -1; 166 } 167 break; 168 } 169 if (i == 0) 170 fillPath->MoveTo(CFX_PointF(cp1.x, cp1.y + fRadius1)); 171 172 if (bRound) { 173 if (fRadius1 < 0) 174 sx -= FX_PI; 175 if (bInverted) 176 sy *= -1; 177 178 CFX_RectF rtRadius(cp1.x, cp1.y, fRadius1 * 2 * vx, fRadius1 * 2 * vy); 179 rtRadius.Normalize(); 180 if (bInverted) 181 rtRadius.Offset(-fRadius1 * vx, -fRadius1 * vy); 182 183 fillPath->ArcTo(rtRadius.TopLeft(), rtRadius.Size(), sx, sy); 184 } else { 185 CFX_PointF cp; 186 if (bInverted) { 187 cp.x = cp1.x + fRadius1 * vx; 188 cp.y = cp1.y + fRadius1 * vy; 189 } else { 190 cp = cp1; 191 } 192 fillPath->LineTo(cp); 193 fillPath->LineTo( 194 CFX_PointF(cp1.x + fRadius1 * sx, cp1.y + fRadius1 * sy)); 195 } 196 fillPath->LineTo(CFX_PointF(cp2.x + fRadius2 * nx, cp2.y + fRadius2 * ny)); 197 } 198 } 199 200 void CXFA_Rectangle::Draw(const std::vector<CXFA_Stroke*>& strokes, 201 CXFA_Graphics* pGS, 202 CFX_RectF rtWidget, 203 const CFX_Matrix& matrix) { 204 bool bVisible = false; 205 for (int32_t j = 0; j < 4; j++) { 206 if (strokes[j * 2 + 1]->IsVisible()) { 207 bVisible = true; 208 break; 209 } 210 } 211 if (!bVisible) 212 return; 213 214 for (int32_t i = 1; i < 8; i += 2) { 215 float fThickness = std::fmax(0.0, strokes[i]->GetThickness()); 216 float fHalf = fThickness / 2; 217 XFA_AttributeEnum iHand = GetHand(); 218 switch (i) { 219 case 1: 220 if (iHand == XFA_AttributeEnum::Left) { 221 rtWidget.top -= fHalf; 222 rtWidget.height += fHalf; 223 } else if (iHand == XFA_AttributeEnum::Right) { 224 rtWidget.top += fHalf; 225 rtWidget.height -= fHalf; 226 } 227 break; 228 case 3: 229 if (iHand == XFA_AttributeEnum::Left) { 230 rtWidget.width += fHalf; 231 } else if (iHand == XFA_AttributeEnum::Right) { 232 rtWidget.width -= fHalf; 233 } 234 break; 235 case 5: 236 if (iHand == XFA_AttributeEnum::Left) { 237 rtWidget.height += fHalf; 238 } else if (iHand == XFA_AttributeEnum::Right) { 239 rtWidget.height -= fHalf; 240 } 241 break; 242 case 7: 243 if (iHand == XFA_AttributeEnum::Left) { 244 rtWidget.left -= fHalf; 245 rtWidget.width += fHalf; 246 } else if (iHand == XFA_AttributeEnum::Right) { 247 rtWidget.left += fHalf; 248 rtWidget.width -= fHalf; 249 } 250 break; 251 } 252 } 253 Stroke(strokes, pGS, rtWidget, matrix); 254 } 255 256 void CXFA_Rectangle::Stroke(const std::vector<CXFA_Stroke*>& strokes, 257 CXFA_Graphics* pGS, 258 CFX_RectF rtWidget, 259 const CFX_Matrix& matrix) { 260 bool bVisible; 261 float fThickness; 262 XFA_AttributeEnum i3DType; 263 std::tie(i3DType, bVisible, fThickness) = Get3DStyle(); 264 if (i3DType != XFA_AttributeEnum::Unknown) { 265 if (!bVisible || fThickness < 0.001f) 266 return; 267 268 switch (i3DType) { 269 case XFA_AttributeEnum::Lowered: 270 StrokeLowered(pGS, rtWidget, fThickness, matrix); 271 break; 272 case XFA_AttributeEnum::Raised: 273 StrokeRaised(pGS, rtWidget, fThickness, matrix); 274 break; 275 case XFA_AttributeEnum::Etched: 276 StrokeEtched(pGS, rtWidget, fThickness, matrix); 277 break; 278 case XFA_AttributeEnum::Embossed: 279 StrokeEmbossed(pGS, rtWidget, fThickness, matrix); 280 break; 281 default: 282 NOTREACHED(); 283 break; 284 } 285 return; 286 } 287 288 bool bClose = false; 289 bool bSameStyles = true; 290 CXFA_Stroke* stroke1 = strokes[0]; 291 for (int32_t i = 1; i < 8; i++) { 292 CXFA_Stroke* stroke2 = strokes[i]; 293 if (!stroke1->SameStyles(stroke2, 0)) { 294 bSameStyles = false; 295 break; 296 } 297 stroke1 = stroke2; 298 } 299 if (bSameStyles) { 300 stroke1 = strokes[0]; 301 bClose = true; 302 for (int32_t i = 2; i < 8; i += 2) { 303 CXFA_Stroke* stroke2 = strokes[i]; 304 if (!stroke1->SameStyles(stroke2, XFA_STROKE_SAMESTYLE_NoPresence | 305 XFA_STROKE_SAMESTYLE_Corner)) { 306 bSameStyles = false; 307 break; 308 } 309 stroke1 = stroke2; 310 } 311 if (bSameStyles) { 312 stroke1 = strokes[0]; 313 if (stroke1->IsInverted()) 314 bSameStyles = false; 315 if (stroke1->GetJoinType() != XFA_AttributeEnum::Square) 316 bSameStyles = false; 317 } 318 } 319 320 bool bStart = true; 321 CXFA_GEPath path; 322 for (int32_t i = 0; i < 8; i++) { 323 CXFA_Stroke* stroke = strokes[i]; 324 if ((i % 1) == 0 && stroke->GetRadius() < 0) { 325 bool bEmpty = path.IsEmpty(); 326 if (!bEmpty) { 327 if (stroke) 328 stroke->Stroke(&path, pGS, matrix); 329 path.Clear(); 330 } 331 bStart = true; 332 continue; 333 } 334 GetPath(strokes, rtWidget, path, i, bStart, !bSameStyles); 335 336 bStart = !stroke->SameStyles(strokes[(i + 1) % 8], 0); 337 if (bStart) { 338 if (stroke) 339 stroke->Stroke(&path, pGS, matrix); 340 path.Clear(); 341 } 342 } 343 bool bEmpty = path.IsEmpty(); 344 if (!bEmpty) { 345 if (bClose) { 346 path.Close(); 347 } 348 if (strokes[7]) 349 strokes[7]->Stroke(&path, pGS, matrix); 350 } 351 } 352 353 void CXFA_Rectangle::StrokeRect(CXFA_Graphics* pGraphic, 354 const CFX_RectF& rt, 355 float fLineWidth, 356 const CFX_Matrix& matrix, 357 FX_ARGB argbTopLeft, 358 FX_ARGB argbBottomRight) { 359 float fBottom = rt.bottom(); 360 float fRight = rt.right(); 361 CXFA_GEPath pathLT; 362 pathLT.MoveTo(CFX_PointF(rt.left, fBottom)); 363 pathLT.LineTo(CFX_PointF(rt.left, rt.top)); 364 pathLT.LineTo(CFX_PointF(fRight, rt.top)); 365 pathLT.LineTo(CFX_PointF(fRight - fLineWidth, rt.top + fLineWidth)); 366 pathLT.LineTo(CFX_PointF(rt.left + fLineWidth, rt.top + fLineWidth)); 367 pathLT.LineTo(CFX_PointF(rt.left + fLineWidth, fBottom - fLineWidth)); 368 pathLT.LineTo(CFX_PointF(rt.left, fBottom)); 369 pGraphic->SetFillColor(CXFA_GEColor(argbTopLeft)); 370 pGraphic->FillPath(&pathLT, FXFILL_WINDING, &matrix); 371 372 CXFA_GEPath pathRB; 373 pathRB.MoveTo(CFX_PointF(fRight, rt.top)); 374 pathRB.LineTo(CFX_PointF(fRight, fBottom)); 375 pathRB.LineTo(CFX_PointF(rt.left, fBottom)); 376 pathRB.LineTo(CFX_PointF(rt.left + fLineWidth, fBottom - fLineWidth)); 377 pathRB.LineTo(CFX_PointF(fRight - fLineWidth, fBottom - fLineWidth)); 378 pathRB.LineTo(CFX_PointF(fRight - fLineWidth, rt.top + fLineWidth)); 379 pathRB.LineTo(CFX_PointF(fRight, rt.top)); 380 pGraphic->SetFillColor(CXFA_GEColor(argbBottomRight)); 381 pGraphic->FillPath(&pathRB, FXFILL_WINDING, &matrix); 382 } 383 384 void CXFA_Rectangle::StrokeLowered(CXFA_Graphics* pGS, 385 CFX_RectF rt, 386 float fThickness, 387 const CFX_Matrix& matrix) { 388 float fHalfWidth = fThickness / 2.0f; 389 CFX_RectF rtInner(rt); 390 rtInner.Deflate(fHalfWidth, fHalfWidth); 391 392 CXFA_GEPath path; 393 path.AddRectangle(rt.left, rt.top, rt.width, rt.height); 394 path.AddRectangle(rtInner.left, rtInner.top, rtInner.width, rtInner.height); 395 pGS->SetFillColor(CXFA_GEColor(0xFF000000)); 396 pGS->FillPath(&path, FXFILL_ALTERNATE, &matrix); 397 398 StrokeRect(pGS, rtInner, fHalfWidth, matrix, 0xFF808080, 0xFFC0C0C0); 399 } 400 401 void CXFA_Rectangle::StrokeRaised(CXFA_Graphics* pGS, 402 CFX_RectF rt, 403 float fThickness, 404 const CFX_Matrix& matrix) { 405 float fHalfWidth = fThickness / 2.0f; 406 CFX_RectF rtInner(rt); 407 rtInner.Deflate(fHalfWidth, fHalfWidth); 408 409 CXFA_GEPath path; 410 path.AddRectangle(rt.left, rt.top, rt.width, rt.height); 411 path.AddRectangle(rtInner.left, rtInner.top, rtInner.width, rtInner.height); 412 pGS->SetFillColor(CXFA_GEColor(0xFF000000)); 413 pGS->FillPath(&path, FXFILL_ALTERNATE, &matrix); 414 415 StrokeRect(pGS, rtInner, fHalfWidth, matrix, 0xFFFFFFFF, 0xFF808080); 416 } 417 418 void CXFA_Rectangle::StrokeEtched(CXFA_Graphics* pGS, 419 CFX_RectF rt, 420 float fThickness, 421 const CFX_Matrix& matrix) { 422 float fHalfWidth = fThickness / 2.0f; 423 StrokeRect(pGS, rt, fThickness, matrix, 0xFF808080, 0xFFFFFFFF); 424 425 CFX_RectF rtInner(rt); 426 rtInner.Deflate(fHalfWidth, fHalfWidth); 427 StrokeRect(pGS, rtInner, fHalfWidth, matrix, 0xFFFFFFFF, 0xFF808080); 428 } 429 430 void CXFA_Rectangle::StrokeEmbossed(CXFA_Graphics* pGS, 431 CFX_RectF rt, 432 float fThickness, 433 const CFX_Matrix& matrix) { 434 float fHalfWidth = fThickness / 2.0f; 435 StrokeRect(pGS, rt, fThickness, matrix, 0xFF808080, 0xFF000000); 436 437 CFX_RectF rtInner(rt); 438 rtInner.Deflate(fHalfWidth, fHalfWidth); 439 StrokeRect(pGS, rtInner, fHalfWidth, matrix, 0xFF000000, 0xFF808080); 440 } 441 442 void CXFA_Rectangle::GetPath(const std::vector<CXFA_Stroke*>& strokes, 443 CFX_RectF rtWidget, 444 CXFA_GEPath& path, 445 int32_t nIndex, 446 bool bStart, 447 bool bCorner) { 448 ASSERT(nIndex >= 0 && nIndex < 8); 449 450 int32_t n = (nIndex & 1) ? nIndex - 1 : nIndex; 451 auto* corner1 = static_cast<CXFA_Corner*>(strokes[n]); 452 auto* corner2 = static_cast<CXFA_Corner*>(strokes[(n + 2) % 8]); 453 float fRadius1 = bCorner ? corner1->GetRadius() : 0.0f; 454 float fRadius2 = bCorner ? corner2->GetRadius() : 0.0f; 455 bool bInverted = corner1->IsInverted(); 456 float offsetY = 0.0f; 457 float offsetX = 0.0f; 458 bool bRound = corner1->GetJoinType() == XFA_AttributeEnum::Round; 459 float halfAfter = 0.0f; 460 float halfBefore = 0.0f; 461 462 CXFA_Stroke* stroke = strokes[nIndex]; 463 if (stroke->IsCorner()) { 464 CXFA_Stroke* strokeBefore = strokes[(nIndex + 1 * 8 - 1) % 8]; 465 CXFA_Stroke* strokeAfter = strokes[nIndex + 1]; 466 if (stroke->IsInverted()) { 467 if (!stroke->SameStyles(strokeBefore, 0)) 468 halfBefore = strokeBefore->GetThickness() / 2; 469 if (!stroke->SameStyles(strokeAfter, 0)) 470 halfAfter = strokeAfter->GetThickness() / 2; 471 } 472 } else { 473 CXFA_Stroke* strokeBefore = strokes[(nIndex + 8 - 2) % 8]; 474 CXFA_Stroke* strokeAfter = strokes[(nIndex + 2) % 8]; 475 if (!bRound && !bInverted) { 476 halfBefore = strokeBefore->GetThickness() / 2; 477 halfAfter = strokeAfter->GetThickness() / 2; 478 } 479 } 480 481 float offsetEX = 0.0f; 482 float offsetEY = 0.0f; 483 float sx = 0.0f; 484 float sy = 0.0f; 485 float vx = 1.0f; 486 float vy = 1.0f; 487 float nx = 1.0f; 488 float ny = 1.0f; 489 CFX_PointF cpStart; 490 CFX_PointF cp1; 491 CFX_PointF cp2; 492 if (bRound) 493 sy = FX_PI / 2; 494 495 switch (nIndex) { 496 case 0: 497 case 1: 498 cp1 = rtWidget.TopLeft(); 499 cp2 = rtWidget.TopRight(); 500 if (nIndex == 0) { 501 cpStart.x = cp1.x - halfBefore; 502 cpStart.y = cp1.y + fRadius1, offsetY = -halfAfter; 503 } else { 504 cpStart.x = cp1.x + fRadius1 - halfBefore, cpStart.y = cp1.y, 505 offsetEX = halfAfter; 506 } 507 vx = 1, vy = 1; 508 nx = -1, ny = 0; 509 if (bRound) { 510 sx = bInverted ? FX_PI / 2 : FX_PI; 511 } else { 512 sx = 1, sy = 0; 513 } 514 break; 515 case 2: 516 case 3: 517 cp1 = rtWidget.TopRight(); 518 cp2 = rtWidget.BottomRight(); 519 if (nIndex == 2) { 520 cpStart.x = cp1.x - fRadius1, cpStart.y = cp1.y - halfBefore, 521 offsetX = halfAfter; 522 } else { 523 cpStart.x = cp1.x, cpStart.y = cp1.y + fRadius1 - halfBefore, 524 offsetEY = halfAfter; 525 } 526 vx = -1, vy = 1; 527 nx = 0, ny = -1; 528 if (bRound) { 529 sx = bInverted ? FX_PI : FX_PI * 3 / 2; 530 } else { 531 sx = 0, sy = 1; 532 } 533 break; 534 case 4: 535 case 5: 536 cp1 = rtWidget.BottomRight(); 537 cp2 = rtWidget.BottomLeft(); 538 if (nIndex == 4) { 539 cpStart.x = cp1.x + halfBefore, cpStart.y = cp1.y - fRadius1, 540 offsetY = halfAfter; 541 } else { 542 cpStart.x = cp1.x - fRadius1 + halfBefore, cpStart.y = cp1.y, 543 offsetEX = -halfAfter; 544 } 545 vx = -1, vy = -1; 546 nx = 1, ny = 0; 547 if (bRound) { 548 sx = bInverted ? FX_PI * 3 / 2 : 0; 549 } else { 550 sx = -1, sy = 0; 551 } 552 break; 553 case 6: 554 case 7: 555 cp1 = rtWidget.BottomLeft(); 556 cp2 = rtWidget.TopLeft(); 557 if (nIndex == 6) { 558 cpStart.x = cp1.x + fRadius1, cpStart.y = cp1.y + halfBefore, 559 offsetX = -halfAfter; 560 } else { 561 cpStart.x = cp1.x, cpStart.y = cp1.y - fRadius1 + halfBefore, 562 offsetEY = -halfAfter; 563 } 564 vx = 1; 565 vy = -1; 566 nx = 0; 567 ny = 1; 568 if (bRound) { 569 sx = bInverted ? 0 : FX_PI / 2; 570 } else { 571 sx = 0; 572 sy = -1; 573 } 574 break; 575 } 576 if (bStart) { 577 path.MoveTo(cpStart); 578 } 579 if (nIndex & 1) { 580 path.LineTo(CFX_PointF(cp2.x + fRadius2 * nx + offsetEX, 581 cp2.y + fRadius2 * ny + offsetEY)); 582 return; 583 } 584 if (bRound) { 585 if (fRadius1 < 0) 586 sx -= FX_PI; 587 if (bInverted) 588 sy *= -1; 589 590 CFX_RectF rtRadius(cp1.x + offsetX * 2, cp1.y + offsetY * 2, 591 fRadius1 * 2 * vx - offsetX * 2, 592 fRadius1 * 2 * vy - offsetY * 2); 593 rtRadius.Normalize(); 594 if (bInverted) 595 rtRadius.Offset(-fRadius1 * vx, -fRadius1 * vy); 596 597 path.ArcTo(rtRadius.TopLeft(), rtRadius.Size(), sx, sy); 598 } else { 599 CFX_PointF cp; 600 if (bInverted) { 601 cp.x = cp1.x + fRadius1 * vx; 602 cp.y = cp1.y + fRadius1 * vy; 603 } else { 604 cp = cp1; 605 } 606 path.LineTo(cp); 607 path.LineTo(CFX_PointF(cp1.x + fRadius1 * sx + offsetX, 608 cp1.y + fRadius1 * sy + offsetY)); 609 } 610 } 611