1From 0d730a94e9f6676d5cde45f955fe025a4549817e Mon Sep 17 00:00:00 2001 2From: George Wright <gw@gwright.org.uk> 3Date: Thu, 23 Aug 2012 16:45:38 -0400 4Subject: [PATCH 4/9] Bug 777614 - Re-apply bug 719872 - Fix crash on Android 5 by reverting to older FontHost r=nrc 6 7--- 8 gfx/skia/src/ports/SkFontHost_android_old.cpp | 664 ++++++++++++++++++++++++++ 9 1 file changed, 664 insertions(+) 10 create mode 100644 gfx/skia/src/ports/SkFontHost_android_old.cpp 11 12diff --git a/gfx/skia/src/ports/SkFontHost_android_old.cpp b/gfx/skia/src/ports/SkFontHost_android_old.cpp 13new file mode 100644 14index 0000000..b5c4f3c 15--- /dev/null 16+++ b/gfx/skia/src/ports/SkFontHost_android_old.cpp 17@@ -0,0 +1,664 @@ 18+ 19+/* 20+ * Copyright 2006 The Android Open Source Project 21+ * 22+ * Use of this source code is governed by a BSD-style license that can be 23+ * found in the LICENSE file. 24+ */ 25+ 26+ 27+#include "SkFontHost.h" 28+#include "SkDescriptor.h" 29+#include "SkMMapStream.h" 30+#include "SkPaint.h" 31+#include "SkString.h" 32+#include "SkStream.h" 33+#include "SkThread.h" 34+#include "SkTSearch.h" 35+#include <stdio.h> 36+ 37+#define FONT_CACHE_MEMORY_BUDGET (768 * 1024) 38+ 39+#ifndef SK_FONT_FILE_PREFIX 40+ #define SK_FONT_FILE_PREFIX "/fonts/" 41+#endif 42+ 43+bool find_name_and_attributes(SkStream* stream, SkString* name, SkTypeface::Style* style, 44+ bool* isFixedWidth); 45+ 46+static void GetFullPathForSysFonts(SkString* full, const char name[]) { 47+ full->set(getenv("ANDROID_ROOT")); 48+ full->append(SK_FONT_FILE_PREFIX); 49+ full->append(name); 50+} 51+ 52+/////////////////////////////////////////////////////////////////////////////// 53+ 54+struct FamilyRec; 55+ 56+/* This guy holds a mapping of a name -> family, used for looking up fonts. 57+ Since it is stored in a stretchy array that doesn't preserve object 58+ semantics, we don't use constructor/destructors, but just have explicit 59+ helpers to manage our internal bookkeeping. 60+*/ 61+struct NameFamilyPair { 62+ const char* fName; // we own this 63+ FamilyRec* fFamily; // we don't own this, we just reference it 64+ 65+ void construct(const char name[], FamilyRec* family) { 66+ fName = strdup(name); 67+ fFamily = family; // we don't own this, so just record the referene 68+ } 69+ 70+ void destruct() { 71+ free((char*)fName); 72+ // we don't own family, so just ignore our reference 73+ } 74+}; 75+ 76+// we use atomic_inc to grow this for each typeface we create 77+static int32_t gUniqueFontID; 78+ 79+// this is the mutex that protects these globals 80+static SkMutex gFamilyMutex; 81+static FamilyRec* gFamilyHead; 82+static SkTDArray<NameFamilyPair> gNameList; 83+ 84+struct FamilyRec { 85+ FamilyRec* fNext; 86+ SkTypeface* fFaces[4]; 87+ 88+ FamilyRec() 89+ { 90+ fNext = gFamilyHead; 91+ memset(fFaces, 0, sizeof(fFaces)); 92+ gFamilyHead = this; 93+ } 94+}; 95+ 96+static SkTypeface* find_best_face(const FamilyRec* family, 97+ SkTypeface::Style style) { 98+ SkTypeface* const* faces = family->fFaces; 99+ 100+ if (faces[style] != NULL) { // exact match 101+ return faces[style]; 102+ } 103+ // look for a matching bold 104+ style = (SkTypeface::Style)(style ^ SkTypeface::kItalic); 105+ if (faces[style] != NULL) { 106+ return faces[style]; 107+ } 108+ // look for the plain 109+ if (faces[SkTypeface::kNormal] != NULL) { 110+ return faces[SkTypeface::kNormal]; 111+ } 112+ // look for anything 113+ for (int i = 0; i < 4; i++) { 114+ if (faces[i] != NULL) { 115+ return faces[i]; 116+ } 117+ } 118+ // should never get here, since the faces list should not be empty 119+ SkASSERT(!"faces list is empty"); 120+ return NULL; 121+} 122+ 123+static FamilyRec* find_family(const SkTypeface* member) { 124+ FamilyRec* curr = gFamilyHead; 125+ while (curr != NULL) { 126+ for (int i = 0; i < 4; i++) { 127+ if (curr->fFaces[i] == member) { 128+ return curr; 129+ } 130+ } 131+ curr = curr->fNext; 132+ } 133+ return NULL; 134+} 135+ 136+/* Returns the matching typeface, or NULL. If a typeface is found, its refcnt 137+ is not modified. 138+ */ 139+static SkTypeface* find_from_uniqueID(uint32_t uniqueID) { 140+ FamilyRec* curr = gFamilyHead; 141+ while (curr != NULL) { 142+ for (int i = 0; i < 4; i++) { 143+ SkTypeface* face = curr->fFaces[i]; 144+ if (face != NULL && face->uniqueID() == uniqueID) { 145+ return face; 146+ } 147+ } 148+ curr = curr->fNext; 149+ } 150+ return NULL; 151+} 152+ 153+/* Remove reference to this face from its family. If the resulting family 154+ is empty (has no faces), return that family, otherwise return NULL 155+*/ 156+static FamilyRec* remove_from_family(const SkTypeface* face) { 157+ FamilyRec* family = find_family(face); 158+ SkASSERT(family->fFaces[face->style()] == face); 159+ family->fFaces[face->style()] = NULL; 160+ 161+ for (int i = 0; i < 4; i++) { 162+ if (family->fFaces[i] != NULL) { // family is non-empty 163+ return NULL; 164+ } 165+ } 166+ return family; // return the empty family 167+} 168+ 169+// maybe we should make FamilyRec be doubly-linked 170+static void detach_and_delete_family(FamilyRec* family) { 171+ FamilyRec* curr = gFamilyHead; 172+ FamilyRec* prev = NULL; 173+ 174+ while (curr != NULL) { 175+ FamilyRec* next = curr->fNext; 176+ if (curr == family) { 177+ if (prev == NULL) { 178+ gFamilyHead = next; 179+ } else { 180+ prev->fNext = next; 181+ } 182+ SkDELETE(family); 183+ return; 184+ } 185+ prev = curr; 186+ curr = next; 187+ } 188+ SkASSERT(!"Yikes, couldn't find family in our list to remove/delete"); 189+} 190+ 191+static SkTypeface* find_typeface(const char name[], SkTypeface::Style style) { 192+ NameFamilyPair* list = gNameList.begin(); 193+ int count = gNameList.count(); 194+ 195+ int index = SkStrLCSearch(&list[0].fName, count, name, sizeof(list[0])); 196+ 197+ if (index >= 0) { 198+ return find_best_face(list[index].fFamily, style); 199+ } 200+ return NULL; 201+} 202+ 203+static SkTypeface* find_typeface(const SkTypeface* familyMember, 204+ SkTypeface::Style style) { 205+ const FamilyRec* family = find_family(familyMember); 206+ return family ? find_best_face(family, style) : NULL; 207+} 208+ 209+static void add_name(const char name[], FamilyRec* family) { 210+ SkAutoAsciiToLC tolc(name); 211+ name = tolc.lc(); 212+ 213+ NameFamilyPair* list = gNameList.begin(); 214+ int count = gNameList.count(); 215+ 216+ int index = SkStrLCSearch(&list[0].fName, count, name, sizeof(list[0])); 217+ 218+ if (index < 0) { 219+ list = gNameList.insert(~index); 220+ list->construct(name, family); 221+ } 222+} 223+ 224+static void remove_from_names(FamilyRec* emptyFamily) 225+{ 226+#ifdef SK_DEBUG 227+ for (int i = 0; i < 4; i++) { 228+ SkASSERT(emptyFamily->fFaces[i] == NULL); 229+ } 230+#endif 231+ 232+ SkTDArray<NameFamilyPair>& list = gNameList; 233+ 234+ // must go backwards when removing 235+ for (int i = list.count() - 1; i >= 0; --i) { 236+ NameFamilyPair* pair = &list[i]; 237+ if (pair->fFamily == emptyFamily) { 238+ pair->destruct(); 239+ list.remove(i); 240+ } 241+ } 242+} 243+ 244+/////////////////////////////////////////////////////////////////////////////// 245+ 246+class FamilyTypeface : public SkTypeface { 247+public: 248+ FamilyTypeface(Style style, bool sysFont, SkTypeface* familyMember, 249+ bool isFixedWidth) 250+ : SkTypeface(style, sk_atomic_inc(&gUniqueFontID) + 1, isFixedWidth) { 251+ fIsSysFont = sysFont; 252+ 253+ SkAutoMutexAcquire ac(gFamilyMutex); 254+ 255+ FamilyRec* rec = NULL; 256+ if (familyMember) { 257+ rec = find_family(familyMember); 258+ SkASSERT(rec); 259+ } else { 260+ rec = SkNEW(FamilyRec); 261+ } 262+ rec->fFaces[style] = this; 263+ } 264+ 265+ virtual ~FamilyTypeface() { 266+ SkAutoMutexAcquire ac(gFamilyMutex); 267+ 268+ // remove us from our family. If the family is now empty, we return 269+ // that and then remove that family from the name list 270+ FamilyRec* family = remove_from_family(this); 271+ if (NULL != family) { 272+ remove_from_names(family); 273+ detach_and_delete_family(family); 274+ } 275+ } 276+ 277+ bool isSysFont() const { return fIsSysFont; } 278+ 279+ virtual SkStream* openStream() = 0; 280+ virtual const char* getUniqueString() const = 0; 281+ virtual const char* getFilePath() const = 0; 282+ 283+private: 284+ bool fIsSysFont; 285+ 286+ typedef SkTypeface INHERITED; 287+}; 288+ 289+/////////////////////////////////////////////////////////////////////////////// 290+ 291+class StreamTypeface : public FamilyTypeface { 292+public: 293+ StreamTypeface(Style style, bool sysFont, SkTypeface* familyMember, 294+ SkStream* stream, bool isFixedWidth) 295+ : INHERITED(style, sysFont, familyMember, isFixedWidth) { 296+ SkASSERT(stream); 297+ stream->ref(); 298+ fStream = stream; 299+ } 300+ virtual ~StreamTypeface() { 301+ fStream->unref(); 302+ } 303+ 304+ // overrides 305+ virtual SkStream* openStream() { 306+ // we just ref our existing stream, since the caller will call unref() 307+ // when they are through 308+ fStream->ref(); 309+ // must rewind each time, since the caller assumes a "new" stream 310+ fStream->rewind(); 311+ return fStream; 312+ } 313+ virtual const char* getUniqueString() const { return NULL; } 314+ virtual const char* getFilePath() const { return NULL; } 315+ 316+private: 317+ SkStream* fStream; 318+ 319+ typedef FamilyTypeface INHERITED; 320+}; 321+ 322+class FileTypeface : public FamilyTypeface { 323+public: 324+ FileTypeface(Style style, bool sysFont, SkTypeface* familyMember, 325+ const char path[], bool isFixedWidth) 326+ : INHERITED(style, sysFont, familyMember, isFixedWidth) { 327+ SkString fullpath; 328+ 329+ if (sysFont) { 330+ GetFullPathForSysFonts(&fullpath, path); 331+ path = fullpath.c_str(); 332+ } 333+ fPath.set(path); 334+ } 335+ 336+ // overrides 337+ virtual SkStream* openStream() { 338+ SkStream* stream = SkNEW_ARGS(SkMMAPStream, (fPath.c_str())); 339+ 340+ // check for failure 341+ if (stream->getLength() <= 0) { 342+ SkDELETE(stream); 343+ // maybe MMAP isn't supported. try FILE 344+ stream = SkNEW_ARGS(SkFILEStream, (fPath.c_str())); 345+ if (stream->getLength() <= 0) { 346+ SkDELETE(stream); 347+ stream = NULL; 348+ } 349+ } 350+ return stream; 351+ } 352+ virtual const char* getUniqueString() const { 353+ const char* str = strrchr(fPath.c_str(), '/'); 354+ if (str) { 355+ str += 1; // skip the '/' 356+ } 357+ return str; 358+ } 359+ virtual const char* getFilePath() const { 360+ return fPath.c_str(); 361+ } 362+ 363+private: 364+ SkString fPath; 365+ 366+ typedef FamilyTypeface INHERITED; 367+}; 368+ 369+/////////////////////////////////////////////////////////////////////////////// 370+/////////////////////////////////////////////////////////////////////////////// 371+ 372+static bool get_name_and_style(const char path[], SkString* name, 373+ SkTypeface::Style* style, 374+ bool* isFixedWidth, bool isExpected) { 375+ SkString fullpath; 376+ GetFullPathForSysFonts(&fullpath, path); 377+ 378+ SkMMAPStream stream(fullpath.c_str()); 379+ if (stream.getLength() > 0) { 380+ find_name_and_attributes(&stream, name, style, isFixedWidth); 381+ return true; 382+ } 383+ else { 384+ SkFILEStream stream(fullpath.c_str()); 385+ if (stream.getLength() > 0) { 386+ find_name_and_attributes(&stream, name, style, isFixedWidth); 387+ return true; 388+ } 389+ } 390+ 391+ if (isExpected) { 392+ SkDebugf("---- failed to open <%s> as a font\n", fullpath.c_str()); 393+ } 394+ return false; 395+} 396+ 397+// used to record our notion of the pre-existing fonts 398+struct FontInitRec { 399+ const char* fFileName; 400+ const char* const* fNames; // null-terminated list 401+}; 402+ 403+static const char* gSansNames[] = { 404+ "sans-serif", "arial", "helvetica", "tahoma", "verdana", NULL 405+}; 406+ 407+static const char* gSerifNames[] = { 408+ "serif", "times", "times new roman", "palatino", "georgia", "baskerville", 409+ "goudy", "fantasy", "cursive", "ITC Stone Serif", NULL 410+}; 411+ 412+static const char* gMonoNames[] = { 413+ "monospace", "courier", "courier new", "monaco", NULL 414+}; 415+ 416+// deliberately empty, but we use the address to identify fallback fonts 417+static const char* gFBNames[] = { NULL }; 418+ 419+/* Fonts must be grouped by family, with the first font in a family having the 420+ list of names (even if that list is empty), and the following members having 421+ null for the list. The names list must be NULL-terminated 422+*/ 423+static const FontInitRec gSystemFonts[] = { 424+ { "DroidSans.ttf", gSansNames }, 425+ { "DroidSans-Bold.ttf", NULL }, 426+ { "DroidSerif-Regular.ttf", gSerifNames }, 427+ { "DroidSerif-Bold.ttf", NULL }, 428+ { "DroidSerif-Italic.ttf", NULL }, 429+ { "DroidSerif-BoldItalic.ttf", NULL }, 430+ { "DroidSansMono.ttf", gMonoNames }, 431+ /* These are optional, and can be ignored if not found in the file system. 432+ These are appended to gFallbackFonts[] as they are seen, so we list 433+ them in the order we want them to be accessed by NextLogicalFont(). 434+ */ 435+ { "DroidSansArabic.ttf", gFBNames }, 436+ { "DroidSansHebrew.ttf", gFBNames }, 437+ { "DroidSansThai.ttf", gFBNames }, 438+ { "MTLmr3m.ttf", gFBNames }, // Motoya Japanese Font 439+ { "MTLc3m.ttf", gFBNames }, // Motoya Japanese Font 440+ { "DroidSansJapanese.ttf", gFBNames }, 441+ { "DroidSansFallback.ttf", gFBNames } 442+}; 443+ 444+#define DEFAULT_NAMES gSansNames 445+ 446+// these globals are assigned (once) by load_system_fonts() 447+static FamilyRec* gDefaultFamily; 448+static SkTypeface* gDefaultNormal; 449+ 450+/* This is sized conservatively, assuming that it will never be a size issue. 451+ It will be initialized in load_system_fonts(), and will be filled with the 452+ fontIDs that can be used for fallback consideration, in sorted order (sorted 453+ meaning element[0] should be used first, then element[1], etc. When we hit 454+ a fontID==0 in the array, the list is done, hence our allocation size is 455+ +1 the total number of possible system fonts. Also see NextLogicalFont(). 456+ */ 457+static uint32_t gFallbackFonts[SK_ARRAY_COUNT(gSystemFonts)+1]; 458+ 459+/* Called once (ensured by the sentinel check at the beginning of our body). 460+ Initializes all the globals, and register the system fonts. 461+ */ 462+static void load_system_fonts() { 463+ // check if we've already be called 464+ if (NULL != gDefaultNormal) { 465+ return; 466+ } 467+ 468+ const FontInitRec* rec = gSystemFonts; 469+ SkTypeface* firstInFamily = NULL; 470+ int fallbackCount = 0; 471+ 472+ for (size_t i = 0; i < SK_ARRAY_COUNT(gSystemFonts); i++) { 473+ // if we're the first in a new family, clear firstInFamily 474+ if (rec[i].fNames != NULL) { 475+ firstInFamily = NULL; 476+ } 477+ 478+ bool isFixedWidth; 479+ SkString name; 480+ SkTypeface::Style style; 481+ 482+ // we expect all the fonts, except the "fallback" fonts 483+ bool isExpected = (rec[i].fNames != gFBNames); 484+ if (!get_name_and_style(rec[i].fFileName, &name, &style, 485+ &isFixedWidth, isExpected)) { 486+ continue; 487+ } 488+ 489+ SkTypeface* tf = SkNEW_ARGS(FileTypeface, 490+ (style, 491+ true, // system-font (cannot delete) 492+ firstInFamily, // what family to join 493+ rec[i].fFileName, 494+ isFixedWidth) // filename 495+ ); 496+ 497+ if (rec[i].fNames != NULL) { 498+ // see if this is one of our fallback fonts 499+ if (rec[i].fNames == gFBNames) { 500+ // SkDebugf("---- adding %s as fallback[%d] fontID %d\n", 501+ // rec[i].fFileName, fallbackCount, tf->uniqueID()); 502+ gFallbackFonts[fallbackCount++] = tf->uniqueID(); 503+ } 504+ 505+ firstInFamily = tf; 506+ FamilyRec* family = find_family(tf); 507+ const char* const* names = rec[i].fNames; 508+ 509+ // record the default family if this is it 510+ if (names == DEFAULT_NAMES) { 511+ gDefaultFamily = family; 512+ } 513+ // add the names to map to this family 514+ while (*names) { 515+ add_name(*names, family); 516+ names += 1; 517+ } 518+ } 519+ } 520+ 521+ // do this after all fonts are loaded. This is our default font, and it 522+ // acts as a sentinel so we only execute load_system_fonts() once 523+ gDefaultNormal = find_best_face(gDefaultFamily, SkTypeface::kNormal); 524+ // now terminate our fallback list with the sentinel value 525+ gFallbackFonts[fallbackCount] = 0; 526+} 527+ 528+/////////////////////////////////////////////////////////////////////////////// 529+ 530+void SkFontHost::Serialize(const SkTypeface* face, SkWStream* stream) { 531+ const char* name = ((FamilyTypeface*)face)->getUniqueString(); 532+ 533+ stream->write8((uint8_t)face->style()); 534+ 535+ if (NULL == name || 0 == *name) { 536+ stream->writePackedUInt(0); 537+// SkDebugf("--- fonthost serialize null\n"); 538+ } else { 539+ uint32_t len = strlen(name); 540+ stream->writePackedUInt(len); 541+ stream->write(name, len); 542+// SkDebugf("--- fonthost serialize <%s> %d\n", name, face->style()); 543+ } 544+} 545+ 546+SkTypeface* SkFontHost::Deserialize(SkStream* stream) { 547+ load_system_fonts(); 548+ 549+ int style = stream->readU8(); 550+ 551+ int len = stream->readPackedUInt(); 552+ if (len > 0) { 553+ SkString str; 554+ str.resize(len); 555+ stream->read(str.writable_str(), len); 556+ 557+ const FontInitRec* rec = gSystemFonts; 558+ for (size_t i = 0; i < SK_ARRAY_COUNT(gSystemFonts); i++) { 559+ if (strcmp(rec[i].fFileName, str.c_str()) == 0) { 560+ // backup until we hit the fNames 561+ for (int j = i; j >= 0; --j) { 562+ if (rec[j].fNames != NULL) { 563+ return SkFontHost::CreateTypeface(NULL, 564+ rec[j].fNames[0], (SkTypeface::Style)style); 565+ } 566+ } 567+ } 568+ } 569+ } 570+ return NULL; 571+} 572+ 573+/////////////////////////////////////////////////////////////////////////////// 574+ 575+SkTypeface* SkFontHost::CreateTypeface(const SkTypeface* familyFace, 576+ const char familyName[], 577+ SkTypeface::Style style) { 578+ load_system_fonts(); 579+ 580+ SkAutoMutexAcquire ac(gFamilyMutex); 581+ 582+ // clip to legal style bits 583+ style = (SkTypeface::Style)(style & SkTypeface::kBoldItalic); 584+ 585+ SkTypeface* tf = NULL; 586+ 587+ if (NULL != familyFace) { 588+ tf = find_typeface(familyFace, style); 589+ } else if (NULL != familyName) { 590+// SkDebugf("======= familyName <%s>\n", familyName); 591+ tf = find_typeface(familyName, style); 592+ } 593+ 594+ if (NULL == tf) { 595+ tf = find_best_face(gDefaultFamily, style); 596+ } 597+ 598+ // we ref(), since the symantic is to return a new instance 599+ tf->ref(); 600+ return tf; 601+} 602+ 603+SkStream* SkFontHost::OpenStream(uint32_t fontID) { 604+ SkAutoMutexAcquire ac(gFamilyMutex); 605+ 606+ FamilyTypeface* tf = (FamilyTypeface*)find_from_uniqueID(fontID); 607+ SkStream* stream = tf ? tf->openStream() : NULL; 608+ 609+ if (stream && stream->getLength() == 0) { 610+ stream->unref(); 611+ stream = NULL; 612+ } 613+ return stream; 614+} 615+ 616+size_t SkFontHost::GetFileName(SkFontID fontID, char path[], size_t length, 617+ int32_t* index) { 618+ SkAutoMutexAcquire ac(gFamilyMutex); 619+ 620+ FamilyTypeface* tf = (FamilyTypeface*)find_from_uniqueID(fontID); 621+ const char* src = tf ? tf->getFilePath() : NULL; 622+ 623+ if (src) { 624+ size_t size = strlen(src); 625+ if (path) { 626+ memcpy(path, src, SkMin32(size, length)); 627+ } 628+ if (index) { 629+ *index = 0; // we don't have collections (yet) 630+ } 631+ return size; 632+ } else { 633+ return 0; 634+ } 635+} 636+ 637+SkFontID SkFontHost::NextLogicalFont(SkFontID currFontID, SkFontID origFontID) { 638+ load_system_fonts(); 639+ 640+ /* First see if fontID is already one of our fallbacks. If so, return 641+ its successor. If fontID is not in our list, then return the first one 642+ in our list. Note: list is zero-terminated, and returning zero means 643+ we have no more fonts to use for fallbacks. 644+ */ 645+ const uint32_t* list = gFallbackFonts; 646+ for (int i = 0; list[i] != 0; i++) { 647+ if (list[i] == currFontID) { 648+ return list[i+1]; 649+ } 650+ } 651+ return list[0]; 652+} 653+ 654+/////////////////////////////////////////////////////////////////////////////// 655+ 656+SkTypeface* SkFontHost::CreateTypefaceFromStream(SkStream* stream) { 657+ if (NULL == stream || stream->getLength() <= 0) { 658+ return NULL; 659+ } 660+ 661+ bool isFixedWidth; 662+ SkString name; 663+ SkTypeface::Style style; 664+ find_name_and_attributes(stream, &name, &style, &isFixedWidth); 665+ 666+ if (!name.isEmpty()) { 667+ return SkNEW_ARGS(StreamTypeface, (style, false, NULL, stream, isFixedWidth)); 668+ } else { 669+ return NULL; 670+ } 671+} 672+ 673+SkTypeface* SkFontHost::CreateTypefaceFromFile(const char path[]) { 674+ SkStream* stream = SkNEW_ARGS(SkMMAPStream, (path)); 675+ SkTypeface* face = SkFontHost::CreateTypefaceFromStream(stream); 676+ // since we created the stream, we let go of our ref() here 677+ stream->unref(); 678+ return face; 679+} 680+ 681+/////////////////////////////////////////////////////////////////////////////// 682-- 6831.7.11.4 684 685