Skip to content

Commit 6a1ae81

Browse files
authored
Add method to get a font's font family (scp-fs2open#6782)
* Add method to get a font's font family * nullptr * add more safety checks * pass font data instead of font data members * return an empty string instead of unknown
1 parent 54c00ce commit 6a1ae81

7 files changed

Lines changed: 211 additions & 0 deletions

File tree

code/graphics/software/FSFont.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ namespace font
7070
this->filename = newName;
7171
}
7272

73+
void FSFont::setFamilyName(const SCP_string& newName)
74+
{
75+
this->familyName = newName;
76+
}
77+
7378
[[nodiscard]] bool FSFont::getAutoScaleBehavior() const
7479
{
7580
return this->autoScale;
@@ -105,6 +110,11 @@ namespace font
105110
return this->filename;
106111
}
107112

113+
const SCP_string& FSFont::getFamilyName() const
114+
{
115+
return this->familyName;
116+
}
117+
108118
void FSFont::computeFontMetrics() {
109119
_height = this->getTextHeight() + this->offsetTop + this->offsetBottom;
110120

code/graphics/software/FSFont.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ namespace font
3939
private:
4040
SCP_string name = "<Invalid>"; //!< The name of this font
4141
SCP_string filename; //!< The file name used to retrieve this font
42+
SCP_string familyName; //!< The family name of the font. Will be "volition font" for bitmap fonts
4243

4344
protected:
4445
bool autoScale = false; //!< If the font is allowed to auto scale. Only used for VFNT fonts as NVG fonts do the auto scale calculation during parse time
@@ -86,6 +87,15 @@ namespace font
8687
*/
8788
void setFilename(const SCP_string& newName);
8889

90+
/**
91+
* @brief Sets the family name of this font.
92+
*
93+
* @date 17.6.2025
94+
*
95+
* @param newName The new famly name.
96+
*/
97+
void setFamilyName(const SCP_string& newName);
98+
8999
/**
90100
* @brief Gets the name of this font.
91101
*
@@ -104,6 +114,15 @@ namespace font
104114
*/
105115
const SCP_string& getFilename() const;
106116

117+
/**
118+
* @brief Gets the family name of this font. Will be "Volition font" for bitmap fonts
119+
*
120+
* @date 17.6.2025
121+
*
122+
* @return The family name.
123+
*/
124+
virtual const SCP_string& getFamilyName() const;
125+
107126
/**
108127
* @brief Gets the type of this font.
109128
*

code/graphics/software/FontManager.cpp

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,153 @@ namespace font
310310
}
311311
}
312312

313+
// This function will extract the font family name (Name ID 1) from TrueType font data.
314+
// It handles UCS-2 (UTF-16BE) to UTF-8 conversion.
315+
static SCP_string extractFamilyNameFromTTF(const TrueTypeFontData& fontData)
316+
{
317+
try {
318+
// TTF/OTF fonts start the table directory at byte 12.
319+
constexpr size_t table_offset = 12;
320+
321+
const ubyte* data = fontData.data.get();
322+
const size_t size = fontData.size;
323+
324+
if (size < table_offset) {
325+
throw std::runtime_error("Font data too small for table offset");
326+
}
327+
328+
// Offset to the start of the table directory (after SFNT header)
329+
const uint8_t* tableDir = data + table_offset;
330+
331+
// Read numTables (ushort at offset 4 in SFNT header)
332+
// The SFNT header bytes are big-endian
333+
uint16_t numTables = (static_cast<uint16_t>(data[4]) << 8) | static_cast<uint16_t>(data[5]);
334+
335+
const uint8_t* nameTable = nullptr;
336+
337+
// Each table entry is 16 bytes
338+
constexpr size_t tableEntrySize = 16;
339+
const size_t tableDirSize = static_cast<size_t>(numTables) * tableEntrySize;
340+
if (size < table_offset + tableDirSize) {
341+
throw std::runtime_error("Table directory extends past file size");
342+
}
343+
344+
// Iterate through table directory entries to find the 'name' table
345+
for (int i = 0; i < numTables; ++i) {
346+
const uint8_t* entry = tableDir + i * tableEntrySize;
347+
348+
// Ensure entry access is within range
349+
if (entry + 12 >= data + size) {
350+
throw std::runtime_error("Table entry access out of bounds");
351+
}
352+
353+
// Read table tag (4 bytes)
354+
uint32_t tag = (static_cast<uint32_t>(entry[0]) << 24) | (static_cast<uint32_t>(entry[1]) << 16) |
355+
(static_cast<uint32_t>(entry[2]) << 8) | static_cast<uint32_t>(entry[3]);
356+
357+
// Check if it's the 'name' table (tag 0x6E616D65)
358+
if (tag == 0x6E616D65) {
359+
// Read offset to the 'name' table (4 bytes, big-endian)
360+
uint32_t offset = (static_cast<uint32_t>(entry[8]) << 24) |
361+
(static_cast<uint32_t>(entry[9]) << 16) |
362+
(static_cast<uint32_t>(entry[10]) << 8) | static_cast<uint32_t>(entry[11]);
363+
364+
if (offset >= static_cast<uint32_t>(size)) {
365+
throw std::runtime_error("Name table offset beyond file size");
366+
}
367+
368+
nameTable = data + offset;
369+
break;
370+
}
371+
}
372+
373+
if (!nameTable || nameTable + 6 > data + size) {
374+
throw std::runtime_error("Name table header is missing or truncated");
375+
}
376+
377+
// Name table header - All values are big-endian
378+
uint16_t count = (static_cast<uint16_t>(nameTable[2]) << 8) | static_cast<uint16_t>(nameTable[3]);
379+
uint16_t stringOffset = (static_cast<uint16_t>(nameTable[4]) << 8) | static_cast<uint16_t>(nameTable[5]);
380+
381+
constexpr size_t recordSize = 12;
382+
const uint8_t* recordBase = nameTable + 6;
383+
384+
if (recordBase + count * recordSize > data + size) {
385+
throw std::runtime_error("Name records extend beyond font data");
386+
}
387+
388+
// Iterate through name records
389+
for (int i = 0; i < count; ++i) {
390+
const uint8_t* record =
391+
recordBase + i * recordSize; // 6 bytes for name table header, 12 bytes per record
392+
393+
// Read metadata for the record
394+
uint16_t platformID = (static_cast<uint16_t>(record[0]) << 8) | static_cast<uint16_t>(record[1]);
395+
uint16_t encodingID = (static_cast<uint16_t>(record[2]) << 8) | static_cast<uint16_t>(record[3]);
396+
uint16_t nameID = (static_cast<uint16_t>(record[6]) << 8) | static_cast<uint16_t>(record[7]);
397+
uint16_t length = (static_cast<uint16_t>(record[8]) << 8) | static_cast<uint16_t>(record[9]);
398+
uint16_t offset = (static_cast<uint16_t>(record[10]) << 8) | static_cast<uint16_t>(record[11]);
399+
400+
// We are looking for Name ID 1 (Font Family Name)
401+
// Prefer Unicode (Platform ID 0, 3) or Apple Roman (Platform ID 1) if available
402+
if (nameID == 1) {
403+
// Check for Unicode (Platform ID 0, Encoding ID 3 or 4) or (Platform ID 3, Encoding ID 1)
404+
// or Mac Roman (Platform ID 1, Encoding ID 0)
405+
bool isUnicode = (platformID == 0 && (encodingID == 3 || encodingID == 4)) ||
406+
(platformID == 3 && encodingID == 1);
407+
bool isMacRoman = (platformID == 1 && encodingID == 0);
408+
409+
if (isUnicode || isMacRoman) {
410+
const uint8_t* nameString = nameTable + stringOffset + offset;
411+
SCP_string familyName;
412+
413+
// Bounds check to prevent reading past the end of the font metadata
414+
if (nameString + length > data + size) {
415+
throw std::runtime_error("Name string extends past font data");
416+
}
417+
418+
if (isUnicode) {
419+
// UCS-2 (UTF-16BE) to UTF-8 conversion
420+
// This assumes standard UCS-2, which is UTF-16BE for basic multilingual plane.
421+
for (int j = 0; j < length; j += 2) {
422+
uint16_t unicodeChar = (static_cast<uint16_t>(nameString[j]) << 8) |
423+
static_cast<uint16_t>(nameString[j + 1]);
424+
425+
if (unicodeChar <= 0x7F) { // 1-byte UTF-8
426+
familyName += static_cast<char>(unicodeChar);
427+
} else if (unicodeChar <= 0x7FF) { // 2-byte UTF-8
428+
familyName += static_cast<char>(0xC0 | (unicodeChar >> 6));
429+
familyName += static_cast<char>(0x80 | (unicodeChar & 0x3F));
430+
} else { // 3-byte UTF-8
431+
familyName += static_cast<char>(0xE0 | (unicodeChar >> 12));
432+
familyName += static_cast<char>(0x80 | ((unicodeChar >> 6) & 0x3F));
433+
familyName += static_cast<char>(0x80 | (unicodeChar & 0x3F));
434+
}
435+
// For supplementary planes (4-byte UTF-8, characters > 0xFFFF),
436+
// this simple conversion is not sufficient and would require surrogate pair handling.
437+
// Most common font names will be in BMP (Basic Multilingual Plane) according to my
438+
// research - Mjn
439+
}
440+
} else { // Mac Roman (single byte) - this encoding is less common but supported easily enough
441+
for (int j = 0; j < length; ++j) {
442+
familyName += static_cast<char>(nameString[j]);
443+
}
444+
}
445+
return familyName;
446+
}
447+
}
448+
}
449+
450+
throw std::runtime_error("No suitable family name found");
451+
} catch (const std::exception& e) {
452+
mprintf(("Failed to extract font name: %s\n", e.what()));
453+
return "";
454+
} catch (...) {
455+
mprintf(("Failed to extract font name: Unknown exception\n"));
456+
return "";
457+
}
458+
}
459+
313460
std::pair<NVGFont*, int> FontManager::loadNVGFont(const SCP_string& fileName, float fontSize)
314461
{
315462
auto iter = allocatedData.find(fileName);
@@ -360,6 +507,7 @@ namespace font
360507
std::unique_ptr<NVGFont> nvgFont(new NVGFont());
361508
nvgFont->setHandle(handle);
362509
nvgFont->setSize(fontSize);
510+
nvgFont->setFamilyName(extractFamilyNameFromTTF(*data));
363511

364512
auto ptr = nvgFont.get();
365513

code/graphics/software/VFNTFont.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ namespace font
3333
return this->fontPtr;
3434
}
3535

36+
const SCP_string& VFNTFont::getFamilyName() const
37+
{
38+
static const SCP_string volitionFontName = "Volition Font";
39+
return volitionFontName;
40+
}
41+
3642
extern int get_char_width_old(font* fnt, ubyte c1, ubyte c2, int *width, int* spacing);
3743
void VFNTFont::getStringSize(const char *text, size_t textSize, int /* resize_mode */, float *w1, float *h1, float scaleMultiplier) const
3844
{

code/graphics/software/VFNTFont.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ namespace font
5555
*/
5656
float getTextHeight() const override;
5757

58+
/**
59+
* @brief Gets the family name of this font
60+
*
61+
* @see FSFont::getFamilyName()
62+
*
63+
* @return The family name.
64+
*/
65+
const SCP_string& getFamilyName() const override;
66+
5867
/**
5968
* @brief Gets the size of the specified string in pixels.
6069
*

code/graphics/software/font.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ namespace
315315

316316
font->setName(fontName);
317317
font->setFilename(fontFilename);
318+
font->setFamilyName("Volition Font");
318319

319320
int font_id = vfntPair.second;
320321

code/scripting/api/objs/font.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,24 @@ ADE_VIRTVAR(Name, l_Font, "string", "Name of font (including extension)", "strin
9696
return ade_set_args(L, "s", fh->Get()->getName().c_str());
9797
}
9898

99+
ADE_VIRTVAR(FamilyName, l_Font, "string", "Family Name of font. Bitmap fonts always return 'Volition Font'.", "string", nullptr)
100+
{
101+
font_h *fh = nullptr;
102+
const char* newname = nullptr;
103+
if (!ade_get_args(L, "o|s", l_Font.GetPtr(&fh), &newname))
104+
return ade_set_error(L, "s", "");
105+
106+
if (fh != nullptr && !fh->isValid())
107+
return ade_set_error(L, "s", "");
108+
109+
if (ADE_SETTING_VAR)
110+
{
111+
LuaError(L, "Setting font family name is not supported!");
112+
}
113+
114+
return ade_set_args(L, "s", fh->Get()->getFamilyName().c_str());
115+
}
116+
99117
ADE_VIRTVAR(Height, l_Font, "number", "Height of font (in pixels)", "number", "Font height, or 0 if the handle is invalid")
100118
{
101119
font_h *fh = NULL;

0 commit comments

Comments
 (0)