/* =========================================================================== Copyright (C) 2024 the OpenMoHAA team This file is part of OpenMoHAA source code. OpenMoHAA source code 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 2 of the License, or (at your option) any later version. OpenMoHAA source code 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 OpenMoHAA source code; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA =========================================================================== */ // tr_font.cpp -- font rendering #include "tr_local.h" #define MAX_LOADED_FONTS 255 static fontheader_sgl_t s_loadedFonts_sgl[MAX_LOADED_FONTS]; static int s_numLoadedFonts_sgl = 0; static fontheader_t s_loadedFonts[MAX_LOADED_FONTS]; static int s_numLoadedFonts = 0; static float s_fontHeightScale = 1.0; static float s_fontGeneralScale = 1.0; static float s_fontZ = 0.0; void R_ShutdownFont() { int i; fontheader_t *header; fontheader_sgl_t *header_sgl; for (i = 0; i < s_numLoadedFonts; i++) { header = &s_loadedFonts[i]; if (header->charTable) { ri.Free(header->charTable); header->charTable = NULL; } memset(header, 0, sizeof(*header)); } for (i = 0; i < s_numLoadedFonts_sgl; i++) { header_sgl = &s_loadedFonts_sgl[i]; memset(header_sgl, 0, sizeof(*header_sgl)); } } void R_SetFontHeightScale(float scale) { s_fontHeightScale = scale; } void R_SetFontScale(float scale) { s_fontGeneralScale = scale; } void R_SetFontZ(float zed) { s_fontZ = zed; } static int CodeSearch(const fontheader_t* font, unsigned short uch) { int mid; int l, r; r = font->charTableLength; l = 0; while (l < r) { mid = (l + r) / 2; if (font->charTable[mid].cp > uch) { r = (l + r) / 2; continue; } if (uch == font->charTable[mid].cp) { return (l + r) / 2; } l = mid + 1; } if (uch != font->charTable[l].cp) { return -1; } return l; } static qboolean DBCSIsLeadByte(const fontheader_t* font, unsigned short uch) { // Byte ranges found in Wikipedia articles with relevant search strings in each case switch (font->codePage) { case 932: // Shift_jis return ((uch >= 0x81) && (uch <= 0x9F)) || ((uch >= 0xE0) && (uch <= 0xFC)); // Lead bytes F0 to FC may be a Microsoft addition. case 936: // GBK return (uch >= 0x81) && (uch <= 0xFE); case 949: // Korean Wansung KS C-5601-1987 return (uch >= 0x81) && (uch <= 0xFE); case 950: // Big5 return (uch >= 0x81) && (uch <= 0xFE); case 1361: // Korean Johab KS C-5601-1992 return ((uch >= 0x84) && (uch <= 0xD3)) || ((uch >= 0xD8) && (uch <= 0xDE)) || ((uch >= 0xE0) && (uch <= 0xF9)); } return false; } fontheader_sgl_t* R_LoadFont_sgl(const char* name) { int i; char* theFile; fontheader_sgl_t* header; char* ref; const char* token; qboolean error; error = qfalse; for (i = 0; i < s_numLoadedFonts_sgl; i++) { header = &s_loadedFonts_sgl[i]; if (!Q_stricmp(name, header->name)) { return header; } } if (s_numLoadedFonts >= MAX_LOADED_FONTS) { ri.Printf(PRINT_WARNING, "LoadFont: Too many fonts loaded! Couldn't load %s\n", name); return NULL; } if (ri.FS_ReadFile(va("fonts/%s.RitualFont", name), (void**)&theFile) == -1) { ri.Printf(PRINT_WARNING, "LoadFont: Couldn't load font %s\n", name); return NULL; } header = &s_loadedFonts_sgl[s_numLoadedFonts_sgl]; header->height = 0.0; header->aspectRatio = 0.0; Q_strncpyz(header->name, name, sizeof(header->name)); ref = theFile; while (ref && !error) { token = COM_Parse(&ref); if (!Q_stricmp(token, "RitFont")) { // ignore this token continue; } if (!Q_stricmp(token, "indirections")) { token = COM_Parse(&ref); if (Q_stricmp(token, "{")) { error = qtrue; break; } for (i = 0; i < 256; i++) { token = COM_Parse(&ref); if (!token[0]) { error = qtrue; break; } header->indirection[i] = atoi(token); } if (error) { break; } token = COM_Parse(&ref); if (Q_stricmp(token, "}")) { error = qtrue; break; } } else if (!Q_stricmp(token, "locations")) { token = COM_Parse(&ref); if (Q_stricmp(token, "{")) { error = qtrue; break; } for (i = 0; i < 256; i++) { token = COM_Parse(&ref); if (Q_stricmp(token, "{")) { error = qtrue; break; } if (header->aspectRatio == 0.0) { ri.Printf(PRINT_WARNING, "WARNING: aspect decl must be before locations in font '%s'", name); break; } header->locations[i].pos[0] = atof(COM_Parse(&ref)) / 256.0; header->locations[i].pos[1] = atof(COM_Parse(&ref)) * header->aspectRatio / 256.0; header->locations[i].size[0] = atof(COM_Parse(&ref)) / 256.0; header->locations[i].size[1] = atof(COM_Parse(&ref)) * header->aspectRatio / 256.0; token = COM_Parse(&ref); if (Q_stricmp(token, "}")) { error = qtrue; break; } } if (error) { break; } token = COM_Parse(&ref); if (Q_stricmp(token, "}")) { error = qtrue; break; } } else if (!Q_stricmp(token, "height")) { // parse the header height token = COM_Parse(&ref); header->height = atof(token); } else if (!Q_stricmp(token, "aspect")) { // parse the aspect ratio token = COM_Parse(&ref); header->aspectRatio = atof(token); } else { // unknown token break; } } if (token[0]) { ri.Printf(PRINT_WARNING, "WARNING: Unknown token '%s' parsing font '%s'\n", token, name); error = qtrue; } R_LoadFontShader(header); if (!header->height || !header->aspectRatio) { // invalid height or aspect ratio error = qtrue; } ri.FS_FreeFile(theFile); if (error) { ri.Printf(3, "WARNING: Error parsing font %s.\n", name); return NULL; } else { s_numLoadedFonts_sgl++; return header; } return NULL; } fontheader_t* R_LoadFont(const char* name) { int i; char* theFile; fontheader_t* header; char* ref; const char* token; qboolean error; char* pRitFontNames[32]; error = qfalse; for (i = 0; i < s_numLoadedFonts; i++) { header = &s_loadedFonts[i]; if (!Q_stricmp(name, header->name)) { return header; } } if (s_numLoadedFonts >= MAX_LOADED_FONTS) { ri.Printf(PRINT_WARNING, "LoadFont: Too many fonts loaded! Couldn't load %s\n", name); return NULL; } if (ri.FS_ReadFile(va("fonts/%s.RitualFont", name), (void**)&theFile) == -1) { ri.Printf(PRINT_WARNING, "LoadFont: Couldn't load font %s\n", name); return NULL; } memset(pRitFontNames, 0, sizeof(pRitFontNames)); ref = theFile; header = &s_loadedFonts[s_numLoadedFonts]; token = COM_Parse(&ref); if (Q_stricmp(token, "RitFontList")) { if (Q_stricmp(token, "RitFont")) { ri.Printf(PRINT_WARNING, "LoadFont: Not actual font %s\n", name); return NULL; } header->numPages = 0; } else { while (ref && !error) { token = COM_Parse(&ref); if (!Q_stricmp(token, "CodePage")) { header->codePage = atoi(COM_Parse(&ref)); } else if (!Q_stricmp(token, "Chars")) { header->charTableLength = atoi(COM_Parse(&ref)); header->charTable = (fontchartable_t*)ri.Malloc(header->charTableLength * sizeof(fontchartable_t)); if (!header->charTable) { ri.Printf(PRINT_WARNING, "LoadFont: Couldn't alloc mem %s\n", name); error = qtrue; break; } } else if (!Q_stricmp(token, "Pages")) { header->numPages = atoi(COM_Parse(&ref)); } else if (!Q_stricmp(token, "RitFontName")) { token = COM_Parse(&ref); if (Q_stricmp(token, "{")) { ri.Printf(PRINT_WARNING, "LoadFont: Bad Format %s\n", name); error = qtrue; break; } for (i = 0; i < header->numPages; i++) { token = COM_Parse(&ref); if (!token[0]) { ri.Printf(PRINT_WARNING, "LoadFont: Bad Token %s\n", name); error = qtrue; break; } pRitFontNames[i] = (char*)ri.Malloc(strlen(token) + 1); if (!pRitFontNames[i]) { ri.Printf(PRINT_WARNING, "LoadFont: Couldn't alloc mem %s\n", name); error = qtrue; break; } strcpy(pRitFontNames[i], token); } if (error) { break; } token = COM_Parse(&ref); if (Q_stricmp(token, "}")) { ri.Printf(PRINT_WARNING, "LoadFont: Bad Format %s\n", name); error = qtrue; break; } } else if (!Q_stricmp(token, "CharTable")) { token = COM_Parse(&ref); if (Q_stricmp(token, "{")) { ri.Printf(PRINT_WARNING, "LoadFont: Bad Format %s\n", name); error = qtrue; break; } for (i = 0; i < header->charTableLength; i++) { token = COM_Parse(&ref); if (Q_stricmp(token, "{")) { ri.Printf(PRINT_WARNING, "LoadFont: Bad Token %s\n", name); error = qtrue; break; } header->charTable[i].cp = atoi(COM_Parse(&ref)); header->charTable[i].index = atoi(COM_Parse(&ref)); header->charTable[i].loc = atoi(COM_Parse(&ref)); atoi(COM_Parse(&ref)); token = COM_Parse(&ref); if (Q_stricmp(token, "}")) { ri.Printf(PRINT_WARNING, "LoadFont: Bad Format %s\n", name); error = qtrue; break; } } if (error) { break; } token = COM_Parse(&ref); if (Q_stricmp(token, "}")) { ri.Printf(PRINT_WARNING, "LoadFont: Bad Format %s\n", name); error = qtrue; break; } } else { ri.Printf(PRINT_WARNING, "LoadFont: Bad Token %s\n", name); error = qtrue; break; } } } ri.FS_FreeFile(theFile); if (!header->numPages) { header->charTableLength = 0; header->charTable = NULL; header->sgl[0] = R_LoadFont_sgl(name); if (!header->sgl[0]) { ri.Printf(PRINT_WARNING, "LoadFont: failed %s\n", name); return NULL; } } else { for (i = 0; i < header->numPages; i++) { header->sgl[i] = R_LoadFont_sgl(pRitFontNames[i]); if (!header->sgl[i]) { ri.Printf(PRINT_WARNING, "LoadFont: failed %s(%s)\n", pRitFontNames[i], name); error = qtrue; break; } } // Free all allocated strings for (i = 0; i < header->numPages; i++) { ri.Free(pRitFontNames[i]); } if (error) { if (header->charTable) { ri.Free(header->charTable); } header->numPages = 0; header->charTableLength = 0; header->charTable = NULL; } } strcpy(header->name, name); s_numLoadedFonts++; return header; } void R_LoadFontShader(fontheader_sgl_t* font) { int i; int save; char filename[64]; shader_t* fontshader; save = r_sequencenumber; r_sequencenumber = -1; Com_sprintf(filename, sizeof(filename), "gfx/fonts/%s", font->name); font->shader = R_FindShader(filename, -1, qfalse); r_sequencenumber = save; if (!font->shader) { ri.Error(ERR_DROP, "Could not load font shader for %s\n", filename); } fontshader = (shader_t*)font->shader; if (fontshader->numUnfoggedPasses > 0) { for (i = 0; i < fontshader->numUnfoggedPasses; i++) { if (fontshader->stages[0] != NULL && fontshader->stages[0]->active) { fontshader->stages[0]->rgbGen = CGEN_ENTITY; fontshader->stages[0]->alphaGen = AGEN_ENTITY; } } font->trhandle = r_sequencenumber; } else { font->trhandle = r_sequencenumber; } } void R_DrawString_sgl(fontheader_sgl_t* font, const char* text, float x, float y, int maxlen, const float *pvVirtualScreen) { float charHeight; float startx, starty; int i; float fWidthScale, fHeightScale; i = 0; startx = x; starty = y; if (pvVirtualScreen) { if (pvVirtualScreen[0]) { fWidthScale = pvVirtualScreen[0]; } else { fWidthScale = (double)glConfig.vidWidth / 640.0; } if (pvVirtualScreen[1]) { fHeightScale = pvVirtualScreen[1]; } else { fHeightScale = (double)glConfig.vidHeight / 480.0; } } if (!font) { return; } R_IssuePendingRenderCommands(); if (font->trhandle != r_sequencenumber) { font->shader = NULL; } if (!font->shader) { R_LoadFontShader(font); } Vector4Copy(backEnd.color2D, backEnd.entity2D.e.shaderRGBA); backEnd.currentEntity = &backEnd.entity2D; charHeight = s_fontHeightScale * font->height * s_fontGeneralScale; RB_BeginSurface((shader_t*)font->shader, 0, 0); for (i = 0; text[i]; i++) { unsigned char c; int indirected; letterloc_t* loc; c = text[i]; if (maxlen != -1 && i >= maxlen) { break; } switch (c) { case '\t': indirected = font->indirection[32]; if (indirected == -1) { ri.Printf(PRINT_DEVELOPER, "R_DrawString: no space-character in font!\n"); } else { x = s_fontGeneralScale * font->locations[indirected].size[0] * 256.0 * 3.0 + x; } break; case '\n': starty = charHeight + starty; x = startx; y = starty; break; case '\r': x = startx; break; default: indirected = font->indirection[c]; if (indirected == -1) { ri.Printf(PRINT_DEVELOPER, "R_DrawString: no 0x%02x-character in font!\n", c); indirected = font->indirection['?']; if (indirected == -1) { ri.Printf(PRINT_DEVELOPER, "R_DrawString: no '?' character in font!\n"); break; } // set the indirection for the next time font->indirection[c] = indirected; } RB_CHECKOVERFLOW(4, 6); loc = &font->locations[indirected]; // texture coordinates tess.texCoords[tess.numVertexes][0] = loc->pos[0]; tess.texCoords[tess.numVertexes][1] = loc->pos[1]; tess.texCoords[tess.numVertexes + 1][0] = loc->size[0] + loc->pos[0]; tess.texCoords[tess.numVertexes + 1][1] = loc->pos[1]; tess.texCoords[tess.numVertexes + 2][0] = loc->pos[0]; tess.texCoords[tess.numVertexes + 2][1] = loc->size[1] + loc->pos[1]; tess.texCoords[tess.numVertexes + 3][0] = loc->size[0] + loc->pos[0]; tess.texCoords[tess.numVertexes + 3][1] = loc->size[1] + loc->pos[1]; // vertices position tess.xyz[tess.numVertexes][0] = x; tess.xyz[tess.numVertexes][1] = y; tess.xyz[tess.numVertexes][2] = s_fontZ; tess.xyz[tess.numVertexes + 1][0] = x + s_fontGeneralScale * loc->size[0] * 256.0; tess.xyz[tess.numVertexes + 1][1] = y; tess.xyz[tess.numVertexes + 1][2] = s_fontZ; tess.xyz[tess.numVertexes + 2][0] = x; tess.xyz[tess.numVertexes + 2][1] = y + charHeight; tess.xyz[tess.numVertexes + 2][2] = s_fontZ; tess.xyz[tess.numVertexes + 3][0] = x + s_fontGeneralScale * loc->size[0] * 256.0; tess.xyz[tess.numVertexes + 3][1] = y + charHeight; tess.xyz[tess.numVertexes + 3][2] = s_fontZ; // indices tess.indexes[tess.numIndexes] = tess.numVertexes; tess.indexes[tess.numIndexes + 1] = tess.numVertexes + 1; tess.indexes[tess.numIndexes + 2] = tess.numVertexes + 2; tess.indexes[tess.numIndexes + 3] = tess.numVertexes + 1; tess.indexes[tess.numIndexes + 4] = tess.numVertexes + 3; tess.indexes[tess.numIndexes + 5] = tess.numVertexes + 2; if (pvVirtualScreen) { // scale the string properly if virtual screen tess.xyz[tess.numVertexes][0] *= fWidthScale; tess.xyz[tess.numVertexes][1] *= fHeightScale; tess.xyz[tess.numVertexes + 1][0] *= fWidthScale; tess.xyz[tess.numVertexes + 1][1] *= fHeightScale; tess.xyz[tess.numVertexes + 2][0] *= fWidthScale; tess.xyz[tess.numVertexes + 2][1] *= fHeightScale; tess.xyz[tess.numVertexes + 3][0] *= fWidthScale; tess.xyz[tess.numVertexes + 3][1] *= fHeightScale; } x += s_fontGeneralScale * loc->size[0] * 256.0; tess.numVertexes += 4; tess.numIndexes += 6; break; } } RB_EndSurface(); } void R_DrawString(fontheader_t* font, const char* text, float x, float y, int maxlen, const float *pvVirtualScreen) { int i; int code; unsigned short uch; char buffer[512]; size_t buflen; int cursgl; float curX, curY; float curHeight; if (!font->numPages) { if (font->sgl[0]) { R_DrawString_sgl(font->sgl[0], text, x, y, maxlen, pvVirtualScreen); } return; } if (maxlen < 0) { maxlen = strlen(text); } curX = x; curY = y; curHeight = 0.f; cursgl = -1; buflen = 0; i = 0; while(i < maxlen) { fontchartable_t* ct; uch = text[i]; i++; if (DBCSIsLeadByte(font, uch)) { uch = (uch << 8) | text[i]; i++; } if (!uch) { break; } if (uch == '\n' || uch == '\r') { buffer[buflen] = 0; R_DrawString_sgl(font->sgl[cursgl], buffer, curX, curY, maxlen, pvVirtualScreen); curX = x; curHeight = 0.f; buflen = 0; if (uch == '\n') { curY += font->sgl[0]->height * s_fontGeneralScale * s_fontHeightScale; } continue; } code = CodeSearch(font, uch); if (code < 0) { continue; } if (cursgl == -1) { cursgl = font->charTable[code].index; } ct = &font->charTable[code]; if (cursgl != ct->index || buflen >= ARRAY_LEN(buffer) - 2) { buffer[buflen] = 0; R_DrawString_sgl(font->sgl[cursgl], buffer, curX, curY, maxlen, pvVirtualScreen); curX += curHeight; curHeight = 0.f; buflen = 0; ct = &font->charTable[code]; cursgl = ct->index; } buffer[buflen++] = ct->loc; curHeight += font->sgl[cursgl]->locations[ct->loc].size[0] * 256.0f; } if (buflen) { buffer[buflen] = 0; R_DrawString_sgl(font->sgl[cursgl], buffer, curX, y, buflen, pvVirtualScreen); } } void R_DrawFloatingString_sgl(fontheader_sgl_t* font, const char* text, const vec3_t org, const vec4_t color, float scale, int maxlen) { shader_t* fontshader; qhandle_t fsh; float charWidth, charHeight; int i; vec3_t pos; polyVert_t verts[4]; if (!font) { return; } R_IssuePendingRenderCommands(); if (font->trhandle != r_sequencenumber) { font->shader = NULL; } if (!font->shader) { R_LoadFontShader(font); } i = 0; fontshader = (shader_t*)font->shader; fsh = 0; for (i = 0; i < tr.numShaders; i++) { if (fontshader == tr.shaders[i]) { fsh = i; break; } } i = 0; charHeight = font->height * s_fontHeightScale * s_fontGeneralScale * scale; VectorCopy(org, pos); for (i = 0; text[i]; i++) { unsigned char c; int indirected; letterloc_t* loc; c = text[i]; indirected = font->indirection[c]; if (indirected == -1) { ri.Printf(PRINT_ALL, "R_DrawFloatingString: no 0x%02x-character in font!\n", c); continue; } loc = &font->locations[indirected]; // vertices color verts[0].modulate[0] = (int)(color[0] * 255.0); verts[0].modulate[1] = (int)(color[1] * 255.0); verts[0].modulate[2] = (int)(color[2] * 255.0); verts[0].modulate[3] = (int)(color[3] * 255.0); verts[1].modulate[0] = verts[0].modulate[0]; verts[1].modulate[1] = verts[0].modulate[1]; verts[1].modulate[2] = verts[0].modulate[2]; verts[1].modulate[3] = verts[0].modulate[3]; verts[2].modulate[0] = verts[0].modulate[0]; verts[2].modulate[1] = verts[0].modulate[1]; verts[2].modulate[2] = verts[0].modulate[2]; verts[2].modulate[3] = verts[0].modulate[3]; verts[3].modulate[0] = verts[0].modulate[0]; verts[3].modulate[1] = verts[0].modulate[1]; verts[3].modulate[2] = verts[0].modulate[2]; verts[3].modulate[3] = verts[0].modulate[3]; // texture coordinates verts[0].st[0] = loc->pos[0]; verts[0].st[1] = loc->pos[1]; verts[1].st[0] = loc->pos[0] + font->locations[indirected].size[0]; verts[1].st[1] = loc->pos[1]; verts[2].st[0] = verts[1].st[0]; verts[2].st[1] = loc->pos[1] + loc->size[1]; verts[3].st[0] = loc->pos[0]; verts[3].st[1] = verts[2].st[1]; VectorCopy(pos, verts[3].xyz); charWidth = font->locations[indirected].size[0] * 256.0 * s_fontGeneralScale * scale; verts[2].xyz[0] = pos[0] + tr.refdef.viewaxis[1][0] * -charWidth; verts[2].xyz[1] = pos[1] + tr.refdef.viewaxis[1][1] * -charWidth; verts[2].xyz[2] = pos[2] + tr.refdef.viewaxis[1][2] * -charWidth; verts[1].xyz[0] = verts[2].xyz[0] + charHeight * tr.refdef.viewaxis[2][0]; verts[1].xyz[1] = verts[2].xyz[1] + charHeight * tr.refdef.viewaxis[2][1]; verts[1].xyz[2] = verts[2].xyz[2] + charHeight * tr.refdef.viewaxis[2][2]; verts[0].xyz[1] = verts[1].xyz[1] + tr.refdef.viewaxis[1][1] * charWidth; verts[0].xyz[2] = verts[1].xyz[2] + tr.refdef.viewaxis[1][2] * charWidth; verts[0].xyz[0] = verts[1].xyz[0] + tr.refdef.viewaxis[1][0] * charWidth; if (RE_AddPolyToScene2(fsh, 4, verts, 0)) { ++tr.refdef.numPolys; } pos[0] = verts[2].xyz[0]; pos[1] = verts[2].xyz[1]; pos[2] = verts[2].xyz[2]; } } void R_DrawFloatingString(fontheader_t* font, const char* text, const vec3_t org, const vec4_t color, float scale, int maxlen) { return R_DrawFloatingString_sgl(font->sgl[0], text, org, color, scale, maxlen); } float R_GetFontHeight(const fontheader_t* font) { if (!font || !font->sgl[0]) { return 0.0; } return font->sgl[0]->height * s_fontGeneralScale * s_fontHeightScale; } float R_GetFontStringWidth_sgl(const fontheader_sgl_t* font, const char* s) { float widths; int i; widths = 0.0; if (!font) { return 0.0; } for (i = 0; s[i]; i++) { int indirected; char c = *s; if (c == '\t') { indirected = font->indirection[32]; if (indirected != -1) { widths += font->locations[indirected].size[0] * 3.0; } else { ri.Printf(PRINT_ALL, "R_GetFontStringWidth: no space-character in font!\n"); } } else { indirected = font->indirection[c]; if (indirected != -1) { widths += font->locations[indirected].size[0]; } else { ri.Printf(PRINT_ALL, "R_GetFontStringWidth: no 0x%02x-character in font!\n", c); } } } return widths * s_fontGeneralScale * 256.0; } float R_GetFontStringWidth(const fontheader_t* font, const char* s) { int i; int code; fontchartable_t* ct; float width = 0.f; if (!font->numPages) { return R_GetFontStringWidth_sgl(font->sgl[0], s); } i = 0; while(s[i]) { unsigned char uch = s[i++]; if (DBCSIsLeadByte(font, uch)) { uch = (uch << 8) | s[i++]; if (!uch) { break; } } if (uch == '\t') { code = CodeSearch(font, ' '); if (code >= 0) { ct = &font->charTable[code]; width += font->sgl[ct->index]->locations[ct->loc].size[0] * 3.f; } } else { code = CodeSearch(font, uch); if (code >= 0) { ct = &font->charTable[code]; width += font->sgl[ct->index]->locations[ct->loc].size[0]; } } } return width; }