Chiharu の日記

絵描き C/C++ プログラマーの日記です。

Windows フォントの英語名を Win32API で列挙する

タイトルの通りです。システムロケールに依存せずに Windows フォントの英語名を列挙する。休日の頭の体操として、やってみました。バリアブルフォントの取り扱いが面倒くさいことを除けば、name テーブルを参照するだけの教科書通りの処理ですね。応用すれば、任意ロケールのフォント名を取得することも難しくありません。さて、この後は頭を切り替えて、絵でも描きましょうかね。

#define UNICODE
#define _UNICODE

#include <windows.h>
#include <io.h>
#include <fcntl.h>

#include <iostream>
#include <map>
#include <set>
#include <string>
#include <vector>

#pragma comment(lib, "gdi32")
#pragma comment(lib, "user32")

namespace {

using TString = std::basic_string<TCHAR>;
using FontList = std::set<TString>;

constexpr auto tag(unsigned a, unsigned b, unsigned c, unsigned d)
{
 return (a << 0) + (b << 8) + (c << 16) + (d << 24);
}

std::vector<BYTE> getFontData(const LOGFONT& lf, DWORD table,
 bool ignoreError = false)
{
 std::vector<BYTE> data;

 HDC dc = nullptr, offscreenDc = nullptr;
 HGDIOBJ oldFont = nullptr;

 try {
  do {
   dc = GetDC(nullptr);
   offscreenDc = CreateCompatibleDC(dc);
   oldFont = SelectObject(offscreenDc, CreateFontIndirect(&lf));

   auto size = GetFontData(offscreenDc, table, 0, nullptr, 0);
   if (size == GDI_ERROR) {
    if (ignoreError) break;
    throw std::exception("GetFontData failured.");
   }

   data.resize(size);
   size = GetFontData(offscreenDc, table, 0, data.data(), size);
   if (size == GDI_ERROR) {
    if (ignoreError) break;
    throw std::exception("GetFontData failured.");
   }
  } while (0);
 } catch (std::exception& e) {
  std::cerr << e.what() << std::endl;
 }

 if (oldFont) DeleteObject(SelectObject(offscreenDc, oldFont));
 if (offscreenDc) DeleteDC(offscreenDc);
 if (dc) ReleaseDC(nullptr, dc);

 return data;
}

TString getName(unsigned platformId, const BYTE* data, unsigned length)
{
 TString name;

 switch (platformId) {
 case 0:
  // Unicode: Probably, needs to handle the original data as a UTF-8 text.
  for (auto i = 0u; i < length; ++i) {
   name.push_back(data[i]);
  }
  std::cerr << "!! Probably, should be processed as a UTF-8 text !!" << std::endl;
  break;

 case 1:
  // Machintosh: Omitted in this sample since it is too complex.
  std::cerr << "!! Inored, should be processed as per the encoding ID !!" << std::endl;
  break;

 case 3:
  // Windows: Handle the original data as a UTF-16BE text.
  for (auto i = 0u; i < (length / 2); ++i) {
   name.push_back((data[(i * 2) + 0] << 8) | data[(i * 2) + 1]);
  }
  break;
 }

 return name;
}

int CALLBACK enumProc(const LOGFONT* lf, const TEXTMETRIC* tm, DWORD fType, LPARAM lp)
{
 auto lfexdv = reinterpret_cast<const ENUMLOGFONTEXDV*>(lf);
 auto lfex = &lfexdv->elfEnumLogfontEx;
 auto fontList = reinterpret_cast<FontList*>(lp);

 try {
  if ((lfex->elfFullName[0] != '\0') && (lfex->elfFullName[0] != '@') &&
      (fType != RASTER_FONTTYPE)) {
   // Enumerate variations
   std::set<unsigned> subfamilyNameIds;

   auto data = getFontData(*lf, tag('f', 'v', 'a', 'r'), true);
   if (data.size() > 0) {
    auto majorVersion    = (data.at( 0) << 8) | data.at( 1);
    auto minorVersion    = (data.at( 2) << 8) | data.at( 3);
    auto axesArrayOffset = (data.at( 4) << 8) | data.at( 5);
    auto reserved        = (data.at( 6) << 8) | data.at( 7);
    auto axisCount       = (data.at( 8) << 8) | data.at( 9);
    auto axisSize        = (data.at(10) << 8) | data.at(11);
    auto instanceCount   = (data.at(12) << 8) | data.at(13);
    auto instanceSize    = (data.at(14) << 8) | data.at(15);

    auto c = 16;
    c += axisCount * axisSize; // Skip the array of VariationAxisRecord/

    for (auto i = 0u; i < instanceCount; ++i, c += instanceSize) {
     auto subfamilyNameId = (data.at(c) << 8) | data.at(c + 1);
     subfamilyNameIds.emplace(subfamilyNameId);
    }
   }

   // Enumerate names
   TString familyName;
   std::set<TString> subfamilyNames;

   data = getFontData(*lf, tag('n', 'a', 'm', 'e'));
   if (data.size() > 0) {
    auto formatSelector   = (data.at(0) << 8) | data.at(1); // Always 0
    auto nameRecordsCount = (data.at(2) << 8) | data.at(3);
    auto storageOffset    = (data.at(4) << 8) | data.at(5);

    auto c = 6;
    for (auto i = 0u; i < nameRecordsCount; ++i, c += 12) {
     auto platformId   = (data.at(c +  0) << 8) | data.at(c +  1);
     auto encodingId   = (data.at(c +  2) << 8) | data.at(c +  3);
     auto languageId   = (data.at(c +  4) << 8) | data.at(c +  5);
     auto nameId       = (data.at(c +  6) << 8) | data.at(c +  7);
     auto length       = (data.at(c +  8) << 8) | data.at(c +  9);
     auto stringOffset = (data.at(c + 10) << 8) | data.at(c + 11); // from 'storageOffset'

     //wprintf(TEXT("%u, %u, %u, %u, %u, %s\n"), formatSelector,
     // platformId, encodingId, languageId, nameId,
     // getName(platformId, data.data() + storageOffset + stringOffset, length).c_str());

     if (languageId == 0x0409) {
      if (nameId == 1) {
       // Font family name
       auto family = getName(platformId, data.data() + storageOffset + stringOffset, length);
       if (!family.empty()) {
        familyName = family;
       }
      }
      if (subfamilyNameIds.count(nameId)) {
       // Font subfamily name for variation
       auto style = getName(platformId, data.data() + storageOffset + stringOffset, length);
       if (!style.empty()) {
        subfamilyNames.emplace(style);
       }
      }
     }
    }
   }

   // Generate font names
   if (!familyName.empty()) {
    fontList->emplace(familyName);
    for (auto&& subfamilyName : subfamilyNames) {
     fontList->emplace(familyName + TEXT(" ") + subfamilyName);
    }
   } else {
    wprintf(TEXT("%s does not have an English family name.\n"), lfex->elfFullName);
   }
  }
 } catch (std::exception& e) {
  std::cerr << e.what() << std::endl;
 }

 return 1;
}

} // namespace

int main()
{
 auto oldMode = _setmode(_fileno(stdout), _O_U8TEXT);

 auto dc = GetDC(nullptr);

 LOGFONT lf = {};
 lf.lfCharSet = DEFAULT_CHARSET;

 FontList fontList;
 EnumFontFamiliesEx(dc, &lf, enumProc, reinterpret_cast<LPARAM>(&fontList), 0);

 ReleaseDC(nullptr, dc);

 for (auto&& font : fontList) {
  wprintf(TEXT("%s\n"), font.c_str());
 }

 fflush(stdout);
 _setmode(_fileno(stdout), oldMode);

 return 0;
}

私の自宅の PC では下記のような出力が得られました。.fon などの一部フォント形式は上手く処理できませんが、一般的な TrueType や OpenType は問題なく処理できるようです。よかったよかった。

Modern does not have an English family name.
Roman does not have an English family name.
Script does not have an English family name.
Adobe Arabic
Adobe Caslon Pro
Adobe Caslon Pro Bold
Adobe Devanagari
Adobe Fan Heiti Std B
Adobe Fangsong Std R
Adobe Garamond Pro
Adobe Garamond Pro Bold
Adobe Gothic Std B
Adobe Gurmukhi
..
Bahnschrift
Bahnschrift Bold
Bahnschrift Bold Condensed
Bahnschrift Bold SemiCondensed
Bahnschrift Condensed
Bahnschrift Light
Bahnschrift Light Condensed
Bahnschrift Light SemiCondensed
Bahnschrift Regular
Bahnschrift SemiBold
Bahnschrift SemiBold Condensed
Bahnschrift SemiBold SemiCondensed
Bahnschrift SemiCondensed
Bahnschrift SemiLight
Bahnschrift SemiLight Condensed
Bahnschrift SemiLight SemiCondensed
..
MS Gothic
MS Mincho
MS Outlook
MS PGothic
MS PMincho
..