00001
00002
00003
00004
00005
00006
00007
00008
00009
00012 #include "stdafx.h"
00013 #include "fontcache.h"
00014 #include "fontdetection.h"
00015 #include "blitter/factory.hpp"
00016 #include "core/math_func.hpp"
00017 #include "core/smallmap_type.hpp"
00018 #include "strings_func.h"
00019 #include "zoom_type.h"
00020 #include "gfx_layout.h"
00021
00022 #include "table/sprites.h"
00023 #include "table/control_codes.h"
00024 #include "table/unicode.h"
00025
00026 static const int ASCII_LETTERSTART = 32;
00027 static const int MAX_FONT_SIZE = 72;
00028
00030 static const int _default_font_height[FS_END] = {10, 6, 18, 10};
00031 static const int _default_font_ascender[FS_END] = { 8, 5, 15, 8};
00032
00037 FontCache::FontCache(FontSize fs) : parent(FontCache::Get(fs)), fs(fs), height(_default_font_height[fs]),
00038 ascender(_default_font_ascender[fs]), descender(_default_font_ascender[fs] - _default_font_height[fs]),
00039 units_per_em(1)
00040 {
00041 assert(parent == NULL || this->fs == parent->fs);
00042 FontCache::caches[this->fs] = this;
00043 Layouter::ResetFontCache(this->fs);
00044 }
00045
00047 FontCache::~FontCache()
00048 {
00049 assert(this->fs == parent->fs);
00050 FontCache::caches[this->fs] = this->parent;
00051 Layouter::ResetFontCache(this->fs);
00052 }
00053
00054
00060 int GetCharacterHeight(FontSize size)
00061 {
00062 return FontCache::Get(size)->GetHeight();
00063 }
00064
00065
00067 class SpriteFontCache : public FontCache {
00068 private:
00069 SpriteID **glyph_to_spriteid_map;
00070
00071 void ClearGlyphToSpriteMap();
00072 public:
00073 SpriteFontCache(FontSize fs);
00074 ~SpriteFontCache();
00075 virtual SpriteID GetUnicodeGlyph(WChar key);
00076 virtual void SetUnicodeGlyph(WChar key, SpriteID sprite);
00077 virtual void InitializeUnicodeGlyphMap();
00078 virtual void ClearFontCache();
00079 virtual const Sprite *GetGlyph(GlyphID key);
00080 virtual uint GetGlyphWidth(GlyphID key);
00081 virtual bool GetDrawGlyphShadow();
00082 virtual GlyphID MapCharToGlyph(WChar key) { assert(IsPrintable(key)); return SPRITE_GLYPH | key; }
00083 virtual const void *GetFontTable(uint32 tag, size_t &length) { length = 0; return NULL; }
00084 virtual const char *GetFontName() { return "sprite"; }
00085 };
00086
00091 SpriteFontCache::SpriteFontCache(FontSize fs) : FontCache(fs), glyph_to_spriteid_map(NULL)
00092 {
00093 this->InitializeUnicodeGlyphMap();
00094 }
00095
00099 SpriteFontCache::~SpriteFontCache()
00100 {
00101 this->ClearGlyphToSpriteMap();
00102 }
00103
00104 SpriteID SpriteFontCache::GetUnicodeGlyph(GlyphID key)
00105 {
00106 if (this->glyph_to_spriteid_map[GB(key, 8, 8)] == NULL) return 0;
00107 return this->glyph_to_spriteid_map[GB(key, 8, 8)][GB(key, 0, 8)];
00108 }
00109
00110 void SpriteFontCache::SetUnicodeGlyph(GlyphID key, SpriteID sprite)
00111 {
00112 if (this->glyph_to_spriteid_map == NULL) this->glyph_to_spriteid_map = CallocT<SpriteID*>(256);
00113 if (this->glyph_to_spriteid_map[GB(key, 8, 8)] == NULL) this->glyph_to_spriteid_map[GB(key, 8, 8)] = CallocT<SpriteID>(256);
00114 this->glyph_to_spriteid_map[GB(key, 8, 8)][GB(key, 0, 8)] = sprite;
00115 }
00116
00117 void SpriteFontCache::InitializeUnicodeGlyphMap()
00118 {
00119
00120 this->ClearGlyphToSpriteMap();
00121
00122 SpriteID base;
00123 switch (this->fs) {
00124 default: NOT_REACHED();
00125 case FS_MONO:
00126 case FS_NORMAL: base = SPR_ASCII_SPACE; break;
00127 case FS_SMALL: base = SPR_ASCII_SPACE_SMALL; break;
00128 case FS_LARGE: base = SPR_ASCII_SPACE_BIG; break;
00129 }
00130
00131 for (uint i = ASCII_LETTERSTART; i < 256; i++) {
00132 SpriteID sprite = base + i - ASCII_LETTERSTART;
00133 if (!SpriteExists(sprite)) continue;
00134 this->SetUnicodeGlyph(i, sprite);
00135 this->SetUnicodeGlyph(i + SCC_SPRITE_START, sprite);
00136 }
00137
00138 for (uint i = 0; i < lengthof(_default_unicode_map); i++) {
00139 byte key = _default_unicode_map[i].key;
00140 if (key == CLRA) {
00141
00142
00143
00144 this->SetUnicodeGlyph(_default_unicode_map[i].code, 0);
00145 } else {
00146 SpriteID sprite = base + key - ASCII_LETTERSTART;
00147 this->SetUnicodeGlyph(_default_unicode_map[i].code, sprite);
00148 }
00149 }
00150 }
00151
00155 void SpriteFontCache::ClearGlyphToSpriteMap()
00156 {
00157 if (this->glyph_to_spriteid_map == NULL) return;
00158
00159 for (uint i = 0; i < 256; i++) {
00160 free(this->glyph_to_spriteid_map[i]);
00161 }
00162 free(this->glyph_to_spriteid_map);
00163 this->glyph_to_spriteid_map = NULL;
00164 }
00165
00166 void SpriteFontCache::ClearFontCache()
00167 {
00168 Layouter::ResetFontCache(this->fs);
00169 }
00170
00171 const Sprite *SpriteFontCache::GetGlyph(GlyphID key)
00172 {
00173 SpriteID sprite = this->GetUnicodeGlyph(key);
00174 if (sprite == 0) sprite = this->GetUnicodeGlyph('?');
00175 return GetSprite(sprite, ST_FONT);
00176 }
00177
00178 uint SpriteFontCache::GetGlyphWidth(GlyphID key)
00179 {
00180 SpriteID sprite = this->GetUnicodeGlyph(key);
00181 if (sprite == 0) sprite = this->GetUnicodeGlyph('?');
00182 return SpriteExists(sprite) ? GetSprite(sprite, ST_FONT)->width + (this->fs != FS_NORMAL) : 0;
00183 }
00184
00185 bool SpriteFontCache::GetDrawGlyphShadow()
00186 {
00187 return false;
00188 }
00189
00190 FontCache *FontCache::caches[FS_END] = { new SpriteFontCache(FS_NORMAL), new SpriteFontCache(FS_SMALL), new SpriteFontCache(FS_LARGE), new SpriteFontCache(FS_MONO) };
00191
00192 #ifdef WITH_FREETYPE
00193 #include <ft2build.h>
00194 #include FT_FREETYPE_H
00195 #include FT_GLYPH_H
00196 #include FT_TRUETYPE_TABLES_H
00197
00199 class FreeTypeFontCache : public FontCache {
00200 private:
00201 FT_Face face;
00202
00203 typedef SmallMap<uint32, SmallPair<size_t, const void*> > FontTable;
00204 FontTable font_tables;
00205
00207 struct GlyphEntry {
00208 Sprite *sprite;
00209 byte width;
00210 bool duplicate;
00211 };
00212
00226 GlyphEntry **glyph_to_sprite;
00227
00228 GlyphEntry *GetGlyphPtr(GlyphID key);
00229 void SetGlyphPtr(GlyphID key, const GlyphEntry *glyph, bool duplicate = false);
00230
00231 public:
00232 FreeTypeFontCache(FontSize fs, FT_Face face, int pixels);
00233 ~FreeTypeFontCache();
00234 virtual SpriteID GetUnicodeGlyph(WChar key) { return this->parent->GetUnicodeGlyph(key); }
00235 virtual void SetUnicodeGlyph(WChar key, SpriteID sprite) { this->parent->SetUnicodeGlyph(key, sprite); }
00236 virtual void InitializeUnicodeGlyphMap() { this->parent->InitializeUnicodeGlyphMap(); }
00237 virtual void ClearFontCache();
00238 virtual const Sprite *GetGlyph(GlyphID key);
00239 virtual uint GetGlyphWidth(GlyphID key);
00240 virtual bool GetDrawGlyphShadow();
00241 virtual GlyphID MapCharToGlyph(WChar key);
00242 virtual const void *GetFontTable(uint32 tag, size_t &length);
00243 virtual const char *GetFontName() { return face->family_name; }
00244 };
00245
00246 FT_Library _library = NULL;
00247
00248 FreeTypeSettings _freetype;
00249
00250 static const byte FACE_COLOUR = 1;
00251 static const byte SHADOW_COLOUR = 2;
00252
00259 FreeTypeFontCache::FreeTypeFontCache(FontSize fs, FT_Face face, int pixels) : FontCache(fs), face(face), glyph_to_sprite(NULL)
00260 {
00261 assert(face != NULL);
00262
00263 if (pixels == 0) {
00264
00265 pixels = _default_font_height[this->fs];
00266
00267 TT_Header *head = (TT_Header *)FT_Get_Sfnt_Table(this->face, ft_sfnt_head);
00268 if (head != NULL) {
00269
00270
00271 int diff = _default_font_height[this->fs] - _default_font_height[FS_SMALL];
00272 pixels = Clamp(min(head->Lowest_Rec_PPEM, 20) + diff, _default_font_height[this->fs], MAX_FONT_SIZE);
00273 }
00274 }
00275
00276 FT_Error err = FT_Set_Pixel_Sizes(this->face, 0, pixels);
00277 if (err != FT_Err_Ok) {
00278
00279
00280 FT_Bitmap_Size *bs = this->face->available_sizes;
00281 int i = this->face->num_fixed_sizes;
00282 if (i > 0) {
00283 int n = bs->height;
00284 FT_Int chosen = 0;
00285 for (; --i; bs++) {
00286 if (abs(pixels - bs->height) >= abs(pixels - n)) continue;
00287 n = bs->height;
00288 chosen = this->face->num_fixed_sizes - i;
00289 }
00290
00291
00292
00293 err = FT_Select_Size(this->face, chosen);
00294 }
00295 }
00296
00297 if (err == FT_Err_Ok) {
00298 this->units_per_em = this->face->units_per_EM;
00299 this->ascender = this->face->size->metrics.ascender >> 6;
00300 this->descender = this->face->size->metrics.descender >> 6;
00301 this->height = this->ascender - this->descender;
00302 } else {
00303
00304 DEBUG(freetype, 0, "Font size selection failed. Using FontCache defaults.");
00305 }
00306 }
00307
00315 static void LoadFreeTypeFont(FontSize fs)
00316 {
00317 FreeTypeSubSetting *settings = NULL;
00318 switch (fs) {
00319 default: NOT_REACHED();
00320 case FS_SMALL: settings = &_freetype.small; break;
00321 case FS_NORMAL: settings = &_freetype.medium; break;
00322 case FS_LARGE: settings = &_freetype.large; break;
00323 case FS_MONO: settings = &_freetype.mono; break;
00324 }
00325
00326 if (StrEmpty(settings->font)) return;
00327
00328 if (_library == NULL) {
00329 if (FT_Init_FreeType(&_library) != FT_Err_Ok) {
00330 ShowInfoF("Unable to initialize FreeType, using sprite fonts instead");
00331 return;
00332 }
00333
00334 DEBUG(freetype, 2, "Initialized");
00335 }
00336
00337 FT_Face face = NULL;
00338 FT_Error error = FT_New_Face(_library, settings->font, 0, &face);
00339
00340 if (error != FT_Err_Ok) error = GetFontByFaceName(settings->font, &face);
00341
00342 if (error == FT_Err_Ok) {
00343 DEBUG(freetype, 2, "Requested '%s', using '%s %s'", settings->font, face->family_name, face->style_name);
00344
00345
00346 error = FT_Select_Charmap(face, ft_encoding_unicode);
00347 if (error == FT_Err_Ok) goto found_face;
00348
00349 if (error == FT_Err_Invalid_CharMap_Handle) {
00350
00351
00352
00353 FT_CharMap found = face->charmaps[0];
00354 int i;
00355
00356 for (i = 0; i < face->num_charmaps; i++) {
00357 FT_CharMap charmap = face->charmaps[i];
00358 if (charmap->platform_id == 0 && charmap->encoding_id == 0) {
00359 found = charmap;
00360 }
00361 }
00362
00363 if (found != NULL) {
00364 error = FT_Set_Charmap(face, found);
00365 if (error == FT_Err_Ok) goto found_face;
00366 }
00367 }
00368 }
00369
00370 FT_Done_Face(face);
00371
00372 static const char *SIZE_TO_NAME[] = { "medium", "small", "large", "mono" };
00373 ShowInfoF("Unable to use '%s' for %s font, FreeType reported error 0x%X, using sprite font instead", settings->font, SIZE_TO_NAME[fs], error);
00374 return;
00375
00376 found_face:
00377 new FreeTypeFontCache(fs, face, settings->size);
00378 }
00379
00380
00384 FreeTypeFontCache::~FreeTypeFontCache()
00385 {
00386 FT_Done_Face(this->face);
00387 this->ClearFontCache();
00388
00389 for (FontTable::iterator iter = this->font_tables.Begin(); iter != this->font_tables.End(); iter++) {
00390 free(iter->second.second);
00391 }
00392 }
00393
00397 void FreeTypeFontCache::ClearFontCache()
00398 {
00399 if (this->glyph_to_sprite == NULL) return;
00400
00401 for (int i = 0; i < 256; i++) {
00402 if (this->glyph_to_sprite[i] == NULL) continue;
00403
00404 for (int j = 0; j < 256; j++) {
00405 if (this->glyph_to_sprite[i][j].duplicate) continue;
00406 free(this->glyph_to_sprite[i][j].sprite);
00407 }
00408
00409 free(this->glyph_to_sprite[i]);
00410 }
00411
00412 free(this->glyph_to_sprite);
00413 this->glyph_to_sprite = NULL;
00414
00415 Layouter::ResetFontCache(this->fs);
00416 }
00417
00418 FreeTypeFontCache::GlyphEntry *FreeTypeFontCache::GetGlyphPtr(GlyphID key)
00419 {
00420 if (this->glyph_to_sprite == NULL) return NULL;
00421 if (this->glyph_to_sprite[GB(key, 8, 8)] == NULL) return NULL;
00422 return &this->glyph_to_sprite[GB(key, 8, 8)][GB(key, 0, 8)];
00423 }
00424
00425
00426 void FreeTypeFontCache::SetGlyphPtr(GlyphID key, const GlyphEntry *glyph, bool duplicate)
00427 {
00428 if (this->glyph_to_sprite == NULL) {
00429 DEBUG(freetype, 3, "Allocating root glyph cache for size %u", this->fs);
00430 this->glyph_to_sprite = CallocT<GlyphEntry*>(256);
00431 }
00432
00433 if (this->glyph_to_sprite[GB(key, 8, 8)] == NULL) {
00434 DEBUG(freetype, 3, "Allocating glyph cache for range 0x%02X00, size %u", GB(key, 8, 8), this->fs);
00435 this->glyph_to_sprite[GB(key, 8, 8)] = CallocT<GlyphEntry>(256);
00436 }
00437
00438 DEBUG(freetype, 4, "Set glyph for unicode character 0x%04X, size %u", key, this->fs);
00439 this->glyph_to_sprite[GB(key, 8, 8)][GB(key, 0, 8)].sprite = glyph->sprite;
00440 this->glyph_to_sprite[GB(key, 8, 8)][GB(key, 0, 8)].width = glyph->width;
00441 this->glyph_to_sprite[GB(key, 8, 8)][GB(key, 0, 8)].duplicate = duplicate;
00442 }
00443
00444 static void *AllocateFont(size_t size)
00445 {
00446 return MallocT<byte>(size);
00447 }
00448
00449
00450
00451 static bool GetFontAAState(FontSize size)
00452 {
00453
00454 if (BlitterFactory::GetCurrentBlitter()->GetScreenDepth() != 32) return false;
00455
00456 switch (size) {
00457 default: NOT_REACHED();
00458 case FS_NORMAL: return _freetype.medium.aa;
00459 case FS_SMALL: return _freetype.small.aa;
00460 case FS_LARGE: return _freetype.large.aa;
00461 case FS_MONO: return _freetype.mono.aa;
00462 }
00463 }
00464
00465
00466 const Sprite *FreeTypeFontCache::GetGlyph(GlyphID key)
00467 {
00468 if ((key & SPRITE_GLYPH) != 0) return parent->GetGlyph(key);
00469
00470
00471 GlyphEntry *glyph = this->GetGlyphPtr(key);
00472 if (glyph != NULL && glyph->sprite != NULL) return glyph->sprite;
00473
00474 FT_GlyphSlot slot = this->face->glyph;
00475
00476 bool aa = GetFontAAState(this->fs);
00477
00478 GlyphEntry new_glyph;
00479 if (key == 0) {
00480 GlyphID question_glyph = this->MapCharToGlyph('?');
00481 if (question_glyph == 0) {
00482
00483
00484 #define CPSET { 0, 0, 0, 0, 1 }
00485 #define CP___ { 0, 0, 0, 0, 0 }
00486 static SpriteLoader::CommonPixel builtin_questionmark_data[10 * 8] = {
00487 CP___, CP___, CPSET, CPSET, CPSET, CPSET, CP___, CP___,
00488 CP___, CPSET, CPSET, CP___, CP___, CPSET, CPSET, CP___,
00489 CP___, CP___, CP___, CP___, CP___, CPSET, CPSET, CP___,
00490 CP___, CP___, CP___, CP___, CPSET, CPSET, CP___, CP___,
00491 CP___, CP___, CP___, CPSET, CPSET, CP___, CP___, CP___,
00492 CP___, CP___, CP___, CPSET, CPSET, CP___, CP___, CP___,
00493 CP___, CP___, CP___, CPSET, CPSET, CP___, CP___, CP___,
00494 CP___, CP___, CP___, CP___, CP___, CP___, CP___, CP___,
00495 CP___, CP___, CP___, CPSET, CPSET, CP___, CP___, CP___,
00496 CP___, CP___, CP___, CPSET, CPSET, CP___, CP___, CP___,
00497 };
00498 #undef CPSET
00499 #undef CP___
00500 static const SpriteLoader::Sprite builtin_questionmark = {
00501 10,
00502 8,
00503 0,
00504 0,
00505 ST_FONT,
00506 builtin_questionmark_data
00507 };
00508
00509 Sprite *spr = BlitterFactory::GetCurrentBlitter()->Encode(&builtin_questionmark, AllocateFont);
00510 assert(spr != NULL);
00511 new_glyph.sprite = spr;
00512 new_glyph.width = spr->width + (this->fs != FS_NORMAL);
00513 this->SetGlyphPtr(key, &new_glyph, false);
00514 return new_glyph.sprite;
00515 } else {
00516
00517 this->GetGlyph(question_glyph);
00518 glyph = this->GetGlyphPtr(question_glyph);
00519 this->SetGlyphPtr(key, glyph, true);
00520 return glyph->sprite;
00521 }
00522 }
00523 FT_Load_Glyph(this->face, key, FT_LOAD_DEFAULT);
00524 FT_Render_Glyph(this->face->glyph, aa ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO);
00525
00526
00527 aa = (slot->bitmap.pixel_mode == FT_PIXEL_MODE_GRAY);
00528
00529
00530 int width = max(1, slot->bitmap.width + (this->fs == FS_NORMAL));
00531 int height = max(1, slot->bitmap.rows + (this->fs == FS_NORMAL));
00532
00533
00534 if (width > 256 || height > 256) usererror("Font glyph is too large");
00535
00536
00537 SpriteLoader::Sprite sprite;
00538 sprite.AllocateData(ZOOM_LVL_NORMAL, width * height);
00539 sprite.type = ST_FONT;
00540 sprite.width = width;
00541 sprite.height = height;
00542 sprite.x_offs = slot->bitmap_left;
00543 sprite.y_offs = this->ascender - slot->bitmap_top;
00544
00545
00546 if (this->fs == FS_NORMAL && !aa) {
00547 for (int y = 0; y < slot->bitmap.rows; y++) {
00548 for (int x = 0; x < slot->bitmap.width; x++) {
00549 if (aa ? (slot->bitmap.buffer[x + y * slot->bitmap.pitch] > 0) : HasBit(slot->bitmap.buffer[(x / 8) + y * slot->bitmap.pitch], 7 - (x % 8))) {
00550 sprite.data[1 + x + (1 + y) * sprite.width].m = SHADOW_COLOUR;
00551 sprite.data[1 + x + (1 + y) * sprite.width].a = aa ? slot->bitmap.buffer[x + y * slot->bitmap.pitch] : 0xFF;
00552 }
00553 }
00554 }
00555 }
00556
00557 for (int y = 0; y < slot->bitmap.rows; y++) {
00558 for (int x = 0; x < slot->bitmap.width; x++) {
00559 if (aa ? (slot->bitmap.buffer[x + y * slot->bitmap.pitch] > 0) : HasBit(slot->bitmap.buffer[(x / 8) + y * slot->bitmap.pitch], 7 - (x % 8))) {
00560 sprite.data[x + y * sprite.width].m = FACE_COLOUR;
00561 sprite.data[x + y * sprite.width].a = aa ? slot->bitmap.buffer[x + y * slot->bitmap.pitch] : 0xFF;
00562 }
00563 }
00564 }
00565
00566 new_glyph.sprite = BlitterFactory::GetCurrentBlitter()->Encode(&sprite, AllocateFont);
00567 new_glyph.width = slot->advance.x >> 6;
00568
00569 this->SetGlyphPtr(key, &new_glyph);
00570
00571 return new_glyph.sprite;
00572 }
00573
00574
00575 bool FreeTypeFontCache::GetDrawGlyphShadow()
00576 {
00577 return this->fs == FS_NORMAL && GetFontAAState(FS_NORMAL);
00578 }
00579
00580
00581 uint FreeTypeFontCache::GetGlyphWidth(GlyphID key)
00582 {
00583 if ((key & SPRITE_GLYPH) != 0) return this->parent->GetGlyphWidth(key);
00584
00585 GlyphEntry *glyph = this->GetGlyphPtr(key);
00586 if (glyph == NULL || glyph->sprite == NULL) {
00587 this->GetGlyph(key);
00588 glyph = this->GetGlyphPtr(key);
00589 }
00590
00591 return glyph->width;
00592 }
00593
00594 GlyphID FreeTypeFontCache::MapCharToGlyph(WChar key)
00595 {
00596 assert(IsPrintable(key));
00597
00598 if (key >= SCC_SPRITE_START && key <= SCC_SPRITE_END) {
00599 return this->parent->MapCharToGlyph(key);
00600 }
00601
00602 return FT_Get_Char_Index(this->face, key);
00603 }
00604
00605 const void *FreeTypeFontCache::GetFontTable(uint32 tag, size_t &length)
00606 {
00607 const FontTable::iterator iter = this->font_tables.Find(tag);
00608 if (iter != this->font_tables.End()) {
00609 length = iter->second.first;
00610 return iter->second.second;
00611 }
00612
00613 FT_ULong len = 0;
00614 FT_Byte *result = NULL;
00615
00616 FT_Load_Sfnt_Table(this->face, tag, 0, NULL, &len);
00617
00618 if (len > 0) {
00619 result = MallocT<FT_Byte>(len);
00620 FT_Load_Sfnt_Table(this->face, tag, 0, result, &len);
00621 }
00622 length = len;
00623
00624 this->font_tables.Insert(tag, SmallPair<size_t, const void *>(length, result));
00625 return result;
00626 }
00627
00628 #endif
00629
00634 void InitFreeType(bool monospace)
00635 {
00636 for (FontSize fs = FS_BEGIN; fs < FS_END; fs++) {
00637 if (monospace != (fs == FS_MONO)) continue;
00638
00639 FontCache *fc = FontCache::Get(fs);
00640 if (fc->HasParent()) delete fc;
00641
00642 #ifdef WITH_FREETYPE
00643 LoadFreeTypeFont(fs);
00644 #endif
00645 }
00646 }
00647
00651 void UninitFreeType()
00652 {
00653 for (FontSize fs = FS_BEGIN; fs < FS_END; fs++) {
00654 FontCache *fc = FontCache::Get(fs);
00655 if (fc->HasParent()) delete fc;
00656 }
00657
00658 #ifdef WITH_FREETYPE
00659 FT_Done_FreeType(_library);
00660 _library = NULL;
00661 #endif
00662 }