@@ -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
0 commit comments