/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ // Font management and font drawing module #include "saga/saga.h" #include "saga/gfx.h" #include "saga/resource.h" #include "saga/scene.h" #include "saga/font.h" #include "saga/render.h" #include "graphics/sjis.h" #include "common/unicode-bidi.h" #include "saga/ite8.h" #include "saga/small8.h" namespace Saga { static const GameFontDescription ITEDEMO_GameFonts[] = { {0}, {1} }; static const GameFontDescription ITEWINDEMO_GameFonts[] = { {2}, {0} }; static const GameFontDescription ITE_GameFonts[] = { {2}, {0}, {1} }; static const GameFontDescription IHNMDEMO_GameFonts[] = { {2}, {3}, {4} }; // Font 6 is kIHNMFont8, font 8 is kIHNMMainFont static const GameFontDescription IHNMCD_GameFonts[] = { {2}, {3}, {4}, {5}, {6}, {7}, {8} }; // Resource 2 is a CJK font. Resource 3 looks like some image. 4 to 8 are single-byte // fonts (not really useful) static const GameFontDescription IHNMZH_GameFonts[] = { {2}, {4}, {5}, {6}, {7}, {8} }; static struct { const GameFontDescription *list; int count; } FontLists[FONTLIST_MAX] = { /* FONTLIST_NONE */ { nullptr, 0 }, /* FONTLIST_ITE */ { ITE_GameFonts, ARRAYSIZE(ITE_GameFonts) }, /* FONTLIST_ITE_DEMO */ { ITEDEMO_GameFonts, ARRAYSIZE(ITEDEMO_GameFonts) }, /* FONTLIST_ITE_WIN_DEMO */ { ITEWINDEMO_GameFonts, ARRAYSIZE(ITEWINDEMO_GameFonts) }, /* FONTLIST_IHNM_DEMO */ { IHNMDEMO_GameFonts, ARRAYSIZE(IHNMDEMO_GameFonts) }, /* FONTLIST_IHNM_CD */ { IHNMCD_GameFonts, ARRAYSIZE(IHNMCD_GameFonts) }, /* FONTLIST_IHNM_ZH */ { IHNMZH_GameFonts, ARRAYSIZE(IHNMZH_GameFonts) }, }; Font::FontId Font::knownFont2FontIdx(KnownFont font) { FontId fontId = kSmallFont; // The demo version of IHNM has 3 font types (like ITE), not 6 (like the full version of IHNM) if (_vm->getGameId() == GID_ITE || _vm->isIHNMDemo()) { switch (font) { case (kKnownFontSmall): default: fontId = kSmallFont; break; case (kKnownFontMedium): fontId = kMediumFont; break; case (kKnownFontBig): fontId = kBigFont; break; case (kKnownFontVerb): fontId = kSmallFont; break; case (kKnownFontScript): fontId = kMediumFont; break; case (kKnownFontPause): fontId = _vm->_font->valid(kBigFont) ? kBigFont : kMediumFont; break; } #ifdef ENABLE_IHNM } else if (_vm->getGameId() == GID_IHNM && _vm->getLanguage() == Common::ZH_TWN) { // There is only one Chinese font in Chinese version AFAICT. // And very little non-Chinese characters to care about them fontId = kSmallFont; } else if (_vm->getGameId() == GID_IHNM && !_vm->isIHNMDemo()) { switch (font) { case (kKnownFontSmall): default: fontId = kSmallFont; break; case (kKnownFontMedium): fontId = kMediumFont; break; case (kKnownFontBig): fontId = kBigFont; break; case (kKnownFontVerb): fontId = kIHNMFont8; break; case (kKnownFontScript): fontId = kIHNMMainFont; break; case (kKnownFontPause): fontId = kMediumFont; // unchecked break; } #endif } return fontId; } void Font::textDraw(FontId fontId, const char *text, const Common::Point &point, int color, int effectColor, FontEffectFlags flags) { int textWidth; int textLength; int fitWidth; Common::Point textPoint(point); textLength = getStringLength(text); if (!(flags & kFontCentered)) { // Text is not centered; No formatting required draw(fontId, text, textLength, point, color, effectColor, flags); return; } // Text is centered... format output // Enforce minimum and maximum center points for centered text if (textPoint.x < TEXT_CENTERLIMIT) { textPoint.x = TEXT_CENTERLIMIT; } if (textPoint.x > _vm->_gfx->getBackBufferWidth() - TEXT_CENTERLIMIT) { textPoint.x = _vm->_gfx->getBackBufferWidth() - TEXT_CENTERLIMIT; } if (textPoint.x < (TEXT_MARGIN * 2)) { // Text can't be centered if it's too close to the margin return; } textWidth = getStringWidth(fontId, text, textLength, flags); if (textPoint.x < (_vm->_gfx->getBackBufferWidth() / 2)) { // Fit to right side fitWidth = (textPoint.x - TEXT_MARGIN) * 2; } else { // Fit to left side fitWidth = ((_vm->_gfx->getBackBufferWidth() - TEXT_MARGIN) - textPoint.x) * 2; } if (fitWidth < textWidth) { warning("text too long to be displayed in one line"); textWidth = fitWidth; } // Entire string fits, draw it textPoint.x = textPoint.x - (textWidth / 2); draw(fontId, text, textLength, textPoint, color, effectColor, flags); } DefaultFont::DefaultFont(SagaEngine *vm) : Font(vm), _fontMapping(0), _chineseFont(nullptr), _cjkFontWidth(0), _cjkFontHeight(0), _koreanFont(nullptr) { int i; // Load font module resource context GameFontList index = _vm->getFontList(); assert(index < FONTLIST_MAX && index >= FONTLIST_NONE); assert(FontLists[index].list || FontLists[index].count == 0); assert(FontLists[index].count > 0 || (_vm->getFeatures() & GF_EMBED_FONT)); _fonts.resize(MAX(FontLists[index].count, (_vm->getFeatures() & GF_EMBED_FONT) ? 2 : 0)); for (i = 0; i < FontLists[index].count; i++) { #ifdef __DS__ _fonts[i].outline.font = NULL; _fonts[i].normal.font = NULL; #endif if (i == 0 && index == FONTLIST_IHNM_ZH) loadChineseFontIHNM(&_fonts[i], FontLists[index].list[i].fontResourceId); else loadFont(&_fonts[i], FontLists[index].list[i].fontResourceId); } if (_vm->getFeatures() & GF_EMBED_FONT) { loadFont(&_fonts[kSmallFont], ByteArray(font_small8, sizeof(font_small8)), true); loadFont(&_fonts[kMediumFont], ByteArray(font_ite8, sizeof(font_ite8)), true); } if (_vm->getGameId() == GID_ITE && _vm->getLanguage() == Common::ZH_TWN) loadChineseFontITE("ite.fnt"); if (_vm->getGameId() == GID_IHNM && _vm->getLanguage() == Common::KO_KOR) loadKoreanFontIHNM("sbh1616.fnt"); } DefaultFont::~DefaultFont() { debug(8, "DefaultFont::~DefaultFont(): Freeing fonts."); #ifdef __DS__ for (uint i = 0; i < _fonts.size(); i++) { if (_fonts[i].outline.font) { free(_fonts[i].outline.font); } if (_fonts[i].normal.font) { free(_fonts[i].normal.font); } } #endif if (_chineseFont) { delete[] _chineseFont; _chineseFont = nullptr; } if (_koreanFont) { delete[] _koreanFont; _koreanFont = nullptr; } } void DefaultFont::loadChineseFontITE(const Common::Path &fileName) { Common::File f; if (!f.open(fileName)) return; _cjkFontWidth = 16; _cjkFontHeight = 14; _chineseFontIndex = Common::move(Common::Array(0x8000, -1)); size_t sz = f.size(); _chineseFont = new byte[sz]; f.read(_chineseFont, sz); static const int kGlyphSize = 30; for (unsigned i = 0; i < sz / kGlyphSize; i++) { uint16 ch = READ_BE_UINT16(_chineseFont + kGlyphSize * i); if (!(ch & 0x8000)) continue; _chineseFontIndex[ch&0x7fff] = kGlyphSize * i + 2; } } void DefaultFont::loadKoreanFontIHNM(const Common::Path &fileName) { Common::File f; if (!f.open(fileName)) return; size_t sz = f.size(); if (sz < kIHNMKoreanNonJamoOffset * kIHNMKoreanGlyphBytes) return; _cjkFontWidth = 16; _cjkFontHeight = 16; _koreanFont = new byte[sz]; f.read(_koreanFont, sz); } void DefaultFont::saveBig5Index(byte head, byte tail, uint curIdx) { _chineseFontIndex[((head & 0x7f) << 8) | tail] = curIdx; } void DefaultFont::loadChineseFontIHNM(FontData *font, uint32 fontResourceId) { ByteArray fontResourceData; int c; ResourceContext *fontContext; debug(1, "Font::loadChineseFontIHNM(): Reading fontResourceId %d...", fontResourceId); fontContext = _vm->_resource->getContext(GAME_RESOURCEFILE); if (fontContext == nullptr) { error("DefaultFont::Font() resource context not found"); } // Load font resource _vm->_resource->loadResource(fontContext, fontResourceId, fontResourceData); ByteArrayReadStreamEndian readS(fontResourceData, fontContext->isBigEndian()); // Read font header font->normal.header.charHeight = 15; font->normal.header.charWidth = 8; font->normal.header.rowLength = 1; for (c = 0; c < FONT_CHARCOUNT; c++) { font->normal.fontCharEntry[c].width = 8; font->normal.fontCharEntry[c].byteWidth = 1; font->normal.fontCharEntry[c].flag = 0; font->normal.fontCharEntry[c].tracking = 8; } _chineseFont = new byte[fontResourceData.size()]; memcpy(_chineseFont, fontResourceData.getBuffer(), fontResourceData.size()); _cjkFontWidth = 16; _cjkFontHeight = 15; _chineseFontIndex = Common::move(Common::Array(0x8000, -1)); // No idea what is the beginning, some 3 values and then some bitmask, // anyway file is constant and as long as we know how to interpret // and match glyphs we're good int curIdx = 1286; // It's just sequential big5 codepoints by compartments specified in // Big5 specification. Compartments are out of order // 0x8140 to 0xA0FE Reserved for user-defined characters 造字 // Not present // 0xA440 to 0xC67E Frequently used characters 常用字 for (byte head = 0xa4; head <= 0xc5; head++) { for (byte tail = 0x40; tail <= 0x7e; tail++, curIdx += 30) saveBig5Index(head, tail, curIdx); for (byte tail = 0xa1; tail <= 0xfe; tail++, curIdx += 30) saveBig5Index(head, tail, curIdx); } for (byte tail = 0x40; tail <= 0x7e; tail++, curIdx += 30) saveBig5Index(0xc6, tail, curIdx); // 0xC6A1 to 0xC8FE Reserved for user-defined characters. // Not present // 0xC940 to 0xF9D5 Less frequently used characters 次常用字 // Rounded up to F9FE with pseudographics characters for (byte head = 0xc9; head <= 0xf9; head++) { for (byte tail = 0x40; tail <= 0x7e; tail++, curIdx += 30) saveBig5Index(head, tail, curIdx); for (byte tail = 0xa1; tail <= 0xfe; tail++, curIdx += 30) saveBig5Index(head, tail, curIdx); } // 0xFA40 to 0xFEFE Reserved for user-defined characters // Not present // Then comes back to a140 // 0xA140 to 0xA3BF "Graphical characters" 圖形碼 for (byte head = 0xa1; head <= 0xa2; head++) { for (byte tail = 0x40; tail <= 0x7e; tail++, curIdx += 30) saveBig5Index(head, tail, curIdx); for (byte tail = 0xa1; tail <= 0xfe; tail++, curIdx += 30) saveBig5Index(head, tail, curIdx); } for (byte tail = 0x40; tail <= 0x7e; tail++, curIdx += 30) saveBig5Index(0xa3, tail, curIdx); for (byte tail = 0xa1; tail <= 0xbf; tail++, curIdx += 30) saveBig5Index(0xa3, tail, curIdx); // 0xA3C0 to 0xA3FE Reserved, not for user-defined characters // Not present // Then single-width ASCII int startASCII = curIdx; for (c = 0; c < FONT_CHARCOUNT; c++, curIdx += 15) { font->normal.fontCharEntry[c].index = curIdx - startASCII; } #ifndef __DS__ font->normal.font.resize(fontResourceData.size() - startASCII); memcpy(font->normal.font.getBuffer(), fontResourceData.getBuffer() + startASCII, fontResourceData.size() - startASCII); #else if (font->normal.font) { free(font->normal.font); } font->normal.font = (byte *)malloc(fontResourceData.size() - startASCII); memcpy(font->normal.font, fontResourceData.getBuffer() + startASCII, fontResourceData.size() - startASCII); #endif // Create outline font style createOutline(font); } void DefaultFont::textDrawRect(FontId fontId, const char *text, const Common::Rect &rect, int color, int effectColor, FontEffectFlags flags) { int textWidth; int textLength; int fitWidth; const char *startPointer; const char *searchPointer; const char *measurePointer; const char *foundPointer; int len; int w; const char *endPointer; int h; int wc; int w_total; Common::Point textPoint; Common::Point textPoint2; textLength = getStringLength(text); textWidth = getStringWidth(fontId, text, textLength, flags); fitWidth = rect.width(); textPoint.x = rect.left + (fitWidth / 2); textPoint.y = rect.top; if (fitWidth >= textWidth) { // Entire string fits, draw it textPoint.x -= (textWidth / 2); draw(fontId, text, textLength, textPoint, color, effectColor, flags); return; } // String won't fit on one line h = getHeight(fontId, text); w_total = 0; wc = 0; startPointer = text; measurePointer = text; searchPointer = text; endPointer = text + textLength; // IHNM korean uses spaces, so we use western algorithm for it. bool isBig5 = !!_chineseFont; for (;;) { if (isBig5) { if ((searchPointer[0] & 0x80) && searchPointer[1]) foundPointer = searchPointer + 2; else if (*searchPointer) foundPointer = searchPointer + 1; else foundPointer = nullptr; } else foundPointer = strchr(searchPointer, ' '); if (foundPointer == nullptr) { // Ran to the end of the buffer len = endPointer - measurePointer; } else { len = foundPointer - measurePointer; } w = getStringWidth(fontId, measurePointer, len, flags); measurePointer = foundPointer; if ((w_total + w) > fitWidth) { // This word won't fit if (wc == 0) { if (measurePointer) searchPointer = measurePointer; else searchPointer = endPointer; w_total = fitWidth; } // Wrap what we've got and restart textPoint2.x = textPoint.x - (w_total / 2); textPoint2.y = textPoint.y; draw(fontId, startPointer, searchPointer - startPointer, textPoint2, color, effectColor, flags); textPoint.y += h + TEXT_LINESPACING; if (textPoint.y >= rect.bottom) { return; } w_total = 0; wc = 0; // Advance the search pointer to the next non-space. // Otherwise, the first "word" to be measured will be // an empty string. Measuring or drawing a string of // length 0 is interpreted as measure/draw the entire // buffer, which certainly is not what we want here. // // This happes because a string may contain several // spaces in a row, e.g. after a period. while (*searchPointer == ' ') searchPointer++; measurePointer = searchPointer; startPointer = searchPointer; } else { // Word will fit ok w_total += w; wc++; if (foundPointer == nullptr) { // Since word hit NULL but fit, we are done textPoint2.x = textPoint.x - (w_total / 2); textPoint2.y = textPoint.y; draw(fontId, startPointer, endPointer - startPointer, textPoint2, color, effectColor, flags); return; } if (isBig5 && (*measurePointer & 0x80)) searchPointer = measurePointer + 2; else searchPointer = measurePointer + 1; } } } int DefaultFont::translateChar(int charId) { if (charId <= 127 || (_vm->getLanguage() == Common::RU_RUS && charId <= 255) || (_vm->getLanguage() == Common::HE_ISR && charId <= 255)) return charId; // normal character else return _charMap[charId - 128]; // extended character } // Returns the horizontal length in pixels of the graphical representation // of at most 'count' characters of the string 'text', taking // into account any formatting options specified by 'flags'. // If 'count' is 0, all characters of 'test' are counted. int DefaultFont::getStringWidth(FontId fontId, const char *text, size_t count, FontEffectFlags flags) { size_t ct; int width = 0; int ch; const byte *txt; FontData *font = getFont(fontId); txt = (const byte *) text; bool isCJK = _chineseFont || _koreanFont; for (ct = count; *txt && (!count || ct > 0); txt++, ct--) { ch = *txt & 0xFFU; if ((ch & 0x80) && isCJK) { byte trailing = *++txt & 0xFFU; ct--; if (ct == 0 || trailing == 0) break; width += _cjkFontWidth; continue; } // Translate character ch = translateChar(ch); assert(ch < FONT_CHARCOUNT); width += font->normal.fontCharEntry[ch].tracking; } if ((flags & kFontBold) || (flags & kFontOutline)) { width += 1; } return width; } int DefaultFont::getHeight(FontId fontId, const char *text, int width, FontEffectFlags flags) { int textWidth; int textLength; int fitWidth; const char *searchPointer; const char *measurePointer; const char *foundPointer; int len; int w; const char *endPointer; int h; int wc; int w_total; Common::Point textPoint; textLength = getStringLength(text); textWidth = getStringWidth(fontId, text, textLength, flags); h = getHeight(fontId, text); fitWidth = width; textPoint.x = (fitWidth / 2); textPoint.y = 0; if (fitWidth >= textWidth) { return h; } // String won't fit on one line w_total = 0; wc = 0; measurePointer = text; searchPointer = text; endPointer = text + textLength; // IHNM korean uses spaces, so we use western algorithm for it. bool isBig5 = !!_chineseFont; for (;;) { if (isBig5) { if (*searchPointer & 0x80) foundPointer = searchPointer + 2; else if (*searchPointer) foundPointer = searchPointer + 1; else foundPointer = nullptr; } else foundPointer = strchr(searchPointer, ' '); if (foundPointer == nullptr) { // Ran to the end of the buffer len = endPointer - measurePointer; } else { len = foundPointer - measurePointer; } w = getStringWidth(fontId, measurePointer, len, flags); measurePointer = foundPointer; if ((w_total + w) > fitWidth) { // This word won't fit if (wc == 0) { // The first word in the line didn't fit. Still print it if (isBig5 && (*measurePointer & 0x80)) searchPointer = measurePointer + 2; else searchPointer = measurePointer + 1; } // Wrap what we've got and restart textPoint.y += h + TEXT_LINESPACING; if (foundPointer == nullptr) { // Since word hit NULL but fit, we are done return textPoint.y + h; } w_total = 0; wc = 0; measurePointer = searchPointer; } else { // Word will fit ok w_total += w; wc++; if (foundPointer == nullptr) { // Since word hit NULL but fit, we are done return textPoint.y + h; } if (isBig5 && (*measurePointer & 0x80)) searchPointer = measurePointer + 2; else searchPointer = measurePointer + 1; } } } void DefaultFont::draw(FontId fontId, const char *text, size_t count, const Common::Point &point, int color, int effectColor, FontEffectFlags flags) { Point offsetPoint(point); FontData *font = getFont(fontId); if (_vm->getLanguage() == Common::HE_ISR) { Common::String textstr(text, count); text = Common::convertBiDiString(textstr, Common::kWindows1255).c_str(); } if (flags & kFontOutline) { offsetPoint.x--; offsetPoint.y--; outFont(font->outline, text, count, offsetPoint, effectColor, flags); outFont(font->normal, text, count, point, color, flags); } else if (flags & kFontShadow) { offsetPoint.x--; offsetPoint.y++; outFont(font->normal, text, count, offsetPoint, effectColor, flags); outFont(font->normal, text, count, point, color, flags); } else { // FONT_NORMAL outFont(font->normal, text, count, point, color, flags); } } int DefaultFont::getHeight(FontId fontId, const char *text) { int singleByteHeight = getHeight(fontId); if ((!_chineseFont && !_koreanFont) || _cjkFontHeight < singleByteHeight) return singleByteHeight; for (const byte *textPointer = (const byte *)text; *textPointer; textPointer++) if (*textPointer & 0x80) return _cjkFontHeight; return singleByteHeight; } void DefaultFont::blitGlyph(const Common::Point &textPoint, const byte* bitmap, int charWidth, int charHeight, int rowLength, byte color) { // Get length of character in bytes int c_byte_len = ((charWidth - 1) / 8) + 1; int rowLimit = (_vm->_gfx->getBackBufferHeight() < (textPoint.y + charHeight)) ? _vm->_gfx->getBackBufferHeight() : textPoint.y + charHeight; int charRow = 0; for (int row = textPoint.y; row < rowLimit; row++, charRow++) { // Clip negative rows */ if (row < 0) { continue; } byte *outputPointer = _vm->_gfx->getBackBufferPixels() + (_vm->_gfx->getBackBufferPitch() * row) + textPoint.x; byte *outputPointer_min = _vm->_gfx->getBackBufferPixels() + (_vm->_gfx->getBackBufferPitch() * row) + (textPoint.x > 0 ? textPoint.x : 0); byte *outputPointer_max = outputPointer + (_vm->_gfx->getBackBufferPitch() - textPoint.x); // If character starts off the screen, jump to next character if (outputPointer < outputPointer_min) { break; } const byte *c_dataPointer = bitmap + charRow * rowLength; for (int c_byte = 0; c_byte < c_byte_len; c_byte++, c_dataPointer++) { // Check each bit, draw pixel if bit is set for (int c_bit = 7; c_bit >= 0 && (outputPointer < outputPointer_max); c_bit--) { if ((*c_dataPointer >> c_bit) & 0x01) { *outputPointer = (byte)color; } outputPointer++; } // end per-bit processing } // end per-byte processing } // end per-row processing } void DefaultFont::outFont(const FontStyle &drawFont, const char *text, size_t count, const Common::Point &point, int color, FontEffectFlags flags) { const byte *textPointer; int c_code; Point textPoint(point); int ct; if ((point.x > _vm->_gfx->getBackBufferWidth()) || (point.y > _vm->_gfx->getBackBufferHeight())) { // Output string can't be visible return; } textPointer = (const byte *)text; ct = count; bool isBig5 = !!_chineseFont; bool isJohab = !!_koreanFont; // Draw string one character at a time, maximum of 'draw_str'_ct // characters, or no limit if 'draw_str_ct' is 0 for (; *textPointer && (!count || ct); textPointer++, ct--) { c_code = *textPointer & 0xFFU; if ((c_code & 0x80) && isJohab) { byte leading = c_code; byte trailing = *++textPointer & 0xFFU; ct--; if (ct == 0 || trailing == 0) break; uint16 full = ((leading & 0x7f) << 8) | trailing; int initial = (full >> 10) & 0x1f; int mid = (full >> 5) & 0x1f; int fin = full & 0x1f; int initidx = initial - 1; static const int mididxlut[0x20] = { -1, -1, 0, 1, 2, 3, 4, 5, -1, -1, 6, 7, 8, 9, 10, 11, -1, -1, 12, 13, 14, 15, 16, 17, -1, -1, 18, 19, 20, 21, -1, -1}; int mididx = mididxlut[mid]; int finidx = fin >= 0x12 ? fin - 2 : fin - 1; // Validate character if (initial >= 0x15 || initidx < 0 || mididx < 0 || fin == 0 || fin == 0x12 || fin >= 0x1e) { // Characters with initial over 0x15 means "non-jamo-based", e.g. Hanja, pictograms // and so on. They are present in the font but not supported by renderer neither in // the original nor in the scummvm textPoint.x += _cjkFontWidth; continue; } static const int mid2inivariant[2][32] = { // Special case: empty final { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 3, 0, 0, 3, 1, 2, 4, 4, 4, 0, 0, 2, 1, 3, 0, 0, 0, }, // Otherwise we have a final { 0, 0, 0, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 6, 7, 7, 0, 0, 7, 6, 6, 7, 7, 7, 0, 0, 6, 6, 7, 5, 0, 0, } }; int inivariant = mid2inivariant[fin != 1][mid]; int midvariant = 0; if (fin != 1) midvariant += 2; if (initial == 2 || initial == 17) midvariant++; static const int mid2finvariant[32] = { 0, 0, 0, 0, 2, 0, 2, 1, 0, 0, 2, 1, 2, 3, 0, 2, 0, 0, 1, 3, 3, 1, 2, 1, 0, 0, 3, 3, 1, 1, 0, 0, }; int finvariant = mid2finvariant[mid]; int initialoff = kIHNMKoreanGlyphBytes * (initidx + inivariant * kIHNMKoreanInitials); blitGlyph(textPoint, _koreanFont + initialoff, _cjkFontWidth, _cjkFontHeight, _cjkFontWidth / 8, (byte)color); int midoff = kIHNMKoreanGlyphBytes * (mididx + midvariant * kIHNMKoreanMids + kIHNMKoreanMidOffset); blitGlyph(textPoint, _koreanFont + midoff, _cjkFontWidth, _cjkFontHeight, _cjkFontWidth / 8, (byte)color); int finoff = kIHNMKoreanGlyphBytes * (finidx + finvariant * kIHNMKoreanFinals + kIHNMKoreanFinalsOffset); blitGlyph(textPoint, _koreanFont + finoff, _cjkFontWidth, _cjkFontHeight, _cjkFontWidth / 8, (byte)color); // Advance tracking position textPoint.x += _cjkFontWidth; continue; } if ((c_code & 0x80) && isBig5) { byte leading = c_code; byte trailing = *++textPointer & 0xFFU; ct--; if (ct == 0 || trailing == 0) break; int idx = _chineseFontIndex[((leading & 0x7f) << 8) | trailing]; if (idx < 0) { textPoint.x += _cjkFontWidth; continue; } blitGlyph(textPoint, _chineseFont + idx, _cjkFontWidth, _cjkFontHeight, _cjkFontWidth / 8, (byte)color); // Advance tracking position textPoint.x += _cjkFontWidth; continue; } // Translate character if (_fontMapping == 0) { // Check font mapping debug flag // Default game behavior // It seems that this font mapping causes problems with non-english // versions of IHNM, so it has been changed to apply for ITE only. // It doesn't make any difference for the English version of IHNM. // Fixes bug #3405: "IHNM: Spanish font wrong". if (!(flags & kFontDontmap) && _vm->getGameId() == GID_ITE) { if (_vm->getLanguage() != Common::IT_ITA) { c_code = translateChar(c_code); } else { // The in-game fonts of the Italian version should not be mapped. // The ones in the intro are hardcoded and should be mapped normally. if (_vm->_scene->isInIntro()) c_code = translateChar(c_code); } } } else if (_fontMapping == 1) { // Force font mapping c_code = translateChar(c_code); } else { // In all other cases, ignore font mapping } assert(c_code < FONT_CHARCOUNT); // Check if character is defined if ((drawFont.fontCharEntry[c_code].index == 0) && (c_code != FONT_FIRSTCHAR)) { #if FONT_SHOWUNDEFINED // A tab character appears in the IHNM demo instructions screen, so filter // it out here if (c_code == FONT_CH_SPACE || c_code == FONT_CH_TAB) { textPoint.x += drawFont.fontCharEntry[c_code].tracking; continue; } c_code = FONT_CH_QMARK; #else // Character code is not defined, but advance tracking // ( Not defined if offset is 0, except for 33 ('!') which // is defined ) textPoint.x += drawFont.fontCharEntry[c_code].tracking; continue; #endif } blitGlyph(textPoint, &drawFont.font[drawFont.fontCharEntry[c_code].index], drawFont.fontCharEntry[c_code].width, drawFont.header.charHeight, drawFont.header.rowLength, (byte)color); // Advance tracking position textPoint.x += drawFont.fontCharEntry[c_code].tracking; } // end per-character processing int rowLimit = (_vm->_gfx->getBackBufferHeight() < (textPoint.y + drawFont.header.charHeight)) ? _vm->_gfx->getBackBufferHeight() : textPoint.y + drawFont.header.charHeight; _vm->_render->addDirtyRect(Common::Rect(point.x, point.y, textPoint.x, rowLimit)); } void DefaultFont::loadFont(FontData *font, uint32 fontResourceId) { ByteArray fontResourceData; ResourceContext *fontContext; debug(1, "Font::loadFont(): Reading fontResourceId %d...", fontResourceId); fontContext = _vm->_resource->getContext(GAME_RESOURCEFILE); if (fontContext == nullptr) { error("DefaultFont::Font() resource context not found"); } // Load font resource _vm->_resource->loadResource(fontContext, fontResourceId, fontResourceData); loadFont(font, fontResourceData, fontContext->isBigEndian()); } void DefaultFont::loadFont(FontData *font, const ByteArray& fontResourceData, bool isBigEndian) { int numBits; int c; if (fontResourceData.size() < FONT_DESCSIZE) { error("DefaultFont::loadFont() Invalid font length (%i < %i)", (int)fontResourceData.size(), FONT_DESCSIZE); } ByteArrayReadStreamEndian readS(fontResourceData, isBigEndian); // Read font header font->normal.header.charHeight = readS.readUint16(); font->normal.header.charWidth = readS.readUint16(); font->normal.header.rowLength = readS.readUint16(); debug(2, "Character width: %d", font->normal.header.charWidth); debug(2, "Character height: %d", font->normal.header.charHeight); debug(2, "Row padding: %d", font->normal.header.rowLength); for (c = 0; c < FONT_CHARCOUNT; c++) { font->normal.fontCharEntry[c].index = readS.readUint16(); } for (c = 0; c < FONT_CHARCOUNT; c++) { numBits = font->normal.fontCharEntry[c].width = readS.readByte(); font->normal.fontCharEntry[c].byteWidth = getByteLen(numBits); } for (c = 0; c < FONT_CHARCOUNT; c++) { font->normal.fontCharEntry[c].flag = readS.readByte(); } for (c = 0; c < FONT_CHARCOUNT; c++) { font->normal.fontCharEntry[c].tracking = readS.readByte(); } if (readS.pos() != FONT_DESCSIZE) { error("Invalid font resource size"); } #ifndef __DS__ font->normal.font.resize(fontResourceData.size() - FONT_DESCSIZE); memcpy(font->normal.font.getBuffer(), fontResourceData.getBuffer() + FONT_DESCSIZE, fontResourceData.size() - FONT_DESCSIZE); #else if (font->normal.font) { free(font->normal.font); } font->normal.font = (byte *)malloc(fontResourceData.size() - FONT_DESCSIZE); memcpy(font->normal.font, fontResourceData.getBuffer() + FONT_DESCSIZE, fontResourceData.size() - FONT_DESCSIZE); #endif // Create outline font style createOutline(font); } void DefaultFont::createOutline(FontData *font) { int i; int row; int newByteWidth; int newRowLength = 0; int currentByte; byte *basePointer; byte *srcPointer; byte *destPointer1; byte *destPointer2; byte *destPointer3; byte charRep; // Populate new font style character data for (i = 0; i < FONT_CHARCOUNT; i++) { newByteWidth = 0; font->outline.fontCharEntry[i].index = newRowLength; font->outline.fontCharEntry[i].tracking = font->normal.fontCharEntry[i].tracking; font->outline.fontCharEntry[i].flag = font->normal.fontCharEntry[i].flag; if (font->normal.fontCharEntry[i].width != 0) newByteWidth = getByteLen(font->normal.fontCharEntry[i].width + 2); font->outline.fontCharEntry[i].width = font->normal.fontCharEntry[i].width + 2; font->outline.fontCharEntry[i].byteWidth = newByteWidth; newRowLength += newByteWidth; } debug(2, "New row length: %d", newRowLength); font->outline.header = font->normal.header; font->outline.header.charWidth += 2; font->outline.header.charHeight += 2; font->outline.header.rowLength = newRowLength; // Allocate new font representation storage #ifdef __DS__ if (font->outline.font) { free(font->outline.font); } font->outline.font = (byte *)calloc(newRowLength * font->outline.header.charHeight, 1); #else font->outline.font.resize(newRowLength * font->outline.header.charHeight); #endif // Generate outline font representation for (i = 0; i < FONT_CHARCOUNT; i++) { for (row = 0; row < font->normal.header.charHeight; row++) { for (currentByte = 0; currentByte < font->outline.fontCharEntry[i].byteWidth; currentByte++) { basePointer = &font->outline.font[font->outline.fontCharEntry[i].index + currentByte]; destPointer1 = basePointer + newRowLength * row; destPointer2 = basePointer + newRowLength * (row + 1); destPointer3 = basePointer + newRowLength * (row + 2); if (currentByte > 0) { // Get last two columns from previous byte srcPointer = &font->normal.font[font->normal.header.rowLength * row + font->normal.fontCharEntry[i].index + (currentByte - 1)]; charRep = *srcPointer; *destPointer1 |= ((charRep << 6) | (charRep << 7)); *destPointer2 |= ((charRep << 6) | (charRep << 7)); *destPointer3 |= ((charRep << 6) | (charRep << 7)); } if (currentByte < font->normal.fontCharEntry[i].byteWidth) { srcPointer = &font->normal.font[font->normal.header.rowLength * row + font->normal.fontCharEntry[i].index + currentByte]; charRep = *srcPointer; *destPointer1 |= charRep | (charRep >> 1) | (charRep >> 2); *destPointer2 |= charRep | (charRep >> 1) | (charRep >> 2); *destPointer3 |= charRep | (charRep >> 1) | (charRep >> 2); } } } // "Hollow out" character to prevent overdraw for (row = 0; row < font->normal.header.charHeight; row++) { for (currentByte = 0; currentByte < font->outline.fontCharEntry[i].byteWidth; currentByte++) { destPointer2 = &font->outline.font[font->outline.header.rowLength * (row + 1) + font->outline.fontCharEntry[i].index + currentByte]; if (currentByte > 0) { // Get last two columns from previous byte srcPointer = &font->normal.font[font->normal.header.rowLength * row + font->normal.fontCharEntry[i].index + (currentByte - 1)]; *destPointer2 &= ((*srcPointer << 7) ^ 0xFFU); } if (currentByte < font->normal.fontCharEntry[i].byteWidth) { srcPointer = &font->normal.font[font->normal.header.rowLength * row + font->normal.fontCharEntry[i].index + currentByte]; *destPointer2 &= ((*srcPointer >> 1) ^ 0xFFU); } } } } } SJISFont::SJISFont(SagaEngine *vm) : Font(vm), _font(nullptr) { _font = Graphics::FontSJIS::createFont(vm->getPlatform()); assert(_font); } SJISFont::~SJISFont() { delete _font; } void SJISFont::textDrawRect(FontId fontId, const char *text, const Common::Rect &rect, int color, int effectColor, FontEffectFlags flags) { Common::Point textPoint(rect.left, rect.top); int curW = 0; int numChar = 0; const char *pos = text; const char *last = nullptr; int checkWidth = (rect.width() - 16) & ~7; for (uint16 c = fetchChar(pos); c; c = fetchChar(pos)) { curW += (_font->getCharWidth(c) >> 1); if ((curW > checkWidth && !preventLineBreakForCharacter(c)) || c == (uint16)'\r' || c == (uint16)'\n') { draw(fontId, text, numChar, textPoint, color, effectColor, flags); numChar = 0; textPoint.x = rect.left; textPoint.y += (getHeight(fontId)); // Abort if there is no more space inside the rect if (textPoint.y + getHeight(fontId) > rect.bottom) return; // Skip linebreak characters if (c == (uint16)'\r' || c == (uint16)'\n') last++; pos = text = last; last = nullptr; curW = 0; } else { numChar++; last = pos; } } // If the whole string fits into one line it gets aligned to the center if (textPoint.y == rect.top) textPoint.x = textPoint.x + (rect.width() - getStringWidth(fontId, text, 0, flags)) / 2; draw(fontId, text, numChar, textPoint, color, effectColor, flags); } int SJISFont::getStringLength(const char *text) { int res = 0; while (fetchChar(text)) res++; return res; } int SJISFont::getStringWidth(FontId fontId, const char *text, size_t count, FontEffectFlags flags) { // The spacing is always the same regardless of the fontId and font style _font->setDrawingMode(Graphics::FontSJIS::kDefaultMode); int curW = 0; int maxW = 0; for (uint16 c = fetchChar(text); c; c = fetchChar(text)) { if (c == (uint16)'\r' || c == (uint16)'\n') { maxW = MAX(curW, maxW); curW = 0; continue; } curW += _font->getCharWidth(c); if (!--count) break; } return MAX(curW, maxW) >> 1; } int SJISFont::getHeight(FontId fontId, const char *text, int width, FontEffectFlags flags) { Graphics::FontSJIS::DrawingMode mode = Graphics::FontSJIS::kDefaultMode; if (flags & kFontOutline) mode = Graphics::FontSJIS::kOutlineMode; else if (flags & kFontShadow) mode = Graphics::FontSJIS::kShadowRightMode; _font->setDrawingMode(mode); int res = _font->getFontHeight(); int checkWidth = (width - 16) & ~7; int tmpWidth = 0; for (uint16 c = fetchChar(text); c; c = fetchChar(text)) { // The spacing is always the same (regardless of the fontId and font style) for the char spacing, but not for the line spacing. _font->setDrawingMode(Graphics::FontSJIS::kDefaultMode); tmpWidth += (_font->getCharWidth(c) >> 1); if ((tmpWidth > checkWidth && !preventLineBreakForCharacter(c)) || c == (uint16)'\r' || c == (uint16)'\n') { tmpWidth = tmpWidth > width ? _font->getCharWidth(c) >> 1 : 0; _font->setDrawingMode(mode); res += _font->getFontHeight(); } } return (res + 1) >> 1; } int SJISFont::getHeight(FontId fontId) { // The spacing here is always the same regardless of the style _font->setDrawingMode(Graphics::FontSJIS::kDefaultMode); return (_font->getFontHeight() >> 1) + 1; } void SJISFont::draw(FontId fontId, const char *text, size_t count, const Common::Point &point, int color, int effectColor, FontEffectFlags flags) { int16 x = point.x << 1; int16 y = point.y << 1; Graphics::FontSJIS::DrawingMode mode = Graphics::FontSJIS::kDefaultMode; if (effectColor != 0x80) { if (flags & kFontOutline) mode = Graphics::FontSJIS::kOutlineMode; else if (flags & kFontShadow) mode = Graphics::FontSJIS::kShadowRightMode; } // DEBUG: The Graphics::FontSJIS code currently does not allow glyphs to be outlined and shaded at the same time. I'll implement it if I have to, but currently I don't think that this is the case... assert((flags & 3) != 3); _font->setDrawingMode(mode); Common::Rect dirtyRect((flags & kFontShadow) ? MAX(point.x - 1, 0) : point.x, point.y, point.x + 1, point.y + (_font->getFontHeight() >> 1)); while (*text) { uint16 ch = fetchChar(text); _font->setDrawingMode(mode); if (ch == (uint16)'\r' || ch == (uint16)'\n') { dirtyRect.right = MAX(x >> 1, dirtyRect.right); y += _font->getFontHeight(); x = point.x << 1; continue; } _font->drawChar(_vm->_gfx->getSJISBackBuffer(), ch, x, y, color, effectColor); // Reset drawing mode for the shadow mode extra drawing and for the character spacing (not line spacing) _font->setDrawingMode(Graphics::FontSJIS::kDefaultMode); if (flags & kFontShadow) _font->drawChar(_vm->_gfx->getSJISBackBuffer(), ch, MAX(x - 1, 0), y, color, 0); x += _font->getCharWidth(ch); if (!--count) break; } dirtyRect.right = MAX(x >> 1, dirtyRect.right); dirtyRect.bottom = (y + _font->getFontHeight()) >> 1; _vm->_render->addDirtyRect(dirtyRect); } uint16 SJISFont::fetchChar(const char *&s) const { uint16 ch = (uint8)*s++; if (ch <= 0x7F || (ch >= 0xA1 && ch <= 0xDF)) return ch; ch |= (uint8)(*s++) << 8; return ch; } bool SJISFont::preventLineBreakForCharacter(uint16 ch) const { uint8 c = (ch >> 8) & 0xFF; return c && ((c >= 0x81 && c <= 0x9F) || c >= 0xE0); } } // End of namespace Saga