1 /************************************************************** 2 * 3 * Licensed to the Apache Software Foundation (ASF) under one 4 * or more contributor license agreements. See the NOTICE file 5 * distributed with this work for additional information 6 * regarding copyright ownership. The ASF licenses this file 7 * to you under the Apache License, Version 2.0 (the 8 * "License"); you may not use this file except in compliance 9 * with the License. You may obtain a copy of the License at 10 * 11 * http://www.apache.org/licenses/LICENSE-2.0 12 * 13 * Unless required by applicable law or agreed to in writing, 14 * software distributed under the License is distributed on an 15 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 * KIND, either express or implied. See the License for the 17 * specific language governing permissions and limitations 18 * under the License. 19 * 20 *************************************************************/ 21 22 23 24 // MARKER(update_precomp.py): autogen include statement, do not remove 25 #include "precompiled_sw.hxx" 26 27 28 29 30 #include <hintids.hxx> 31 #include <svl/urihelper.hxx> 32 #include <unotools/pathoptions.hxx> 33 #include <tools/stream.hxx> 34 #ifndef _SFX_INIMGR_HXX 35 #endif 36 #include <sfx2/docfile.hxx> 37 #include <svl/itemiter.hxx> 38 #include <editeng/brshitem.hxx> 39 40 41 #include <tools/resid.hxx> 42 #include <fmtornt.hxx> 43 #include <swtypes.hxx> // Leerstring 44 #include <wrtsh.hxx> 45 #include <uinums.hxx> 46 #include <poolfmt.hxx> 47 #include <charfmt.hxx> 48 #include <frmatr.hxx> 49 50 #include <unomid.h> 51 52 using namespace ::com::sun::star; 53 54 55 #define VERSION_30B ((sal_uInt16)250) 56 #define VERSION_31B ((sal_uInt16)326) 57 #define VERSION_40A ((sal_uInt16)364) 58 #define VERSION_50A ((sal_uInt16)373) 59 #define VERSION_53A ((sal_uInt16)596) 60 #define ACT_NUM_VERSION VERSION_53A 61 62 #define NUMRULE_FILENAME "numrule.cfg" 63 #define CHAPTER_FILENAME "chapter.cfg" 64 65 /*------------------------------------------------------------------------ 66 Beschreibung: Ops. zum Laden / Speichern 67 ------------------------------------------------------------------------*/ 68 69 70 SV_IMPL_PTRARR( _SwNumFmtsAttrs, SfxPoolItem* ) 71 72 73 // SwNumRulesWithName ---------------------------------------------------- 74 // PUBLIC METHODES ------------------------------------------------------- 75 /*------------------------------------------------------------------------ 76 Beschreibung: Speichern einer Regel 77 Parameter: rCopy -- die zu speichernde Regel 78 nIdx -- Position, an der die Regel zu speichern ist. 79 Eine alte Regel an dieser Position wird ueberschrieben. 80 ------------------------------------------------------------------------*/ 81 82 SwBaseNumRules::SwBaseNumRules( const String& rFileName ) 83 : 84 sFileName( rFileName ), 85 nVersion(0), 86 bModified( sal_False ) 87 { 88 Init(); 89 } 90 91 /*-----------------26.06.97 08.30------------------- 92 93 --------------------------------------------------*/ 94 SwBaseNumRules::~SwBaseNumRules() 95 { 96 if( bModified ) 97 { 98 SvtPathOptions aPathOpt; 99 String sNm( aPathOpt.GetUserConfigPath() ); 100 sNm += INET_PATH_TOKEN; 101 sNm += sFileName; 102 INetURLObject aTempObj(sNm); 103 sNm = aTempObj.GetFull(); 104 SfxMedium aStrm( sNm, STREAM_WRITE | STREAM_TRUNC | 105 STREAM_SHARE_DENYALL, sal_True ); 106 Store( *aStrm.GetOutStream() ); 107 } 108 109 for( sal_uInt16 i = 0; i < nMaxRules; ++i ) 110 delete pNumRules[i]; 111 } 112 113 /*------------------------------------------------------------------------ 114 Beschreibung: 115 ------------------------------------------------------------------------*/ 116 void SwBaseNumRules::Init() 117 { 118 for(sal_uInt16 i = 0; i < nMaxRules; ++i ) 119 pNumRules[i] = 0; 120 121 String sNm( sFileName ); 122 SvtPathOptions aOpt; 123 if( aOpt.SearchFile( sNm, SvtPathOptions::PATH_USERCONFIG )) 124 { 125 SfxMedium aStrm( sNm, STREAM_STD_READ, sal_True ); 126 Load( *aStrm.GetInStream() ); 127 } 128 } 129 130 /*-----------------26.06.97 08.30------------------- 131 132 --------------------------------------------------*/ 133 134 void SwBaseNumRules::ApplyNumRules(const SwNumRulesWithName &rCopy, sal_uInt16 nIdx) 135 { 136 ASSERT(nIdx < nMaxRules, Array der NumRules ueberindiziert.); 137 if( !pNumRules[nIdx] ) 138 pNumRules[nIdx] = new SwNumRulesWithName( rCopy ); 139 else 140 *pNumRules[nIdx] = rCopy; 141 } 142 143 // PROTECTED METHODES ---------------------------------------------------- 144 /*------------------------------------------------------------------------ 145 Beschreibung: Speichern 146 ------------------------------------------------------------------------*/ 147 148 sal_Bool /**/ SwBaseNumRules::Store(SvStream &rStream) 149 { 150 rStream << ACT_NUM_VERSION; 151 // Schreiben, welche Positionen durch eine Regel belegt sind 152 // Anschliessend Schreiben der einzelnen Rules 153 for(sal_uInt16 i = 0; i < nMaxRules; ++i) 154 { 155 if(pNumRules[i]) 156 { 157 rStream << (unsigned char) sal_True; 158 pNumRules[i]->Store( rStream ); 159 } 160 else 161 rStream << (unsigned char) sal_False; 162 } 163 return sal_True; 164 } 165 166 167 168 /*------------------------------------------------------------------------ 169 Beschreibung: Speichern / Laden 170 ------------------------------------------------------------------------*/ 171 172 173 int SwBaseNumRules::Load(SvStream &rStream) 174 { 175 int rc = 0; 176 177 rStream >> nVersion; 178 179 // wegen eines kleinen aber schweren Fehlers schreibt die PreFinal die 180 // gleiche VERSION_40A wie das SP2 #55402# 181 if(VERSION_40A == nVersion) 182 { 183 DBG_ERROR("Version 364 ist nicht eindeutig #55402#"); 184 } 185 else if( VERSION_30B == nVersion || VERSION_31B == nVersion || 186 ACT_NUM_VERSION >= nVersion ) 187 { 188 unsigned char bRule = sal_False; 189 for(sal_uInt16 i = 0; i < nMaxRules; ++i) 190 { 191 rStream >> bRule; 192 if(bRule) 193 pNumRules[i] = new SwNumRulesWithName( rStream, nVersion ); 194 } 195 } 196 else 197 { 198 rc = 1; 199 } 200 201 return rc; 202 } 203 204 /*-----------------26.06.97 08.34------------------- 205 206 --------------------------------------------------*/ 207 208 /*------------------------------------------------------------------------*/ 209 210 211 SwChapterNumRules::SwChapterNumRules() : 212 SwBaseNumRules(C2S(CHAPTER_FILENAME)) 213 { 214 } 215 216 /*------------------------------------------------------------------------*/ 217 218 SwChapterNumRules::~SwChapterNumRules() 219 { 220 } 221 222 /*-----------------26.06.97 08.23------------------- 223 224 --------------------------------------------------*/ 225 void SwChapterNumRules::ApplyNumRules(const SwNumRulesWithName &rCopy, sal_uInt16 nIdx) 226 { 227 bModified = sal_True; 228 SwBaseNumRules::ApplyNumRules(rCopy, nIdx); 229 } 230 231 /*------------------------------------------------------------------------*/ 232 233 SwNumRulesWithName::SwNumRulesWithName( const SwNumRule &rCopy, 234 const String &rName ) 235 : aName(rName) 236 { 237 for( sal_uInt16 n = 0; n < MAXLEVEL; ++n ) 238 { 239 const SwNumFmt* pFmt = rCopy.GetNumFmt( n ); 240 if( pFmt ) 241 aFmts[ n ] = new _SwNumFmtGlobal( *pFmt ); 242 else 243 aFmts[ n ] = 0; 244 } 245 } 246 247 /*------------------------------------------------------------------------ 248 Beschreibung: 249 ------------------------------------------------------------------------*/ 250 SwNumRulesWithName::SwNumRulesWithName( const SwNumRulesWithName& rCopy ) 251 { 252 memset( aFmts, 0, sizeof( aFmts )); 253 *this = rCopy; 254 } 255 256 257 /*------------------------------------------------------------------------ 258 Beschreibung: 259 ------------------------------------------------------------------------*/ 260 SwNumRulesWithName::~SwNumRulesWithName() 261 { 262 for( int n = 0; n < MAXLEVEL; ++n ) 263 delete aFmts[ n ]; 264 } 265 266 /*------------------------------------------------------------------------ 267 Beschreibung: 268 ------------------------------------------------------------------------*/ 269 const SwNumRulesWithName& SwNumRulesWithName::operator=(const SwNumRulesWithName &rCopy) 270 { 271 if( this != &rCopy ) 272 { 273 aName = rCopy.aName; 274 for( int n = 0; n < MAXLEVEL; ++n ) 275 { 276 delete aFmts[ n ]; 277 278 _SwNumFmtGlobal* pFmt = rCopy.aFmts[ n ]; 279 if( pFmt ) 280 aFmts[ n ] = new _SwNumFmtGlobal( *pFmt ); 281 else 282 aFmts[ n ] = 0; 283 } 284 } 285 return *this; 286 } 287 288 /*------------------------------------------------------------------------ 289 Beschreibung: 290 ------------------------------------------------------------------------*/ 291 SwNumRulesWithName::SwNumRulesWithName( SvStream &rStream, sal_uInt16 nVersion ) 292 { 293 CharSet eEncoding = gsl_getSystemTextEncoding(); 294 rStream.ReadByteString(aName, eEncoding); 295 296 char c; 297 for(sal_uInt16 n = 0; n < MAXLEVEL; ++n ) 298 { 299 if( VERSION_30B == nVersion ) 300 c = 1; 301 // wegen eines kleinen aber schweren Fehlers schreibt die PreFinal die 302 // gleiche VERSION_40A wie das SP2 #55402# 303 else if(nVersion < VERSION_40A && n > 5) 304 // else if(nVersion < VERSION_50A && n > 5) 305 c = 0; 306 else 307 rStream >> c; 308 309 if( c ) 310 aFmts[ n ] = new _SwNumFmtGlobal( rStream, nVersion ); 311 else 312 aFmts[ n ] = 0; 313 } 314 } 315 316 /*------------------------------------------------------------------------ 317 Beschreibung: 318 ------------------------------------------------------------------------*/ 319 320 void SwNumRulesWithName::MakeNumRule( SwWrtShell& rSh, SwNumRule& rChg ) const 321 { 322 // --> OD 2008-02-11 #newlistlevelattrs# 323 // --> OD 2008-06-06 #i89178# 324 rChg = SwNumRule( aName, numfunc::GetDefaultPositionAndSpaceMode() ); 325 // <-- 326 rChg.SetAutoRule( sal_False ); 327 _SwNumFmtGlobal* pFmt; 328 for( sal_uInt16 n = 0; n < MAXLEVEL; ++n ) 329 if( 0 != ( pFmt = aFmts[ n ] ) ) 330 { 331 SwNumFmt aNew; 332 pFmt->ChgNumFmt( rSh, aNew ); 333 rChg.Set( n, aNew ); 334 } 335 } 336 337 /*------------------------------------------------------------------------ 338 Beschreibung: 339 ------------------------------------------------------------------------*/ 340 void SwNumRulesWithName::Store( SvStream &rStream ) 341 { 342 CharSet eEncoding = gsl_getSystemTextEncoding(); 343 rStream.WriteByteString(aName, eEncoding); 344 345 for( sal_uInt16 n = 0; n < MAXLEVEL; ++n ) 346 { 347 _SwNumFmtGlobal* pFmt = aFmts[ n ]; 348 if( pFmt ) 349 { 350 rStream << (char)1; 351 pFmt->Store( rStream ); 352 } 353 else 354 rStream << (char)0; 355 } 356 } 357 /*------------------------------------------------------------------------ 358 Beschreibung: 359 ------------------------------------------------------------------------*/ 360 361 362 SwNumRulesWithName::_SwNumFmtGlobal::_SwNumFmtGlobal( const SwNumFmt& rFmt ) 363 : aFmt( rFmt ), nCharPoolId( USHRT_MAX ) 364 { 365 // relative Abstaende ????? 366 367 SwCharFmt* pFmt = rFmt.GetCharFmt(); 368 if( pFmt ) 369 { 370 sCharFmtName = pFmt->GetName(); 371 nCharPoolId = pFmt->GetPoolFmtId(); 372 if( pFmt->GetAttrSet().Count() ) 373 { 374 SfxItemIter aIter( pFmt->GetAttrSet() ); 375 const SfxPoolItem *pCurr = aIter.GetCurItem(); 376 while( sal_True ) 377 { 378 aItems.Insert( pCurr->Clone(), aItems.Count() ); 379 if( aIter.IsAtEnd() ) 380 break; 381 pCurr = aIter.NextItem(); 382 } 383 } 384 385 aFmt.SetCharFmt( 0 ); 386 } 387 } 388 389 /*------------------------------------------------------------------------ 390 Beschreibung: 391 ------------------------------------------------------------------------*/ 392 393 SwNumRulesWithName::_SwNumFmtGlobal::_SwNumFmtGlobal( const _SwNumFmtGlobal& rFmt ) 394 : 395 aFmt( rFmt.aFmt ), 396 sCharFmtName( rFmt.sCharFmtName ), 397 nCharPoolId( rFmt.nCharPoolId ) 398 { 399 for( sal_uInt16 n = rFmt.aItems.Count(); n; ) 400 aItems.Insert( rFmt.aItems[ --n ]->Clone(), aItems.Count() ); 401 } 402 403 /*------------------------------------------------------------------------ 404 Beschreibung: 405 ------------------------------------------------------------------------*/ 406 407 SwNumRulesWithName::_SwNumFmtGlobal::_SwNumFmtGlobal( SvStream& rStream, 408 sal_uInt16 nVersion ) 409 : nCharPoolId( USHRT_MAX ) 410 { 411 CharSet eEncoding = gsl_getSystemTextEncoding(); 412 { 413 sal_uInt16 nUS; 414 sal_Char cChar; 415 short nShort; 416 sal_Bool bFlag; 417 String sStr; 418 419 rStream >> nUS; aFmt.SetNumberingType((sal_Int16)nUS ); 420 if( VERSION_53A > nVersion ) 421 { 422 rStream >> cChar; aFmt.SetBulletChar( cChar ); 423 } 424 else 425 { 426 rStream >> nUS; aFmt.SetBulletChar( nUS ); 427 } 428 429 rStream >> bFlag; aFmt.SetIncludeUpperLevels( bFlag ); 430 431 if( VERSION_30B == nVersion ) 432 { 433 long nL; 434 rStream >> cChar; aFmt.SetStart( (sal_uInt16)cChar ); 435 436 rStream.ReadByteString(sStr, eEncoding); 437 aFmt.SetPrefix( sStr ); 438 rStream.ReadByteString(sStr, eEncoding); 439 aFmt.SetSuffix( sStr ); 440 rStream >> nUS; aFmt.SetNumAdjust( SvxAdjust( nUS ) ); 441 rStream >> nL; aFmt.SetLSpace( lNumIndent ); 442 rStream >> nL; aFmt.SetFirstLineOffset( (short)nL ); 443 } 444 else // alter StartWert war ein Byte 445 { 446 rStream >> nUS; aFmt.SetStart( nUS ); 447 rStream.ReadByteString(sStr, eEncoding); 448 aFmt.SetPrefix( sStr ); 449 rStream.ReadByteString(sStr, eEncoding); 450 aFmt.SetSuffix( sStr ); 451 rStream >> nUS; aFmt.SetNumAdjust( SvxAdjust( nUS ) ); 452 rStream >> nUS; aFmt.SetAbsLSpace( nUS ); 453 rStream >> nShort; aFmt.SetFirstLineOffset( nShort ); 454 rStream >> nUS; aFmt.SetCharTextDistance( nUS ); 455 rStream >> nShort; aFmt.SetLSpace( nShort ); 456 rStream >> bFlag; 457 } 458 459 sal_uInt16 nFamily; 460 sal_uInt16 nCharSet; 461 short nWidth; 462 short nHeight; 463 sal_uInt16 nPitch; 464 String aName; 465 466 rStream.ReadByteString(aName, eEncoding); 467 rStream >> nFamily >> nCharSet >> nWidth >> nHeight >> nPitch; 468 469 if( aName.Len() ) 470 { 471 Font aFont( nFamily, Size( nWidth, nHeight ) ); 472 aFont.SetName( aName ); 473 aFont.SetCharSet( (CharSet)nCharSet ); 474 aFont.SetPitch( (FontPitch)nPitch ); 475 476 aFmt.SetBulletFont( &aFont ); 477 } 478 else 479 nCharSet = RTL_TEXTENCODING_SYMBOL; 480 481 if( VERSION_53A > nVersion ) 482 aFmt.SetBulletChar( ByteString::ConvertToUnicode( 483 sal_Char(aFmt.GetBulletChar()), nCharSet )); 484 } 485 486 if( VERSION_30B != nVersion ) 487 { 488 sal_uInt16 nItemCount; 489 rStream >> nCharPoolId; 490 rStream.ReadByteString(sCharFmtName, eEncoding); 491 rStream >> nItemCount; 492 493 while( nItemCount-- ) 494 { 495 sal_uInt16 nWhich, nVers; 496 rStream >> nWhich >> nVers; 497 aItems.Insert( GetDfltAttr( nWhich )->Create( rStream, nVers ), 498 aItems.Count() ); 499 } 500 } 501 502 if( VERSION_40A == nVersion && SVX_NUM_BITMAP == aFmt.GetNumberingType() ) 503 { 504 sal_uInt8 cF; 505 Size aSz; 506 507 rStream >> aSz.Width() >> aSz.Height(); 508 509 rStream >> cF; 510 if( cF ) 511 { 512 SvxBrushItem* pBrush = 0; 513 SwFmtVertOrient* pVOrient = 0; 514 sal_uInt16 nVer; 515 516 if( cF & 1 ) 517 { 518 rStream >> nVer; 519 pBrush = (SvxBrushItem*)GetDfltAttr( RES_BACKGROUND ) 520 ->Create( rStream, nVer ); 521 } 522 523 if( cF & 2 ) 524 { 525 rStream >> nVer; 526 pVOrient = (SwFmtVertOrient*)GetDfltAttr( RES_VERT_ORIENT ) 527 ->Create( rStream, nVer ); 528 } 529 sal_Int16 eOrient = text::VertOrientation::NONE; 530 if(pVOrient) 531 eOrient = (sal_Int16)pVOrient->GetVertOrient(); 532 aFmt.SetGraphicBrush( pBrush, &aSz, pVOrient ? &eOrient : 0 ); 533 } 534 } 535 } 536 537 538 /*------------------------------------------------------------------------ 539 Beschreibung: 540 ------------------------------------------------------------------------*/ 541 542 SwNumRulesWithName::_SwNumFmtGlobal::~_SwNumFmtGlobal() 543 { 544 } 545 /*------------------------------------------------------------------------ 546 Beschreibung: 547 ------------------------------------------------------------------------*/ 548 549 550 void SwNumRulesWithName::_SwNumFmtGlobal::Store( SvStream& rStream ) 551 { 552 CharSet eEncoding = gsl_getSystemTextEncoding(); 553 { 554 String aName; 555 sal_uInt16 nFamily = FAMILY_DONTKNOW, nCharSet = 0, nPitch = 0; 556 short nWidth = 0, nHeight = 0; 557 558 const Font* pFnt = aFmt.GetBulletFont(); 559 if( pFnt ) 560 { 561 aName = pFnt->GetName(); 562 nFamily = (sal_uInt16)pFnt->GetFamily(); 563 nCharSet = (sal_uInt16)pFnt->GetCharSet(); 564 nWidth = (short)pFnt->GetSize().Width(); 565 nHeight = (short)pFnt->GetSize().Height(); 566 nPitch = (sal_uInt16)pFnt->GetPitch(); 567 } 568 569 rStream << sal_uInt16(aFmt.GetNumberingType()) 570 << aFmt.GetBulletChar() 571 << (aFmt.GetIncludeUpperLevels() > 0) 572 << aFmt.GetStart(); 573 rStream.WriteByteString( aFmt.GetPrefix(), eEncoding ); 574 rStream.WriteByteString( aFmt.GetSuffix(), eEncoding ); 575 rStream << sal_uInt16( aFmt.GetNumAdjust() ) 576 << aFmt.GetAbsLSpace() 577 << aFmt.GetFirstLineOffset() 578 << aFmt.GetCharTextDistance() 579 << aFmt.GetLSpace() 580 << sal_False;//aFmt.IsRelLSpace(); 581 rStream.WriteByteString( aName, eEncoding ); 582 rStream << nFamily 583 << nCharSet 584 << nWidth 585 << nHeight 586 << nPitch; 587 } 588 rStream << nCharPoolId; 589 rStream.WriteByteString( sCharFmtName, eEncoding ); 590 rStream << aItems.Count(); 591 592 for( sal_uInt16 n = aItems.Count(); n; ) 593 { 594 SfxPoolItem* pItem = aItems[ --n ]; 595 sal_uInt16 nIVers = pItem->GetVersion( SOFFICE_FILEFORMAT_50 ); 596 ASSERT( nIVers != USHRT_MAX, 597 "Was'n das: Item-Version USHRT_MAX in der aktuellen Version" ); 598 rStream << pItem->Which() 599 << nIVers; 600 pItem->Store( rStream, nIVers ); 601 } 602 603 // Erweiterungen fuer Version 40A 604 605 if( SVX_NUM_BITMAP == aFmt.GetNumberingType() ) 606 { 607 rStream << (sal_Int32)aFmt.GetGraphicSize().Width() 608 << (sal_Int32)aFmt.GetGraphicSize().Height(); 609 sal_uInt8 cFlg = ( 0 != aFmt.GetBrush() ? 1 : 0 ) + 610 ( 0 != aFmt.GetGraphicOrientation() ? 2 : 0 ); 611 rStream << cFlg; 612 613 if( aFmt.GetBrush() ) 614 { 615 sal_uInt16 nVersion = aFmt.GetBrush()->GetVersion( SOFFICE_FILEFORMAT_50 ); 616 rStream << nVersion; 617 aFmt.GetBrush()->Store( rStream, nVersion ); 618 } 619 if( aFmt.GetGraphicOrientation() ) 620 { 621 sal_uInt16 nVersion = aFmt.GetGraphicOrientation()->GetVersion( SOFFICE_FILEFORMAT_50 ); 622 rStream << nVersion; 623 aFmt.GetGraphicOrientation()->Store( rStream, nVersion ); 624 } 625 } 626 } 627 628 /*------------------------------------------------------------------------ 629 Beschreibung: 630 ------------------------------------------------------------------------*/ 631 632 void SwNumRulesWithName::_SwNumFmtGlobal::ChgNumFmt( SwWrtShell& rSh, 633 SwNumFmt& rNew ) const 634 { 635 SwCharFmt* pFmt = 0; 636 if( sCharFmtName.Len() ) 637 { 638 // suche erstmal ueber den Namen 639 sal_uInt16 nArrLen = rSh.GetCharFmtCount(); 640 for( sal_uInt16 i = 1; i < nArrLen; ++i ) 641 { 642 pFmt = &rSh.GetCharFmt( i ); 643 if( COMPARE_EQUAL == pFmt->GetName().CompareTo( sCharFmtName )) 644 // ist vorhanden, also belasse die Attribute wie sie sind! 645 break; 646 pFmt = 0; 647 } 648 649 if( !pFmt ) 650 { 651 if( IsPoolUserFmt( nCharPoolId ) ) 652 { 653 pFmt = rSh.MakeCharFmt( sCharFmtName ); 654 pFmt->SetAuto( sal_False ); 655 } 656 else 657 pFmt = rSh.GetCharFmtFromPool( nCharPoolId ); 658 659 if( !pFmt->GetDepends() ) // Attribute setzen 660 for( sal_uInt16 n = aItems.Count(); n; ) 661 pFmt->SetFmtAttr( *aItems[ --n ] ); 662 } 663 } 664 ((SwNumFmt&)aFmt).SetCharFmt( pFmt ); 665 rNew = aFmt; 666 if( pFmt ) 667 ((SwNumFmt&)aFmt).SetCharFmt( 0 ); 668 } 669 670